Refactored how filter tags are handled in the scan settings
This commit is contained in:
@@ -29,234 +29,222 @@
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template <typename T>
|
||||
std::optional<T> findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
{
|
||||
auto it = std::find_first_of(std::cbegin(metadataMap), std::cend(metadataMap), std::cbegin(tags), std::cend(tags), [](const auto& it, const auto& str) { return it.first == str; });
|
||||
if (it == std::cend(metadataMap))
|
||||
return std::nullopt;
|
||||
|
||||
template <typename T>
|
||||
std::optional<T>
|
||||
findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
{
|
||||
auto it = std::find_first_of(std::cbegin(metadataMap), std::cend(metadataMap), std::cbegin(tags), std::cend(tags), [](const auto& it, const auto& str) { return it.first == str; });
|
||||
if (it == std::cend(metadataMap))
|
||||
return std::nullopt;
|
||||
return StringUtils::readAs<T>(StringUtils::stringTrim(it->second));
|
||||
}
|
||||
|
||||
return StringUtils::readAs<T>(StringUtils::stringTrim(it->second));
|
||||
}
|
||||
template <>
|
||||
std::optional<std::vector<UUID>> findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
{
|
||||
std::optional<std::string> str{ findFirstValueOfAs<std::string>(metadataMap, tags) };
|
||||
if (!str)
|
||||
return std::nullopt;
|
||||
|
||||
template <>
|
||||
std::optional<std::vector<UUID>>
|
||||
findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
{
|
||||
std::optional<std::string> str {findFirstValueOfAs<std::string>(metadataMap, tags)};
|
||||
if (!str)
|
||||
return std::nullopt;
|
||||
const std::vector<std::string_view> strUuids{ StringUtils::splitString(*str, "/") };
|
||||
std::vector<UUID> res;
|
||||
|
||||
const std::vector<std::string_view> strUuids {StringUtils::splitString(*str, "/")};
|
||||
std::vector<UUID> res;
|
||||
for (std::string_view strUuid : strUuids)
|
||||
{
|
||||
std::optional<UUID> uuid{ UUID::fromString(strUuid) };
|
||||
if (!uuid)
|
||||
return std::nullopt;
|
||||
|
||||
for (std::string_view strUuid : strUuids)
|
||||
{
|
||||
std::optional<UUID> uuid {UUID::fromString(strUuid)};
|
||||
if (!uuid)
|
||||
return std::nullopt;
|
||||
res.push_back(std::move(*uuid));
|
||||
}
|
||||
|
||||
res.push_back(std::move(*uuid));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
std::vector<Artist> getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> res;
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> res;
|
||||
auto name{ findFirstValueOfAs<std::string>(metadataMap, {"ALBUM_ARTIST"}) };
|
||||
if (!name)
|
||||
return res;
|
||||
|
||||
auto name {findFirstValueOfAs<std::string>(metadataMap, {"ALBUM_ARTIST"})};
|
||||
if (!name)
|
||||
return res;
|
||||
auto mbid{ findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"}) };
|
||||
|
||||
auto mbid {findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"})};
|
||||
return { Artist {mbid, *name, std::nullopt} };
|
||||
}
|
||||
|
||||
return {Artist {mbid, *name, std::nullopt} };
|
||||
}
|
||||
std::vector<Artist> getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> artists;
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> artists;
|
||||
std::vector<std::string_view> artistNames;
|
||||
if (metadataMap.find("ARTISTS") != metadataMap.end())
|
||||
{
|
||||
artistNames = StringUtils::splitString(metadataMap.find("ARTISTS")->second, "/;");
|
||||
}
|
||||
else if (metadataMap.find("ARTIST") != metadataMap.end())
|
||||
{
|
||||
artistNames = { metadataMap.find("ARTIST")->second };
|
||||
}
|
||||
|
||||
std::vector<std::string_view> artistNames;
|
||||
if (metadataMap.find("ARTISTS") != metadataMap.end())
|
||||
{
|
||||
artistNames = StringUtils::splitString(metadataMap.find("ARTISTS")->second, "/;");
|
||||
}
|
||||
else if (metadataMap.find("ARTIST") != metadataMap.end())
|
||||
{
|
||||
artistNames = {metadataMap.find("ARTIST")->second};
|
||||
}
|
||||
auto artistMBIDs{ findFirstValueOfAs<std::vector<UUID>>(metadataMap, {"MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ/ARTIST ID"}) };
|
||||
|
||||
auto artistMBIDs {findFirstValueOfAs<std::vector<UUID>>(metadataMap, {"MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ/ARTIST ID"})};
|
||||
for (std::size_t i{}; i < artistNames.size(); ++i)
|
||||
{
|
||||
if (artistMBIDs && artistNames.size() == artistMBIDs->size())
|
||||
artists.emplace_back(Artist{ (*artistMBIDs)[i], artistNames[i], std::nullopt });
|
||||
else
|
||||
artists.emplace_back(Artist{ std::nullopt, artistNames[i], std::nullopt });
|
||||
}
|
||||
|
||||
for (std::size_t i {}; i < artistNames.size(); ++i)
|
||||
{
|
||||
if (artistMBIDs && artistNames.size() == artistMBIDs->size())
|
||||
artists.emplace_back(Artist {(*artistMBIDs)[i], artistNames[i], std::nullopt});
|
||||
else
|
||||
artists.emplace_back(Artist {std::nullopt, artistNames[i], std::nullopt});
|
||||
}
|
||||
return artists;
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
std::optional<Release> getRelease(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Release> res;
|
||||
|
||||
static
|
||||
std::optional<Release>
|
||||
getRelease(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Release> res;
|
||||
std::optional<std::string> releaseName{ findFirstValueOfAs<std::string>(metadataMap, {"ALBUM", "TALB", "WM/ALBUMTITLE"}) };
|
||||
if (!releaseName)
|
||||
return res;
|
||||
|
||||
std::optional<std::string> releaseName {findFirstValueOfAs<std::string>(metadataMap, {"ALBUM", "TALB", "WM/ALBUMTITLE"})};
|
||||
if (!releaseName)
|
||||
return res;
|
||||
res.emplace();
|
||||
res->name = std::move(*releaseName);
|
||||
res->mbid = findFirstValueOfAs<UUID>(metadataMap, { "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID" });
|
||||
res->artists = getReleaseArtists(metadataMap);
|
||||
res->mediumCount = findFirstValueOfAs<std::size_t>(metadataMap, { "TOTALDISCS", "DISCTOTAL" });
|
||||
if (!res->mediumCount)
|
||||
{
|
||||
// mediumCount may be encoded as position/count
|
||||
if (const auto value{ findFirstValueOfAs<std::string>(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"}) })
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
|
||||
if (strings.size() == 2)
|
||||
res->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
|
||||
res.emplace();
|
||||
res->name = std::move(*releaseName);
|
||||
res->mbid = findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID"});
|
||||
res->artists = getReleaseArtists(metadataMap);
|
||||
res->mediumCount = findFirstValueOfAs<std::size_t>(metadataMap, {"TOTALDISCS", "DISCTOTAL"});
|
||||
if (!res->mediumCount)
|
||||
{
|
||||
// mediumCount may be encoded as position/count
|
||||
if (const auto value {findFirstValueOfAs<std::string>(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"})})
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") };
|
||||
if (strings.size() == 2)
|
||||
res->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
std::optional<Medium> getMedium(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Medium> res;
|
||||
res.emplace();
|
||||
|
||||
static
|
||||
std::optional<Medium>
|
||||
getMedium(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Medium> res;
|
||||
res.emplace();
|
||||
res->type = findFirstValueOfAs<std::string>(metadataMap, { "TMED", "MEDIA", "WM/MEDIA" }).value_or("");
|
||||
res->name = findFirstValueOfAs<std::string>(metadataMap, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
|
||||
res->trackCount = findFirstValueOfAs<std::size_t>(metadataMap, { "TOTALTRACKS", "TRACKTOTAL" });
|
||||
if (!res->trackCount)
|
||||
{
|
||||
// totalTracks may be encoded as "position/count"
|
||||
if (const auto value{ findFirstValueOfAs<std::string>(metadataMap, {"TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER"}) })
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
|
||||
if (strings.size() == 2)
|
||||
res->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
|
||||
res->type = findFirstValueOfAs<std::string>(metadataMap, {"TMED", "MEDIA", "WM/MEDIA"}).value_or("");
|
||||
res->name = findFirstValueOfAs<std::string>(metadataMap, {"TSST", "DISCSUBTITLE", "SETSUBTITLE"}).value_or("");
|
||||
res->trackCount = findFirstValueOfAs<std::size_t>(metadataMap, {"TOTALTRACKS", "TRACKTOTAL"});
|
||||
if (!res->trackCount)
|
||||
{
|
||||
// totalTracks may be encoded as "position/count"
|
||||
if (const auto value {findFirstValueOfAs<std::string>(metadataMap, {"TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER"})})
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") };
|
||||
if (strings.size() == 2)
|
||||
res->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
// position may be encoded in TPOS/DISC/DISK as "position/count". Expecting 'Number[/Total]'
|
||||
res->position = findFirstValueOfAs<std::size_t>(metadataMap, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" });
|
||||
res->release = getRelease(metadataMap);
|
||||
|
||||
// position may be encoded in TPOS/DISC/DISK as "position/count". Expecting 'Number[/Total]'
|
||||
res->position = findFirstValueOfAs<std::size_t>(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"});
|
||||
res->release = getRelease(metadataMap);
|
||||
if (res->type.empty()
|
||||
&& res->name.empty()
|
||||
&& !res->trackCount
|
||||
&& !res->position
|
||||
&& !res->release
|
||||
&& !res->replayGain)
|
||||
{
|
||||
res.reset();
|
||||
}
|
||||
|
||||
if (res->type.empty()
|
||||
&& res->name.empty()
|
||||
&& !res->trackCount
|
||||
&& !res->position
|
||||
&& !res->release
|
||||
&& !res->replayGain)
|
||||
{
|
||||
res.reset();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
std::optional<Track> AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
Track track;
|
||||
|
||||
std::optional<Track>
|
||||
AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
Track track;
|
||||
try
|
||||
{
|
||||
const auto mediaFile{ Av::parseAudioFile(p) };
|
||||
|
||||
try
|
||||
{
|
||||
const auto mediaFile {Av::parseAudioFile(p)};
|
||||
Av::ContainerInfo info{ mediaFile->getContainerInfo() };
|
||||
track.duration = info.duration;
|
||||
track.bitrate = info.bitrate;
|
||||
track.hasCover = mediaFile->hasAttachedPictures();
|
||||
|
||||
Av::ContainerInfo info{ mediaFile->getContainerInfo() };
|
||||
track.duration = info.duration;
|
||||
track.bitrate = info.bitrate;
|
||||
track.hasCover = mediaFile->hasAttachedPictures();
|
||||
MetaData::Tags tags;
|
||||
|
||||
MetaData::Tags tags;
|
||||
const Av::IAudioFile::MetadataMap metadataMap{ mediaFile->getMetaData() };
|
||||
|
||||
const Av::IAudioFile::MetadataMap metadataMap {mediaFile->getMetaData()};
|
||||
track.artists = getArtists(metadataMap);
|
||||
track.medium = getMedium(metadataMap);
|
||||
|
||||
track.artists = getArtists(metadataMap);
|
||||
track.medium = getMedium(metadataMap);
|
||||
for (const auto& [tag, value] : metadataMap)
|
||||
{
|
||||
if (debug)
|
||||
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
|
||||
|
||||
for (const auto& [tag, value] : metadataMap)
|
||||
{
|
||||
if (debug)
|
||||
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
|
||||
if (tag == "TITLE")
|
||||
track.title = value;
|
||||
else if (tag == "TRACK")
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
track.position = StringUtils::readAs<std::size_t>(value);
|
||||
}
|
||||
else if (tag == "DATE"
|
||||
|| tag == "YEAR"
|
||||
|| tag == "WM/YEAR")
|
||||
{
|
||||
track.date = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "TDOR" // Original release time (ID3v2 2.4)
|
||||
|| tag == "TORY") // Original release year
|
||||
{
|
||||
track.originalDate = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "ACOUSTID ID")
|
||||
{
|
||||
track.acoustID = UUID::fromString(value);
|
||||
}
|
||||
else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
|
||||
|| tag == "MUSICBRAINZ_RELEASETRACKID")
|
||||
{
|
||||
track.mbid = UUID::fromString(value);
|
||||
}
|
||||
else if (tag == "MUSICBRAINZ_TRACKID"
|
||||
|| tag == "MUSICBRAINZ/TRACK ID")
|
||||
{
|
||||
track.recordingMBID = UUID::fromString(value);
|
||||
}
|
||||
else if (std::find(std::cbegin(_extraTags), std::cend(_extraTags), tag) != std::cend(_extraTags))
|
||||
{
|
||||
const std::vector<std::string_view> tagValues{ StringUtils::splitString(value, "/,;") };
|
||||
|
||||
if (tag == "TITLE")
|
||||
track.title = value;
|
||||
else if (tag == "TRACK")
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
track.position = StringUtils::readAs<std::size_t>(value);
|
||||
}
|
||||
else if (tag == "DATE"
|
||||
|| tag == "YEAR"
|
||||
|| tag == "WM/YEAR")
|
||||
{
|
||||
track.date = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "TDOR" // Original release time (ID3v2 2.4)
|
||||
|| tag == "TORY") // Original release year
|
||||
{
|
||||
track.originalDate = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "ACOUSTID ID")
|
||||
{
|
||||
track.acoustID = UUID::fromString(value);
|
||||
}
|
||||
else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
|
||||
|| tag == "MUSICBRAINZ_RELEASETRACKID")
|
||||
{
|
||||
track.mbid = UUID::fromString(value);
|
||||
}
|
||||
else if (tag == "MUSICBRAINZ_TRACKID"
|
||||
|| tag == "MUSICBRAINZ/TRACK ID")
|
||||
{
|
||||
track.recordingMBID = UUID::fromString(value);
|
||||
}
|
||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||
{
|
||||
const std::vector<std::string_view> clusterNames {StringUtils::splitString(value, "/,;")};
|
||||
if (!tagValues.empty())
|
||||
{
|
||||
std::set<std::string> values;
|
||||
std::transform(std::cbegin(tagValues), std::cend(tagValues), std::inserter(values, std::begin(values)), [](std::string_view v) { return std::string{ v }; });
|
||||
track.tags[tag] = std::move(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Av::Exception& e)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!clusterNames.empty())
|
||||
{
|
||||
std::set<std::string> values;
|
||||
std::transform(std::cbegin(clusterNames), std::cend(clusterNames),
|
||||
std::inserter(values, std::begin(values)),
|
||||
[](std::string_view clusterName) { return std::string {clusterName}; });
|
||||
track.tags[tag] = std::move(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Av::Exception& e)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return track;
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
} // namespace MetaData
|
||||
|
||||
|
||||
@@ -368,18 +368,18 @@ namespace MetaData
|
||||
track.replayGain = StringUtils::readAs<float>(value);
|
||||
else if (tag == "ARTIST")
|
||||
track.artistDisplayName = value;
|
||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||
else if (std::find(std::cbegin(_extraTags), std::cend(_extraTags), tag) != std::cend(_extraTags))
|
||||
{
|
||||
std::set<std::string> clusterNames;
|
||||
std::set<std::string> tagValues;
|
||||
for (std::string_view valueList : values)
|
||||
{
|
||||
const std::vector<std::string_view> splittedValues{ splitAndTrimString(valueList, "/,;") };
|
||||
const std::vector<std::string_view> splittedValues{ splitAndTrimString(valueList, "/,;") }; // handle possibily bad split tags
|
||||
for (std::string_view value : splittedValues)
|
||||
clusterNames.insert(std::string{ value });
|
||||
tagValues.insert(std::string{ value });
|
||||
}
|
||||
|
||||
if (!clusterNames.empty())
|
||||
track.tags[tag] = std::move(clusterNames);
|
||||
if (!tagValues.empty())
|
||||
track.tags[tag] = std::move(tagValues);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
using Tags = std::map<std::string /* type */, std::set<std::string> /* names */>;
|
||||
using Tags = std::map<std::string /* type */, std::set<std::string> /* values */>;
|
||||
|
||||
// Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema
|
||||
|
||||
@@ -131,10 +131,10 @@ namespace MetaData
|
||||
|
||||
virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
|
||||
|
||||
void setClusterTypeNames(const std::set<std::string>& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; }
|
||||
void setExtraTags(const std::vector<std::string>& extraTags) { _extraTags = std::set(extraTags.cbegin(), extraTags.cend()); }
|
||||
|
||||
protected:
|
||||
std::set<std::string> _clusterTypeNames;
|
||||
std::set<std::string> _extraTags;
|
||||
};
|
||||
|
||||
enum class ParserType
|
||||
|
||||
@@ -288,7 +288,7 @@ namespace Database
|
||||
return Utils::execQuery<ArtistId>(query, range);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
|
||||
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -300,8 +300,8 @@ namespace Database
|
||||
where.And(WhereClause("a.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
for (ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
@@ -44,14 +44,19 @@ namespace Database
|
||||
query.join("track_cluster t_c ON t_c.cluster_id = c.id");
|
||||
query.join("track t ON t.id = t_c.track_id");
|
||||
}
|
||||
if (!params.clusterTypeName.empty())
|
||||
query.join("cluster_type c_t ON c_t.id = c.cluster_type_id");
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
assert(!params.clusterType.isValid() || params.clusterTypeName.empty());
|
||||
if (params.clusterType.isValid())
|
||||
query.where("c.cluster_type_id = ?").bind(params.clusterType);
|
||||
else if (!params.clusterTypeName.empty())
|
||||
query.where("c_t.name = ?").bind(params.clusterTypeName);
|
||||
|
||||
return query;
|
||||
}
|
||||
@@ -170,7 +175,7 @@ namespace Database
|
||||
}
|
||||
|
||||
|
||||
RangeResults<ClusterTypeId> ClusterType::findOrphans(Session& session, std::optional<Range> range)
|
||||
RangeResults<ClusterTypeId> ClusterType::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
@@ -207,7 +212,7 @@ namespace Database
|
||||
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<ClusterTypeId> ClusterType::find(Session& session, std::optional<Range> range)
|
||||
RangeResults<ClusterTypeId> ClusterType::findIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
|
||||
@@ -249,6 +249,24 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
session.getDboSession().execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")");
|
||||
}
|
||||
|
||||
void migrateFromV46(Session& session)
|
||||
{
|
||||
// add extra tags to parse
|
||||
session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null
|
||||
);)");
|
||||
session.getDboSession().execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type");
|
||||
session.getDboSession().execute("DROP TABLE cluster_type");
|
||||
session.getDboSession().execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type");
|
||||
|
||||
session.getDboSession().execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT");
|
||||
|
||||
// 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)
|
||||
{
|
||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -273,6 +291,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
{43, migrateFromV43},
|
||||
{44, migrateFromV44},
|
||||
{45, migrateFromV45},
|
||||
{46, migrateFromV46},
|
||||
};
|
||||
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Database
|
||||
class Session;
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 46 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 47 };
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -481,7 +481,7 @@ namespace Database
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -494,8 +494,8 @@ namespace Database
|
||||
where.And(WhereClause("r.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
for (const ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
|
||||
@@ -30,30 +30,6 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
const std::set<std::string_view> defaultClusterTypeNames =
|
||||
{
|
||||
"GENRE",
|
||||
"ALBUMGROUPING",
|
||||
"MOOD",
|
||||
"ALBUMMOOD",
|
||||
};
|
||||
|
||||
}
|
||||
void ScanSettings::init(Session& session)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
pointer settings{ get(session) };
|
||||
if (settings)
|
||||
return;
|
||||
|
||||
settings = session.getDboSession().add(std::make_unique<ScanSettings>());
|
||||
settings.modify()->setClusterTypes(session, defaultClusterTypeNames);
|
||||
}
|
||||
|
||||
ScanSettings::pointer ScanSettings::get(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
@@ -77,9 +53,9 @@ namespace Database
|
||||
_audioFileExtensions += " " + ext.string();
|
||||
}
|
||||
|
||||
std::vector<ClusterType::pointer> ScanSettings::getClusterTypes() const
|
||||
std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const
|
||||
{
|
||||
return std::vector<ClusterType::pointer>(std::cbegin(_clusterTypes), std::cend(_clusterTypes));
|
||||
return StringUtils::splitString(_extraTagsToScan, ";");
|
||||
}
|
||||
|
||||
void ScanSettings::setMediaDirectory(const std::filesystem::path& p)
|
||||
@@ -87,58 +63,17 @@ namespace Database
|
||||
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
|
||||
}
|
||||
|
||||
template <typename It>
|
||||
std::set<std::string> getNames(It begin, It end)
|
||||
void ScanSettings::setExtraTagsToScan(const std::vector<std::string_view>& extraTags)
|
||||
{
|
||||
std::set<std::string> names;
|
||||
std::transform(begin, end, std::inserter(names, std::cbegin(names)),
|
||||
[](const ClusterType::pointer& clusterType)
|
||||
{
|
||||
return clusterType->getName();
|
||||
});
|
||||
std::string newTagsToScan{ StringUtils::joinStrings(extraTags, ";") };
|
||||
if (newTagsToScan != _extraTagsToScan)
|
||||
incScanVersion();
|
||||
|
||||
return names;
|
||||
_extraTagsToScan = std::move(newTagsToScan);
|
||||
}
|
||||
|
||||
void ScanSettings::setClusterTypes(Session& session, const std::set<std::string_view>& clusterTypeNames)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
bool needRescan{};
|
||||
|
||||
// Create any missing cluster type
|
||||
for (const std::string_view clusterTypeName : clusterTypeNames)
|
||||
{
|
||||
ClusterType::pointer clusterType{ ClusterType::find(session, clusterTypeName) };
|
||||
if (!clusterType)
|
||||
{
|
||||
LMS_LOG(DB, INFO, "Creating cluster type " << clusterTypeName);
|
||||
clusterType = session.create<ClusterType>(clusterTypeName);
|
||||
_clusterTypes.insert(getDboPtr(clusterType));
|
||||
|
||||
needRescan = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete no longer existing cluster types
|
||||
for (Wt::Dbo::ptr<ClusterType> clusterType : _clusterTypes)
|
||||
{
|
||||
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
|
||||
[clusterType](std::string_view name) { return name == clusterType->getName(); }))
|
||||
{
|
||||
LMS_LOG(DB, INFO, "Deleting cluster type " << clusterType->getName());
|
||||
clusterType.remove();
|
||||
}
|
||||
}
|
||||
|
||||
if (needRescan)
|
||||
_scanVersion += 1;
|
||||
}
|
||||
|
||||
void
|
||||
ScanSettings::incScanVersion()
|
||||
void ScanSettings::incScanVersion()
|
||||
{
|
||||
_scanVersion += 1;
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -171,13 +171,6 @@ namespace Database
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)");
|
||||
}
|
||||
|
||||
// Initial settings tables
|
||||
{
|
||||
auto uniqueTransaction{ createWriteTransaction() };
|
||||
|
||||
ScanSettings::init(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void Session::analyze()
|
||||
|
||||
@@ -67,11 +67,11 @@ WhereClause::get() const
|
||||
}
|
||||
|
||||
WhereClause&
|
||||
WhereClause::bind(const std::string& bindArg)
|
||||
WhereClause::bind(std::string_view bindArg)
|
||||
{
|
||||
assert(_bindArgs.size() < static_cast<std::size_t>(std::count(_clause.begin(), _clause.end(), '?')));
|
||||
|
||||
_bindArgs.push_back(bindArg);
|
||||
_bindArgs.push_back(std::string{ bindArg });
|
||||
|
||||
return *this;
|
||||
}
|
||||
@@ -102,8 +102,8 @@ SelectStatement::And(const std::string& statement)
|
||||
{
|
||||
_statement.push_back(statement);
|
||||
|
||||
_statement.sort();
|
||||
_statement.unique();
|
||||
std::sort(_statement.begin(), _statement.end());
|
||||
_statement.erase(std::unique(_statement.begin(), _statement.end()), _statement.end());
|
||||
|
||||
return *this;
|
||||
}
|
||||
@@ -113,7 +113,7 @@ SelectStatement::get() const
|
||||
{
|
||||
std::string res = "SELECT ";
|
||||
|
||||
for (std::list<std::string>::const_iterator it = _statement.begin(); it != _statement.end(); ++it)
|
||||
for (auto it = _statement.begin(); it != _statement.end(); ++it)
|
||||
{
|
||||
if (it != _statement.begin())
|
||||
res += ",";
|
||||
@@ -150,8 +150,8 @@ FromClause::And(const FromClause& clause)
|
||||
_clause.push_back(fromClause);
|
||||
}
|
||||
|
||||
_clause.sort();
|
||||
_clause.unique();
|
||||
std::sort(_clause.begin(), _clause.end());
|
||||
_clause.erase(std::unique(_clause.begin(), _clause.end()), _clause.end());
|
||||
|
||||
return *this;
|
||||
}
|
||||
@@ -164,7 +164,7 @@ FromClause::get() const
|
||||
if (!_clause.empty())
|
||||
{
|
||||
oss << "FROM ";
|
||||
for (std::list<std::string>::const_iterator it = _clause.begin(); it != _clause.end(); ++it) {
|
||||
for (auto it = _clause.begin(); it != _clause.end(); ++it) {
|
||||
if (it != _clause.begin())
|
||||
oss << ",";
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <list>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
|
||||
@@ -33,14 +33,14 @@ class WhereClause
|
||||
WhereClause& Or(const WhereClause& clause);
|
||||
|
||||
// Arguments binding (for each '?' in where clause)
|
||||
WhereClause& bind(const std::string& arg);
|
||||
WhereClause& bind(std::string_view arg);
|
||||
|
||||
std::string get() const;
|
||||
const std::list<std::string>& getBindArgs() const {return _bindArgs;}
|
||||
const std::vector<std::string>& getBindArgs() const {return _bindArgs;}
|
||||
|
||||
private:
|
||||
std::string _clause; // WHERE clause
|
||||
std::list<std::string> _bindArgs;
|
||||
std::vector<std::string> _bindArgs;
|
||||
};
|
||||
|
||||
class InnerJoinClause
|
||||
@@ -81,7 +81,7 @@ class SelectStatement
|
||||
std::string get() const;
|
||||
|
||||
private:
|
||||
std::list<std::string> _statement;
|
||||
std::vector<std::string> _statement;
|
||||
};
|
||||
|
||||
class FromClause
|
||||
@@ -95,7 +95,7 @@ class FromClause
|
||||
std::string get() const;
|
||||
|
||||
private:
|
||||
std::list<std::string> _clause;
|
||||
std::vector<std::string> _clause;
|
||||
};
|
||||
|
||||
class SqlQuery
|
||||
|
||||
@@ -479,7 +479,7 @@ namespace Database
|
||||
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end());
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
@@ -493,8 +493,8 @@ namespace Database
|
||||
where.And(WhereClause("t.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
for (ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
@@ -514,8 +514,8 @@ namespace Database
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (auto cluster_list : clusters)
|
||||
res.push_back(cluster_list.second);
|
||||
for (const auto& [type, clusters] : clusters)
|
||||
res.push_back(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -187,12 +187,12 @@ namespace Database
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
|
||||
if (clusterTypes.empty())
|
||||
if (clusterTypeIds.empty())
|
||||
return res;
|
||||
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c from cluster c") };
|
||||
@@ -208,12 +208,12 @@ namespace Database
|
||||
std::ostringstream oss;
|
||||
oss << "c_type.id IN (";
|
||||
bool first{ true };
|
||||
for (auto clusterType : clusterTypes)
|
||||
for (ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
{
|
||||
if (!first)
|
||||
oss << ", ";
|
||||
oss << "?";
|
||||
query.bind(clusterType->getId());
|
||||
query.bind(clusterTypeId);
|
||||
first = false;
|
||||
}
|
||||
oss << ")";
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace Database
|
||||
// Get the cluster of the tracks made by this artist
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ObjectPtr<ClusterType>> clusterTypes, std::size_t size) const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const;
|
||||
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
|
||||
@@ -38,7 +38,6 @@ namespace Database {
|
||||
|
||||
class Track;
|
||||
class ClusterType;
|
||||
class ScanSettings;
|
||||
class Session;
|
||||
|
||||
class Cluster final : public Object<Cluster, ClusterId>
|
||||
@@ -47,12 +46,14 @@ namespace Database {
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
|
||||
ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
|
||||
std::string clusterTypeName; // if non empty, clusters that belong to this cluster type
|
||||
TrackId track; // if set, clusters involved in this track
|
||||
ReleaseId release; // if set, clusters involved in this release
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; }
|
||||
FindParameters& setClusterTypeName(std::string_view _name) { clusterTypeName = _name; return *this; }
|
||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
};
|
||||
@@ -117,10 +118,10 @@ namespace Database {
|
||||
|
||||
// Getters
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<ClusterTypeId> find(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<ClusterTypeId> findIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static pointer find(Session& session, std::string_view name);
|
||||
static pointer find(Session& session, ClusterTypeId id);
|
||||
static RangeResults<ClusterTypeId> findOrphans(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<ClusterTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<ClusterTypeId> findUsed(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
static void remove(Session& session, const std::string& name);
|
||||
@@ -135,7 +136,6 @@ namespace Database {
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
|
||||
Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -147,7 +147,6 @@ namespace Database {
|
||||
|
||||
std::string _name;
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
|
||||
Wt::Dbo::ptr<ScanSettings> _scanSettings;
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Database
|
||||
// Get the cluster of the tracks that belong to this release
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
|
||||
// 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;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
|
||||
|
||||
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
||||
Wt::WDate getReleaseDate() const;
|
||||
|
||||
@@ -32,9 +32,8 @@
|
||||
|
||||
LMS_DECLARE_IDTYPE(ScanSettingsId)
|
||||
|
||||
namespace Database {
|
||||
|
||||
class ClusterType;
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
|
||||
class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
|
||||
@@ -67,7 +66,7 @@ namespace Database {
|
||||
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::vector<ObjectPtr<ClusterType>> getClusterTypes() const;
|
||||
std::vector<std::string_view> getExtraTagsToScan() const;
|
||||
std::vector<std::filesystem::path> getAudioFileExtensions() const;
|
||||
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
||||
|
||||
@@ -76,7 +75,7 @@ namespace Database {
|
||||
void setMediaDirectory(const std::filesystem::path& p);
|
||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
||||
void setClusterTypes(Session& session, const std::set<std::string_view>& clusterTypeNames);
|
||||
void setExtraTagsToScan(const std::vector<std::string_view>& extraTags);
|
||||
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
||||
void incScanVersion();
|
||||
|
||||
@@ -89,20 +88,17 @@ namespace Database {
|
||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
|
||||
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
||||
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
|
||||
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
||||
}
|
||||
|
||||
private:
|
||||
int _scanVersion{};
|
||||
std::string _mediaDirectory;
|
||||
Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
|
||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||
int _scanVersion{};
|
||||
std::string _mediaDirectory;
|
||||
Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
|
||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
||||
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
|
||||
std::string _extraTagsToScan;
|
||||
};
|
||||
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace Database {
|
||||
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;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Database {
|
||||
|
||||
// Get clusters, order by occurence
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
|
||||
|
||||
bool hasTrack(TrackId trackId) const;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ TEST_F(DatabaseFixture, Cluster)
|
||||
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
||||
}
|
||||
|
||||
auto clusterTypes{ ClusterType::find(session) };
|
||||
auto clusterTypes{ ClusterType::findIds(session) };
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
|
||||
@@ -68,7 +68,7 @@ TEST_F(DatabaseFixture, Cluster)
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
|
||||
clusterTypes = ClusterType::findOrphans(session);
|
||||
clusterTypes = ClusterType::findOrphanIds(session);
|
||||
EXPECT_TRUE(clusterTypes.results.empty());
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ TEST_F(DatabaseFixture, Cluster)
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto clusterTypes{ ClusterType::findOrphans(session) };
|
||||
auto clusterTypes{ ClusterType::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
|
||||
@@ -92,7 +92,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
auto clusterTypes{ ClusterType::findOrphans(session) };
|
||||
auto clusterTypes{ ClusterType::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
}
|
||||
@@ -131,7 +131,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster2.getId());
|
||||
|
||||
EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
|
||||
EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -237,6 +237,38 @@ TEST_F(DatabaseFixture, Cluster_multiTracks)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST_F(DatabaseFixture, ClusterType_singleTrack)
|
||||
{
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::find(session, Cluster::FindParameters{}).results.empty());
|
||||
EXPECT_TRUE(Cluster::find(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results.empty());
|
||||
}
|
||||
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto clusters {Cluster::findIds(session, Cluster::FindParameters{}).results};
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
EXPECT_EQ(clusters.front(), cluster.getId());
|
||||
|
||||
clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterType(clusterType.getId())).results;
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
EXPECT_EQ(clusters.front(), cluster.getId());
|
||||
|
||||
clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results;
|
||||
EXPECT_EQ(clusters.size(), 0);
|
||||
|
||||
clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("MyClusterType")).results;
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
EXPECT_EQ(clusters.front(), cluster.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
@@ -329,7 +361,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
|
||||
EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
|
||||
EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 2);
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
@@ -542,7 +574,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
|
||||
EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
@@ -59,17 +59,6 @@ void DatabaseFixture::SetUpTestCase()
|
||||
Database::Session s{ _tmpDb->getDb() };
|
||||
s.prepareTables();
|
||||
s.analyze();
|
||||
|
||||
// remove default created entries
|
||||
{
|
||||
auto transaction{ s.createWriteTransaction() };
|
||||
|
||||
for (const Database::ClusterTypeId clusterTypeId : Database::ClusterType::find(s).results)
|
||||
{
|
||||
auto clusterType{ Database::ClusterType::find(s, clusterTypeId) };
|
||||
clusterType.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,6 @@ TEST_F(DatabaseFixture, Release_findByNameAndPath)
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
std::cout << "OK HERE" << std::endl;
|
||||
{
|
||||
const auto releases{ Release::find(session, "MyRelease", "/tmp/foo") };
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace Recommendation
|
||||
const auto& refVector = network.getRefVector({ x, y });
|
||||
|
||||
boost::property_tree::ptree node;
|
||||
for (auto value : refVector)
|
||||
for (const auto& value : refVector)
|
||||
node.add("values.value", value);
|
||||
|
||||
node.put("coord_x", x);
|
||||
|
||||
@@ -74,6 +74,7 @@ namespace Scanner
|
||||
{
|
||||
removeOrphanTracks(context);
|
||||
removeOrphanClusters();
|
||||
removeOrphanClusterTypes();
|
||||
removeOrphanArtists();
|
||||
removeOrphanReleases();
|
||||
}
|
||||
@@ -152,6 +153,12 @@ namespace Scanner
|
||||
removeOrphanEntries<Database::Cluster>(_db.getTLSSession(), _abortScan);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanDbFiles::removeOrphanClusterTypes()
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan cluster types...");
|
||||
removeOrphanEntries<Database::ClusterType>(_db.getTLSSession(), _abortScan);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan artists...");
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace Scanner
|
||||
|
||||
void removeOrphanTracks(ScanContext& context);
|
||||
void removeOrphanClusters();
|
||||
void removeOrphanClusterTypes();
|
||||
void removeOrphanArtists();
|
||||
void removeOrphanReleases();
|
||||
bool checkFile(const std::filesystem::path& p);
|
||||
|
||||
@@ -241,9 +241,9 @@ namespace Scanner
|
||||
{
|
||||
auto clusterType = ClusterType::find(session, tag);
|
||||
if (!clusterType)
|
||||
continue;
|
||||
clusterType = session.create<ClusterType>(tag);
|
||||
|
||||
for (auto clusterName : values)
|
||||
for (const auto& clusterName : values)
|
||||
{
|
||||
auto cluster = clusterType->getCluster(clusterName);
|
||||
if (!cluster)
|
||||
@@ -279,7 +279,10 @@ namespace Scanner
|
||||
|
||||
void ScanStepScanFiles::process(ScanContext& context)
|
||||
{
|
||||
_metadataParser->setClusterTypeNames(_settings.clusterTypeNames);
|
||||
std::vector<std::string> tagsToParse{ _tagsToParse };
|
||||
tagsToParse.insert(std::end(tagsToParse), std::cbegin(_settings.extraTags), std::cend(_settings.extraTags));
|
||||
|
||||
_metadataParser->setExtraTags(tagsToParse);
|
||||
|
||||
context.currentStepStats.totalElems = context.stats.filesScanned;
|
||||
|
||||
|
||||
@@ -20,24 +20,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "metadata/IParser.hpp"
|
||||
#include "ScanStepBase.hpp"
|
||||
|
||||
namespace Scanner
|
||||
{
|
||||
class ScanStepScanFiles : public ScanStepBase
|
||||
{
|
||||
public:
|
||||
ScanStepScanFiles(InitParams& initParams);
|
||||
class ScanStepScanFiles : public ScanStepBase
|
||||
{
|
||||
public:
|
||||
ScanStepScanFiles(InitParams& initParams);
|
||||
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::ScanningFiles; }
|
||||
std::string_view getStepName() const override { return "Scanning files"; }
|
||||
void process(ScanContext& context) override;
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::ScanningFiles; }
|
||||
std::string_view getStepName() const override { return "Scanning files"; }
|
||||
void process(ScanContext& context) override;
|
||||
|
||||
void scanAudioFile(const std::filesystem::path& file, ScanContext& context);
|
||||
void scanAudioFile(const std::filesystem::path& file, ScanContext& context);
|
||||
|
||||
std::unique_ptr<MetaData::IParser> _metadataParser;
|
||||
};
|
||||
std::unique_ptr<MetaData::IParser> _metadataParser;
|
||||
const std::vector<std::string> _tagsToParse{ "GENRE", "MOOD", "LANGUAGE", "ALBUMGROUPING" };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -361,14 +361,10 @@ namespace Scanner
|
||||
}
|
||||
newSettings.mediaDirectory = scanSettings->getMediaDirectory();
|
||||
|
||||
const auto clusterTypes = scanSettings->getClusterTypes();
|
||||
std::set<std::string> clusterTypeNames;
|
||||
|
||||
std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes),
|
||||
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
|
||||
[](const ClusterType::pointer& clusterType) { return std::string{ clusterType->getName() }; });
|
||||
|
||||
newSettings.clusterTypeNames = std::move(clusterTypeNames);
|
||||
{
|
||||
const auto& tags{ scanSettings->getExtraTagsToScan() };
|
||||
std::transform(std::cbegin(tags), std::cend(tags), std::back_inserter(newSettings.extraTags), [](std::string_view tag) { return std::string{ tag };});
|
||||
}
|
||||
}
|
||||
|
||||
return newSettings;
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <Wt/WDateTime.h>
|
||||
@@ -36,7 +35,7 @@ namespace Scanner
|
||||
std::vector<std::filesystem::path> supportedExtensions;
|
||||
std::filesystem::path mediaDirectory;
|
||||
bool skipDuplicateMBID {};
|
||||
std::set<std::string> clusterTypeNames;
|
||||
std::vector<std::string> extraTags;
|
||||
|
||||
bool operator==(const ScannerSettings& rhs) const
|
||||
{
|
||||
@@ -46,7 +45,7 @@ namespace Scanner
|
||||
&& supportedExtensions == rhs.supportedExtensions
|
||||
&& mediaDirectory == rhs.mediaDirectory
|
||||
&& skipDuplicateMBID == rhs.skipDuplicateMBID
|
||||
&& clusterTypeNames == rhs.clusterTypeNames;
|
||||
&& extraTags == rhs.extraTags;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace API::Subsonic
|
||||
const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") };
|
||||
if (genreClusterType)
|
||||
{
|
||||
auto clusters{ release->getClusterGroups({genreClusterType}, 1) };
|
||||
auto clusters{ release->getClusterGroups({genreClusterType->getId()}, 1) };
|
||||
if (!clusters.empty() && !clusters.front().empty())
|
||||
albumNode.setAttribute("genre", clusters.front().front()->getName());
|
||||
}
|
||||
@@ -160,16 +160,12 @@ namespace API::Subsonic
|
||||
{
|
||||
albumNode.createEmptyArrayValue(field);
|
||||
|
||||
ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, clusterTypeName) };
|
||||
if (clusterType)
|
||||
{
|
||||
Cluster::FindParameters params;
|
||||
params.setRelease(release->getId());
|
||||
params.setClusterType(clusterType->getId());
|
||||
Cluster::FindParameters params;
|
||||
params.setRelease(release->getId());
|
||||
params.setClusterTypeName(clusterTypeName);
|
||||
|
||||
for (const auto& cluster : Cluster::find(context.dbSession, params).results)
|
||||
albumNode.addArrayValue(field, cluster->getName());
|
||||
}
|
||||
for (const auto& cluster : Cluster::find(context.dbSession, params).results)
|
||||
albumNode.addArrayValue(field, cluster->getName());
|
||||
} };
|
||||
|
||||
addClusters("moods", "MOOD");
|
||||
|
||||
@@ -162,12 +162,15 @@ namespace API::Subsonic
|
||||
trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime));
|
||||
|
||||
// Report the first GENRE for this track
|
||||
const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") };
|
||||
if (genreClusterType)
|
||||
std::vector<Cluster::pointer> genres;
|
||||
{
|
||||
auto clusters{ track->getClusterGroups({genreClusterType}, 1) };
|
||||
if (!clusters.empty() && !clusters.front().empty())
|
||||
trackResponse.setAttribute("genre", clusters.front().front()->getName());
|
||||
Cluster::FindParameters params;
|
||||
params.setTrack(track->getId());
|
||||
params.setClusterTypeName("GENRE");
|
||||
|
||||
genres = Cluster::find(context.dbSession, params).results;
|
||||
if (!genres.empty())
|
||||
trackResponse.setAttribute("genre", genres.front()->getName());
|
||||
}
|
||||
|
||||
// OpenSubsonic specific fields (must always be set)
|
||||
@@ -227,31 +230,20 @@ namespace API::Subsonic
|
||||
{
|
||||
trackResponse.createEmptyArrayValue(field);
|
||||
|
||||
ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, clusterTypeName) };
|
||||
if (clusterType)
|
||||
{
|
||||
Cluster::FindParameters params;
|
||||
params.setTrack(track->getId());
|
||||
params.setClusterType(clusterType->getId());
|
||||
Cluster::FindParameters params;
|
||||
params.setTrack(track->getId());
|
||||
params.setClusterTypeName(clusterTypeName);
|
||||
|
||||
for (const auto& cluster : Cluster::find(context.dbSession, params).results)
|
||||
trackResponse.addArrayValue(field, cluster->getName());
|
||||
}
|
||||
for (const auto& cluster : Cluster::find(context.dbSession, params).results)
|
||||
trackResponse.addArrayValue(field, cluster->getName());
|
||||
} };
|
||||
|
||||
addClusters("moods", "MOOD");
|
||||
|
||||
// Genres
|
||||
trackResponse.createEmptyArrayChild("genres");
|
||||
if (genreClusterType)
|
||||
{
|
||||
Cluster::FindParameters params;
|
||||
params.setTrack(track->getId());
|
||||
params.setClusterType(genreClusterType->getId());
|
||||
|
||||
for (const auto& cluster : Cluster::find(context.dbSession, params).results)
|
||||
trackResponse.addArrayChild("genres", createItemGenreNode(cluster->getName()));
|
||||
}
|
||||
for (const auto& genre : genres)
|
||||
trackResponse.addArrayChild("genres", createItemGenreNode(genre->getName()));
|
||||
|
||||
trackResponse.addChild("replayGain", createReplayGainNode(track));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user