coverinfo.cpp 15.1 KB
Newer Older
1 2
/**
 * Copyright (C) 2004 Nathan Toone <nathan@toonetown.com>
3
 * Copyright (C) 2005, 2008, 2018 Michael Pyne <mpyne@kde.org>
4 5 6 7 8 9 10 11 12 13 14 15 16
 *
 * 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
#include <QScreen>
35

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

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

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

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

        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
74
        label->setFrameStyle(QFrame::Box | QFrame::Raised);
75 76 77
        label->setLineWidth(1);
        label->setPixmap(image);

78 79
        setGeometry(QRect(p, imageSize.toSize()));

80 81
        show();
    }
82 83
    virtual void leaveEvent(QEvent *) override { close(); }
    virtual void mouseReleaseEvent(QMouseEvent *) override { close(); }
84 85
};

86 87 88 89 90
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////


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

99 100
}

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

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

120 121
    // Check if it's embedded in the file itself.

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

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

134 135 136
    return m_hasCover;
}

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

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

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

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

Michael Pyne's avatar
Michael Pyne committed
156 157
    m_haveCheckedForCover = true;
    m_hasCover = true;
158

159
    QPixmap cover = QPixmap::fromImage(image);
160

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

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

    // Inform CoverManager of the change.
    CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey);
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 203 204
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
205 206
        // unless the conversion is forced.
        if(!overwriteExistingCovers && (*it)->file().coverInfo()->coverId() != CoverManager::NoMatch)
207 208 209 210 211 212
            continue;

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

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

Michael Pyne's avatar
Michael Pyne committed
218 219
    return m_coverKey;
}
220

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

230 231
    QImage cover;

232 233 234 235 236 237 238 239 240 241
    // 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();
        }
242
        return QPixmap::fromImage(cover);
243 244 245
    }

    // If we get here, see if there is an embedded cover.
246
    cover = embeddedAlbumArt();
247
    if(!cover.isNull() && size == Thumbnail)
248
        cover = scaleCoverToThumbnail(cover);
249

250 251 252 253
    if(cover.isNull()) {
        return QPixmap();
    }

254 255 256
    return QPixmap::fromImage(cover);
}

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

285 286 287 288 289 290 291 292 293 294
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);

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

300 301 302 303
        // Look for attached picture frames.
        TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"];
        return !frames.isEmpty();
    }
304 305 306 307 308
    else if (TagLib::Ogg::XiphComment *oggTag =
            dynamic_cast<TagLib::Ogg::XiphComment *>(fileTag->tag()))
    {
        return !oggTag->pictureList().isEmpty();
    }
Marcus Soll's avatar
Marcus Soll committed
309 310 311 312 313 314
    else if (TagLib::FLAC::File *flacFile =
            dynamic_cast<TagLib::FLAC::File *>(fileTag.data()))
    {
        // Look if images are embedded.
        return !flacFile->pictureList().isEmpty();
    }
315 316 317 318 319
#ifdef TAGLIB_WITH_MP4
    else if(TagLib::MP4::File *mp4File =
            dynamic_cast<TagLib::MP4::File *>(fileTag.data()))
    {
        TagLib::MP4::Tag *tag = mp4File->tag();
320 321 322 323
        if (tag) {
            TagLib::MP4::ItemListMap &items = tag->itemListMap();
            return items.contains("covr");
        }
324 325 326 327 328 329 330 331
    }
#endif

    return false;
}

static QImage embeddedMPEGAlbumArt(TagLib::ID3v2::Tag *id3tag)
{
332
    if(!id3tag)
333
        return QImage();
334 335

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

    if(frames.isEmpty())
339
        return QImage();
340

341 342 343 344 345
    // 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
346
    TagLib::ID3v2::AttachedPictureFrame *selectedFrame = 0;
347 348

    if(frames.size() != 1) {
Pau Garcia i Quiles's avatar
Pau Garcia i Quiles committed
349
        TagLib::ID3v2::FrameList::Iterator it = frames.begin();
350
        for(; it != frames.end(); ++it) {
351 352 353

            // 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
354 355
            TagLib::ID3v2::AttachedPictureFrame *frame =
                dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(*it);
356 357 358

            // Both thumbnail and full size should use FrontCover, as
            // FileIcon may be too small even for thumbnail.
359
            if(frame && frame->type() != TagLib::ID3v2::AttachedPictureFrame::FrontCover)
360 361 362 363 364 365 366 367 368 369 370
                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)
371 372 373
        selectedFrame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(frames.front());

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

376 377 378 379 380
    TagLib::ByteVector picture = selectedFrame->picture();
    return QImage::fromData(
            reinterpret_cast<const uchar *>(picture.data()),
            picture.size());
}
381

382
static QImage embeddedFLACAlbumArt(const TagLib::List<TagLib::FLAC::Picture *> &flacPictures)
Marcus Soll's avatar
Marcus Soll committed
383 384 385 386 387 388 389 390
{
    if(flacPictures.isEmpty()) {
        return QImage();
    }

    // Always use first picture - even if multiple are embedded.
    TagLib::ByteVector coverData = flacPictures[0]->data();

391 392 393 394
    // Will return an image or a null image on error, works either way
    return QImage::fromData(
            reinterpret_cast<const uchar *>(coverData.data()),
            coverData.size());
Marcus Soll's avatar
Marcus Soll committed
395 396
}

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

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

425
void CoverInfo::popup() const
426 427
{
    QPixmap image = pixmap(FullSize);
428
    QPoint mouse  = QCursor::pos();
429 430
    QScreen *primaryScreen = QApplication::primaryScreen();
    QRect desktop = primaryScreen->availableGeometry();
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
QImage CoverInfo::embeddedAlbumArt() const
{
    QScopedPointer<TagLib::File> fileTag(
            MediaFiles::fileFactoryByType(m_file.absFilePath()));

461
    if (auto *mpegFile =
462 463
            dynamic_cast<TagLib::MPEG::File *>(fileTag.data()))
    {
464
        return embeddedMPEGAlbumArt(mpegFile->ID3v2Tag(false));
465
    }
466 467 468 469 470 471
    else if (auto *oggTag =
            dynamic_cast<TagLib::Ogg::XiphComment *>(fileTag->tag()))
    {
        return embeddedFLACAlbumArt(oggTag->pictureList());
    }
    else if (auto *flacFile =
Marcus Soll's avatar
Marcus Soll committed
472 473
            dynamic_cast<TagLib::FLAC::File *>(fileTag.data()))
    {
474
        return embeddedFLACAlbumArt(flacFile->pictureList());
Marcus Soll's avatar
Marcus Soll committed
475
    }
476
#ifdef TAGLIB_WITH_MP4
477
    else if(auto *mp4File =
478 479
            dynamic_cast<TagLib::MP4::File *>(fileTag.data()))
    {
480
        auto *tag = mp4File->tag();
481 482 483
        if (tag) {
            return embeddedMP4AlbumArt(tag);
        }
484 485 486 487 488 489
    }
#endif

    return QImage();
}

490 491 492 493 494
QImage CoverInfo::scaleCoverToThumbnail(const QImage &image) const
{
    return image.scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}

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