File indexing completed on 2023-05-30 11:30:45

0001 /**
0002  * Copyright (C) 2004 Nathan Toone <nathan@toonetown.com>
0003  * Copyright (C) 2005, 2008, 2018 Michael Pyne <mpyne@kde.org>
0004  *
0005  * This program is free software; you can redistribute it and/or modify it under
0006  * the terms of the GNU General Public License as published by the Free Software
0007  * Foundation; either version 2 of the License, or (at your option) any later
0008  * version.
0009  *
0010  * This program is distributed in the hope that it will be useful, but WITHOUT ANY
0011  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
0012  * PARTICULAR PURPOSE. See the GNU General Public License for more details.
0013  *
0014  * You should have received a copy of the GNU General Public License along with
0015  * this program.  If not, see <http://www.gnu.org/licenses/>.
0016  */
0017 
0018 #include "coverinfo.h"
0019 
0020 #include <QApplication>
0021 #include <QLabel>
0022 #include <QCursor>
0023 #include <QPixmap>
0024 #include <QMouseEvent>
0025 #include <QFrame>
0026 #include <QHBoxLayout>
0027 #include <QEvent>
0028 #include <QFile>
0029 #include <QFileInfo>
0030 #include <QImage>
0031 #include <QScopedPointer>
0032 #include <QScreen>
0033 
0034 // Taglib includes
0035 #include <mpegfile.h>
0036 #include <tstring.h>
0037 #include <id3v2tag.h>
0038 #include <attachedpictureframe.h>
0039 #include <flacfile.h>
0040 #include <xiphcomment.h>
0041 #include <mp4coverart.h>
0042 #include <mp4file.h>
0043 #include <mp4tag.h>
0044 #include <mp4item.h>
0045 
0046 #include "mediafiles.h"
0047 #include "collectionlist.h"
0048 #include "playlistsearch.h"
0049 #include "playlistitem.h"
0050 #include "juktag.h"
0051 #include "juk_debug.h"
0052 
0053 struct CoverPopup : public QWidget
0054 {
0055     CoverPopup(QPixmap &image, const QPoint &p) :
0056         QWidget(0, Qt::WindowFlags(Qt::WA_DeleteOnClose | Qt::X11BypassWindowManagerHint))
0057     {
0058         QHBoxLayout *layout = new QHBoxLayout(this);
0059         QLabel *label = new QLabel(this);
0060         layout->addWidget(label);
0061 
0062         const auto pixRatio = this->devicePixelRatioF();
0063         QSizeF imageSize(label->width(), label->height());
0064 
0065         if (!qFuzzyCompare(pixRatio, 1.0)) {
0066             imageSize /= pixRatio;
0067             image.setDevicePixelRatio(pixRatio);
0068         }
0069 
0070         label->setFrameStyle(QFrame::Box | QFrame::Raised);
0071         label->setLineWidth(1);
0072         label->setPixmap(image);
0073 
0074         setGeometry(QRect(p, imageSize.toSize()));
0075 
0076         show();
0077     }
0078     virtual void leaveEvent(QEvent *) override { close(); }
0079     virtual void mouseReleaseEvent(QMouseEvent *) override { close(); }
0080 };
0081 
0082 ////////////////////////////////////////////////////////////////////////////////
0083 // public members
0084 ////////////////////////////////////////////////////////////////////////////////
0085 
0086 
0087 CoverInfo::CoverInfo(const FileHandle &file) :
0088     m_file(file),
0089     m_hasCover(false),
0090     m_hasAttachedCover(false),
0091     m_haveCheckedForCover(false),
0092     m_coverKey(CoverManager::NoMatch)
0093 {
0094 
0095 }
0096 
0097 bool CoverInfo::hasCover() const
0098 {
0099     if(m_haveCheckedForCover)
0100         return m_hasCover || m_hasAttachedCover;
0101 
0102     m_haveCheckedForCover = true;
0103 
0104     // Check for new-style covers.  First let's determine what our coverKey is
0105     // if it's not already set, as that's also tracked by the CoverManager.
0106     if(m_coverKey == CoverManager::NoMatch)
0107         m_coverKey = CoverManager::idForTrack(m_file.absFilePath());
0108 
0109     // We were assigned a key, let's see if we already have a cover.  Notice
0110     // that due to the way the CoverManager is structured, we should have a
0111     // cover if we have a cover key.  If we don't then either there's a logic
0112     // error, or the user has been mucking around where they shouldn't.
0113     if(m_coverKey != CoverManager::NoMatch)
0114         m_hasCover = CoverManager::hasCover(m_coverKey);
0115 
0116     // Check if it's embedded in the file itself.
0117 
0118     m_hasAttachedCover = hasEmbeddedAlbumArt();
0119 
0120     if(m_hasAttachedCover)
0121         return true;
0122 
0123     // Look for cover.jpg or cover.png in the directory.
0124     if(QFile::exists(m_file.fileInfo().absolutePath() + "/cover.jpg") ||
0125        QFile::exists(m_file.fileInfo().absolutePath() + "/cover.png"))
0126     {
0127         m_hasCover = true;
0128     }
0129 
0130     return m_hasCover;
0131 }
0132 
0133 void CoverInfo::clearCover()
0134 {
0135     m_hasCover = false;
0136     m_hasAttachedCover = false;
0137 
0138     // Re-search for cover since we may still have a different type of cover.
0139     m_haveCheckedForCover = false;
0140 
0141     // We don't need to call removeCover because the CoverManager will
0142     // automatically unlink the cover if we were the last track to use it.
0143     CoverManager::setIdForTrack(m_file.absFilePath(), CoverManager::NoMatch);
0144     m_coverKey = CoverManager::NoMatch;
0145 }
0146 
0147 void CoverInfo::setCover(const QImage &image)
0148 {
0149     if(image.isNull())
0150         return;
0151 
0152     m_haveCheckedForCover = true;
0153     m_hasCover = true;
0154 
0155     QPixmap cover = QPixmap::fromImage(image);
0156 
0157     // If we use replaceCover we'll change the cover for every other track
0158     // with the same coverKey, which we don't want since that case will be
0159     // handled by Playlist.  Instead just replace this track's cover.
0160     m_coverKey = CoverManager::addCover(cover, m_file.tag()->artist(), m_file.tag()->album());
0161     if(m_coverKey != CoverManager::NoMatch)
0162         CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey);
0163 }
0164 
0165 void CoverInfo::setCoverId(coverKey id)
0166 {
0167     m_coverKey = id;
0168     m_haveCheckedForCover = true;
0169     m_hasCover = id != CoverManager::NoMatch;
0170 
0171     // Inform CoverManager of the change.
0172     CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey);
0173 }
0174 
0175 void CoverInfo::applyCoverToWholeAlbum(bool overwriteExistingCovers) const
0176 {
0177     QString artist = m_file.tag()->artist();
0178     QString album = m_file.tag()->album();
0179     PlaylistSearch::ComponentList components;
0180     ColumnList columns;
0181 
0182     columns.append(PlaylistItem::ArtistColumn);
0183     components.append(PlaylistSearch::Component(artist, false, columns, PlaylistSearch::Component::Exact));
0184 
0185     columns.clear();
0186     columns.append(PlaylistItem::AlbumColumn);
0187     components.append(PlaylistSearch::Component(album, false, columns, PlaylistSearch::Component::Exact));
0188 
0189     PlaylistList playlists;
0190     playlists.append(CollectionList::instance());
0191 
0192     PlaylistSearch search(playlists, components, PlaylistSearch::MatchAll);
0193 
0194     // Search done, iterate through results.
0195 
0196     const auto playlistFoundItems = search.matchedItems();
0197     PlaylistItemList results;
0198     for(QModelIndex i : playlistFoundItems)
0199         results.append(static_cast<PlaylistItem*>(CollectionList::instance()->itemAt(i.row(), i.column())));
0200 
0201     for(const auto &playlistItem : qAsConst(results)) {
0202 
0203         // Don't worry about files that somehow already have a tag,
0204         // unless the conversion is forced.
0205         if(!overwriteExistingCovers && playlistItem->file().coverInfo()->coverId() != CoverManager::NoMatch)
0206             continue;
0207 
0208         playlistItem->file().coverInfo()->setCoverId(m_coverKey);
0209     }
0210 }
0211 
0212 coverKey CoverInfo::coverId() const
0213 {
0214     if(m_coverKey == CoverManager::NoMatch)
0215         m_coverKey = CoverManager::idForTrack(m_file.absFilePath());
0216 
0217     return m_coverKey;
0218 }
0219 
0220 QPixmap CoverInfo::pixmap(CoverSize size) const
0221 {
0222     if(hasCover() && m_coverKey != CoverManager::NoMatch) {
0223         return CoverManager::coverFromId(m_coverKey,
0224             size == Thumbnail
0225                ? CoverManager::Thumbnail
0226                : CoverManager::FullSize);
0227     }
0228 
0229     QImage cover;
0230 
0231     // If m_hasCover is still true we must have a directory cover image.
0232     if(m_hasCover) {
0233         QString fileName = m_file.fileInfo().absolutePath() + "/cover.jpg";
0234 
0235         if(!cover.load(fileName)) {
0236             fileName = m_file.fileInfo().absolutePath() + "/cover.png";
0237 
0238             if(!cover.load(fileName))
0239                 return QPixmap();
0240         }
0241         return QPixmap::fromImage(cover);
0242     }
0243 
0244     // If we get here, see if there is an embedded cover.
0245     cover = embeddedAlbumArt();
0246     if(!cover.isNull() && size == Thumbnail)
0247         cover = scaleCoverToThumbnail(cover);
0248 
0249     if(cover.isNull()) {
0250         return QPixmap();
0251     }
0252 
0253     return QPixmap::fromImage(cover);
0254 }
0255 
0256 QString CoverInfo::localPathToCover(const QString &fallbackFileName) const
0257 {
0258     if(m_coverKey != CoverManager::NoMatch) {
0259         QString path = CoverManager::coverInfo(m_coverKey).path;
0260         if(!path.isEmpty())
0261             return path;
0262     }
0263 
0264     if(hasEmbeddedAlbumArt()) {
0265         QFile albumArtFile(fallbackFileName);
0266         if(!albumArtFile.open(QIODevice::ReadWrite)) {
0267             return QString();
0268         }
0269 
0270         QImage albumArt = embeddedAlbumArt();
0271         albumArt.save(&albumArtFile, "PNG");
0272         return fallbackFileName;
0273     }
0274 
0275     QString basePath = m_file.fileInfo().absolutePath();
0276     if(QFile::exists(basePath + "/cover.jpg"))
0277         return basePath + "/cover.jpg";
0278     else if(QFile::exists(basePath + "/cover.png"))
0279         return basePath + "/cover.png";
0280 
0281     return QString();
0282 }
0283 
0284 bool CoverInfo::hasEmbeddedAlbumArt() const
0285 {
0286     QScopedPointer<TagLib::File> fileTag(
0287             MediaFiles::fileFactoryByType(m_file.absFilePath()));
0288 
0289     if (!fileTag->isValid()) {
0290         return false;
0291     }
0292 
0293     if (TagLib::MPEG::File *mpegFile =
0294             dynamic_cast<TagLib::MPEG::File *>(fileTag.data()))
0295     {
0296         TagLib::ID3v2::Tag *id3tag = mpegFile->ID3v2Tag(false);
0297 
0298         if (!id3tag) {
0299             qCCritical(JUK_LOG) << m_file.absFilePath() << "seems to have invalid ID3 tag";
0300             return false;
0301         }
0302 
0303         // Look for attached picture frames.
0304         TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"];
0305         return !frames.isEmpty();
0306     }
0307     else if (TagLib::Ogg::XiphComment *oggTag =
0308             dynamic_cast<TagLib::Ogg::XiphComment *>(fileTag->tag()))
0309     {
0310         return !oggTag->pictureList().isEmpty();
0311     }
0312     else if (TagLib::FLAC::File *flacFile =
0313             dynamic_cast<TagLib::FLAC::File *>(fileTag.data()))
0314     {
0315         // Look if images are embedded.
0316         return !flacFile->pictureList().isEmpty();
0317     }
0318     else if(TagLib::MP4::File *mp4File =
0319             dynamic_cast<TagLib::MP4::File *>(fileTag.data()))
0320     {
0321         TagLib::MP4::Tag *tag = mp4File->tag();
0322         if (tag) {
0323             return tag->contains("covr");
0324         }
0325     }
0326 
0327     return false;
0328 }
0329 
0330 static QImage embeddedMPEGAlbumArt(TagLib::ID3v2::Tag *id3tag)
0331 {
0332     if(!id3tag)
0333         return QImage();
0334 
0335     // Look for attached picture frames.
0336     TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"];
0337 
0338     if(frames.isEmpty())
0339         return QImage();
0340 
0341     // According to the spec attached picture frames have different types.
0342     // So we should look for the corresponding picture depending on what
0343     // type of image (i.e. front cover, file info) we want.  If only 1
0344     // frame, just return that (scaled if necessary).
0345 
0346     TagLib::ID3v2::AttachedPictureFrame *selectedFrame = 0;
0347 
0348     if(frames.size() != 1) {
0349         TagLib::ID3v2::FrameList::Iterator it = frames.begin();
0350         for(; it != frames.end(); ++it) {
0351 
0352             // This must be dynamic_cast<>, TagLib will return UnknownFrame in APIC for
0353             // encrypted frames.
0354             TagLib::ID3v2::AttachedPictureFrame *frame =
0355                 dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(*it);
0356 
0357             // Both thumbnail and full size should use FrontCover, as
0358             // FileIcon may be too small even for thumbnail.
0359             if(frame && frame->type() != TagLib::ID3v2::AttachedPictureFrame::FrontCover)
0360                 continue;
0361 
0362             selectedFrame = frame;
0363             break;
0364         }
0365     }
0366 
0367     // If we get here we failed to pick a picture, or there was only one,
0368     // so just use the first picture.
0369 
0370     if(!selectedFrame)
0371         selectedFrame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(frames.front());
0372 
0373     if(!selectedFrame) // Could occur for encrypted picture frames.
0374         return QImage();
0375 
0376     TagLib::ByteVector picture = selectedFrame->picture();
0377     return QImage::fromData(
0378             reinterpret_cast<const uchar *>(picture.data()),
0379             picture.size());
0380 }
0381 
0382 static QImage embeddedFLACAlbumArt(const TagLib::List<TagLib::FLAC::Picture *> &flacPictures)
0383 {
0384     if(flacPictures.isEmpty()) {
0385         return QImage();
0386     }
0387 
0388     // Always use first picture - even if multiple are embedded.
0389     TagLib::ByteVector coverData = flacPictures[0]->data();
0390 
0391     // Will return an image or a null image on error, works either way
0392     return QImage::fromData(
0393             reinterpret_cast<const uchar *>(coverData.data()),
0394             coverData.size());
0395 }
0396 
0397 static QImage embeddedMP4AlbumArt(TagLib::MP4::Tag *tag)
0398 {
0399     if(!tag->contains("covr"))
0400         return QImage();
0401 
0402     const TagLib::MP4::CoverArtList covers = tag->item("covr").toCoverArtList();
0403     for(const auto &cover : covers) {
0404         TagLib::ByteVector coverData = cover.data();
0405 
0406         QImage result = QImage::fromData(
0407                 reinterpret_cast<const uchar *>(coverData.data()),
0408                 coverData.size());
0409 
0410         if(!result.isNull())
0411             return result;
0412     }
0413 
0414     // No appropriate image found
0415     return QImage();
0416 }
0417 
0418 void CoverInfo::popup() const
0419 {
0420     QPixmap image = pixmap(FullSize);
0421     QPoint mouse  = QCursor::pos();
0422     QScreen *primaryScreen = QApplication::primaryScreen();
0423     QRect desktop = primaryScreen->availableGeometry();
0424 
0425     int x = mouse.x();
0426     int y = mouse.y();
0427     int height = image.size().height() + 4;
0428     int width  = image.size().width() + 4;
0429 
0430     // Detect the right direction to pop up (always towards the center of the
0431     // screen), try to pop up with the mouse pointer 10 pixels into the image in
0432     // both directions.  If we're too close to the screen border for this margin,
0433     // show it at the screen edge, accounting for the four pixels (two on each
0434     // side) for the window border.
0435 
0436     if(x - desktop.x() < desktop.width() / 2)
0437         x = (x - desktop.x() < 10) ? desktop.x() : (x - 10);
0438     else
0439         x = (x - desktop.x() > desktop.width() - 10) ? desktop.width() - width +desktop.x() : (x - width + 10);
0440 
0441     if(y - desktop.y() < desktop.height() / 2)
0442         y = (y - desktop.y() < 10) ? desktop.y() : (y - 10);
0443     else
0444         y = (y - desktop.y() > desktop.height() - 10) ? desktop.height() - height + desktop.y() : (y - height + 10);
0445 
0446     new CoverPopup(image, QPoint(x, y));
0447 }
0448 
0449 QImage CoverInfo::embeddedAlbumArt() const
0450 {
0451     QScopedPointer<TagLib::File> fileTag(
0452             MediaFiles::fileFactoryByType(m_file.absFilePath()));
0453 
0454     if (auto *mpegFile =
0455             dynamic_cast<TagLib::MPEG::File *>(fileTag.data()))
0456     {
0457         return embeddedMPEGAlbumArt(mpegFile->ID3v2Tag(false));
0458     }
0459     else if (auto *oggTag =
0460             dynamic_cast<TagLib::Ogg::XiphComment *>(fileTag->tag()))
0461     {
0462         return embeddedFLACAlbumArt(oggTag->pictureList());
0463     }
0464     else if (auto *flacFile =
0465             dynamic_cast<TagLib::FLAC::File *>(fileTag.data()))
0466     {
0467         return embeddedFLACAlbumArt(flacFile->pictureList());
0468     }
0469     else if(auto *mp4File =
0470             dynamic_cast<TagLib::MP4::File *>(fileTag.data()))
0471     {
0472         auto *tag = mp4File->tag();
0473         if (tag) {
0474             return embeddedMP4AlbumArt(tag);
0475         }
0476     }
0477 
0478     return QImage();
0479 }
0480 
0481 QImage CoverInfo::scaleCoverToThumbnail(const QImage &image) const
0482 {
0483     return image.scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0484 }
0485 
0486 // vim: set et sw=4 tw=0 sta: