From c7db5fc61acf0e6e38f6ba6d8855fe3880376076 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 17 Jan 2025 18:03:16 +0100 Subject: [PATCH] Added support for explcit tags (rtng, ITUNESADVISORY) --- src/libs/database/impl/Migration.cpp | 12 +++++++- src/libs/database/impl/Release.cpp | 13 ++++++++ .../database/include/database/Release.hpp | 1 + src/libs/database/include/database/Track.hpp | 11 +++---- src/libs/database/include/database/Types.hpp | 8 +++++ src/libs/metadata/impl/AvFormatTagReader.cpp | 1 + src/libs/metadata/impl/ITagReader.hpp | 3 +- src/libs/metadata/impl/Parser.cpp | 20 +++++++++++++ src/libs/metadata/impl/TagLibTagReader.cpp | 17 ++++++++--- src/libs/metadata/include/metadata/Types.hpp | 7 +++++ src/libs/metadata/test/Parser.cpp | 30 +++++++++++++++++++ .../impl/scanners/AudioFileScanner.cpp | 20 +++++++++++++ src/libs/subsonic/impl/responses/Album.cpp | 13 ++++++++ src/libs/subsonic/impl/responses/Song.cpp | 17 +++++++++++ src/tools/metadata/LmsMetadata.cpp | 21 +++++++++++++ 15 files changed, 181 insertions(+), 13 deletions(-) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 9efc0766..0df933eb 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -35,7 +35,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 78 }; + static constexpr Version LMS_DATABASE_VERSION{ 79 }; } VersionInfo::VersionInfo() @@ -1035,6 +1035,15 @@ FROM tracklist)"); utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN skip_single_release_playlists BOOLEAN NOT NULL DEFAULT(FALSE)"); } + void migrateFromV78(Session& session) + { + // added advisory tag support + utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD COLUMN advisory INTEGER NOT NULL DEFAULT(0)"); // 0 means unset + + // Just increment the scan version of the settings to make the next scan rescan everything + utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1"); + } + bool doDbMigration(Session& session) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -1089,6 +1098,7 @@ FROM tracklist)"); { 75, migrateFromV75 }, { 76, migrateFromV76 }, { 77, migrateFromV77 }, + { 78, migrateFromV78 }, }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 4ab4437a..4c40b506 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -666,6 +666,19 @@ namespace lms::db utils::forEachQueryResult(query, _func); } + core::EnumSet Release::getAdvisories() const + { + core::EnumSet res; + + auto query{ session()->query("SELECT DISTINCT advisory FROM track t").where("t.release_id = ?").bind(getId()) }; + + utils::forEachQueryResult(query, [&](Advisory advisory) { + res.insert(advisory); + }); + + return res; + } + std::chrono::milliseconds Release::getDuration() const { assert(session()); diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index bebe3d1b..a381c2cb 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -252,6 +252,7 @@ namespace lms::db std::vector getLabelNames() const; std::vector getReleaseTypeNames() const; void visitLabels(const std::function& _func) const; + core::EnumSet getAdvisories() const; std::string_view getBarcode() const { return _barcode; } ObjectPtr getImage() const; diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 409562d0..6594f17b 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -193,12 +193,6 @@ namespace lms::db } }; - struct PathResult - { - TrackId trackId; - std::filesystem::path path; - }; - Track() = default; // Find utility functions @@ -244,6 +238,7 @@ namespace lms::db void setRecordingMBID(const std::optional& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } void setCopyright(std::string_view copyright); void setCopyrightURL(std::string_view copyrightURL); + void setAdvisory(Advisory advisory) { _advisory = advisory; } void setTrackReplayGain(std::optional replayGain) { _trackReplayGain = replayGain; } void setReleaseReplayGain(std::optional replayGain) { _releaseReplayGain = replayGain; } // may be by disc! void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } @@ -285,6 +280,7 @@ namespace lms::db std::optional getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); } std::optional getCopyright() const; std::optional getCopyrightURL() const; + Advisory getAdvisory() const { return _advisory; } std::optional getTrackReplayGain() const { return _trackReplayGain; } std::optional getReleaseReplayGain() const { return _releaseReplayGain; } std::string_view getArtistDisplayName() const { return _artistDisplayName; } @@ -333,6 +329,7 @@ namespace lms::db Wt::Dbo::field(a, _recordingMBID, "recording_mbid"); Wt::Dbo::field(a, _copyright, "copyright"); Wt::Dbo::field(a, _copyrightURL, "copyright_url"); + Wt::Dbo::field(a, _advisory, "advisory"); Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?) Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); @@ -381,11 +378,11 @@ namespace lms::db std::string _recordingMBID; std::string _copyright; std::string _copyrightURL; + Advisory _advisory{ Advisory::UnSet }; std::optional _trackReplayGain; std::optional _releaseReplayGain; std::string _artistDisplayName; std::string _comment; - Wt::Dbo::ptr _release; Wt::Dbo::ptr _mediaLibrary; Wt::Dbo::ptr _directory; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 851e88d8..7330c80a 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -256,4 +256,12 @@ namespace lms::db PlayList = 0, // user controlled playlists Internal = 1, // internal usage (current playqueue, history, ...) }; + + enum class Advisory + { + UnSet = 0, + Unknown = 1, + Clean = 2, + Explicit = 3, + }; } // namespace lms::db diff --git a/src/libs/metadata/impl/AvFormatTagReader.cpp b/src/libs/metadata/impl/AvFormatTagReader.cpp index 7891890a..a7a330a7 100644 --- a/src/libs/metadata/impl/AvFormatTagReader.cpp +++ b/src/libs/metadata/impl/AvFormatTagReader.cpp @@ -32,6 +32,7 @@ namespace lms::metadata // Mapping to internal avformat names and/or common alternative custom names static const std::unordered_map> tagMapping{ { TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } }, + { TagType::Advisory, { "ITUNESADVISORY" } }, { TagType::Album, { "ALBUM", "TALB", "WM/ALBUMTITLE" } }, { TagType::AlbumArtist, { "ALBUMARTIST", "ALBUM_ARTIST" } }, { TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT", "TSO2" } }, diff --git a/src/libs/metadata/impl/ITagReader.hpp b/src/libs/metadata/impl/ITagReader.hpp index 5cad170a..55c37e8c 100644 --- a/src/libs/metadata/impl/ITagReader.hpp +++ b/src/libs/metadata/impl/ITagReader.hpp @@ -26,12 +26,13 @@ namespace lms::metadata { - // using picard internal names + // prefer using picard internal names // see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html enum class TagType { AcoustID, AcoustIDFingerprint, + Advisory, // non standard Album, AlbumArtist, AlbumArtists, // non standard diff --git a/src/libs/metadata/impl/Parser.cpp b/src/libs/metadata/impl/Parser.cpp index 13926c8f..153652ea 100644 --- a/src/libs/metadata/impl/Parser.cpp +++ b/src/libs/metadata/impl/Parser.cpp @@ -246,6 +246,25 @@ namespace lms::metadata return artistDisplayName; } + + std::optional getAdvisory(const ITagReader& tagReader) + { + if (const auto value{ getTagValueAs(tagReader, TagType::Advisory) }) + { + switch (*value) + { + case 1: + case 4: + return Track::Advisory::Explicit; + case 2: + return Track::Advisory::Clean; + case 0: + return Track::Advisory::Unknown; + } + } + + return std::nullopt; + } } // namespace std::unique_ptr createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle) @@ -370,6 +389,7 @@ namespace lms::metadata track.originalYear = utils::parseYear(*dateStr); } + track.advisory = getAdvisory(tagReader); track.lyrics = getLyrics(tagReader); // no custom delimiter on lyrics track.comments = getTagValuesAs(tagReader, TagType::Comment, {} /* no custom delimiter on comments */); track.copyright = getTagValueAs(tagReader, TagType::Copyright).value_or(""); diff --git a/src/libs/metadata/impl/TagLibTagReader.cpp b/src/libs/metadata/impl/TagLibTagReader.cpp index 2fffb777..9fffd6d3 100644 --- a/src/libs/metadata/impl/TagLibTagReader.cpp +++ b/src/libs/metadata/impl/TagLibTagReader.cpp @@ -59,6 +59,7 @@ namespace lms::metadata // Mapping to internal taglib names and/or common alternative custom names const std::unordered_map> tagMapping{ { TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } }, + { TagType::Advisory, { "ITUNESADVISORY" } }, { TagType::Album, { "ALBUM" } }, { TagType::AlbumArtist, { "ALBUMARTIST" } }, { TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT" } }, @@ -319,10 +320,18 @@ namespace lms::metadata // MP4 else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.file()) }) { - TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") }; - TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() }; - if (!coverArtList.isEmpty()) - _hasEmbeddedCover = true; + if (const TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") }; coverItem.isValid()) + { + if (coverItem.type() == TagLib::MP4::Item::Type::CoverArtList) + _hasEmbeddedCover = true; + } + + // Taglib does not expose rtng in properties + if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid()) + { + if (rtngItem.type() == TagLib::MP4::Item::Type::Byte) + _propertyMap["ITUNESADVISORY"] = TagLib::String{ std::to_string(rtngItem.toByte()) }; + } if (!_propertyMap.contains("ORIGINALDATE")) { diff --git a/src/libs/metadata/include/metadata/Types.hpp b/src/libs/metadata/include/metadata/Types.hpp index 1674b927..6c044210 100644 --- a/src/libs/metadata/include/metadata/Types.hpp +++ b/src/libs/metadata/include/metadata/Types.hpp @@ -102,6 +102,12 @@ namespace lms::metadata struct Track { + enum class Advisory + { + Unknown, + Explicit, + Clean, + }; AudioProperties audioProperties; std::optional mbid; std::optional recordingMBID; @@ -117,6 +123,7 @@ namespace lms::metadata Wt::WDate date; std::optional originalYear{}; Wt::WDate originalDate; + std::optional advisory; bool hasCover{}; std::optional acoustID; std::string copyright; diff --git a/src/libs/metadata/test/Parser.cpp b/src/libs/metadata/test/Parser.cpp index 3345e604..6ee0d2af 100644 --- a/src/libs/metadata/test/Parser.cpp +++ b/src/libs/metadata/test/Parser.cpp @@ -33,6 +33,7 @@ namespace lms::metadata TestTagReader testTags{ { { TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } }, + { TagType::Advisory, { "2" } }, { TagType::Album, { "MyAlbum" } }, { TagType::AlbumSortOrder, { "MyAlbumSortName" } }, { TagType::Artist, { "MyArtist1 & MyArtist2" } }, @@ -99,6 +100,8 @@ namespace lms::metadata } EXPECT_EQ(track->acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418")); + ASSERT_TRUE(track->advisory.has_value()); + EXPECT_EQ(track->advisory.value(), Track::Advisory::Clean); EXPECT_EQ(track->artistDisplayName, "MyArtist1 & MyArtist2"); ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "MyArtist1"); @@ -581,4 +584,31 @@ namespace lms::metadata EXPECT_EQ(track->artists[1].mbid, std::nullopt); EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name } + + TEST(Parser, advisory) + { + auto doTest = [](std::string_view value, std::optional expectedValue) { + const TestTagReader testTags{ + { + { TagType::Advisory, { value } }, + } + }; + + Parser parser; + std::unique_ptr track{ Parser{}.parse(testTags) }; + + ASSERT_EQ(track->advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'"; + if (track->advisory.has_value()) + { + EXPECT_EQ(track->advisory.value(), expectedValue); + } + }; + + doTest("0", Track::Advisory::Unknown); + doTest("1", Track::Advisory::Explicit); + doTest("4", Track::Advisory::Explicit); + doTest("2", Track::Advisory::Clean); + doTest("", std::nullopt); + doTest("3", std::nullopt); + } } // namespace lms::metadata diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp index c9104a0d..7d6c7b69 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp @@ -35,6 +35,7 @@ #include "database/TrackArtistLink.hpp" #include "database/TrackFeatures.hpp" #include "database/TrackLyrics.hpp" +#include "database/Types.hpp" #include "metadata/Exception.hpp" #include "metadata/IParser.hpp" @@ -309,6 +310,24 @@ namespace lms::scanner return lyrics; } + db::Advisory getAdvisory(std::optional advisory) + { + if (!advisory) + return db::Advisory::UnSet; + + switch (advisory.value()) + { + case metadata::Track::Advisory::Clean: + return db::Advisory::Clean; + case metadata::Track::Advisory::Explicit: + return db::Advisory::Explicit; + case metadata::Track::Advisory::Unknown: + return db::Advisory::Unknown; + } + + return db::Advisory::UnSet; + } + class AudioFileScanOperation : public IFileScanOperation { public: @@ -552,6 +571,7 @@ namespace lms::scanner track.modify()->setHasCover(_parsedTrack->hasCover); track.modify()->setCopyright(_parsedTrack->copyright); track.modify()->setCopyrightURL(_parsedTrack->copyrightURL); + track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory)); track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now track.modify()->setTrackReplayGain(_parsedTrack->replayGain); track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName); diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 3ace6ba1..cb790e23 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -28,6 +28,7 @@ #include "database/Image.hpp" #include "database/Release.hpp" #include "database/Track.hpp" +#include "database/Types.hpp" #include "database/User.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -223,6 +224,18 @@ namespace lms::api::subsonic albumNode.addArrayChild("recordLabels", createRecordLabel(label)); }); + auto advisoryToExplicitStatus = [&](const core::EnumSet advisories) -> std::string_view { + if (advisories.contains(db::Advisory::Explicit)) + return "explicit"; + + if (advisories.contains(db::Advisory::Clean)) + return "clean"; + + return ""; + }; + + albumNode.setAttribute("explicitStatus", advisoryToExplicitStatus(release->getAdvisories())); + return albumNode; } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index bc74729a..bdc8b3a6 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -32,6 +32,7 @@ #include "database/Release.hpp" #include "database/Track.hpp" #include "database/TrackArtistLink.hpp" +#include "database/Types.hpp" #include "database/User.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -227,6 +228,22 @@ namespace lms::api::subsonic for (const auto& genre : genres) trackResponse.addArrayChild("genres", createItemGenreNode(genre->getName())); + auto advisoryToExplicitStatus = [](db::Advisory advisory) -> std::string_view { + switch (advisory) + { + case db::Advisory::Clean: + return "clean"; + case db::Advisory::Explicit: + return "expicit"; + case db::Advisory::Unknown: + case db::Advisory::UnSet: + break; + } + + return ""; + }; + trackResponse.setAttribute("explicitStatus", advisoryToExplicitStatus(track->getAdvisory())); + trackResponse.addChild("replayGain", createReplayGainNode(track)); return trackResponse; diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 75bae6cd..72a7a387 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -135,6 +135,24 @@ namespace lms::metadata return os; } + std::ostream& operator<<(std::ostream& os, Track::Advisory advisory) + { + switch (advisory) + { + case Track::Advisory::Clean: + os << "clean"; + break; + case Track::Advisory::Explicit: + os << "Explicit"; + break; + case Track::Advisory::Unknown: + os << "Unknown"; + break; + } + + return os; + } + void parse(IParser& parser, const std::filesystem::path& file) { using namespace metadata; @@ -250,6 +268,9 @@ namespace lms::metadata if (!track->copyrightURL.empty()) std::cout << "CopyrightURL: " << track->copyrightURL << std::endl; + if (track->advisory) + std::cout << "Advisory: " << *track->advisory << std::endl; + std::cout << std::endl; } } // namespace lms::metadata