From 8dd5750efa7565f341e8ed2cabfdc43588e3d441 Mon Sep 17 00:00:00 2001 From: smithjd15 <46389639+smithjd15@users.noreply.github.com> Date: Wed, 13 Feb 2019 23:37:44 -0700 Subject: [PATCH] Improved support for APEv2 tags. (#6280) --- ext/libclementine-tagreader/tagreader.cpp | 215 +++++++++++++++++- .../tagreadermessages.proto | 1 + src/core/song.h | 1 + 3 files changed, 208 insertions(+), 9 deletions(-) diff --git a/ext/libclementine-tagreader/tagreader.cpp b/ext/libclementine-tagreader/tagreader.cpp index d3a89099f..900f042b4 100644 --- a/ext/libclementine-tagreader/tagreader.cpp +++ b/ext/libclementine-tagreader/tagreader.cpp @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -161,6 +162,83 @@ void TagReader::ReadFile(const QString& filename, QString compilation; QString lyrics; + auto parseApeTag = [&](TagLib::APE::Tag* tag) { + const TagLib::APE::ItemListMap& items = tag->itemListMap(); + + // Find album artists + TagLib::APE::ItemListMap::ConstIterator it = items.find("ALBUM ARTIST"); + if (it != items.end()) { + TagLib::StringList album_artists = it->second.toStringList(); + if (!album_artists.isEmpty()) { + Decode(album_artists.front(), nullptr, song->mutable_albumartist()); + } + } + + // Find album cover art + if (items.find("COVER ART (FRONT)") != items.end()) { + song->set_art_automatic(kEmbeddedCover); + } + + if (items.contains("COMPILATION")) { + compilation = TStringToQString( + TagLib::String::number(items["COMPILATION"].toString().toInt())); + } + + if (items.contains("DISC")) { + disc = TStringToQString( + TagLib::String::number(items["DISC"].toString().toInt())); + } + + if (items.contains("FMPS_RATING")) { + float rating = + TStringToQString(items["FMPS_RATING"].toString()).toFloat(); + if (song->rating() <= 0 && rating > 0) { + song->set_rating(rating); + } + } + if (items.contains("FMPS_PLAYCOUNT")) { + int playcount = + TStringToQString(items["FMPS_PLAYCOUNT"].toString()).toFloat(); + if (song->playcount() <= 0 && playcount > 0) { + song->set_playcount(playcount); + } + } + if (items.contains("FMPS_RATING_AMAROK_SCORE")) { + int score = TStringToQString(items["FMPS_RATING_AMAROK_SCORE"].toString()) + .toFloat() * + 100; + if (song->score() <= 0 && score > 0) { + song->set_score(score); + } + } + + if (items.contains("BPM")) { + Decode(items["BPM"].toStringList().toString(", "), nullptr, + song->mutable_performer()); + } + + if (items.contains("PERFORMER")) { + Decode(items["PERFORMER"].toStringList().toString(", "), nullptr, + song->mutable_performer()); + } + + if (items.contains("COMPOSER")) { + Decode(items["COMPOSER"].toStringList().toString(", "), nullptr, + song->mutable_composer()); + } + + if (items.contains("GROUPING")) { + Decode(items["GROUPING"].toStringList().toString(" "), nullptr, + song->mutable_grouping()); + } + + if (items.contains("LYRICS")) { + Decode(items["LYRICS"].toString(), nullptr, song->mutable_lyrics()); + } + + Decode(tag->comment(), nullptr, song->mutable_comment()); + }; + // Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same // way; // apart, so we keep specific behavior for some formats by adding another @@ -351,6 +429,21 @@ void TagReader::ReadFile(const QString& filename, Decode(mp4_tag->comment(), nullptr, song->mutable_comment()); } + } else if (TagLib::APE::File* file = + dynamic_cast(fileref->file())) { + if (file->tag()) { + parseApeTag(file->APETag()); + } + } else if (TagLib::MPC::File* file = + dynamic_cast(fileref->file())) { + if (file->tag()) { + parseApeTag(file->APETag()); + } + } else if (TagLib::WavPack::File* file = + dynamic_cast(fileref->file())) { + if (file->tag()) { + parseApeTag(file->APETag()); + } } #ifdef TAGLIB_WITH_ASF else if (TagLib::ASF::File* file = @@ -668,6 +761,8 @@ pb::tagreader::SongMetadata_Type TagReader::GuessFileType( return pb::tagreader::SongMetadata_Type_TRUEAUDIO; if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_Type_WAVPACK; + if (dynamic_cast(fileref->file())) + return pb::tagreader::SongMetadata_Type_APE; return pb::tagreader::SongMetadata_Type_UNKNOWN; } @@ -691,6 +786,38 @@ bool TagReader::SaveFile(const QString& filename, fileref->tag()->setYear(song.year()); fileref->tag()->setTrack(song.track()); + auto saveApeTag = [&](TagLib::APE::Tag* tag) { + tag->setItem( + "disc", + TagLib::APE::Item("disc", TagLib::String::number( + song.disc() <= 0 - 1 ? 0 : song.disc()))); + tag->setItem("bpm", + TagLib::APE::Item( + "bpm", TagLib::StringList( + song.bpm() <= 0 - 1 + ? "0" + : TagLib::String::number(song.bpm())))); + tag->setItem("composer", + TagLib::APE::Item( + "composer", TagLib::StringList(song.composer().c_str()))); + tag->setItem("grouping", + TagLib::APE::Item( + "grouping", TagLib::StringList(song.grouping().c_str()))); + tag->setItem("performer", + TagLib::APE::Item("performer", TagLib::StringList( + song.performer().c_str()))); + tag->setItem( + "album artist", + TagLib::APE::Item("album artist", + TagLib::StringList(song.albumartist().c_str()))); + tag->setItem("lyrics", + TagLib::APE::Item("lyrics", TagLib::String(song.lyrics()))); + tag->setItem( + "compilation", + TagLib::APE::Item("compilation", + TagLib::StringList(song.compilation() ? "1" : "0"))); + }; + if (TagLib::MPEG::File* file = dynamic_cast(fileref->file())) { TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true); @@ -723,17 +850,15 @@ bool TagReader::SaveFile(const QString& filename, tag->itemListMap()["aART"] = TagLib::StringList(song.albumartist().c_str()); tag->itemListMap()["cpil"] = TagLib::StringList(song.compilation() ? "1" : "0"); + } else if (TagLib::APE::File* file = + dynamic_cast(fileref->file())) { + saveApeTag(file->APETag(true)); + } else if (TagLib::MPC::File* file = + dynamic_cast(fileref->file())) { + saveApeTag(file->APETag(true)); } else if (TagLib::WavPack::File* file = dynamic_cast(fileref->file())) { - TagLib::APE::Tag* tag = file->APETag(true); - if (!tag) return false; - tag->setArtist(StdStringToTaglibString(song.artist())); - tag->setAlbum(StdStringToTaglibString(song.album())); - tag->setTitle(StdStringToTaglibString(song.title())); - tag->setGenre(StdStringToTaglibString(song.genre())); - tag->setComment(StdStringToTaglibString(song.comment())); - tag->setYear(song.year()); - tag->setTrack(song.track()); + saveApeTag(file->APETag(true)); } // Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same @@ -768,6 +893,19 @@ bool TagReader::SaveSongStatisticsToFile( if (!fileref || fileref->isNull()) // The file probably doesn't exist return false; + auto saveApeSongStats = [&](TagLib::APE::Tag* tag) { + tag->setItem( + "FMPS_Rating_Amarok_Score", + TagLib::APE::Item("FMPS_Rating_Amarok_Score", + TagLib::StringList(QStringToTaglibString( + QString::number(song.score() / 100.0))))); + tag->setItem( + "FMPS_PlayCount", + TagLib::APE::Item( + "FMPS_PlayCount", + TagLib::StringList(TagLib::String::number(song.playcount())))); + }; + if (TagLib::MPEG::File* file = dynamic_cast(fileref->file())) { TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true); @@ -806,6 +944,15 @@ bool TagReader::SaveSongStatisticsToFile( QStringToTaglibString(QString::number(song.score() / 100.0))); tag->itemListMap()[kMP4_FMPS_Playcount_ID] = TagLib::StringList(TagLib::String::number(song.playcount())); + } else if (TagLib::APE::File* file = + dynamic_cast(fileref->file())) { + saveApeSongStats(file->APETag(true)); + } else if (TagLib::MPC::File* file = + dynamic_cast(fileref->file())) { + saveApeSongStats(file->APETag(true)); + } else if (TagLib::WavPack::File* file = + dynamic_cast(fileref->file())) { + saveApeSongStats(file->APETag(true)); } else { // Nothing to save: stop now return true; @@ -841,6 +988,13 @@ bool TagReader::SaveSongRatingToFile( if (!fileref || fileref->isNull()) // The file probably doesn't exist return false; + auto saveApeSongRating = [&](TagLib::APE::Tag* tag) { + tag->setItem("FMPS_Rating", + TagLib::APE::Item("FMPS_Rating", + TagLib::StringList(QStringToTaglibString( + QString::number(song.rating()))))); + }; + if (TagLib::MPEG::File* file = dynamic_cast(fileref->file())) { TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true); @@ -873,6 +1027,15 @@ bool TagReader::SaveSongRatingToFile( TagLib::MP4::Tag* tag = file->tag(); tag->itemListMap()[kMP4_FMPS_Rating_ID] = TagLib::StringList( QStringToTaglibString(QString::number(song.rating()))); + } else if (TagLib::APE::File* file = + dynamic_cast(fileref->file())) { + saveApeSongRating(file->APETag(true)); + } else if (TagLib::MPC::File* file = + dynamic_cast(fileref->file())) { + saveApeSongRating(file->APETag(true)); + } else if (TagLib::WavPack::File* file = + dynamic_cast(fileref->file())) { + saveApeSongRating(file->APETag(true)); } else { // Nothing to save: stop now return true; @@ -1110,6 +1273,40 @@ QByteArray TagReader::LoadEmbeddedArt(const QString& filename) const { } } + // APE formats + auto apeTagCover = [&](TagLib::APE::Tag* tag) { + QByteArray cover; + const TagLib::APE::ItemListMap& items = tag->itemListMap(); + TagLib::APE::ItemListMap::ConstIterator it = + items.find("COVER ART (FRONT)"); + if (it != items.end()) { + TagLib::ByteVector data = it->second.binaryData(); + + int pos = data.find('\0') + 1; + if ((pos > 0) && (pos < data.size())) { + cover = QByteArray(data.data() + pos, data.size() - pos); + } + } + + return cover; + }; + + TagLib::APE::File* ape_file = dynamic_cast(ref.file()); + if (ape_file) { + return apeTagCover(ape_file->APETag()); + } + + TagLib::MPC::File* mpc_file = dynamic_cast(ref.file()); + if (mpc_file) { + return apeTagCover(mpc_file->APETag()); + } + + TagLib::WavPack::File* wavPack_file = + dynamic_cast(ref.file()); + if (wavPack_file) { + return apeTagCover(wavPack_file->APETag()); + } + return QByteArray(); } diff --git a/ext/libclementine-tagreader/tagreadermessages.proto b/ext/libclementine-tagreader/tagreadermessages.proto index b1b8e3ea7..531efb717 100644 --- a/ext/libclementine-tagreader/tagreadermessages.proto +++ b/ext/libclementine-tagreader/tagreadermessages.proto @@ -21,6 +21,7 @@ message SongMetadata { WAVPACK = 14; SPC = 15; VGM = 16; + APE = 17; STREAM = 99; } diff --git a/src/core/song.h b/src/core/song.h index bc109a622..e2fd9e8c2 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -108,6 +108,7 @@ class Song { Type_WavPack = 14, Type_Spc = 15, Type_VGM = 16, + Type_APE = 17, Type_Stream = 99, }; static QString TextForFiletype(FileType type);