Moved total_disc from Track to Release
This commit is contained in:
@@ -66,23 +66,25 @@ findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initiali
|
||||
|
||||
|
||||
static
|
||||
std::optional<Album>
|
||||
getAlbum(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
std::optional<Release>
|
||||
getRelease(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Album> res;
|
||||
std::optional<Release> res;
|
||||
|
||||
auto album {findFirstValueOfAs<std::string>(metadataMap, {"ALBUM"})};
|
||||
if (!album)
|
||||
std::optional<std::string> releaseName {findFirstValueOfAs<std::string>(metadataMap, {"ALBUM"})};
|
||||
if (!releaseName)
|
||||
return res;
|
||||
|
||||
auto albumMBID {findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID"})};
|
||||
res.emplace();
|
||||
res->name = *releaseName;
|
||||
res->releaseMBID = findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID"});
|
||||
|
||||
return Album{*album, albumMBID};
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getAlbumArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> res;
|
||||
|
||||
@@ -151,6 +153,18 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
|
||||
const Av::IAudioFile::MetadataMap metadataMap {mediaFile->getMetaData()};
|
||||
|
||||
track.artists = getArtists(metadataMap);
|
||||
track.release = getRelease(metadataMap);
|
||||
if (track.release)
|
||||
track.release->releaseArtists = getReleaseArtists(metadataMap);
|
||||
|
||||
auto getOrCreateDisc = [&]() -> Disc&
|
||||
{
|
||||
if (!track.disc)
|
||||
track.disc.emplace();
|
||||
return *track.disc;
|
||||
};
|
||||
|
||||
for (const auto& [tag, value] : metadataMap)
|
||||
{
|
||||
if (debug)
|
||||
@@ -167,7 +181,7 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
track.trackNumber = StringUtils::readAs<std::size_t>(strings[0]);
|
||||
|
||||
if (strings.size() > 1)
|
||||
track.totalTrack = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
getOrCreateDisc().totalTrack = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
else if (tag == "DISC")
|
||||
@@ -178,8 +192,8 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
track.discNumber = StringUtils::readAs<std::size_t>(strings[0]);
|
||||
|
||||
if (strings.size() > 1)
|
||||
track.totalDisc = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
if (strings.size() > 1 && track.release)
|
||||
track.release->totalDisc = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
else if (tag == "DATE"
|
||||
@@ -211,7 +225,7 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
|| tag == "DISCSUBTITLE"
|
||||
|| tag == "SETSUBTITLE")
|
||||
{
|
||||
track.discSubtitle = value;
|
||||
getOrCreateDisc().subtitle = value;
|
||||
}
|
||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||
{
|
||||
@@ -227,10 +241,6 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
track.artists = getArtists(metadataMap);
|
||||
track.album = getAlbum(metadataMap);
|
||||
track.albumArtists = getAlbumArtists(metadataMap);
|
||||
}
|
||||
catch(Av::Exception& e)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "TagLibParser.hpp"
|
||||
|
||||
#include <map>
|
||||
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
@@ -43,23 +45,30 @@
|
||||
namespace MetaData
|
||||
{
|
||||
|
||||
// TODO use string_views here for values
|
||||
using TagMap = std::map<std::string, std::vector<std::string>>;
|
||||
|
||||
template<typename T>
|
||||
std::vector<T>
|
||||
getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::vector<std::string_view>& keys)
|
||||
getPropertyValuesFirstMatchAs(const TagMap& tags, const std::vector<std::string_view>& keys)
|
||||
{
|
||||
std::vector<T> res;
|
||||
|
||||
for (std::string_view key : keys)
|
||||
{
|
||||
const TagLib::StringList& values {properties[std::string {key}]};
|
||||
if (values.isEmpty())
|
||||
const auto itValues {tags.find(std::string {key})};
|
||||
if (itValues == std::cend(tags))
|
||||
continue;
|
||||
|
||||
const std::vector<std::string>& values {itValues->second};
|
||||
if (values.empty())
|
||||
continue;
|
||||
|
||||
res.reserve(values.size());
|
||||
|
||||
for (const auto& value : values)
|
||||
{
|
||||
auto val {StringUtils::readAs<T>(StringUtils::stringTrim(value.to8Bit(true)))};
|
||||
std::optional<T> val {StringUtils::readAs<T>(value)};
|
||||
if (!val)
|
||||
continue;
|
||||
|
||||
@@ -74,33 +83,31 @@ getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::
|
||||
|
||||
template <typename T>
|
||||
std::vector<T>
|
||||
getPropertyValuesAs(const TagLib::PropertyMap& properties, const std::string& key)
|
||||
getPropertyValuesAs(const TagMap& tags, const std::string& key)
|
||||
{
|
||||
return getPropertyValuesFirstMatchAs<T>(properties, {std::move(key)});
|
||||
return getPropertyValuesFirstMatchAs<T>(tags, {key});
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<std::string>
|
||||
splitAndTrimString(const std::string& str, std::string_view delimiters)
|
||||
std::vector<std::string_view>
|
||||
splitAndTrimString(std::string_view str, std::string_view delimiters)
|
||||
{
|
||||
std::vector<std::string> res;
|
||||
|
||||
std::vector<std::string_view> strings {StringUtils::splitString(str, delimiters)};
|
||||
for (std::string_view s : strings)
|
||||
res.emplace_back(StringUtils::stringTrim(s));
|
||||
for (std::string_view& s : strings)
|
||||
s = StringUtils::stringTrim(s);
|
||||
|
||||
return res;
|
||||
return strings;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getArtists(const TagLib::PropertyMap& properties,
|
||||
getArtists(const TagMap& tags,
|
||||
const std::vector<std::string_view>& artistTagNames,
|
||||
const std::vector<std::string_view>& artistSortTagNames,
|
||||
const std::vector<std::string_view>& artistMBIDTagNames
|
||||
)
|
||||
{
|
||||
const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistTagNames)};
|
||||
const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(tags, artistTagNames)};
|
||||
if (artistNames.empty())
|
||||
return {};
|
||||
|
||||
@@ -110,7 +117,7 @@ getArtists(const TagLib::PropertyMap& properties,
|
||||
[&](const std::string& name) { return Artist {name}; });
|
||||
|
||||
{
|
||||
const std::vector<std::string> artistSortNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistSortTagNames)};
|
||||
const std::vector<std::string> artistSortNames {getPropertyValuesFirstMatchAs<std::string>(tags, artistSortTagNames)};
|
||||
if (artistSortNames.size() == artists.size())
|
||||
{
|
||||
for (std::size_t i {}; i < artistSortNames.size(); ++i)
|
||||
@@ -119,12 +126,12 @@ getArtists(const TagLib::PropertyMap& properties,
|
||||
}
|
||||
|
||||
{
|
||||
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(properties, artistMBIDTagNames)};
|
||||
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(tags, artistMBIDTagNames)};
|
||||
|
||||
if (artistNames.size() == artistsMBID.size())
|
||||
{
|
||||
for (std::size_t i {}; i < artistsMBID.size(); ++i)
|
||||
artists[i].musicBrainzArtistID = artistsMBID[i];
|
||||
artists[i].artistMBID = artistsMBID[i];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +141,7 @@ getArtists(const TagLib::PropertyMap& properties,
|
||||
|
||||
static
|
||||
PerformerContainer
|
||||
getPerformerArtists(const TagLib::PropertyMap& properties,
|
||||
getPerformerArtists(const TagMap& tags,
|
||||
const std::vector<std::string_view>& artistTagNames)
|
||||
{
|
||||
PerformerContainer performers;
|
||||
@@ -142,7 +149,7 @@ getPerformerArtists(const TagLib::PropertyMap& properties,
|
||||
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
|
||||
// We may hit both styles for the same track
|
||||
// PERFORMER: artist (role)
|
||||
if (const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistTagNames)}; !artistNames.empty())
|
||||
if (const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(tags, artistTagNames)}; !artistNames.empty())
|
||||
{
|
||||
for (std::string_view entry : artistNames)
|
||||
{
|
||||
@@ -152,11 +159,11 @@ getPerformerArtists(const TagLib::PropertyMap& properties,
|
||||
}
|
||||
}
|
||||
// PERFORMER:role (MP3)
|
||||
for (const auto& [key, values] : properties)
|
||||
for (const auto& [key, values] : tags)
|
||||
{
|
||||
if (key.startsWith("PERFORMER:"))
|
||||
if (key.find("PERFORMER:") == 0)
|
||||
{
|
||||
std::string performerStr {key.to8Bit(true)};
|
||||
std::string performerStr {key};
|
||||
std::string role;
|
||||
if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos)
|
||||
{
|
||||
@@ -165,7 +172,7 @@ getPerformerArtists(const TagLib::PropertyMap& properties,
|
||||
}
|
||||
|
||||
for (const auto& value : values)
|
||||
performers[role].push_back(Artist {value.to8Bit(true)});
|
||||
performers[role].push_back(Artist {value});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,19 +180,23 @@ getPerformerArtists(const TagLib::PropertyMap& properties,
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<Album>
|
||||
getAlbum(const TagLib::PropertyMap& properties)
|
||||
std::optional<Release>
|
||||
getRelease(const TagMap& tags)
|
||||
{
|
||||
std::vector<std::string> albumName {getPropertyValuesAs<std::string>(properties, "ALBUM")};
|
||||
if (albumName.empty())
|
||||
return std::nullopt;
|
||||
std::optional<Release> release;
|
||||
|
||||
const std::vector<UUID> albumMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"})};
|
||||
std::vector<std::string> releaseName {getPropertyValuesAs<std::string>(tags, "ALBUM")};
|
||||
if (releaseName.empty())
|
||||
return release;
|
||||
|
||||
if (albumMBID.empty())
|
||||
return Album {std::move(albumName.front()), {}};
|
||||
else
|
||||
return Album {std::move(albumName.front()), albumMBID.front()};
|
||||
const std::vector<UUID> releaseMBID {getPropertyValuesFirstMatchAs<UUID>(tags, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"})};
|
||||
|
||||
release.emplace();
|
||||
release->name = std::move(releaseName.front());
|
||||
if (!releaseMBID.empty())
|
||||
release->releaseMBID = releaseMBID.front();
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
static
|
||||
@@ -202,27 +213,28 @@ readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
||||
throw LmsException {"Cannot convert read style"};
|
||||
}
|
||||
|
||||
|
||||
TagLibParser::TagLibParser(ParserReadStyle readStyle)
|
||||
: _readStyle {readStyleToTagLibReadStyle(readStyle)}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug)
|
||||
TagLibParser::processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug)
|
||||
{
|
||||
if (debug)
|
||||
{
|
||||
std::vector<std::string> strs;
|
||||
std::transform(values.begin(), values.end(), std::back_inserter(strs), [](const auto& value) { return value.to8Bit(true); });
|
||||
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl;
|
||||
|
||||
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(strs, "*SEP*") << std::endl;
|
||||
}
|
||||
|
||||
if (tag.empty() || values.isEmpty() || values.front().isEmpty())
|
||||
if (tag.empty() || values.empty())
|
||||
return;
|
||||
|
||||
std::string value {StringUtils::stringTrim(values.front().to8Bit(true))};
|
||||
auto getOrCreateDisc = [&]() -> Disc&
|
||||
{
|
||||
if (!track.disc)
|
||||
track.disc.emplace();
|
||||
return *track.disc;
|
||||
};
|
||||
|
||||
std::string_view value {values.front()};
|
||||
|
||||
if (tag == "TITLE")
|
||||
track.title = value;
|
||||
@@ -240,29 +252,26 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
||||
track.acoustID = UUID::fromString(value);
|
||||
else if (tag == "TRACKTOTAL")
|
||||
{
|
||||
auto totalTrack = StringUtils::readAs<std::size_t>(value);
|
||||
if (totalTrack)
|
||||
track.totalTrack = totalTrack;
|
||||
getOrCreateDisc().totalTrack = StringUtils::readAs<std::size_t>(value);
|
||||
}
|
||||
else if (tag == "TRACKNUMBER")
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
std::vector<std::string> strings {splitAndTrimString(value, "/")};
|
||||
std::vector<std::string_view> strings {splitAndTrimString(value, "/")};
|
||||
|
||||
if (!strings.empty())
|
||||
{
|
||||
track.trackNumber = StringUtils::readAs<std::size_t>(strings[0]);
|
||||
|
||||
// Lower priority than TRACKTOTAL
|
||||
if (strings.size() > 1 && !track.totalTrack)
|
||||
track.totalTrack = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
if (strings.size() > 1 && !getOrCreateDisc().totalTrack)
|
||||
getOrCreateDisc().totalTrack = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
else if (tag == "DISCTOTAL")
|
||||
{
|
||||
auto totalDisc = StringUtils::readAs<std::size_t>(value);
|
||||
if (totalDisc)
|
||||
track.totalDisc = totalDisc;
|
||||
if (track.release)
|
||||
track.release->totalDisc = StringUtils::readAs<std::size_t>(value);
|
||||
}
|
||||
else if (tag == "DISCNUMBER")
|
||||
{
|
||||
@@ -273,8 +282,8 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
||||
track.discNumber = StringUtils::readAs<std::size_t>(strings[0]);
|
||||
|
||||
// Lower priority than DISCTOTAL
|
||||
if (strings.size() > 1 && !track.totalDisc)
|
||||
track.totalDisc = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
if (strings.size() > 1 && track.release && !track.release->totalDisc)
|
||||
track.release->totalDisc = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
else if (tag == "DATE")
|
||||
@@ -306,20 +315,20 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
||||
else if (tag == "COPYRIGHTURL")
|
||||
track.copyrightURL = value;
|
||||
else if (tag == "REPLAYGAIN_ALBUM_GAIN")
|
||||
track.albumReplayGain = StringUtils::readAs<float>(value);
|
||||
getOrCreateDisc().replayGain = StringUtils::readAs<float>(value);
|
||||
else if (tag == "REPLAYGAIN_TRACK_GAIN")
|
||||
track.trackReplayGain = StringUtils::readAs<float>(value);
|
||||
track.replayGain = StringUtils::readAs<float>(value);
|
||||
else if (tag == "DISCSUBTITLE" || tag == "SETSUBTITLE")
|
||||
track.discSubtitle = value;
|
||||
getOrCreateDisc().subtitle = value;
|
||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||
{
|
||||
std::set<std::string> clusterNames;
|
||||
for (const auto& valueList : values)
|
||||
{
|
||||
const auto splittedValues {splitAndTrimString(valueList.to8Bit(true), "/,;")};
|
||||
const std::vector<std::string_view> splittedValues {splitAndTrimString(valueList, "/,;")};
|
||||
|
||||
for (const auto& value : splittedValues)
|
||||
clusterNames.insert(value);
|
||||
for (std::string_view value : splittedValues)
|
||||
clusterNames.insert(std::string {value});
|
||||
}
|
||||
|
||||
if (!clusterNames.empty())
|
||||
@@ -327,6 +336,37 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
TagMap
|
||||
constructTagMap(const TagLib::PropertyMap& properties)
|
||||
{
|
||||
TagMap tagMap;
|
||||
|
||||
for (const auto& [propertyName, propertyValues] : properties)
|
||||
{
|
||||
std::vector<std::string>& values {tagMap[propertyName.upper().to8Bit(true)]};
|
||||
for (const TagLib::String& propertyValue : propertyValues)
|
||||
{
|
||||
std::string trimedValue {StringUtils::stringTrim(propertyValue.to8Bit(true))};
|
||||
if (!trimedValue.empty())
|
||||
values.emplace_back(std::move(trimedValue));
|
||||
}
|
||||
}
|
||||
|
||||
return tagMap;
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
mergeTagMaps(TagMap& dst, TagMap&& src)
|
||||
{
|
||||
for (auto&& [tag, values] : src)
|
||||
{
|
||||
if (dst.find(tag) == std::cend(dst))
|
||||
dst[tag] = std::move(values);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Track>
|
||||
TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
@@ -357,21 +397,14 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
track.audioStreams = {std::move(audioStream)};
|
||||
}
|
||||
|
||||
TagLib::PropertyMap properties {f.file()->properties()};
|
||||
TagMap tags {constructTagMap(f.file()->properties())};
|
||||
|
||||
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
|
||||
{
|
||||
if (!apeTag)
|
||||
return;
|
||||
|
||||
for (const auto& [name, values] : apeTag->properties())
|
||||
{
|
||||
if (debug)
|
||||
std::cout << "APE property: '" << name << "'" << std::endl;
|
||||
|
||||
if (!properties.contains(name))
|
||||
properties.insert(name, values);
|
||||
}
|
||||
mergeTagMaps(tags, constructTagMap(apeTag->properties()));
|
||||
};
|
||||
|
||||
// Not that good embedded pictures handling
|
||||
@@ -387,23 +420,23 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
|
||||
for (const auto& [name, attributeList] : tag->attributeListMap())
|
||||
{
|
||||
if (name.to8Bit().find("WM/") == 0 || properties.contains(name))
|
||||
std::string strName {name.to8Bit(true)};
|
||||
if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags))
|
||||
continue;
|
||||
|
||||
TagLib::StringList stringAttributeList;
|
||||
std::vector<std::string> attributes;
|
||||
for (const auto& attribute : attributeList)
|
||||
{
|
||||
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
|
||||
stringAttributeList.append(attribute.toString());
|
||||
attributes.emplace_back(attribute.toString().to8Bit(true));
|
||||
}
|
||||
|
||||
if (!stringAttributeList.isEmpty())
|
||||
if (!attributes.empty())
|
||||
{
|
||||
if (debug)
|
||||
std::cout << "ASF property: '" << name << "'" << std::endl;
|
||||
|
||||
if (!properties.contains(name))
|
||||
properties.insert(name, stringAttributeList);
|
||||
tags[strName] = std::move(attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +451,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
if (!frameListMap["APIC"].isEmpty())
|
||||
track.hasCover = true;
|
||||
if (!frameListMap["TSST"].isEmpty())
|
||||
properties.insert("DISCSUBTITLE", frameListMap["TSST"].front()->toString());
|
||||
tags["DISCSUBTITLE"] = {frameListMap["TSST"].front()->toString().to8Bit(true)};
|
||||
}
|
||||
|
||||
getAPETags(mp3File->APETag());
|
||||
@@ -458,19 +491,20 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
track.hasCover = true;
|
||||
}
|
||||
|
||||
for (const auto& [tag, values] : properties)
|
||||
processTag(track, tag.upper().to8Bit(true), values, debug);
|
||||
track.release = getRelease(tags);
|
||||
if (track.release)
|
||||
track.release->releaseArtists = getArtists(tags, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"});
|
||||
track.artists = getArtists(tags, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID"});
|
||||
track.conductorArtists = getArtists(tags, {"CONDUCTORS", "CONDUCTOR"}, {"CONDUCTORSSORT", "CONDUCTORSORT"}, {});
|
||||
track.composerArtists = getArtists(tags, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {});
|
||||
track.lyricistArtists = getArtists(tags, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {});
|
||||
track.mixerArtists = getArtists(tags, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {});
|
||||
track.producerArtists = getArtists(tags, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {});
|
||||
track.remixerArtists = getArtists(tags, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {});
|
||||
track.performerArtists = getPerformerArtists(tags, {"PERFORMERS", "PERFORMER"});
|
||||
|
||||
track.album = getAlbum(properties);
|
||||
track.artists = getArtists(properties, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID"});
|
||||
track.albumArtists = getArtists(properties, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"});
|
||||
track.conductorArtists = getArtists(properties, {"CONDUCTORS", "CONDUCTOR"}, {"CONDUCTORSSORT", "CONDUCTORSORT"}, {});
|
||||
track.composerArtists = getArtists(properties, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {});
|
||||
track.lyricistArtists = getArtists(properties, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {});
|
||||
track.mixerArtists = getArtists(properties, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {});
|
||||
track.producerArtists = getArtists(properties, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {});
|
||||
track.remixerArtists = getArtists(properties, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {});
|
||||
track.performerArtists = getPerformerArtists(properties, {"PERFORMERS", "PERFORMER"});
|
||||
for (const auto& [tag, values] : tags)
|
||||
processTag(track, tag, values, debug);
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class TagLibParser : public IParser
|
||||
|
||||
private:
|
||||
std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) override;
|
||||
void processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug);
|
||||
void processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug);
|
||||
|
||||
const TagLib::AudioProperties::ReadStyle _readStyle;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
namespace MetaData::Utils
|
||||
{
|
||||
Wt::WDate
|
||||
parseDate(const std::string& dateStr)
|
||||
parseDate(std::string_view dateStr)
|
||||
{
|
||||
static constexpr const char* formats[]
|
||||
{
|
||||
@@ -39,7 +39,7 @@ namespace MetaData::Utils
|
||||
for (const char* format : formats)
|
||||
{
|
||||
std::tm tm = {};
|
||||
std::stringstream ss {dateStr};
|
||||
std::istringstream ss {std::string {dateStr}}; // TODO, remove extra copy here
|
||||
ss >> std::get_time(&tm, format);
|
||||
if (ss.fail())
|
||||
continue;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
namespace MetaData::Utils
|
||||
{
|
||||
Wt::WDate parseDate(const std::string& dateStr);
|
||||
Wt::WDate parseDate(std::string_view dateStr);
|
||||
std::string_view readStyleToString(ParserReadStyle readStyle);
|
||||
|
||||
struct PerformerArtist
|
||||
|
||||
@@ -39,18 +39,27 @@ namespace MetaData
|
||||
{
|
||||
std::string name;
|
||||
std::optional<std::string> sortName;
|
||||
std::optional<UUID> musicBrainzArtistID;
|
||||
std::optional<UUID> artistMBID;
|
||||
|
||||
Artist(std::string_view _name) : name {_name} {}
|
||||
Artist(std::string_view _name, std::optional<std::string> _sortName, std::optional<UUID> _musicBrainzArtistID) : name {_name}, sortName {_sortName}, musicBrainzArtistID {_musicBrainzArtistID} {}
|
||||
Artist(std::string_view _name, std::optional<std::string> _sortName, std::optional<UUID> _artistMBID) : name {_name}, sortName {std::move(_sortName)}, artistMBID {std::move(_artistMBID)} {}
|
||||
};
|
||||
|
||||
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
|
||||
|
||||
struct Album
|
||||
struct Release
|
||||
{
|
||||
std::string name;
|
||||
std::optional<UUID> musicBrainzAlbumID;
|
||||
std::string name;
|
||||
std::vector<Artist> releaseArtists;
|
||||
std::optional<UUID> releaseMBID;
|
||||
std::optional<std::size_t> totalDisc;
|
||||
};
|
||||
|
||||
struct Disc
|
||||
{
|
||||
std::string subtitle;
|
||||
std::optional<float> replayGain;
|
||||
std::optional<std::size_t> totalTrack;
|
||||
};
|
||||
|
||||
struct AudioStream
|
||||
@@ -61,34 +70,30 @@ namespace MetaData
|
||||
struct Track
|
||||
{
|
||||
std::vector<Artist> artists;
|
||||
std::vector<Artist> albumArtists;
|
||||
std::string title;
|
||||
std::optional<UUID> trackMBID;
|
||||
std::optional<UUID> recordingMBID;
|
||||
std::optional<Album> album;
|
||||
std::optional<Release> release;
|
||||
std::optional<Disc> disc;
|
||||
Clusters clusters;
|
||||
std::chrono::milliseconds duration;
|
||||
std::optional<std::size_t> trackNumber;
|
||||
std::optional<std::size_t> totalTrack;
|
||||
std::optional<std::size_t> discNumber;
|
||||
std::optional<std::size_t> totalDisc;
|
||||
Wt::WDate date;
|
||||
Wt::WDate originalDate;
|
||||
bool hasCover {};
|
||||
std::vector<AudioStream> audioStreams;
|
||||
std::optional<UUID> acoustID;
|
||||
std::string copyright;
|
||||
std::string copyrightURL;
|
||||
std::optional<float> trackReplayGain;
|
||||
std::optional<float> albumReplayGain;
|
||||
std::string discSubtitle;
|
||||
std::vector<Artist> conductorArtists;
|
||||
std::vector<Artist> composerArtists;
|
||||
std::vector<Artist> lyricistArtists;
|
||||
std::vector<Artist> mixerArtists;
|
||||
PerformerContainer performerArtists;
|
||||
std::vector<Artist> producerArtists;
|
||||
std::vector<Artist> remixerArtists;
|
||||
std::optional<UUID> acoustID;
|
||||
std::string copyright;
|
||||
std::string copyrightURL;
|
||||
std::optional<float> replayGain;
|
||||
std::vector<Artist> conductorArtists;
|
||||
std::vector<Artist> composerArtists;
|
||||
std::vector<Artist> lyricistArtists;
|
||||
std::vector<Artist> mixerArtists;
|
||||
PerformerContainer performerArtists;
|
||||
std::vector<Artist> producerArtists;
|
||||
std::vector<Artist> remixerArtists;
|
||||
};
|
||||
|
||||
class IParser
|
||||
|
||||
@@ -611,6 +611,48 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV38(Session& session)
|
||||
{
|
||||
// migrate release-specific tags from Track to Release
|
||||
session.getDboSession().execute("ALTER TABLE release ADD total_disc INTEGER");
|
||||
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"scan_version" integer not null,
|
||||
"track_number" integer,
|
||||
"disc_number" integer,
|
||||
"total_track" integer,
|
||||
"disc_subtitle" text not null,
|
||||
"name" text not null,
|
||||
"duration" integer,
|
||||
"date" text,
|
||||
"original_date" text,
|
||||
"file_path" text not null,
|
||||
"file_last_write" text,
|
||||
"file_added" text,
|
||||
"has_cover" boolean not null,
|
||||
"mbid" text not null,
|
||||
"recording_mbid" text not null,
|
||||
"copyright" text not null,
|
||||
"copyright_url" text not null,
|
||||
"track_replay_gain" real,
|
||||
"release_replay_gain" real,
|
||||
"release_id" bigint,
|
||||
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
|
||||
);
|
||||
))");
|
||||
session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track");
|
||||
session.getDboSession().execute("DROP TABLE track");
|
||||
session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
void
|
||||
doDbMigration(Session& session)
|
||||
{
|
||||
@@ -655,6 +697,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
||||
{35, migrateFromV35},
|
||||
{36, migrateFromV36},
|
||||
{37, migrateFromV37},
|
||||
{38, migrateFromV38},
|
||||
};
|
||||
|
||||
while (1)
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Database
|
||||
class Session;
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION {38};
|
||||
static constexpr Version LMS_DATABASE_VERSION {39};
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -263,30 +263,6 @@ Release::find(Session& session, const FindParameters& params)
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
Release::getTotalTrack() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
int res = session()->query<int>("SELECT COALESCE(MAX(total_track),0) FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.bind(getId());
|
||||
|
||||
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
Release::getTotalDisc() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
int res = session()->query<int>("SELECT COALESCE(MAX(total_disc),0) FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.bind(getId());
|
||||
|
||||
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Release::getDiscCount() const
|
||||
{
|
||||
|
||||
@@ -361,30 +361,6 @@ Track::setClusters(const std::vector<ObjectPtr<Cluster>>& clusters)
|
||||
_clusters.insert(getDboPtr(cluster));
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
Track::getTrackNumber() const
|
||||
{
|
||||
return (_trackNumber > 0) ? std::make_optional<std::size_t>(_trackNumber) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
Track::getTotalTrack() const
|
||||
{
|
||||
return (_totalTrack > 0) ? std::make_optional<std::size_t>(_totalTrack) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
Track::getDiscNumber() const
|
||||
{
|
||||
return (_discNumber > 0) ? std::make_optional<std::size_t>(_discNumber) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
Track::getTotalDisc() const
|
||||
{
|
||||
return (_totalDisc > 0) ? std::make_optional<std::size_t>(_totalDisc) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int>
|
||||
Track::getYear() const
|
||||
{
|
||||
|
||||
@@ -97,34 +97,37 @@ class Release : public Object<Release, ReleaseId>
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||
|
||||
// Utility functions
|
||||
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
||||
std::optional<int> getReleaseYear(bool originalDate = false) const;
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
std::optional<std::size_t> getTotalTrack() const;
|
||||
std::optional<std::size_t> getTotalDisc() const;
|
||||
const std::string& getName() const { return _name; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; }
|
||||
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
Wt::WDateTime getLastWritten() const;
|
||||
|
||||
// Get the artists of this release
|
||||
std::vector<ObjectPtr<Artist> > getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||
std::vector<ObjectPtr<Artist> > getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
||||
bool hasVariousArtists() const;
|
||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||
// Setters
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
|
||||
|
||||
// Get the artists of this release
|
||||
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
||||
bool hasVariousArtists() const;
|
||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
||||
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||
}
|
||||
@@ -136,8 +139,9 @@ class Release : public Object<Release, ReleaseId>
|
||||
|
||||
static constexpr std::size_t _maxNameLength {128};
|
||||
|
||||
std::string _name;
|
||||
std::string _MBID;
|
||||
std::string _name;
|
||||
std::string _MBID;
|
||||
std::optional<int> _totalDisc {};
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
|
||||
};
|
||||
|
||||
@@ -119,60 +119,57 @@ class Track : public Object<Track, TrackId>
|
||||
static RangeResults<TrackId> findWithRecordingMBIDAndMissingFeatures(Session& session, Range range);
|
||||
|
||||
// Accessors
|
||||
void setScanVersion(std::size_t version) { _scanVersion = version; }
|
||||
void setTrackNumber(int num) { _trackNumber = num; }
|
||||
void setDiscNumber(int num) { _discNumber = num; }
|
||||
void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack ? *totalTrack : 0; }
|
||||
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc ? *totalDisc : 0; }
|
||||
void setScanVersion(std::size_t version) { _scanVersion = version; }
|
||||
void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
|
||||
void setDiscNumber(std::optional<int> num) { _discNumber = num; }
|
||||
void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack; }
|
||||
void setDiscSubtitle(const std::string& name) { _discSubtitle = name; }
|
||||
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
|
||||
void setPath(const std::filesystem::path& filePath) { _filePath = filePath; }
|
||||
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
||||
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
||||
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
|
||||
void setDate(const Wt::WDate& date) { _date = date; }
|
||||
void setOriginalDate(const Wt::WDate& date) { _originalDate = date; }
|
||||
void setHasCover(bool hasCover) { _hasCover = hasCover; }
|
||||
void setTrackMBID(const std::optional<UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
|
||||
void setRecordingMBID(const std::optional<UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
|
||||
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
|
||||
void setPath(const std::filesystem::path& filePath) { _filePath = filePath; }
|
||||
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
||||
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
||||
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
|
||||
void setDate(const Wt::WDate& date) { _date = date; }
|
||||
void setOriginalDate(const Wt::WDate& date) { _originalDate = date; }
|
||||
void setHasCover(bool hasCover) { _hasCover = hasCover; }
|
||||
void setTrackMBID(const std::optional<UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
|
||||
void setRecordingMBID(const std::optional<UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
|
||||
void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); }
|
||||
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
|
||||
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
||||
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; }
|
||||
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
|
||||
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
||||
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
|
||||
void clearArtistLinks();
|
||||
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
||||
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
||||
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters );
|
||||
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::optional<std::size_t> getTrackNumber() const;
|
||||
std::optional<std::size_t> getTotalTrack() const;
|
||||
std::optional<std::size_t> getDiscNumber() const;
|
||||
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
|
||||
std::optional<std::size_t> getTotalTrack() const { return _totalTrack; }
|
||||
std::optional<std::size_t> getDiscNumber() const { return _discNumber; }
|
||||
const std::string& getDiscSubtitle() const { return _discSubtitle; }
|
||||
std::optional<std::size_t> getTotalDisc() const;
|
||||
std::string getName() const { return _name; }
|
||||
std::filesystem::path getPath() const { return _filePath; }
|
||||
std::chrono::milliseconds getDuration() const { return _duration; }
|
||||
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
|
||||
std::optional<int> getYear() const;
|
||||
std::optional<int> getOriginalYear() const;
|
||||
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; }
|
||||
Wt::WDateTime getAddedTime() const { return _fileAdded; }
|
||||
bool hasCover() const { return _hasCover; }
|
||||
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; }
|
||||
Wt::WDateTime getAddedTime() const { return _fileAdded; }
|
||||
bool hasCover() const { return _hasCover; }
|
||||
std::optional<UUID> getTrackMBID() const { return UUID::fromString(_trackMBID); }
|
||||
std::optional<UUID> getRecordingMBID() const { return UUID::fromString(_recordingMBID); }
|
||||
std::optional<UUID> getRecordingMBID() const { return UUID::fromString(_recordingMBID); }
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||
|
||||
// no artistLinkTypes means get all
|
||||
std::vector<ObjectPtr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||
std::vector<ArtistId> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||
std::vector<ObjectPtr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||
std::vector<ArtistId> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<ClusterId> getClusterIds() const;
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<ClusterId> getClusterIds() const;
|
||||
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||
|
||||
@@ -182,9 +179,8 @@ class Track : public Object<Track, TrackId>
|
||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||
Wt::Dbo::field(a, _trackNumber, "track_number");
|
||||
Wt::Dbo::field(a, _discNumber, "disc_number");
|
||||
Wt::Dbo::field(a, _discSubtitle, "disc_subtitle");
|
||||
Wt::Dbo::field(a, _totalTrack, "total_track");
|
||||
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
||||
Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||
Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _duration, "duration");
|
||||
Wt::Dbo::field(a, _date, "date");
|
||||
@@ -198,7 +194,7 @@ class Track : public Object<Track, TrackId>
|
||||
Wt::Dbo::field(a, _copyright, "copyright");
|
||||
Wt::Dbo::field(a, _copyrightURL, "copyright_url");
|
||||
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
||||
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain");
|
||||
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -214,14 +210,11 @@ class Track : public Object<Track, TrackId>
|
||||
static constexpr std::size_t _maxCopyrightURLLength {128};
|
||||
|
||||
int _scanVersion {};
|
||||
int _trackNumber {};
|
||||
int _discNumber {};
|
||||
std::optional<int> _trackNumber {};
|
||||
std::optional<int> _discNumber {};
|
||||
std::optional<int> _totalTrack {};
|
||||
std::string _discSubtitle;
|
||||
int _totalTrack {};
|
||||
int _totalDisc {};
|
||||
std::string _name;
|
||||
std::string _artistName;
|
||||
std::string _releaseName;
|
||||
std::chrono::duration<int, std::milli> _duration {};
|
||||
Wt::WDate _date;
|
||||
Wt::WDate _originalDate;
|
||||
@@ -236,9 +229,9 @@ class Track : public Object<Track, TrackId>
|
||||
std::optional<float> _trackReplayGain;
|
||||
std::optional<float> _releaseReplayGain;
|
||||
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
|
||||
};
|
||||
|
||||
namespace Debug
|
||||
|
||||
@@ -187,7 +187,6 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
EXPECT_FALSE(release1->getTotalTrack());
|
||||
EXPECT_FALSE(release1->getTotalDisc());
|
||||
}
|
||||
|
||||
@@ -201,7 +200,6 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
EXPECT_FALSE(release1->getTotalTrack());
|
||||
EXPECT_FALSE(release1->getTotalDisc());
|
||||
}
|
||||
|
||||
@@ -209,14 +207,14 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
track1.get().modify()->setTotalTrack(36);
|
||||
track1.get().modify()->setTotalDisc(6);
|
||||
release1.get().modify()->setTotalDisc(6);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
ASSERT_TRUE(release1->getTotalTrack());
|
||||
EXPECT_EQ(*release1->getTotalTrack(), 36);
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
ASSERT_TRUE(release1->getTotalDisc());
|
||||
EXPECT_EQ(*release1->getTotalDisc(), 6);
|
||||
}
|
||||
@@ -227,14 +225,14 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
|
||||
track2.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setTotalTrack(37);
|
||||
track2.get().modify()->setTotalDisc(67);
|
||||
release1.get().modify()->setTotalDisc(67);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
ASSERT_TRUE(release1->getTotalTrack());
|
||||
EXPECT_EQ(*release1->getTotalTrack(), 37);
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
ASSERT_TRUE(release1->getTotalDisc());
|
||||
EXPECT_EQ(*release1->getTotalDisc(), 67);
|
||||
}
|
||||
@@ -243,7 +241,6 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
EXPECT_FALSE(release2->getTotalTrack());
|
||||
EXPECT_FALSE(release2->getTotalDisc());
|
||||
}
|
||||
|
||||
@@ -253,17 +250,17 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
|
||||
track3.get().modify()->setRelease(release2.get());
|
||||
track3.get().modify()->setTotalTrack(7);
|
||||
track3.get().modify()->setTotalDisc(5);
|
||||
release2.get().modify()->setTotalDisc(5);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
ASSERT_TRUE(release1->getTotalTrack());
|
||||
EXPECT_EQ(*release1->getTotalTrack(), 37);
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
ASSERT_TRUE(release1->getTotalDisc());
|
||||
EXPECT_EQ(*release1->getTotalDisc(), 67);
|
||||
ASSERT_TRUE(release2->getTotalTrack());
|
||||
EXPECT_EQ(*release2->getTotalTrack(), 7);
|
||||
EXPECT_EQ(*release2->getTotalDisc(), 5);
|
||||
ASSERT_TRUE(track3->getTotalTrack());
|
||||
EXPECT_EQ(*track3->getTotalTrack(), 7);
|
||||
ASSERT_TRUE(release2->getTotalDisc());
|
||||
EXPECT_EQ(*release2->getTotalDisc(), 5);
|
||||
}
|
||||
@@ -482,7 +479,7 @@ TEST_F(DatabaseFixture, Release_getDiscCount)
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
@@ -42,8 +42,8 @@ namespace
|
||||
{
|
||||
Artist::pointer artist {session.create<Artist>(artistInfo.name)};
|
||||
|
||||
if (artistInfo.musicBrainzArtistID)
|
||||
artist.modify()->setMBID(*artistInfo.musicBrainzArtistID);
|
||||
if (artistInfo.artistMBID)
|
||||
artist.modify()->setMBID(*artistInfo.artistMBID);
|
||||
if (artistInfo.sortName)
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
|
||||
@@ -76,9 +76,9 @@ namespace
|
||||
Artist::pointer artist;
|
||||
|
||||
// First try to get by MBID
|
||||
if (artistInfo.musicBrainzArtistID)
|
||||
if (artistInfo.artistMBID)
|
||||
{
|
||||
artist = Artist::find(session, *artistInfo.musicBrainzArtistID);
|
||||
artist = Artist::find(session, *artistInfo.artistMBID);
|
||||
if (!artist)
|
||||
artist = createArtist(session, artistInfo);
|
||||
else
|
||||
@@ -115,45 +115,49 @@ namespace
|
||||
return artists;
|
||||
}
|
||||
|
||||
void
|
||||
updateReleaseIfNeeded(Release::pointer release, const MetaData::Release& releaseInfo)
|
||||
{
|
||||
if (release->getName() != releaseInfo.name)
|
||||
release.modify()->setName(releaseInfo.name);
|
||||
if (release->getTotalDisc() != releaseInfo.totalDisc)
|
||||
release.modify()->setTotalDisc(releaseInfo.totalDisc);
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
getOrCreateRelease(Session& session, const MetaData::Album& album)
|
||||
getOrCreateRelease(Session& session, const MetaData::Release& releaseInfo)
|
||||
{
|
||||
Release::pointer release;
|
||||
|
||||
// First try to get by MBID
|
||||
if (album.musicBrainzAlbumID)
|
||||
if (releaseInfo.releaseMBID)
|
||||
{
|
||||
release = Release::find(session, *album.musicBrainzAlbumID);
|
||||
release = Release::find(session, *releaseInfo.releaseMBID);
|
||||
if (!release)
|
||||
{
|
||||
release = session.create<Release>(album.name, album.musicBrainzAlbumID);
|
||||
}
|
||||
else if (release->getName() != album.name)
|
||||
{
|
||||
// Name may have been updated
|
||||
release.modify()->setName(album.name);
|
||||
}
|
||||
release = session.create<Release>(releaseInfo.name, releaseInfo.releaseMBID);
|
||||
|
||||
updateReleaseIfNeeded(release, releaseInfo);
|
||||
return release;
|
||||
}
|
||||
|
||||
// Fall back on release name (collisions may occur)
|
||||
if (!album.name.empty())
|
||||
if (!releaseInfo.name.empty())
|
||||
{
|
||||
for (const Release::pointer& sameNamedRelease : Release::find(session, album.name))
|
||||
for (const Release::pointer& sameNamedRelease : Release::find(session, releaseInfo.name))
|
||||
{
|
||||
// do not fallback on properly tagged releases
|
||||
if (!sameNamedRelease->getMBID())
|
||||
{
|
||||
release = sameNamedRelease;
|
||||
break;
|
||||
}
|
||||
if (sameNamedRelease->getMBID())
|
||||
continue;
|
||||
|
||||
release = sameNamedRelease;
|
||||
break;
|
||||
}
|
||||
|
||||
// No release found with the same name and without MBID -> creating
|
||||
if (!release)
|
||||
release = session.create<Release>(album.name);
|
||||
release = session.create<Release>(releaseInfo.name);
|
||||
|
||||
updateReleaseIfNeeded(release, releaseInfo);
|
||||
return release;
|
||||
}
|
||||
|
||||
@@ -389,8 +393,11 @@ namespace Scanner
|
||||
for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackInfo->artists, false))
|
||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist));
|
||||
|
||||
for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackInfo->albumArtists, false))
|
||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist));
|
||||
if (trackInfo->release)
|
||||
{
|
||||
for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackInfo->release->releaseArtists, false))
|
||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist));
|
||||
}
|
||||
|
||||
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
||||
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
||||
@@ -419,10 +426,13 @@ namespace Scanner
|
||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer));
|
||||
|
||||
track.modify()->setScanVersion(_settings.scanVersion);
|
||||
if (trackInfo->album)
|
||||
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->album));
|
||||
if (trackInfo->release)
|
||||
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->release));
|
||||
else
|
||||
track.modify()->setRelease({});
|
||||
track.modify()->setTotalTrack(trackInfo->disc ? trackInfo->disc->totalTrack : std::nullopt);
|
||||
track.modify()->setReleaseReplayGain(trackInfo->disc ? trackInfo->disc->replayGain : std::nullopt);
|
||||
track.modify()->setDiscSubtitle(trackInfo->disc ? trackInfo->disc->subtitle : "");
|
||||
track.modify()->setClusters(getOrCreateClusters(dbSession, trackInfo->clusters));
|
||||
track.modify()->setLastWriteTime(lastWriteTime);
|
||||
track.modify()->setName(title);
|
||||
@@ -430,9 +440,6 @@ namespace Scanner
|
||||
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
|
||||
track.modify()->setTrackNumber(trackInfo->trackNumber ? *trackInfo->trackNumber : 0);
|
||||
track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0);
|
||||
track.modify()->setTotalTrack(trackInfo->totalTrack);
|
||||
track.modify()->setTotalDisc(trackInfo->totalDisc);
|
||||
track.modify()->setDiscSubtitle(trackInfo->discSubtitle);
|
||||
track.modify()->setDate(trackInfo->date);
|
||||
track.modify()->setOriginalDate(trackInfo->originalDate);
|
||||
|
||||
@@ -447,7 +454,6 @@ namespace Scanner
|
||||
track.modify()->setHasCover(trackInfo->hasCover);
|
||||
track.modify()->setCopyright(trackInfo->copyright);
|
||||
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
|
||||
track.modify()->setTrackReplayGain(trackInfo->trackReplayGain);
|
||||
track.modify()->setReleaseReplayGain(trackInfo->albumReplayGain);
|
||||
track.modify()->setTrackReplayGain(trackInfo->replayGain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,10 +132,10 @@ stringTrim(std::string_view str, std::string_view whitespaces)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string
|
||||
std::string_view
|
||||
stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
return std::string {str.substr(0, str.find_last_not_of(whitespaces) + 1)};
|
||||
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
|
||||
}
|
||||
|
||||
std::string
|
||||
|
||||
@@ -48,7 +48,7 @@ std::string_view
|
||||
stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
std::string_view
|
||||
stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include <Wt/WStackedWidget.h>
|
||||
#include <Wt/WTemplateFormView.h>
|
||||
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
|
||||
@@ -29,12 +29,14 @@
|
||||
#include "metadata/IParser.hpp"
|
||||
#include "utils/StreamLogger.hpp"
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const MetaData::Artist& artist)
|
||||
static
|
||||
std::ostream&
|
||||
operator<<(std::ostream& os, const MetaData::Artist& artist)
|
||||
{
|
||||
os << artist.name;
|
||||
|
||||
if (artist.musicBrainzArtistID)
|
||||
os << " (" << artist.musicBrainzArtistID->getAsString() << ")";
|
||||
if (artist.artistMBID)
|
||||
os << " (" << artist.artistMBID->getAsString() << ")";
|
||||
|
||||
if (artist.sortName)
|
||||
os << " '" << *artist.sortName << "'";
|
||||
@@ -42,12 +44,36 @@ std::ostream& operator<<(std::ostream& os, const MetaData::Artist& artist)
|
||||
return os;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const MetaData::Album& album)
|
||||
static
|
||||
std::ostream&
|
||||
operator<<(std::ostream& os, const MetaData::Release& release)
|
||||
{
|
||||
os << album.name;
|
||||
os << release.name;
|
||||
|
||||
if (album.musicBrainzAlbumID)
|
||||
os << " (" << album.musicBrainzAlbumID->getAsString() << ")";
|
||||
if (release.releaseMBID)
|
||||
os << " (" << release.releaseMBID->getAsString() << ")" << std::endl;
|
||||
|
||||
if (release.totalDisc)
|
||||
std::cout << "\tTotalDisc: " << *release.totalDisc << std::endl;
|
||||
|
||||
for (const MetaData::Artist& artist : release.releaseArtists)
|
||||
std::cout << "\tAlbum artist: " << artist << std::endl;
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
static
|
||||
std::ostream&
|
||||
operator<<(std::ostream& os, const MetaData::Disc& disc)
|
||||
{
|
||||
if (!disc.subtitle.empty())
|
||||
os << disc.subtitle << std::endl;
|
||||
|
||||
if (disc.totalTrack)
|
||||
std::cout << "\tTotalTrack: " << *disc.totalTrack << std::endl;
|
||||
|
||||
if (disc.replayGain)
|
||||
std::cout << "\tDisc replay gain: " << *disc.replayGain << std::endl;
|
||||
|
||||
return os;
|
||||
}
|
||||
@@ -74,9 +100,6 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
||||
for (const Artist& artist : track->artists)
|
||||
std::cout << "Artist: " << artist << std::endl;
|
||||
|
||||
for (const Artist& artist : track->albumArtists)
|
||||
std::cout << "Album artist: " << artist << std::endl;
|
||||
|
||||
for (const Artist& artist : track->conductorArtists)
|
||||
std::cout << "Conductor: " << artist << std::endl;
|
||||
|
||||
@@ -105,8 +128,11 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
||||
for (const Artist& artist : track->remixerArtists)
|
||||
std::cout << "Remixer: " << artist << std::endl;
|
||||
|
||||
if (track->album)
|
||||
std::cout << "Album: " << *track->album << std::endl;
|
||||
if (track->release)
|
||||
std::cout << "Release: " << *track->release;
|
||||
|
||||
if (track->disc)
|
||||
std::cout << "Disc: " << *track->disc;
|
||||
|
||||
std::cout << "Title: " << track->title << std::endl;
|
||||
|
||||
@@ -130,18 +156,9 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
||||
if (track->trackNumber)
|
||||
std::cout << "Track: " << *track->trackNumber << std::endl;
|
||||
|
||||
if (track->totalTrack)
|
||||
std::cout << "TotalTrack: " << *track->totalTrack << std::endl;
|
||||
|
||||
if (track->discNumber)
|
||||
std::cout << "Disc: " << *track->discNumber << std::endl;
|
||||
|
||||
if (!track->discSubtitle.empty())
|
||||
std::cout << "Disc Subtitle: " << track->discSubtitle << std::endl;
|
||||
|
||||
if (track->totalDisc)
|
||||
std::cout << "TotalDisc: " << *track->totalDisc << std::endl;
|
||||
|
||||
if (track->date.isValid())
|
||||
std::cout << "Date: " << track->date.toString("yyyy-MM-dd") << std::endl;
|
||||
|
||||
@@ -153,11 +170,8 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
||||
for (const auto& audioStream : track->audioStreams)
|
||||
std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl;
|
||||
|
||||
if (track->trackReplayGain)
|
||||
std::cout << "Track replay gain: " << *track->trackReplayGain << std::endl;
|
||||
|
||||
if (track->albumReplayGain)
|
||||
std::cout << "Album replay gain: " << *track->albumReplayGain << std::endl;
|
||||
if (track->replayGain)
|
||||
std::cout << "Track replay gain: " << *track->replayGain << std::endl;
|
||||
|
||||
if (track->acoustID)
|
||||
std::cout << "AcoustID: " << track->acoustID->getAsString() << std::endl;
|
||||
|
||||
Reference in New Issue
Block a user