Added support for explcit tags (rtng, ITUNESADVISORY)

This commit is contained in:
emeric
2025-01-17 18:03:16 +01:00
parent dd93a3f6c7
commit c7db5fc61a
15 changed files with 181 additions and 13 deletions
+11 -1
View File
@@ -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{};
+13
View File
@@ -666,6 +666,19 @@ namespace lms::db
utils::forEachQueryResult(query, _func);
}
core::EnumSet<Advisory> Release::getAdvisories() const
{
core::EnumSet<Advisory> res;
auto query{ session()->query<Advisory>("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());
@@ -252,6 +252,7 @@ namespace lms::db
std::vector<std::string> getLabelNames() const;
std::vector<std::string> getReleaseTypeNames() const;
void visitLabels(const std::function<void(const Label::pointer& label)>& _func) const;
core::EnumSet<Advisory> getAdvisories() const;
std::string_view getBarcode() const { return _barcode; }
ObjectPtr<Image> getImage() const;
+4 -7
View File
@@ -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<core::UUID>& 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<float> replayGain) { _trackReplayGain = replayGain; }
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
@@ -285,6 +280,7 @@ namespace lms::db
std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); }
std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
Advisory getAdvisory() const { return _advisory; }
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
std::optional<float> 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<float> _trackReplayGain;
std::optional<float> _releaseReplayGain;
std::string _artistDisplayName;
std::string _comment;
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
Wt::Dbo::ptr<Directory> _directory;
@@ -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
@@ -32,6 +32,7 @@ namespace lms::metadata
// Mapping to internal avformat names and/or common alternative custom names
static const std::unordered_map<TagType, std::vector<std::string>> 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" } },
+2 -1
View File
@@ -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
+20
View File
@@ -246,6 +246,25 @@ namespace lms::metadata
return artistDisplayName;
}
std::optional<Track::Advisory> getAdvisory(const ITagReader& tagReader)
{
if (const auto value{ getTagValueAs<int>(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<IParser> 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<std::string>(tagReader, TagType::Comment, {} /* no custom delimiter on comments */);
track.copyright = getTagValueAs<std::string>(tagReader, TagType::Copyright).value_or("");
+13 -4
View File
@@ -59,6 +59,7 @@ namespace lms::metadata
// Mapping to internal taglib names and/or common alternative custom names
const std::unordered_map<TagType, std::vector<std::string>> 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<TagLib::MP4::File*>(_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"))
{
@@ -102,6 +102,12 @@ namespace lms::metadata
struct Track
{
enum class Advisory
{
Unknown,
Explicit,
Clean,
};
AudioProperties audioProperties;
std::optional<core::UUID> mbid;
std::optional<core::UUID> recordingMBID;
@@ -117,6 +123,7 @@ namespace lms::metadata
Wt::WDate date;
std::optional<int> originalYear{};
Wt::WDate originalDate;
std::optional<Advisory> advisory;
bool hasCover{};
std::optional<core::UUID> acoustID;
std::string copyright;
+30
View File
@@ -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<Track::Advisory> expectedValue) {
const TestTagReader testTags{
{
{ TagType::Advisory, { value } },
}
};
Parser parser;
std::unique_ptr<Track> 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
@@ -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<metadata::Track::Advisory> 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);
@@ -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<db::Advisory> 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
+17
View File
@@ -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;
+21
View File
@@ -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