coverinfo.cpp 14.9 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
struct CoverPopup : public QWidget
{
57
    CoverPopup(QPixmap &image, const QPoint &p) :
58
        QWidget(0, Qt::WindowFlags(Qt::WA_DeleteOnClose | Qt::X11BypassWindowManagerHint))
59 60 61 62
    {
        QHBoxLayout *layout = new QHBoxLayout(this);
        QLabel *label = new QLabel(this);
        layout->addWidget(label);
63 64 65 66 67 68 69 70 71

        const auto pixRatio = this->devicePixelRatioF();
        QSizeF imageSize(label->width(), label->height());

        if (!qFuzzyCompare(pixRatio, 1.0)) {
            imageSize /= pixRatio;
            image.setDevicePixelRatio(pixRatio);
        }

Laurent Montel's avatar
Laurent Montel committed
72
        label->setFrameStyle(QFrame::Box | QFrame::Raised);
73 74 75
        label->setLineWidth(1);
        label->setPixmap(image);

76 77
        setGeometry(QRect(p, imageSize.toSize()));

78 79 80
        show();
    }
    virtual void leaveEvent(QEvent *) { close(); }
81
    virtual void mouseReleaseEvent(QMouseEvent *) { close(); }
82 83
};

84 85 86 87 88
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////


89
CoverInfo::CoverInfo(const FileHandle &file) :
90 91
    m_file(file),
    m_hasCover(false),
92
    m_hasAttachedCover(false),
93
    m_haveCheckedForCover(false),
Michael Pyne's avatar
Michael Pyne committed
94
    m_coverKey(CoverManager::NoMatch)
95
{
96

97 98
}

Michael Pyne's avatar
Michael Pyne committed
99
bool CoverInfo::hasCover() const
100
{
101
    if(m_haveCheckedForCover)
102
        return m_hasCover || m_hasAttachedCover;
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117

    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);

118 119
    // Check if it's embedded in the file itself.

120
    m_hasAttachedCover = hasEmbeddedAlbumArt();
121 122 123 124 125 126 127 128 129 130 131

    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;
    }

132 133 134
    return m_hasCover;
}

135
void CoverInfo::clearCover()
Scott Wheeler's avatar
Scott Wheeler committed
136
{
137
    m_hasCover = false;
138
    m_hasAttachedCover = false;
139

140 141
    // Re-search for cover since we may still have a different type of cover.
    m_haveCheckedForCover = false;
Michael Pyne's avatar
Michael Pyne committed
142 143 144

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

149
void CoverInfo::setCover(const QImage &image)
150
{
151 152 153
    if(image.isNull())
        return;

Michael Pyne's avatar
Michael Pyne committed
154 155
    m_haveCheckedForCover = true;
    m_hasCover = true;
156

157
    QPixmap cover = QPixmap::fromImage(image);
158

Michael Pyne's avatar
Michael Pyne committed
159 160 161 162
    // 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());
163
    if(m_coverKey != CoverManager::NoMatch)
Michael Pyne's avatar
Michael Pyne committed
164 165 166 167 168 169 170
        CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey);
}

void CoverInfo::setCoverId(coverKey id)
{
    m_coverKey = id;
    m_haveCheckedForCover = true;
171
    m_hasCover = id != CoverManager::NoMatch;
Michael Pyne's avatar
Michael Pyne committed
172 173 174

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

177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
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
203 204
        // unless the conversion is forced.
        if(!overwriteExistingCovers && (*it)->file().coverInfo()->coverId() != CoverManager::NoMatch)
205 206 207 208 209 210
            continue;

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

Michael Pyne's avatar
Michael Pyne committed
211
coverKey CoverInfo::coverId() const
212
{
Michael Pyne's avatar
Michael Pyne committed
213 214 215
    if(m_coverKey == CoverManager::NoMatch)
        m_coverKey = CoverManager::idForTrack(m_file.absFilePath());

Michael Pyne's avatar
Michael Pyne committed
216 217
    return m_coverKey;
}
218

Michael Pyne's avatar
Michael Pyne committed
219 220 221
QPixmap CoverInfo::pixmap(CoverSize size) const
{
    if(hasCover() && m_coverKey != CoverManager::NoMatch) {
222 223 224 225 226 227
        return CoverManager::coverFromId(m_coverKey,
            size == Thumbnail
               ? CoverManager::Thumbnail
               : CoverManager::FullSize);
    }

228 229
    QImage cover;

230 231 232 233 234 235 236 237 238 239 240 241 242
    // 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.
243
    cover = embeddedAlbumArt();
244
    if(!cover.isNull() && size == Thumbnail)
245
        cover = scaleCoverToThumbnail(cover);
246

247 248 249 250
    if(cover.isNull()) {
        return QPixmap();
    }

251 252 253
    return QPixmap::fromImage(cover);
}

254 255 256
QString CoverInfo::localPathToCover(const QString &fallbackFileName) const
{
    if(m_coverKey != CoverManager::NoMatch) {
Michael Pyne's avatar
Michael Pyne committed
257
        QString path = CoverManager::coverInfo(m_coverKey).path;
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
        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();
}

282 283 284 285 286 287 288 289 290 291
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);

292
        if (!id3tag) {
Michael Pyne's avatar
Michael Pyne committed
293
            qCCritical(JUK_LOG) << m_file.absFilePath() << "seems to have invalid ID3 tag";
294 295 296
            return false;
        }

297 298 299 300
        // Look for attached picture frames.
        TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"];
        return !frames.isEmpty();
    }
Marcus Soll's avatar
Marcus Soll committed
301 302 303 304 305 306 307
    else if (TagLib::FLAC::File *flacFile =
            dynamic_cast<TagLib::FLAC::File *>(fileTag.data()))
    {

        // Look if images are embedded.
        return !flacFile->pictureList().isEmpty();
    }
308 309 310 311 312
#ifdef TAGLIB_WITH_MP4
    else if(TagLib::MP4::File *mp4File =
            dynamic_cast<TagLib::MP4::File *>(fileTag.data()))
    {
        TagLib::MP4::Tag *tag = mp4File->tag();
313 314 315 316
        if (tag) {
            TagLib::MP4::ItemListMap &items = tag->itemListMap();
            return items.contains("covr");
        }
317 318 319 320 321 322 323 324
    }
#endif

    return false;
}

static QImage embeddedMPEGAlbumArt(TagLib::ID3v2::Tag *id3tag)
{
325
    if(!id3tag)
326
        return QImage();
327 328

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

    if(frames.isEmpty())
332
        return QImage();
333

334 335 336 337 338
    // 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
339
    TagLib::ID3v2::AttachedPictureFrame *selectedFrame = 0;
340 341

    if(frames.size() != 1) {
Pau Garcia i Quiles's avatar
Pau Garcia i Quiles committed
342
        TagLib::ID3v2::FrameList::Iterator it = frames.begin();
343
        for(; it != frames.end(); ++it) {
344 345 346

            // 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
347 348
            TagLib::ID3v2::AttachedPictureFrame *frame =
                dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(*it);
349 350 351

            // Both thumbnail and full size should use FrontCover, as
            // FileIcon may be too small even for thumbnail.
352
            if(frame && frame->type() != TagLib::ID3v2::AttachedPictureFrame::FrontCover)
353 354 355 356 357 358 359 360 361 362 363
                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)
364 365 366
        selectedFrame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(frames.front());

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

369 370 371 372 373
    TagLib::ByteVector picture = selectedFrame->picture();
    return QImage::fromData(
            reinterpret_cast<const uchar *>(picture.data()),
            picture.size());
}
374

Marcus Soll's avatar
Marcus Soll committed
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
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();
}

398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
#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;
    }
420

421 422
    // No appropriate image found
    return QImage();
423
}
424
#endif
425

426
void CoverInfo::popup() const
427 428
{
    QPixmap image = pixmap(FullSize);
429
    QPoint mouse  = QCursor::pos();
Laurent Montel's avatar
Laurent Montel committed
430
    QRect desktop = QApplication::desktop()->screenGeometry(mouse);
431

432 433 434 435 436 437 438 439 440 441 442
    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.

443 444
    if(x - desktop.x() < desktop.width() / 2)
        x = (x - desktop.x() < 10) ? desktop.x() : (x - 10);
445
    else
446
        x = (x - desktop.x() > desktop.width() - 10) ? desktop.width() - width +desktop.x() : (x - width + 10);
447

448 449
    if(y - desktop.y() < desktop.height() / 2)
        y = (y - desktop.y() < 10) ? desktop.y() : (y - 10);
450
    else
451
        y = (y - desktop.y() > desktop.height() - 10) ? desktop.height() - height + desktop.y() : (y - height + 10);
452 453

    new CoverPopup(image, QPoint(x, y));
454 455
}

456 457 458 459 460 461 462 463 464 465 466
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
467 468 469 470 471
    else if (TagLib::FLAC::File *flacFile =
            dynamic_cast<TagLib::FLAC::File *>(fileTag.data()))
    {
        return embeddedFLACAlbumArt(flacFile);
    }
472 473 474 475 476
#ifdef TAGLIB_WITH_MP4
    else if(TagLib::MP4::File *mp4File =
            dynamic_cast<TagLib::MP4::File *>(fileTag.data()))
    {
        TagLib::MP4::Tag *tag = mp4File->tag();
477 478 479
        if (tag) {
            return embeddedMP4AlbumArt(tag);
        }
480 481 482 483 484 485
    }
#endif

    return QImage();
}

486 487 488 489 490
QImage CoverInfo::scaleCoverToThumbnail(const QImage &image) const
{
    return image.scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}

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