Added support for explcit tags (rtng, ITUNESADVISORY)
This commit is contained in:
@@ -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{};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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" } },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user