From 7ae0f5e24698e5e968f99e612dab21a6804a73b2 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Fri, 22 Mar 2019 23:13:49 +0100 Subject: [PATCH] Add better support for APE tags --- ext/libstrawberry-tagreader/tagreader.cpp | 261 +++++++++++++++------- ext/libstrawberry-tagreader/tagreader.h | 14 +- 2 files changed, 194 insertions(+), 81 deletions(-) diff --git a/ext/libstrawberry-tagreader/tagreader.cpp b/ext/libstrawberry-tagreader/tagreader.cpp index a4b7d86eb..2be659a3d 100644 --- a/ext/libstrawberry-tagreader/tagreader.cpp +++ b/ext/libstrawberry-tagreader/tagreader.cpp @@ -122,18 +122,42 @@ TagLib::String QStringToTaglibString(const QString &s) { } namespace { -// Tags containing the year the album was originally released (in contrast to -// other tags that contain the release year of the current edition) +// Tags containing the year the album was originally released (in contrast to other tags that contain the release year of the current edition) const char *kMP4_OriginalYear_ID = "----:com.apple.iTunes:ORIGINAL YEAR"; const char *kASF_OriginalDate_ID = "WM/OriginalReleaseTime"; const char *kASF_OriginalYear_ID = "WM/OriginalReleaseYear"; } -TagReader::TagReader() - : factory_(new TagLibFileRefFactory), - network_(new QNetworkAccessManager), - kEmbeddedCover("(embedded)") {} +TagReader::TagReader() : + factory_(new TagLibFileRefFactory), + network_(new QNetworkAccessManager), + kEmbeddedCover("(embedded)") {} + +pb::tagreader::SongMetadata_FileType TagReader::GuessFileType(TagLib::FileRef *fileref) const { + + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_WAV; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_FLAC; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_WAVPACK; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_OGGFLAC; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_OGGVORBIS; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_OGGOPUS; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_OGGSPEEX; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_MPEG; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_MP4; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_ASF; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_AIFF; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_MPC; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_TRUEAUDIO; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_APE; +#ifdef HAVE_TAGLIB_DSFFILE + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_DSF; + if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_DSDIFF; +#endif + + return pb::tagreader::SongMetadata_FileType_UNKNOWN; + +} void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *song) const { @@ -190,7 +214,7 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s song->set_bitdepth(file->audioProperties()->bitsPerSample()); - if ( file->xiphComment() ) { + if (file->xiphComment()) { ParseOggTag(file->xiphComment()->fieldListMap(), nullptr, &disc, &compilation, song); if (!file->pictureList().isEmpty()) { song->set_art_automatic(kEmbeddedCover); @@ -201,7 +225,18 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s else if (TagLib::WavPack::File *file = dynamic_cast(fileref->file())) { song->set_bitdepth(file->audioProperties()->bitsPerSample()); - //if (tag) Decode(tag->comment(), nullptr, song->mutable_comment()); + if (file->tag()) { + ParseAPETag(file->APETag()->itemListMap(), nullptr, &disc, &compilation, song); + } + if (tag) Decode(tag->comment(), nullptr, song->mutable_comment()); + } + + else if (TagLib::APE::File *file = dynamic_cast(fileref->file())) { + if (file->tag()) { + ParseAPETag(file->APETag()->itemListMap(), nullptr, &disc, &compilation, song); + } + song->set_bitdepth(file->audioProperties()->bitsPerSample()); + if (tag) Decode(tag->comment(), nullptr, song->mutable_comment()); } else if (TagLib::MPEG::File *file = dynamic_cast(fileref->file())) { @@ -322,8 +357,11 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s } } - else if (TagLib::APE::File *file = dynamic_cast(fileref->file())) { - song->set_bitdepth(file->audioProperties()->bitsPerSample()); + else if (TagLib::MPC::File* file = dynamic_cast(fileref->file())) { + if (file->tag()) { + ParseAPETag(file->APETag()->itemListMap(), nullptr, &disc, &compilation, song); + } + if (tag) Decode(tag->comment(), nullptr, song->mutable_comment()); } else if (tag) { @@ -366,7 +404,6 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s } - void TagReader::Decode(const TagLib::String &tag, const QTextCodec *codec, std::string *output) { QString tmp; @@ -395,6 +432,74 @@ void TagReader::Decode(const QString &tag, const QTextCodec *codec, std::string } +void TagReader::ParseOggTag(const TagLib::Ogg::FieldListMap &map, const QTextCodec *codec, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const { + + if (!map["COMPOSER"].isEmpty()) Decode(map["COMPOSER"].front(), codec, song->mutable_composer()); + if (!map["PERFORMER"].isEmpty()) Decode(map["PERFORMER"].front(), codec, song->mutable_performer()); + if (!map["CONTENT GROUP"].isEmpty()) Decode(map["CONTENT GROUP"].front(), codec, song->mutable_grouping()); + + if (!map["ALBUMARTIST"].isEmpty()) Decode(map["ALBUMARTIST"].front(), codec, song->mutable_albumartist()); + else if (!map["ALBUM ARTIST"].isEmpty()) Decode(map["ALBUM ARTIST"].front(), codec, song->mutable_albumartist()); + + if (!map["ORIGINALDATE"].isEmpty()) song->set_originalyear(TStringToQString(map["ORIGINALDATE"].front()).left(4).toInt()); + else if (!map["ORIGINALYEAR"].isEmpty()) song->set_originalyear(TStringToQString(map["ORIGINALYEAR"].front()).toInt()); + + if (!map["DISCNUMBER"].isEmpty()) *disc = TStringToQString( map["DISCNUMBER"].front() ).trimmed(); + if (!map["COMPILATION"].isEmpty()) *compilation = TStringToQString( map["COMPILATION"].front() ).trimmed(); + if (!map["COVERART"].isEmpty()) song->set_art_automatic(kEmbeddedCover); + if (!map["METADATA_BLOCK_PICTURE"].isEmpty()) song->set_art_automatic(kEmbeddedCover); + + if (!map["FMPS_PLAYCOUNT"].isEmpty() && song->playcount() <= 0) song->set_playcount(TStringToQString( map["FMPS_PLAYCOUNT"].front() ).trimmed().toFloat()); + + if (!map["LYRICS"].isEmpty()) Decode(map["LYRICS"].front(), codec, song->mutable_lyrics()); + else if (!map["UNSYNCEDLYRICS"].isEmpty()) Decode(map["UNSYNCEDLYRICS"].front(), codec, song->mutable_lyrics()); + +} + +void TagReader::ParseAPETag(const TagLib::APE::ItemListMap &map, const QTextCodec *codec, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const { + + TagLib::APE::ItemListMap::ConstIterator it = map.find("ALBUM ARTIST"); + if (it != map.end()) { + TagLib::StringList album_artists = it->second.toStringList(); + if (!album_artists.isEmpty()) { + Decode(album_artists.front(), nullptr, song->mutable_albumartist()); + } + } + + if (map.find("COVER ART (FRONT)") != map.end()) song->set_art_automatic(kEmbeddedCover); + if (map.contains("COMPILATION")) { + *compilation = TStringToQString(TagLib::String::number(map["COMPILATION"].toString().toInt())); + } + + if (map.contains("DISC")) { + *disc = TStringToQString(TagLib::String::number(map["DISC"].toString().toInt())); + } + + if (map.contains("PERFORMER")) { + Decode(map["PERFORMER"].toStringList().toString(", "), nullptr, song->mutable_performer()); + } + + if (map.contains("COMPOSER")) { + Decode(map["COMPOSER"].toStringList().toString(", "), nullptr, song->mutable_composer()); + } + + if (map.contains("GROUPING")) { + Decode(map["GROUPING"].toStringList().toString(" "), nullptr, song->mutable_grouping()); + } + + if (map.contains("LYRICS")) { + Decode(map["LYRICS"].toString(), nullptr, song->mutable_lyrics()); + } + + if (map.contains("FMPS_PLAYCOUNT")) { + int playcount = TStringToQString(map["FMPS_PLAYCOUNT"].toString()).toFloat(); + if (song->playcount() <= 0 && playcount > 0) { + song->set_playcount(playcount); + } + } + +} + void TagReader::ParseFMPSFrame(const QString &name, const QString &value, pb::tagreader::SongMetadata *song) const { qLog(Debug) << "Parsing FMPSFrame" << name << ", " << value; @@ -422,38 +527,6 @@ void TagReader::ParseFMPSFrame(const QString &name, const QString &value, pb::ta } -void TagReader::ParseOggTag(const TagLib::Ogg::FieldListMap &map, const QTextCodec *codec, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const { - - if (!map["COMPOSER"].isEmpty()) Decode(map["COMPOSER"].front(), codec, song->mutable_composer()); - if (!map["PERFORMER"].isEmpty()) Decode(map["PERFORMER"].front(), codec, song->mutable_performer()); - if (!map["CONTENT GROUP"].isEmpty()) Decode(map["CONTENT GROUP"].front(), codec, song->mutable_grouping()); - - if (!map["ALBUMARTIST"].isEmpty()) { - Decode(map["ALBUMARTIST"].front(), codec, song->mutable_albumartist()); - } - else if (!map["ALBUM ARTIST"].isEmpty()) { - Decode(map["ALBUM ARTIST"].front(), codec, song->mutable_albumartist()); - } - - if (!map["ORIGINALDATE"].isEmpty()) - song->set_originalyear(TStringToQString(map["ORIGINALDATE"].front()).left(4).toInt()); - else if (!map["ORIGINALYEAR"].isEmpty()) - song->set_originalyear(TStringToQString(map["ORIGINALYEAR"].front()).toInt()); - - if (!map["DISCNUMBER"].isEmpty()) *disc = TStringToQString( map["DISCNUMBER"].front() ).trimmed(); - if (!map["COMPILATION"].isEmpty()) *compilation = TStringToQString( map["COMPILATION"].front() ).trimmed(); - if (!map["COVERART"].isEmpty()) song->set_art_automatic(kEmbeddedCover); - if (!map["METADATA_BLOCK_PICTURE"].isEmpty()) song->set_art_automatic(kEmbeddedCover); - - if (!map["FMPS_PLAYCOUNT"].isEmpty() && song->playcount() <= 0) song->set_playcount(TStringToQString( map["FMPS_PLAYCOUNT"].front() ).trimmed().toFloat()); - - if (!map["LYRICS"].isEmpty()) - Decode(map["LYRICS"].front(), codec, song->mutable_lyrics()); - else if (!map["UNSYNCEDLYRICS"].isEmpty()) - Decode(map["UNSYNCEDLYRICS"].front(), codec, song->mutable_lyrics()); - -} - void TagReader::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, const pb::tagreader::SongMetadata &song) const { vorbis_comments->addField("COMPOSER", StdStringToTaglibString(song.composer()), true); @@ -472,31 +545,6 @@ void TagReader::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, con } -pb::tagreader::SongMetadata_FileType TagReader::GuessFileType(TagLib::FileRef *fileref) const { - - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_WAV; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_FLAC; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_WAVPACK; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_OGGFLAC; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_OGGVORBIS; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_OGGOPUS; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_OGGSPEEX; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_MPEG; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_MP4; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_ASF; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_AIFF; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_MPC; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_TRUEAUDIO; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_APE; -#ifdef HAVE_TAGLIB_DSFFILE - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_DSF; - if (dynamic_cast(fileref->file())) return pb::tagreader::SongMetadata_FileType_DSDIFF; -#endif - - return pb::tagreader::SongMetadata_FileType_UNKNOWN; - -} - bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetadata &song) const { if (filename.isNull() || filename.isEmpty()) return false; @@ -516,17 +564,25 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad TagLib::Ogg::XiphComment *tag = file->xiphComment(); SetVorbisComments(tag, song); } + 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(tag, song); } + + else if (TagLib::APE::File *file = dynamic_cast(fileref->file())) { + TagLib::APE::Tag *tag = file->APETag(true); + if (!tag) return false; + SaveAPETag(tag, song); + } + + else if (TagLib::MPC::File *file = dynamic_cast(fileref->file())) { + TagLib::APE::Tag *tag = file->APETag(true); + if (!tag) return false; + SaveAPETag(tag, song); + } + else if (TagLib::MPEG::File *file = dynamic_cast(fileref->file())) { TagLib::ID3v2::Tag *tag = file->ID3v2Tag(true); if (!tag) return false; @@ -539,6 +595,7 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad SetTextFrame("TCMP", std::string(song.compilation() ? "1" : "0"), tag); SetUnsyncLyricsFrame(song.lyrics(), tag); } + else if (TagLib::MP4::File *file = dynamic_cast(fileref->file())) { TagLib::MP4::Tag *tag = file->tag(); tag->itemListMap()["disk"] = TagLib::MP4::Item(song.disc() <= 0 -1 ? 0 : song.disc(), 0); @@ -557,8 +614,7 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad bool ret = fileref->save(); #ifdef Q_OS_LINUX if (ret) { - // Linux: inotify doesn't seem to notice the change to the file unless we - // change the timestamps as well. (this is what touch does) + // Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does) utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0); } #endif // Q_OS_LINUX @@ -566,6 +622,18 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad return ret; } +void TagReader::SaveAPETag(TagLib::APE::Tag *tag, const pb::tagreader::SongMetadata &song) const { + + tag->setItem("album artist", TagLib::APE::Item("album artist", TagLib::StringList(song.albumartist().c_str()))); + tag->setItem("disc", TagLib::APE::Item("disc", TagLib::String::number(song.disc() <= 0 - 1 ? 0 : song.disc()))); + 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("lyrics", TagLib::APE::Item("lyrics", TagLib::String(song.lyrics()))); + tag->setItem("compilation", TagLib::APE::Item("compilation", TagLib::StringList(song.compilation() ? "1" : "0"))); + +} + void TagReader::SetUserTextFrame(const QString &description, const QString &value, TagLib::ID3v2::Tag *tag) const { const QByteArray descr_utf8(description.toUtf8()); @@ -666,6 +734,27 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const { } } + // WavPack + + TagLib::WavPack::File *wavpack_file = dynamic_cast(ref.file()); + if (wavpack_file) { + return LoadEmbeddedAPEArt(wavpack_file->APETag()->itemListMap()); + } + + // APE + + TagLib::APE::File *ape_file = dynamic_cast(ref.file()); + if (ape_file) { + return LoadEmbeddedAPEArt(ape_file->APETag()->itemListMap()); + } + + // MPC + + TagLib::MPC::File *mpc_file = dynamic_cast(ref.file()); + if (mpc_file) { + return LoadEmbeddedAPEArt(mpc_file->APETag()->itemListMap()); + } + // Ogg Vorbis / Speex TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(ref.file()->tag()); if (xiph_comment) { @@ -724,6 +813,24 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const { } +QByteArray TagReader::LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) const { + + QByteArray ret; + + TagLib::APE::ItemListMap::ConstIterator it = map.find("COVER ART (FRONT)"); + if (it != map.end()) { + TagLib::ByteVector data = it->second.binaryData(); + + int pos = data.find('\0') + 1; + if ((pos > 0) && (pos < data.size())) { + ret = QByteArray(data.data() + pos, data.size() - pos); + } + } + + return ret; + +} + void TagReader::SetUnsyncLyricsFrame(const std::string& value, TagLib::ID3v2::Tag* tag) const { TagLib::ByteVector id_vector("USLT"); diff --git a/ext/libstrawberry-tagreader/tagreader.h b/ext/libstrawberry-tagreader/tagreader.h index 354a0f0e3..5fbbe3fbf 100644 --- a/ext/libstrawberry-tagreader/tagreader.h +++ b/ext/libstrawberry-tagreader/tagreader.h @@ -29,6 +29,8 @@ #include #include +#include +#include #include "tagreadermessages.pb.h" @@ -52,20 +54,24 @@ class TagReader { public: TagReader(); + pb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const; + void ReadFile(const QString &filename, pb::tagreader::SongMetadata *song) const; bool SaveFile(const QString &filename, const pb::tagreader::SongMetadata &song) const; bool IsMediaFile(const QString &filename) const; QByteArray LoadEmbeddedArt(const QString &filename) const; + QByteArray LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) const; static void Decode(const TagLib::String& tag, const QTextCodec *codec, std::string *output); static void Decode(const QString &tag, const QTextCodec *codec, std::string *output); - void ParseFMPSFrame(const QString &name, const QString &value, pb::tagreader::SongMetadata *song) const; void ParseOggTag(const TagLib::Ogg::FieldListMap &map, const QTextCodec *codec, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const; - void SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, const pb::tagreader::SongMetadata &song) const; + void ParseAPETag(const TagLib::APE::ItemListMap &map, const QTextCodec *codec, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const; + void ParseFMPSFrame(const QString &name, const QString &value, pb::tagreader::SongMetadata *song) const; - pb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const; + void SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, const pb::tagreader::SongMetadata &song) const; + void SaveAPETag(TagLib::APE::Tag *tag, const pb::tagreader::SongMetadata &song) const; void SetUserTextFrame(const QString &description, const QString &value, TagLib::ID3v2::Tag *tag) const; void SetUserTextFrame(const std::string &description, const std::string& value, TagLib::ID3v2::Tag *tag) const; @@ -74,7 +80,7 @@ class TagReader { void SetTextFrame(const char *id, const std::string &value, TagLib::ID3v2::Tag *tag) const; void SetUnsyncLyricsFrame(const std::string& value, TagLib::ID3v2::Tag* tag) const; -private: + private: FileRefFactory *factory_; QNetworkAccessManager *network_;