diff --git a/coverinfo.cpp b/coverinfo.cpp index 9cc20577..e311afa1 100644 --- a/coverinfo.cpp +++ b/coverinfo.cpp @@ -1,494 +1,495 @@ /** * Copyright (C) 2004 Nathan Toone * Copyright (C) 2005, 2008, 2018 Michael Pyne * * 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 . */ #include "coverinfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include -#include -#include -#include -#include -#include +// Taglib includes +#include +#include +#include +#include +#include +#include #ifdef TAGLIB_WITH_MP4 -#include -#include -#include -#include +#include +#include +#include +#include #endif #include "mediafiles.h" #include "collectionlist.h" #include "playlistsearch.h" #include "playlistitem.h" #include "juktag.h" #include "juk_debug.h" struct CoverPopup : public QWidget { CoverPopup(QPixmap &image, const QPoint &p) : QWidget(0, Qt::WindowFlags(Qt::WA_DeleteOnClose | Qt::X11BypassWindowManagerHint)) { QHBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(this); layout->addWidget(label); const auto pixRatio = this->devicePixelRatioF(); QSizeF imageSize(label->width(), label->height()); if (!qFuzzyCompare(pixRatio, 1.0)) { imageSize /= pixRatio; image.setDevicePixelRatio(pixRatio); } label->setFrameStyle(QFrame::Box | QFrame::Raised); label->setLineWidth(1); label->setPixmap(image); setGeometry(QRect(p, imageSize.toSize())); show(); } virtual void leaveEvent(QEvent *) override { close(); } virtual void mouseReleaseEvent(QMouseEvent *) override { close(); } }; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// CoverInfo::CoverInfo(const FileHandle &file) : m_file(file), m_hasCover(false), m_hasAttachedCover(false), m_haveCheckedForCover(false), m_coverKey(CoverManager::NoMatch) { } bool CoverInfo::hasCover() const { if(m_haveCheckedForCover) return m_hasCover || m_hasAttachedCover; 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); // Check if it's embedded in the file itself. m_hasAttachedCover = hasEmbeddedAlbumArt(); 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; } return m_hasCover; } void CoverInfo::clearCover() { m_hasCover = false; m_hasAttachedCover = false; // Re-search for cover since we may still have a different type of cover. m_haveCheckedForCover = false; // We don't need to call removeCover because the CoverManager will // automatically unlink the cover if we were the last track to use it. CoverManager::setIdForTrack(m_file.absFilePath(), CoverManager::NoMatch); m_coverKey = CoverManager::NoMatch; } void CoverInfo::setCover(const QImage &image) { if(image.isNull()) return; m_haveCheckedForCover = true; m_hasCover = true; QPixmap cover = QPixmap::fromImage(image); // 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()); if(m_coverKey != CoverManager::NoMatch) CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); } void CoverInfo::setCoverId(coverKey id) { m_coverKey = id; m_haveCheckedForCover = true; m_hasCover = id != CoverManager::NoMatch; // Inform CoverManager of the change. CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); } 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, // unless the conversion is forced. if(!overwriteExistingCovers && (*it)->file().coverInfo()->coverId() != CoverManager::NoMatch) continue; (*it)->file().coverInfo()->setCoverId(m_coverKey); } } coverKey CoverInfo::coverId() const { if(m_coverKey == CoverManager::NoMatch) m_coverKey = CoverManager::idForTrack(m_file.absFilePath()); return m_coverKey; } QPixmap CoverInfo::pixmap(CoverSize size) const { if(hasCover() && m_coverKey != CoverManager::NoMatch) { return CoverManager::coverFromId(m_coverKey, size == Thumbnail ? CoverManager::Thumbnail : CoverManager::FullSize); } QImage cover; // 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(); } return QPixmap::fromImage(cover); } // If we get here, see if there is an embedded cover. cover = embeddedAlbumArt(); if(!cover.isNull() && size == Thumbnail) cover = scaleCoverToThumbnail(cover); if(cover.isNull()) { return QPixmap(); } return QPixmap::fromImage(cover); } QString CoverInfo::localPathToCover(const QString &fallbackFileName) const { if(m_coverKey != CoverManager::NoMatch) { QString path = CoverManager::coverInfo(m_coverKey).path; 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(); } bool CoverInfo::hasEmbeddedAlbumArt() const { QScopedPointer fileTag( MediaFiles::fileFactoryByType(m_file.absFilePath())); if (TagLib::MPEG::File *mpegFile = dynamic_cast(fileTag.data())) { TagLib::ID3v2::Tag *id3tag = mpegFile->ID3v2Tag(false); if (!id3tag) { qCCritical(JUK_LOG) << m_file.absFilePath() << "seems to have invalid ID3 tag"; return false; } // Look for attached picture frames. TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"]; return !frames.isEmpty(); } else if (TagLib::Ogg::XiphComment *oggTag = dynamic_cast(fileTag->tag())) { return !oggTag->pictureList().isEmpty(); } else if (TagLib::FLAC::File *flacFile = dynamic_cast(fileTag.data())) { // Look if images are embedded. return !flacFile->pictureList().isEmpty(); } #ifdef TAGLIB_WITH_MP4 else if(TagLib::MP4::File *mp4File = dynamic_cast(fileTag.data())) { TagLib::MP4::Tag *tag = mp4File->tag(); if (tag) { TagLib::MP4::ItemListMap &items = tag->itemListMap(); return items.contains("covr"); } } #endif return false; } static QImage embeddedMPEGAlbumArt(TagLib::ID3v2::Tag *id3tag) { if(!id3tag) return QImage(); // Look for attached picture frames. TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"]; if(frames.isEmpty()) return QImage(); // 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). TagLib::ID3v2::AttachedPictureFrame *selectedFrame = 0; if(frames.size() != 1) { TagLib::ID3v2::FrameList::Iterator it = frames.begin(); for(; it != frames.end(); ++it) { // This must be dynamic_cast<>, TagLib will return UnknownFrame in APIC for // encrypted frames. TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast(*it); // Both thumbnail and full size should use FrontCover, as // FileIcon may be too small even for thumbnail. if(frame && frame->type() != TagLib::ID3v2::AttachedPictureFrame::FrontCover) 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) selectedFrame = dynamic_cast(frames.front()); if(!selectedFrame) // Could occur for encrypted picture frames. return QImage(); TagLib::ByteVector picture = selectedFrame->picture(); return QImage::fromData( reinterpret_cast(picture.data()), picture.size()); } static QImage embeddedFLACAlbumArt(const TagLib::List &flacPictures) { if(flacPictures.isEmpty()) { return QImage(); } // Always use first picture - even if multiple are embedded. TagLib::ByteVector coverData = flacPictures[0]->data(); // Will return an image or a null image on error, works either way return QImage::fromData( reinterpret_cast(coverData.data()), coverData.size()); } #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(coverData.data()), coverData.size()); if(!result.isNull()) return result; } // No appropriate image found return QImage(); } #endif void CoverInfo::popup() const { QPixmap image = pixmap(FullSize); QPoint mouse = QCursor::pos(); QScreen *primaryScreen = QApplication::primaryScreen(); QRect desktop = primaryScreen->availableGeometry(); 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. if(x - desktop.x() < desktop.width() / 2) x = (x - desktop.x() < 10) ? desktop.x() : (x - 10); else x = (x - desktop.x() > desktop.width() - 10) ? desktop.width() - width +desktop.x() : (x - width + 10); if(y - desktop.y() < desktop.height() / 2) y = (y - desktop.y() < 10) ? desktop.y() : (y - 10); else y = (y - desktop.y() > desktop.height() - 10) ? desktop.height() - height + desktop.y() : (y - height + 10); new CoverPopup(image, QPoint(x, y)); } QImage CoverInfo::embeddedAlbumArt() const { QScopedPointer fileTag( MediaFiles::fileFactoryByType(m_file.absFilePath())); if (auto *mpegFile = dynamic_cast(fileTag.data())) { return embeddedMPEGAlbumArt(mpegFile->ID3v2Tag(false)); } else if (auto *oggTag = dynamic_cast(fileTag->tag())) { return embeddedFLACAlbumArt(oggTag->pictureList()); } else if (auto *flacFile = dynamic_cast(fileTag.data())) { return embeddedFLACAlbumArt(flacFile->pictureList()); } #ifdef TAGLIB_WITH_MP4 else if(auto *mp4File = dynamic_cast(fileTag.data())) { auto *tag = mp4File->tag(); if (tag) { return embeddedMP4AlbumArt(tag); } } #endif return QImage(); } QImage CoverInfo::scaleCoverToThumbnail(const QImage &image) const { return image.scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation); } // vim: set et sw=4 tw=0 sta: diff --git a/juktag.cpp b/juktag.cpp index 08783a11..ed7bc330 100644 --- a/juktag.cpp +++ b/juktag.cpp @@ -1,247 +1,247 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2009 Michael Pyne * * 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 . */ #include "juktag.h" #include #include -#include +#include //taglib #include #include #include #include "cache.h" #include "mediafiles.h" #include "stringshare.h" #include "juk_debug.h" //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// Tag::Tag(const QString &fileName) : m_fileName(fileName), m_track(0), m_year(0), m_seconds(0), m_bitrate(0), m_isValid(false) { if(fileName.isEmpty()) { qCCritical(JUK_LOG) << "Trying to add empty file"; return; } TagLib::File *file = MediaFiles::fileFactoryByType(fileName); if(file && file->isValid()) { setup(file); delete file; } else { qCCritical(JUK_LOG) << "Couldn't resolve the mime type of \"" << fileName << "\" -- this shouldn't happen."; } } bool Tag::save() const { bool result; TagLib::ID3v2::FrameFactory::instance()->setDefaultTextEncoding(TagLib::String::UTF8); TagLib::File *file = MediaFiles::fileFactoryByType(m_fileName); if(file && !file->readOnly() && file->isValid() && file->tag()) { file->tag()->setTitle(TagLib::String(m_title.toUtf8().constData(), TagLib::String::UTF8)); file->tag()->setArtist(TagLib::String(m_artist.toUtf8().constData(), TagLib::String::UTF8)); file->tag()->setAlbum(TagLib::String(m_album.toUtf8().constData(), TagLib::String::UTF8)); file->tag()->setGenre(TagLib::String(m_genre.toUtf8().constData(), TagLib::String::UTF8)); file->tag()->setComment(TagLib::String(m_comment.toUtf8().constData(), TagLib::String::UTF8)); file->tag()->setTrack(m_track); file->tag()->setYear(m_year); result = file->save(); } else { qCCritical(JUK_LOG) << "Couldn't save file."; result = false; } delete file; return result; } QString Tag::playingString() const { QString str; if(artist().isEmpty()) str = title(); else { str = i18nc("a playing track, %1 is artist, %2 is song title", "%1 - %2", artist(), title()); } return str; } CacheDataStream &Tag::read(CacheDataStream &s) { switch(s.cacheVersion()) { case 1: { qint32 track; qint32 year; qint32 bitrate; qint32 seconds; s >> m_title >> m_artist >> m_album >> m_genre >> track >> year >> m_comment >> bitrate >> m_lengthString >> seconds; m_track = track; m_year = year; m_bitrate = bitrate; m_seconds = seconds; break; } default: { static QString dummyString; static int dummyInt; QString bitrateString; s >> dummyInt >> m_title >> m_artist >> m_album >> m_genre >> dummyInt >> m_track >> dummyString >> m_year >> dummyString >> m_comment >> bitrateString >> m_lengthString >> m_seconds >> dummyString; bool ok; m_bitrate = bitrateString.toInt(&ok); if(!ok) m_bitrate = 0; break; } } minimizeMemoryUsage(); return s; } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// Tag::Tag(const QString &fileName, bool) : m_fileName(fileName), m_track(0), m_year(0), m_seconds(0), m_bitrate(0), m_isValid(true) { } void Tag::setup(TagLib::File *file) { if(!file || !file->tag()) { qCWarning(JUK_LOG) << "Can't setup invalid file" << m_fileName; return; } m_title = TStringToQString(file->tag()->title()).trimmed(); m_artist = TStringToQString(file->tag()->artist()).trimmed(); m_album = TStringToQString(file->tag()->album()).trimmed(); m_genre = TStringToQString(file->tag()->genre()).trimmed(); m_comment = TStringToQString(file->tag()->comment()).trimmed(); m_track = file->tag()->track(); m_year = file->tag()->year(); m_seconds = file->audioProperties()->length(); m_bitrate = file->audioProperties()->bitrate(); const int seconds = m_seconds % 60; const int minutes = (m_seconds - seconds) / 60; m_lengthString = QString::number(minutes) + (seconds >= 10 ? ":" : ":0") + QString::number(seconds); if(m_title.isEmpty()) { int i = m_fileName.lastIndexOf('/'); int j = m_fileName.lastIndexOf('.'); m_title = i > 0 ? m_fileName.mid(i + 1, j - i - 1) : m_fileName; } minimizeMemoryUsage(); m_isValid = true; } void Tag::minimizeMemoryUsage() { // Try to reduce memory usage: share tags that frequently repeat, squeeze others m_title.squeeze(); m_lengthString.squeeze(); m_comment = StringShare::tryShare(m_comment); m_artist = StringShare::tryShare(m_artist); m_album = StringShare::tryShare(m_album); m_genre = StringShare::tryShare(m_genre); } //////////////////////////////////////////////////////////////////////////////// // related functions //////////////////////////////////////////////////////////////////////////////// QDataStream &operator<<(QDataStream &s, const Tag &t) { s << t.title() << t.artist() << t.album() << t.genre() << qint32(t.track()) << qint32(t.year()) << t.comment() << qint32(t.bitrate()) << t.lengthString() << qint32(t.seconds()); return s; } CacheDataStream &operator>>(CacheDataStream &s, Tag &t) { return t.read(s); } // vim: set et sw=4 tw=0 sta: