coverinfo.cpp 14.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/**
 * Copyright (C) 2004 Nathan Toone <nathan@toonetown.com>
 * Copyright (C) 2005, 2008 Michael Pyne <mpyne@kde.org>
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
17

18 19
#include "coverinfo.h"

20
#include <QApplication>
21 22
#include <QRegExp>
#include <QLabel>
23
#include <QCursor>
Laurent Montel's avatar
Laurent Montel committed
24 25
#include <QPixmap>
#include <QMouseEvent>
Laurent Montel's avatar
Laurent Montel committed
26
#include <QFrame>
Laurent Montel's avatar
Laurent Montel committed
27 28
#include <QHBoxLayout>
#include <QEvent>
Laurent Montel's avatar
Laurent Montel committed
29
#include <QFile>
30
#include <QFileInfo>
31
#include <QDesktopWidget>
Michael Pyne's avatar
Michael Pyne committed
32
#include <QImage>
33
#include <QScopedPointer>
34

35 36 37 38
#include <taglib/mpegfile.h>
#include <taglib/tstring.h>
#include <taglib/id3v2tag.h>
#include <taglib/attachedpictureframe.h>
Marcus Soll's avatar
Marcus Soll committed
39
#include <taglib/flacfile.h>
40

41 42 43 44 45 46 47 48
#ifdef TAGLIB_WITH_MP4
#include <taglib/mp4coverart.h>
#include <taglib/mp4file.h>
#include <taglib/mp4tag.h>
#include <taglib/mp4item.h>
#endif

#include "mediafiles.h"
49 50 51
#include "collectionlist.h"
#include "playlistsearch.h"
#include "playlistitem.h"
52
#include "tag.h"
Michael Pyne's avatar
Michael Pyne committed
53
#include "juk_debug.h"
54

55 56 57
struct CoverPopup : public QWidget
{
    CoverPopup(const QPixmap &image, const QPoint &p) :
58
        QWidget(0, Qt::WindowFlags(Qt::WA_DeleteOnClose | Qt::X11BypassWindowManagerHint))
59 60 61 62 63
    {
        QHBoxLayout *layout = new QHBoxLayout(this);
        QLabel *label = new QLabel(this);

        layout->addWidget(label);
Laurent Montel's avatar
Laurent Montel committed
64
        label->setFrameStyle(QFrame::Box | QFrame::Raised);
65 66 67 68 69 70 71
        label->setLineWidth(1);
        label->setPixmap(image);

        setGeometry(p.x(), p.y(), label->width(), label->height());
        show();
    }
    virtual void leaveEvent(QEvent *) { close(); }
72
    virtual void mouseReleaseEvent(QMouseEvent *) { close(); }
73 74
};

75 76 77 78 79
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////


80
CoverInfo::CoverInfo(const FileHandle &file) :
81 82
    m_file(file),
    m_hasCover(false),
83
    m_hasAttachedCover(false),
84
    m_haveCheckedForCover(false),
Michael Pyne's avatar
Michael Pyne committed
85
    m_coverKey(CoverManager::NoMatch)
86
{
87

88 89
}

Michael Pyne's avatar
Michael Pyne committed
90
bool CoverInfo::hasCover() const
91
{
92
    if(m_haveCheckedForCover)
93
        return m_hasCover || m_hasAttachedCover;
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108

    m_haveCheckedForCover = true;

    // Check for new-style covers.  First let's determine what our coverKey is
    // if it's not already set, as that's also tracked by the CoverManager.
    if(m_coverKey == CoverManager::NoMatch)
        m_coverKey = CoverManager::idForTrack(m_file.absFilePath());

    // We were assigned a key, let's see if we already have a cover.  Notice
    // that due to the way the CoverManager is structured, we should have a
    // cover if we have a cover key.  If we don't then either there's a logic
    // error, or the user has been mucking around where they shouldn't.
    if(m_coverKey != CoverManager::NoMatch)
        m_hasCover = CoverManager::hasCover(m_coverKey);

109 110
    // Check if it's embedded in the file itself.

111
    m_hasAttachedCover = hasEmbeddedAlbumArt();
112 113 114 115 116 117 118 119 120 121 122

    if(m_hasAttachedCover)
        return true;

    // Look for cover.jpg or cover.png in the directory.
    if(QFile::exists(m_file.fileInfo().absolutePath() + "/cover.jpg") ||
       QFile::exists(m_file.fileInfo().absolutePath() + "/cover.png"))
    {
        m_hasCover = true;
    }

123 124 125
    return m_hasCover;
}

126
void CoverInfo::clearCover()
Scott Wheeler's avatar
Scott Wheeler committed
127
{
128
    m_hasCover = false;
129
    m_hasAttachedCover = false;
130

131 132
    // Re-search for cover since we may still have a different type of cover.
    m_haveCheckedForCover = false;
Michael Pyne's avatar
Michael Pyne committed
133 134 135

    // We don't need to call removeCover because the CoverManager will
    // automatically unlink the cover if we were the last track to use it.
136 137
    CoverManager::setIdForTrack(m_file.absFilePath(), CoverManager::NoMatch);
    m_coverKey = CoverManager::NoMatch;
Scott Wheeler's avatar
Scott Wheeler committed
138 139
}

140
void CoverInfo::setCover(const QImage &image)
141
{
142 143 144
    if(image.isNull())
        return;

Michael Pyne's avatar
Michael Pyne committed
145 146
    m_haveCheckedForCover = true;
    m_hasCover = true;
147

148
    QPixmap cover = QPixmap::fromImage(image);
149

Michael Pyne's avatar
Michael Pyne committed
150 151 152 153
    // If we use replaceCover we'll change the cover for every other track
    // with the same coverKey, which we don't want since that case will be
    // handled by Playlist.  Instead just replace this track's cover.
    m_coverKey = CoverManager::addCover(cover, m_file.tag()->artist(), m_file.tag()->album());
154
    if(m_coverKey != CoverManager::NoMatch)
Michael Pyne's avatar
Michael Pyne committed
155 156 157 158 159 160 161
        CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey);
}

void CoverInfo::setCoverId(coverKey id)
{
    m_coverKey = id;
    m_haveCheckedForCover = true;
162
    m_hasCover = id != CoverManager::NoMatch;
Michael Pyne's avatar
Michael Pyne committed
163 164 165

    // Inform CoverManager of the change.
    CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey);
166 167
}

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
void CoverInfo::applyCoverToWholeAlbum(bool overwriteExistingCovers) const
{
    QString artist = m_file.tag()->artist();
    QString album = m_file.tag()->album();
    PlaylistSearch::ComponentList components;
    ColumnList columns;

    columns.append(PlaylistItem::ArtistColumn);
    components.append(PlaylistSearch::Component(artist, false, columns, PlaylistSearch::Component::Exact));

    columns.clear();
    columns.append(PlaylistItem::AlbumColumn);
    components.append(PlaylistSearch::Component(album, false, columns, PlaylistSearch::Component::Exact));

    PlaylistList playlists;
    playlists.append(CollectionList::instance());

    PlaylistSearch search(playlists, components, PlaylistSearch::MatchAll);

    // Search done, iterate through results.

    PlaylistItemList results = search.matchedItems();
    PlaylistItemList::ConstIterator it = results.constBegin();
    for(; it != results.constEnd(); ++it) {

        // Don't worry about files that somehow already have a tag,
Michael Pyne's avatar
Michael Pyne committed
194 195
        // unless the conversion is forced.
        if(!overwriteExistingCovers && (*it)->file().coverInfo()->coverId() != CoverManager::NoMatch)
196 197 198 199 200 201
            continue;

        (*it)->file().coverInfo()->setCoverId(m_coverKey);
    }
}

Michael Pyne's avatar
Michael Pyne committed
202
coverKey CoverInfo::coverId() const
203
{
Michael Pyne's avatar
Michael Pyne committed
204 205 206
    if(m_coverKey == CoverManager::NoMatch)
        m_coverKey = CoverManager::idForTrack(m_file.absFilePath());

Michael Pyne's avatar
Michael Pyne committed
207 208
    return m_coverKey;
}
209

Michael Pyne's avatar
Michael Pyne committed
210 211 212
QPixmap CoverInfo::pixmap(CoverSize size) const
{
    if(hasCover() && m_coverKey != CoverManager::NoMatch) {
213 214 215 216 217 218
        return CoverManager::coverFromId(m_coverKey,
            size == Thumbnail
               ? CoverManager::Thumbnail
               : CoverManager::FullSize);
    }

219 220
    QImage cover;

221 222 223 224 225 226 227 228 229 230 231 232 233
    // If m_hasCover is still true we must have a directory cover image.
    if(m_hasCover) {
        QString fileName = m_file.fileInfo().absolutePath() + "/cover.jpg";

        if(!cover.load(fileName)) {
            fileName = m_file.fileInfo().absolutePath() + "/cover.png";

            if(!cover.load(fileName))
                return QPixmap();
        }
    }

    // If we get here, see if there is an embedded cover.
234
    cover = embeddedAlbumArt();
235
    if(!cover.isNull() && size == Thumbnail)
236
        cover = scaleCoverToThumbnail(cover);
237

238 239 240 241
    if(cover.isNull()) {
        return QPixmap();
    }

242 243 244
    return QPixmap::fromImage(cover);
}

245 246 247
QString CoverInfo::localPathToCover(const QString &fallbackFileName) const
{
    if(m_coverKey != CoverManager::NoMatch) {
Michael Pyne's avatar
Michael Pyne committed
248
        QString path = CoverManager::coverInfo(m_coverKey).path;
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
        if(!path.isEmpty())
            return path;
    }

    if(hasEmbeddedAlbumArt()) {
        QFile albumArtFile(fallbackFileName);
        if(!albumArtFile.open(QIODevice::ReadWrite)) {
            return QString();
        }

        QImage albumArt = embeddedAlbumArt();
        albumArt.save(&albumArtFile, "PNG");
        return fallbackFileName;
    }

    QString basePath = m_file.fileInfo().absolutePath();
    if(QFile::exists(basePath + "/cover.jpg"))
        return basePath + "/cover.jpg";
    else if(QFile::exists(basePath + "/cover.png"))
        return basePath + "/cover.png";

    return QString();
}

273 274 275 276 277 278 279 280 281 282
bool CoverInfo::hasEmbeddedAlbumArt() const
{
    QScopedPointer<TagLib::File> fileTag(
            MediaFiles::fileFactoryByType(m_file.absFilePath()));

    if (TagLib::MPEG::File *mpegFile =
            dynamic_cast<TagLib::MPEG::File *>(fileTag.data()))
    {
        TagLib::ID3v2::Tag *id3tag = mpegFile->ID3v2Tag(false);

283
        if (!id3tag) {
Michael Pyne's avatar
Michael Pyne committed
284
            qCCritical(JUK_LOG) << m_file.absFilePath() << "seems to have invalid ID3 tag";
285 286 287
            return false;
        }

288 289 290 291
        // Look for attached picture frames.
        TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"];
        return !frames.isEmpty();
    }
Marcus Soll's avatar
Marcus Soll committed
292 293 294 295 296 297 298
    else if (TagLib::FLAC::File *flacFile =
            dynamic_cast<TagLib::FLAC::File *>(fileTag.data()))
    {

        // Look if images are embedded.
        return !flacFile->pictureList().isEmpty();
    }
299 300 301 302 303
#ifdef TAGLIB_WITH_MP4
    else if(TagLib::MP4::File *mp4File =
            dynamic_cast<TagLib::MP4::File *>(fileTag.data()))
    {
        TagLib::MP4::Tag *tag = mp4File->tag();
304 305 306 307
        if (tag) {
            TagLib::MP4::ItemListMap &items = tag->itemListMap();
            return items.contains("covr");
        }
308 309 310 311 312 313 314 315
    }
#endif

    return false;
}

static QImage embeddedMPEGAlbumArt(TagLib::ID3v2::Tag *id3tag)
{
316
    if(!id3tag)
317
        return QImage();
318 319

    // Look for attached picture frames.
Pau Garcia i Quiles's avatar
Pau Garcia i Quiles committed
320
    TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"];
321 322

    if(frames.isEmpty())
323
        return QImage();
324

325 326 327 328 329
    // According to the spec attached picture frames have different types.
    // So we should look for the corresponding picture depending on what
    // type of image (i.e. front cover, file info) we want.  If only 1
    // frame, just return that (scaled if necessary).

Pau Garcia i Quiles's avatar
Pau Garcia i Quiles committed
330
    TagLib::ID3v2::AttachedPictureFrame *selectedFrame = 0;
331 332

    if(frames.size() != 1) {
Pau Garcia i Quiles's avatar
Pau Garcia i Quiles committed
333
        TagLib::ID3v2::FrameList::Iterator it = frames.begin();
334
        for(; it != frames.end(); ++it) {
335 336 337

            // This must be dynamic_cast<>, TagLib will return UnknownFrame in APIC for
            // encrypted frames.
Pau Garcia i Quiles's avatar
Pau Garcia i Quiles committed
338 339
            TagLib::ID3v2::AttachedPictureFrame *frame =
                dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(*it);
340 341 342

            // Both thumbnail and full size should use FrontCover, as
            // FileIcon may be too small even for thumbnail.
343
            if(frame && frame->type() != TagLib::ID3v2::AttachedPictureFrame::FrontCover)
344 345 346 347 348 349 350 351 352 353 354
                continue;

            selectedFrame = frame;
            break;
        }
    }

    // If we get here we failed to pick a picture, or there was only one,
    // so just use the first picture.

    if(!selectedFrame)
355 356 357
        selectedFrame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(frames.front());

    if(!selectedFrame) // Could occur for encrypted picture frames.
358
        return QImage();
359

360 361 362 363 364
    TagLib::ByteVector picture = selectedFrame->picture();
    return QImage::fromData(
            reinterpret_cast<const uchar *>(picture.data()),
            picture.size());
}
365

Marcus Soll's avatar
Marcus Soll committed
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
static QImage embeddedFLACAlbumArt(TagLib::FLAC::File *flacFile)
{
    TagLib::List<TagLib::FLAC::Picture *> flacPictures = flacFile->pictureList();
    if(flacPictures.isEmpty()) {

        // No pictures are embedded.
        return QImage();
    }

    // Always use first picture - even if multiple are embedded.
    TagLib::ByteVector coverData = flacPictures[0]->data();
    QImage result = QImage::fromData(
                reinterpret_cast<const uchar *>(coverData.data()),
                coverData.size());

    if(!result.isNull()) {
            return result;
    }

    // Error while casting image.
    return QImage();
}

389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
#ifdef TAGLIB_WITH_MP4
static QImage embeddedMP4AlbumArt(TagLib::MP4::Tag *tag)
{
    TagLib::MP4::ItemListMap &items = tag->itemListMap();

    if(!items.contains("covr"))
        return QImage();

    TagLib::MP4::CoverArtList covers = items["covr"].toCoverArtList();
    TagLib::MP4::CoverArtList::ConstIterator end = covers.end();

    for(TagLib::MP4::CoverArtList::ConstIterator it = covers.begin(); it != end; ++it) {
        TagLib::MP4::CoverArt cover = *it;
        TagLib::ByteVector coverData = cover.data();

        QImage result = QImage::fromData(
                reinterpret_cast<const uchar *>(coverData.data()),
                coverData.size());

        if(!result.isNull())
            return result;
    }
411

412 413
    // No appropriate image found
    return QImage();
414
}
415
#endif
416

417
void CoverInfo::popup() const
418 419
{
    QPixmap image = pixmap(FullSize);
420
    QPoint mouse  = QCursor::pos();
Laurent Montel's avatar
Laurent Montel committed
421
    QRect desktop = QApplication::desktop()->screenGeometry(mouse);
422

423 424 425 426 427 428 429 430 431 432 433
    int x = mouse.x();
    int y = mouse.y();
    int height = image.size().height() + 4;
    int width  = image.size().width() + 4;

    // Detect the right direction to pop up (always towards the center of the
    // screen), try to pop up with the mouse pointer 10 pixels into the image in
    // both directions.  If we're too close to the screen border for this margin,
    // show it at the screen edge, accounting for the four pixels (two on each
    // side) for the window border.

434 435
    if(x - desktop.x() < desktop.width() / 2)
        x = (x - desktop.x() < 10) ? desktop.x() : (x - 10);
436
    else
437
        x = (x - desktop.x() > desktop.width() - 10) ? desktop.width() - width +desktop.x() : (x - width + 10);
438

439 440
    if(y - desktop.y() < desktop.height() / 2)
        y = (y - desktop.y() < 10) ? desktop.y() : (y - 10);
441
    else
442
        y = (y - desktop.y() > desktop.height() - 10) ? desktop.height() - height + desktop.y() : (y - height + 10);
443 444

    new CoverPopup(image, QPoint(x, y));
445 446
}

447 448 449 450 451 452 453 454 455 456 457
QImage CoverInfo::embeddedAlbumArt() const
{
    QScopedPointer<TagLib::File> fileTag(
            MediaFiles::fileFactoryByType(m_file.absFilePath()));

    if (TagLib::MPEG::File *mpegFile =
            dynamic_cast<TagLib::MPEG::File *>(fileTag.data()))
    {
        TagLib::ID3v2::Tag *id3tag = mpegFile->ID3v2Tag(false);
        return embeddedMPEGAlbumArt(id3tag);
    }
Marcus Soll's avatar
Marcus Soll committed
458 459 460 461 462
    else if (TagLib::FLAC::File *flacFile =
            dynamic_cast<TagLib::FLAC::File *>(fileTag.data()))
    {
        return embeddedFLACAlbumArt(flacFile);
    }
463 464 465 466 467
#ifdef TAGLIB_WITH_MP4
    else if(TagLib::MP4::File *mp4File =
            dynamic_cast<TagLib::MP4::File *>(fileTag.data()))
    {
        TagLib::MP4::Tag *tag = mp4File->tag();
468 469 470
        if (tag) {
            return embeddedMP4AlbumArt(tag);
        }
471 472 473 474 475 476
    }
#endif

    return QImage();
}

477 478 479 480 481
QImage CoverInfo::scaleCoverToThumbnail(const QImage &image) const
{
    return image.scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}

482
// vim: set et sw=4 tw=0 sta: