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: