Merge branch 'develop' for release v3.37.0

This commit is contained in:
emeric
2023-04-03 20:54:48 +02:00
38 changed files with 740 additions and 497 deletions
+7 -7
View File
@@ -1,4 +1,4 @@
FROM alpine:3.14 AS build
FROM alpine:3.17 AS build
WORKDIR /tmp/workdir
@@ -35,7 +35,7 @@ ARG BUILD_PACKAGES=" \
RUN apk add --no-cache --update ${BUILD_PACKAGES}
# ffmpeg
ARG FFMPEG_VERSION=4.1.4
ARG FFMPEG_VERSION=4.3.5
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
@@ -77,7 +77,7 @@ RUN \
make distclean
# WT
ARG WT_VERSION=4.7.2
ARG WT_VERSION=4.9.1
RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://github.com/emweb/wt/archive/${WT_VERSION}.tar.gz && \
@@ -90,7 +90,7 @@ RUN \
make install
# STB
ARG STB_VERSION=b42009b3b9d4ca35bc703f5310eedc74f584be58
ARG STB_VERSION=5736b15f7ea0ffb08dd38af21067c314d6a3aae9
RUN \
DIR=/tmp/stb && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://github.com/nothings/stb/archive/${STB_VERSION}.tar.gz && \
@@ -132,12 +132,12 @@ RUN \
rm -rf /tmp/fakeroot/share/Wt/resources/themes
## Release Stage
FROM alpine:3.14 AS release
FROM alpine:3.17 AS release
LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>"
ARG RUNTIME_PACKAGES=" \
libssl1.1 \
libcrypto1.1 \
libssl3 \
libcrypto3 \
openjpeg \
libpng \
libogg \
+1 -1
View File
@@ -3,7 +3,7 @@
<message id="Lms.Admin.Users.template">
<h3>${tr:Lms.Admin.Users.users}</h3>
${users class="d-grid gap-1 mb-3"}
${users class="d-grid gap-1 mb-3 Lms-row-container"}
${<if-can-create-user>}
${add-btn class="btn btn-primary"}
${</if-can-create-user>}
+6 -2
View File
@@ -27,10 +27,14 @@
</div>
<div class="flex-fill text-center text-truncate">
${title class="d-block text-truncate"}
${release class="text-decoration-none text-truncate link-success"}
${artist class="d-inline text-decoration-none text-truncate link-success"}
<span class="d-none d-md-inline">
${separator}
${release class="text-decoration-none text-truncate link-success"}
</span>
</div>
<div class="p-2 d-none d-sm-block text-muted text-nowrap">
<span id="lms-transcoding-active" title="${tr:Lms.Player.transcoding-active}" class="p-2" style="visibility:hidden"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></span>
<span id="lms-transcoding-active" title="${tr:Lms.Player.transcoding-active}" class="p-2" style="display:none"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></span>
<span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
</div>
<div class="p-2 d-none d-md-flex align-items-center Lms-player-volume-container">
+25 -9
View File
@@ -30,28 +30,44 @@
${entries}
</message>
<message id="Lms.PlayQueue.template.entry-container">
${elements class="Lms-row-container"}
${loading-indicator}
</message>
<message id="Lms.PlayQueue.template.entry">
<div class="d-flex align-items-center rounded Lms-bg-dark-hover mb-2">
<div class="p-1 Lms-cover-small">
<div class="d-flex align-items-center mb-2">
<div class="p-1">
${cover}
</div>
<div class="p-2 flex-fill text-truncate">
<div class="d-block text-truncate">${name}</div>
${<if-has-release>}${release class="text-decoration-none link-success text-truncate"}${</if-has-release>}
<div class="row align-items-center flex-fill overflow-hidden">
<div class="col-12 col-md-6 col-lg-4">
<div class="p-2 overflow-hidden">
<div class="d-block text-truncate">${name}</div>
${<if-has-artists>}${artists class="d-block d-md-none text-truncate"}${</if-has-artists>}
</div>
</div>
<div class="col-md-6 col-lg-4">
${<if-has-artists>}${artists-md class="d-none d-md-block text-truncate"}${</if-has-artists>}
</div>
<div class="col-lg-4 text-truncate">
${<if-has-release>}${release class="d-none d-lg-inline text-truncate text-decoration-none link-success"}${</if-has-release>}
</div>
</div>
<div class="p-2 d-none d-sm-block text-muted">
<div class="p-2 d-none d-sm-block text-muted text-center Lms-tracks-entry-duration">
${duration}
</div>
<div class="p-2 d-flex">
${play-btn class="btn btn-sm btn-outline-secondary border-0"}
<div class="dropdown d-none d-sm-block">
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
${del-btn class="btn btn-sm btn-outline-secondary border-0"}
<div class="dropdown d-inline-block">
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
<li>${play class="dropdown-item"}</li>
<li>${star class="dropdown-item"}</li>
<li>${download class="dropdown-item"}</li>
</ul>
</div>
${del-btn class="btn btn-sm btn-outline-secondary border-0"}
</div>
</div>
</message>
+17 -9
View File
@@ -51,30 +51,38 @@
<message id="Lms.Explore.Release.template.entry-disc">
<h4>${disc-title}</h4>
${tracks class="d-grid gap-1"}
${tracks class="d-grid gap-1 Lms-row-container"}
</message>
<message id="Lms.Explore.Release.template.entry-nodisc">
${tracks class="d-grid gap-1"}
${tracks class="d-grid gap-1 Lms-row-container"}
</message>
<message id="Lms.Explore.Release.template.entry">
<div class="d-flex align-items-center rounded Lms-bg-dark-hover">
<div class="p-2 text-muted text-end" style="min-width: 2.1rem;">
<div class="d-flex align-items-center">
<div class="p-2 text-muted text-end Lms-tracks-entry-position">
${<if-has-track-number>}${track-number}${</if-has-track-number>}
</div>
<div class="p-2 flex-fill">
${name}
${<if-has-artists>}${artists}${</if-has-artists>}
<div class="row align-items-center flex-fill overflow-hidden">
<div class="col-12 col-md-6">
<div class="p-2">
${name}
${<if-has-artists>}${artists class="d-block d-md-none"}${</if-has-artists>}
</div>
</div>
<div class="col-md-6">
${<if-has-artists>}${artists-md class="d-none d-md-block"}${</if-has-artists>}
</div>
</div>
<div class="p-2 d-none d-sm-block text-muted">
<div class="p-2 d-none d-sm-block text-muted text-center Lms-tracks-entry-duration">
${duration}
</div>
<div class="p-2 d-flex">
${play-btn class="btn btn-sm btn-outline-secondary border-0"}
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
<div class="dropdown d-inline-block">
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
<li>${play class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li>
<li>${star class="dropdown-item"}</li>
<li>${download class="dropdown-item"}</li>
+5
View File
@@ -21,6 +21,11 @@
${tracks}
</message>
<message id="Lms.Explore.TrackList.template.entry-container">
${elements class="Lms-row-container"}
${loading-indicator}
</message>
<message id="Lms.Explore.TrackList.template.delete-tracklist">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
+23 -11
View File
@@ -37,23 +37,39 @@
${tracks}
</message>
<message id="Lms.Explore.Tracks.template.entry-container">
${elements class="Lms-row-container"}
${loading-indicator}
</message>
<message id="Lms.Explore.Tracks.template.entry">
<div class="d-flex align-items-center rounded Lms-bg-dark-hover mb-2">
<div class="p-1 Lms-cover-small">
<div class="d-flex align-items-center mb-2">
<div class="p-1">
${cover}
</div>
<div class="p-2 flex-fill text-truncate">
<div class="d-block text-truncate">${name}</div>
${<if-has-release>}${release class="text-decoration-none link-success text-truncate"}${</if-has-release>}
<div class="row align-items-center flex-fill overflow-hidden">
<div class="col-12 col-md-6 col-lg-4">
<div class="p-2 overflow-hidden">
<div class="d-block text-truncate">${name}</div>
${<if-has-artists>}${artists class="d-block d-md-none text-truncate"}${</if-has-artists>}
</div>
</div>
<div class="col-md-6 col-lg-4">
${<if-has-artists>}${artists-md class="d-none d-md-block text-truncate"}${</if-has-artists>}
</div>
<div class="col-lg-4 text-truncate">
${<if-has-release>}${release class="d-none d-lg-inline text-truncate text-decoration-none link-success"}${</if-has-release>}
</div>
</div>
<div class="p-2 d-none d-sm-block text-muted">
<div class="p-2 d-none d-sm-block text-muted text-center Lms-tracks-entry-duration">
${duration}
</div>
<div class="p-2 d-flex">
${play-btn class="btn btn-sm btn-outline-secondary border-0"}
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
<div class="dropdown d-inline-block">
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
<li>${play class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li>
<li>${star class="dropdown-item"}</li>
<li>${download class="dropdown-item"}</li>
@@ -64,10 +80,6 @@
</div>
</message>
<message id="Lms.Explore.Tracks.template.entry-artist">
${artist class="text-decoration-none link-secondary"}
</message>
<message id="Lms.Explore.Tracks.template.track-info">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-scrollable">
+26 -1
View File
@@ -1,5 +1,9 @@
/* Generic rules */
:root {
--lms-striped-bg: #022f3a;
}
.link-success {
transition: color 0.15s ease;
}
@@ -16,6 +20,19 @@
background-color: var(--bs-dark);
}
.Lms-row-container {
--lms-striped-bg: #022f3a;
--lms-hover-bg: var(--bs-dark);
}
.Lms-row-container > :nth-child(odd) {
background-color: var(--lms-striped-bg);
}
.Lms-row-container > *:hover {
background-color: var(--lms-hover-bg);
}
/* Specific rules */
body {
padding-bottom: 78px;
@@ -32,7 +49,7 @@ body {
}
.Lms-entry-playing {
background-color: var(--bs-dark);
background-color: #0d4655!important;
}
.Lms-cover {
@@ -144,3 +161,11 @@ body {
display: block;
padding-bottom: 100%;
}
.Lms-tracks-entry-position{
min-width: 2.1rem;
}
.Lms-tracks-entry-duration {
min-width: 4rem;
}
+2 -2
View File
@@ -271,10 +271,10 @@ LMS.mediaplayer = function () {
_elems.audio.addEventListener("canplay", function() {
if (_getAudioMode() == Mode.Transcode) {
_elems.transcodingActive.style.visibility = "visible";
_elems.transcodingActive.style.display = "inline";
}
else {
_elems.transcodingActive.style.visibility = "hidden";
_elems.transcodingActive.style.display = "none";
}
});
+81 -54
View File
@@ -64,25 +64,9 @@ findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initiali
return res;
}
static
std::optional<Album>
getAlbum(const Av::IAudioFile::MetadataMap& metadataMap)
{
std::optional<Album> res;
auto album {findFirstValueOfAs<std::string>(metadataMap, {"ALBUM"})};
if (!album)
return res;
auto albumMBID {findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID"})};
return Album{*album, albumMBID};
}
static
std::vector<Artist>
getAlbumArtists(const Av::IAudioFile::MetadataMap& metadataMap)
getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
{
std::vector<Artist> res;
@@ -92,7 +76,7 @@ getAlbumArtists(const Av::IAudioFile::MetadataMap& metadataMap)
auto mbid {findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"})};
return {Artist {*name, std::nullopt, mbid} };
return {Artist {mbid, *name, std::nullopt} };
}
static
@@ -116,14 +100,83 @@ getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
for (std::size_t i {}; i < artistNames.size(); ++i)
{
if (artistMBIDs && artistNames.size() == artistMBIDs->size())
artists.emplace_back(Artist {artistNames[i], std::nullopt, (*artistMBIDs)[i]});
artists.emplace_back(Artist {(*artistMBIDs)[i], artistNames[i], std::nullopt});
else
artists.emplace_back(Artist {artistNames[i], std::nullopt, {}});
artists.emplace_back(Artist {std::nullopt, artistNames[i], std::nullopt});
}
return artists;
}
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;
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;
}
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]);
}
}
// 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();
}
return res;
}
std::optional<Track>
AvFormatParser::parse(const std::filesystem::path& p, bool debug)
{
@@ -147,10 +200,13 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
track.duration = mediaFile->getDuration();
track.hasCover = mediaFile->hasAttachedPictures();
MetaData::Clusters clusters;
MetaData::Tags tags;
const Av::IAudioFile::MetadataMap metadataMap {mediaFile->getMetaData()};
track.artists = getArtists(metadataMap);
track.medium = getMedium(metadataMap);
for (const auto& [tag, value] : metadataMap)
{
if (debug)
@@ -161,30 +217,11 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
else if (tag == "TRACK")
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings {StringUtils::splitString(value, "/") };
if (strings.size() > 0)
{
track.trackNumber = StringUtils::readAs<std::size_t>(strings[0]);
if (strings.size() > 1)
track.totalTrack = StringUtils::readAs<std::size_t>(strings[1]);
}
}
else if (tag == "DISC")
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings {StringUtils::splitString(value, "/")};
if (strings.size() > 0)
{
track.discNumber = StringUtils::readAs<std::size_t>(strings[0]);
if (strings.size() > 1)
track.totalDisc = StringUtils::readAs<std::size_t>(strings[1]);
}
track.position = StringUtils::readAs<std::size_t>(value);
}
else if (tag == "DATE"
|| tag == "YEAR"
|| tag == "WM/Year")
|| tag == "WM/YEAR")
{
track.date = Utils::parseDate(value);
}
@@ -200,19 +237,13 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
|| tag == "MUSICBRAINZ_RELEASETRACKID")
{
track.trackMBID = UUID::fromString(value);
track.mbid = UUID::fromString(value);
}
else if (tag == "MUSICBRAINZ_TRACKID"
|| tag == "MUSICBRAINZ/TRACK ID")
{
track.recordingMBID = UUID::fromString(value);
}
else if (tag == "TSST"
|| tag == "DISCSUBTITLE"
|| tag == "SETSUBTITLE")
{
track.discSubtitle = value;
}
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
const std::vector<std::string_view> clusterNames {StringUtils::splitString(value, "/,;")};
@@ -223,14 +254,10 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
std::transform(std::cbegin(clusterNames), std::cend(clusterNames),
std::inserter(values, std::begin(values)),
[](std::string_view clusterName) { return std::string {clusterName}; });
track.clusters[tag] = std::move(values);
track.tags[tag] = std::move(values);
}
}
}
track.artists = getArtists(metadataMap);
track.album = getAlbum(metadataMap);
track.albumArtists = getAlbumArtists(metadataMap);
}
catch(Av::Exception& e)
{
+182 -125
View File
@@ -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;
@@ -73,34 +82,51 @@ getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::
}
template <typename T>
std::vector<T>
getPropertyValuesAs(const TagLib::PropertyMap& properties, const std::string& key)
std::optional<T>
getPropertyValueFirstMatchAs(const TagMap& tags, const std::vector<std::string_view>& keys)
{
return getPropertyValuesFirstMatchAs<T>(properties, {std::move(key)});
}
static
std::vector<std::string>
splitAndTrimString(const std::string& 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));
std::optional<T> res;
std::vector<T> values {getPropertyValuesFirstMatchAs<T>(tags, keys)};
if (!values.empty())
res = std::move(values.front());
return res;
}
template <typename T>
std::vector<T>
getPropertyValuesAs(const TagMap& tags, const std::string& key)
{
return getPropertyValuesFirstMatchAs<T>(tags, {key});
}
template <typename T>
std::optional<T>
getPropertyValueAs(const TagMap& tags, const std::string& key)
{
return getPropertyValueFirstMatchAs<T>(tags, {key});
}
static
std::vector<std::string_view>
splitAndTrimString(std::string_view str, std::string_view delimiters)
{
std::vector<std::string_view> strings {StringUtils::splitString(str, delimiters)};
for (std::string_view& s : strings)
s = StringUtils::stringTrim(s);
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 +136,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 +145,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].mbid = artistsMBID[i];
}
}
@@ -134,7 +160,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 +168,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 +178,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 +191,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 +199,72 @@ 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"})};
auto releaseName {getPropertyValueAs<std::string>(tags, "ALBUM")};
if (!releaseName)
return release;
if (albumMBID.empty())
return Album {std::move(albumName.front()), {}};
else
return Album {std::move(albumName.front()), albumMBID.front()};
release.emplace();
release->name = std::move(*releaseName);
release->mbid = getPropertyValueFirstMatchAs<UUID>(tags, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"});
release->artists = getArtists(tags, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"});
release->mediumCount = getPropertyValueAs<std::size_t>(tags, "DISCTOTAL");
if (!release->mediumCount)
{
// mediumCount may be encoded as "position/count"
if (const auto value {getPropertyValueAs<std::string>(tags, "DISCNUMBER")})
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") };
if (strings.size() == 2)
release->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
return release;
}
static
std::optional<Medium>
getMedium(const TagMap& tags)
{
std::optional<Medium> medium;
medium.emplace();
medium->type = getPropertyValueAs<std::string>(tags, "MEDIA").value_or("");
medium->name = getPropertyValueFirstMatchAs<std::string>(tags, {"DISCSUBTITLE", "SETSUBTITLE"}).value_or("");
medium->trackCount = getPropertyValueAs<std::size_t>(tags, "TRACKTOTAL");
if (!medium->trackCount)
{
// totalTracks may be encoded as "position/count"
if (const auto value {getPropertyValueAs<std::string>(tags, "TRACKNUMBER")})
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") };
if (strings.size() == 2)
medium->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
// Expecting 'Number[/Total]'
medium->position = getPropertyValueAs<std::size_t>(tags, "DISCNUMBER");
medium->release = getRelease(tags);
medium->replayGain = getPropertyValueAs<float>(tags, "REPLAYGAIN_ALBUM_GAIN");
if (medium->type.empty()
&& medium->name.empty()
&& !medium->trackCount
&& !medium->position
&& !medium->release
&& !medium->replayGain)
{
medium.reset();
}
return medium;
}
static
@@ -202,27 +281,21 @@ 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))};
std::string_view value {values.front()};
if (tag == "TITLE")
track.title = value;
@@ -230,7 +303,7 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|| tag == "MUSICBRAINZ RELEASE TRACK ID"
|| tag == "MUSICBRAINZ/RELEASE TRACK ID")
{
track.trackMBID = UUID::fromString(value);
track.mbid = UUID::fromString(value);
}
else if (tag == "MUSICBRAINZ_TRACKID"
|| tag == "MUSICBRAINZ TRACK ID"
@@ -238,44 +311,10 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
track.recordingMBID = UUID::fromString(value);
else if (tag == "ACOUSTID_ID")
track.acoustID = UUID::fromString(value);
else if (tag == "TRACKTOTAL")
{
auto totalTrack = StringUtils::readAs<std::size_t>(value);
if (totalTrack)
track.totalTrack = totalTrack;
}
else if (tag == "TRACKNUMBER")
{
// Expecting 'Number/Total'
std::vector<std::string> 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]);
}
}
else if (tag == "DISCTOTAL")
{
auto totalDisc = StringUtils::readAs<std::size_t>(value);
if (totalDisc)
track.totalDisc = totalDisc;
}
else if (tag == "DISCNUMBER")
{
// Expecting 'Number/Total'
std::vector<std::string_view> strings {StringUtils::splitString(value, "/")};
if (!strings.empty())
{
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]);
}
track.position = StringUtils::readAs<std::size_t>(value);
}
else if (tag == "DATE")
{
@@ -305,25 +344,51 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
track.copyright = value;
else if (tag == "COPYRIGHTURL")
track.copyrightURL = value;
else if (tag == "REPLAYGAIN_ALBUM_GAIN")
track.albumReplayGain = StringUtils::readAs<float>(value);
else if (tag == "REPLAYGAIN_TRACK_GAIN")
track.trackReplayGain = StringUtils::readAs<float>(value);
else if (tag == "DISCSUBTITLE" || tag == "SETSUBTITLE")
track.discSubtitle = value;
track.replayGain = StringUtils::readAs<float>(value);
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::set<std::string> clusterNames;
for (const auto& valueList : values)
for (std::string_view valueList : values)
{
const auto splittedValues {splitAndTrimString(valueList.to8Bit(true), "/,;")};
for (const auto& value : splittedValues)
clusterNames.insert(value);
const std::vector<std::string_view> splittedValues {splitAndTrimString(valueList, "/,;")};
for (std::string_view value : splittedValues)
clusterNames.insert(std::string {value});
}
if (!clusterNames.empty())
track.clusters[tag] = clusterNames;
track.tags[tag] = std::move(clusterNames);
}
}
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);
}
}
@@ -354,24 +419,17 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()};
MetaData::AudioStream audioStream {static_cast<unsigned>(properties->bitrate() * 1000)};
track.audioStreams = {std::move(audioStream)};
track.audioStreams = {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 +445,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 +476,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 +516,18 @@ 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.medium = getMedium(tags);
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;
}
+1 -1
View File
@@ -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;
};
+2 -2
View File
@@ -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;
+2 -1
View File
@@ -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
@@ -38,5 +38,6 @@ namespace MetaData::Utils
// format is "artist name (role)"
PerformerArtist extractPerformerAndRole(std::string_view entry);
}
+39 -31
View File
@@ -33,24 +33,38 @@
namespace MetaData
{
using Clusters = std::map<std::string /* type */, std::set<std::string> /* names */>;
using Tags = std::map<std::string /* type */, std::set<std::string> /* names */>;
// Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema
struct Artist
{
std::string name;
std::optional<std::string> sortName;
std::optional<UUID> musicBrainzArtistID;
std::optional<UUID> mbid;
std::string name;
std::optional<std::string> sortName;
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::optional<UUID> _mbid, std::string_view _name, std::optional<std::string> _sortName) : mbid {std::move(_mbid)}, name {_name}, sortName {std::move(_sortName)} {}
};
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
struct Album
struct Release
{
std::string name;
std::optional<UUID> musicBrainzAlbumID;
std::optional<UUID> mbid;
std::string name;
std::vector<Artist> artists;
std::optional<std::size_t> mediumCount;
};
struct Medium
{
std::string type;
std::string name;
std::optional<Release> release;
std::optional<std::size_t> position; // in release
std::optional<std::size_t> trackCount;
std::optional<float> replayGain;
};
struct AudioStream
@@ -60,35 +74,29 @@ namespace MetaData
struct Track
{
std::vector<Artist> artists;
std::vector<Artist> albumArtists;
std::string title;
std::optional<UUID> trackMBID;
std::optional<UUID> mbid;
std::optional<UUID> recordingMBID;
std::optional<Album> album;
Clusters clusters;
std::string title;
std::optional<Medium> medium;
std::optional<std::size_t> position; // in medium
Tags tags;
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> artists;
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
{
+8 -34
View File
@@ -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
{
@@ -425,9 +401,8 @@ Track::getArtists(EnumSet<TrackArtistLinkType> linkTypes) const
oss << " AND t_a_l.type IN (";
bool first {true};
for (TrackArtistLinkType type : linkTypes)
for ([[maybe_unused]] TrackArtistLinkType type : linkTypes)
{
(void) type;
if (!first)
oss << ", ";
oss << "?";
@@ -454,7 +429,7 @@ Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
std::ostringstream oss;
oss <<
"SELECT a.id from artist a"
"SELECT DISTINCT a.id from artist a"
" INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id"
" INNER JOIN track t ON t.id = t_a_l.track_id";
@@ -463,9 +438,8 @@ Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
oss << " AND t_a_l.type IN (";
bool first {true};
for (TrackArtistLinkType type : linkTypes)
for ([[maybe_unused]] TrackArtistLinkType type : linkTypes)
{
(void) type;
if (!first)
oss << ", ";
oss << "?";
@@ -474,14 +448,14 @@ Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
oss << ")";
}
Wt::Dbo::Query<ArtistId> query {session()->query<ArtistId>(oss.str())
.where("t.id = ?").bind(getId())};
auto query {session()->query<ArtistId>(oss.str())};
for (TrackArtistLinkType type : linkTypes)
query.bind(type);
Wt::Dbo::collection<ArtistId> res = query;
return std::vector<ArtistId>(std::begin(res), std::end(res));
query.where("t.id = ?").bind(getId());
auto res {query.resultList()};
return std::vector<ArtistId>(std::cbegin(res), std::cend(res));
}
std::vector<TrackArtistLink::pointer>
@@ -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
@@ -91,6 +91,18 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
EXPECT_EQ(track->getArtists({}).size(), 1);
}
{
auto transaction {session.createSharedTransaction()};
auto artists {track->getArtistIds({TrackArtistLinkType::Artist})};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front(), artist.getId());
ASSERT_EQ(track->getArtistIds({TrackArtistLinkType::Artist}).size(), 1);
EXPECT_TRUE(track->getArtistIds({TrackArtistLinkType::ReleaseArtist}).empty());
EXPECT_EQ(track->getArtistIds({}).size(), 1);
}
{
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist"))};
+13 -16
View File
@@ -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.mbid)
artist.modify()->setMBID(*artistInfo.mbid);
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.mbid)
{
artist = Artist::find(session, *artistInfo.musicBrainzArtistID);
artist = Artist::find(session, *artistInfo.mbid);
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.mediumCount)
release.modify()->setTotalDisc(releaseInfo.mediumCount);
}
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.mbid)
{
release = Release::find(session, *album.musicBrainzAlbumID);
release = Release::find(session, *releaseInfo.mbid);
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.mbid);
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;
}
@@ -161,17 +165,17 @@ namespace
}
std::vector<Cluster::pointer>
getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames)
getOrCreateClusters(Session& session, const MetaData::Tags& tags)
{
std::vector< Cluster::pointer > clusters;
std::vector<Cluster::pointer> clusters;
for (auto clusterNames : clustersNames)
for (const auto& [tag, values] : tags)
{
auto clusterType = ClusterType::find(session, clusterNames.first);
auto clusterType = ClusterType::find(session, tag);
if (!clusterType)
continue;
for (auto clusterName : clusterNames.second)
for (auto clusterName : values)
{
auto cluster = clusterType->getCluster(clusterName);
if (!cluster)
@@ -283,9 +287,9 @@ namespace Scanner
Track::pointer track {Track::findByPath(dbSession, file) };
if (trackInfo->trackMBID && (!track || _settings.skipDuplicateMBID))
if (trackInfo->mbid && (!track || _settings.skipDuplicateMBID))
{
std::vector<Track::pointer> duplicateTracks {Track::findByMBID(dbSession, *trackInfo->trackMBID)};
std::vector<Track::pointer> duplicateTracks {Track::findByMBID(dbSession, *trackInfo->mbid)};
// find for existing MBIDs as the file may have just been moved
if (!track && duplicateTracks.size() == 1)
@@ -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->medium && trackInfo->medium->release)
{
for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackInfo->medium->release->artists, 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,20 +426,20 @@ 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->medium && trackInfo->medium->release)
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->medium->release));
else
track.modify()->setRelease({});
track.modify()->setClusters(getOrCreateClusters(dbSession, trackInfo->clusters));
track.modify()->setTotalTrack(trackInfo->medium ? trackInfo->medium->trackCount : std::nullopt);
track.modify()->setReleaseReplayGain(trackInfo->medium ? trackInfo->medium->replayGain : std::nullopt);
track.modify()->setDiscSubtitle(trackInfo->medium ? trackInfo->medium->name : "");
track.modify()->setClusters(getOrCreateClusters(dbSession, trackInfo->tags));
track.modify()->setLastWriteTime(lastWriteTime);
track.modify()->setName(title);
track.modify()->setDuration(trackInfo->duration);
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()->setTrackNumber(trackInfo->position);
track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt);
track.modify()->setDate(trackInfo->date);
track.modify()->setOriginalDate(trackInfo->originalDate);
@@ -441,13 +448,12 @@ namespace Scanner
track.modify()->setDate(trackInfo->originalDate);
track.modify()->setRecordingMBID(trackInfo->recordingMBID);
track.modify()->setTrackMBID(trackInfo->trackMBID);
track.modify()->setTrackMBID(trackInfo->mbid);
if (auto trackFeatures {TrackFeatures::find(dbSession, track->getId())})
trackFeatures.remove(); // TODO: only if MBID changed?
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);
}
}
@@ -31,6 +31,8 @@ namespace Scrobbling::ListenBrainz
os << ", releaseName = '" << listen.releaseName << "'";
if (listen.trackNumber)
os << ", trackNumber = " << *listen.trackNumber;
if (listen.trackMBID)
os << ", trackMBID = '" << listen.trackMBID->getAsString() << "'";
if (listen.recordingMBID)
os << ", recordingMBID = '" << listen.recordingMBID->getAsString() << "'";
@@ -33,6 +33,7 @@ namespace Scrobbling::ListenBrainz
std::string releaseName;
std::string artistName;
std::optional<UUID> recordingMBID;
std::optional<UUID> trackMBID;
std::optional<UUID> releaseMBID;
std::optional<unsigned> trackNumber;
Wt::WDateTime listenedAt;
@@ -51,6 +51,7 @@ namespace
if (metadata.type("additional_info") == Wt::Json::Type::Object)
{
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
listen.trackMBID = UUID::fromString(additionalInfo.get("track_mbid").orIfNull(""));
listen.recordingMBID = UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""));
listen.releaseMBID = UUID::fromString(additionalInfo.get("release_mbid").orIfNull(""));
@@ -150,7 +150,23 @@ namespace
auto transaction {session.createSharedTransaction()};
// first try to match using recording MBID, and then fallback on possibly ambiguous info
// first try to match using track MBID, and then fallback on possibly ambiguous info
if (listen.trackMBID)
{
const auto tracks {Track::findByMBID(session, *listen.trackMBID)};
// if duplicated files, do not record it (let the user correct its database)
if (tracks.size() == 1)
{
LOG(DEBUG) << "Matched listen '" << listen << "' using track MBID";
return tracks.front()->getId();
}
else if (tracks.size() > 1)
{
LOG(DEBUG) << "Too many matches for listen '" << listen << "' using track MBID!";
return {};
}
}
if (listen.recordingMBID)
{
const auto tracks {Track::findByRecordingMBID(session, *listen.recordingMBID)};
@@ -282,7 +298,7 @@ namespace Scrobbling::ListenBrainz
using namespace Database;
Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()};
auto transaction {session.createUniqueTransaction()}; // TODO: unique only if needed
Database::Listen::pointer dbListen {Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::ListenBrainz, listen.listenedAt)};
if (!dbListen)
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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]]
+10
View File
@@ -210,6 +210,7 @@ MediaPlayer::MediaPlayer()
_title = bindNew<Wt::WText>("title");
_artist = bindNew<Wt::WAnchor>("artist");
_release = bindNew<Wt::WAnchor>("release");
_separator = bindNew<Wt::WText>("separator");
_playQueue = bindNew<Wt::WPushButton>("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(0), Wt::TextFormat::XHTML);
_playQueue->setLink(Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"});
_playQueue->setToolTip(tr("Lms.PlayQueue.playqueue"));
@@ -275,6 +276,8 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain)
_title->setTextFormat(Wt::TextFormat::Plain);
_title->setText(Wt::WString::fromUTF8(track->getName()));
bool needSeparator {true};
if (!artists.empty())
{
_artist->setTextFormat(Wt::TextFormat::Plain);
@@ -285,6 +288,7 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain)
{
_artist->setText("");
_artist->setLink({});
needSeparator = false;
}
if (track->getRelease())
@@ -297,7 +301,13 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain)
{
_release->setText("");
_release->setLink({});
needSeparator = false;
}
if (needSeparator)
_separator->setText("");
else
_separator->setText("");
}
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
+1
View File
@@ -127,6 +127,7 @@ class MediaPlayer : public Wt::WTemplate
Wt::WText* _title {};
Wt::WAnchor* _release {};
Wt::WText* _separator {};
Wt::WAnchor* _artist {};
Wt::WPushButton* _playQueue {};
};
+18 -2
View File
@@ -28,7 +28,7 @@
#include <Wt/WStackedWidget.h>
#include <Wt/WTemplateFormView.h>
#include "services/database/Cluster.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
@@ -137,7 +137,7 @@ PlayQueue::PlayQueue()
saveAsTrackList();
});
_entriesContainer = bindNew<InfiniteScrollingContainer>("entries");
_entriesContainer = bindNew<InfiniteScrollingContainer>("entries", Wt::WString::tr("Lms.PlayQueue.template.entry-container"));
_entriesContainer->onRequestElements.connect([this]
{
addSome();
@@ -490,6 +490,15 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
const auto artists {track->getArtistIds({Database::TrackArtistLinkType::Artist})};
LMS_LOG(UI, DEBUG) << "Found " << artists.size() << " artists!";
if (!artists.empty())
{
entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", Utils::createArtistContainer(artists));
entry->bindWidget("artists-md", Utils::createArtistContainer(artists));
}
const auto release {track->getRelease()};
if (release)
{
@@ -543,6 +552,13 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
});
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
->clicked().connect([=]
{
const std::optional<std::size_t> pos {_entriesContainer->getIndexOf(*entry)};
if (pos)
loadTrack(*pos, true);
});
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
+8 -13
View File
@@ -377,22 +377,12 @@ Release::refreshView()
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
const auto artists {track->getArtists({TrackArtistLinkType::Artist})};
const auto artists {track->getArtistIds({TrackArtistLinkType::Artist})};
if (variousArtists && !artists.empty())
{
entry->setCondition("if-has-artists", true);
Wt::WContainerWidget* artistsContainer {entry->bindNew<Wt::WContainerWidget>("artists")};
bool firstArtist {true};
for (const auto& artist : artists)
{
if (!firstArtist)
artistsContainer->addNew<Wt::WText>(" · ");
auto anchor {Utils::createArtistAnchor(artist)};
anchor->addStyleClass("link-success text-decoration-none"); // hack
artistsContainer->addWidget(std::move(anchor));
firstArtist = false;
}
entry->bindWidget("artists", Utils::createArtistContainer(artists));
entry->bindWidget("artists-md", Utils::createArtistContainer(artists));
}
auto trackNumber {track->getTrackNumber()};
@@ -410,6 +400,11 @@ Release::refreshView()
{
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
->clicked().connect([=]
{
_playQueueController.playTrackInRelease(trackId);
});
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
->clicked().connect([=]
{
+15
View File
@@ -31,6 +31,7 @@
#include "services/database/Track.hpp"
#include "services/database/TrackArtistLink.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "utils/Logger.hpp"
#include "utils/Service.hpp"
#include "common/Template.hpp"
@@ -160,6 +161,15 @@ namespace UserInterface::TrackListHelpers
const Release::pointer release {track->getRelease()};
const TrackId trackId {track->getId()};
const auto artists {track->getArtistIds({TrackArtistLinkType::Artist})};
LMS_LOG(UI, DEBUG) << "Found " << artists.size() << " artists!";
if (!artists.empty())
{
entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", Utils::createArtistContainer(artists));
entry->bindWidget("artists-md", Utils::createArtistContainer(artists));
}
if (track->getRelease())
{
entry->setCondition("if-has-release", true);
@@ -185,6 +195,11 @@ namespace UserInterface::TrackListHelpers
});
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
->clicked().connect([trackId, &playQueueController]
{
playQueueController.processCommand(PlayQueueController::Command::Play, {trackId});
});
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
->clicked().connect([=, &playQueueController]
{
+1 -1
View File
@@ -174,7 +174,7 @@ namespace UserInterface
LmsApp->getModalManager().show(std::move(modal));
});
_container = bindNew<InfiniteScrollingContainer>("tracks");
_container = bindNew<InfiniteScrollingContainer>("tracks", Wt::WString::tr("Lms.Explore.TrackList.template.entry-container"));
_container->onRequestElements.connect([this]
{
addSome();
+1 -1
View File
@@ -86,7 +86,7 @@ Tracks::Tracks(Filters& filters, PlayQueueController& playQueueController)
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, getAllTracks());
});
_container = bindNew<InfiniteScrollingContainer>("tracks");
_container = bindNew<InfiniteScrollingContainer>("tracks", Wt::WString::tr("Lms.Explore.Tracks.template.entry-container"));
_container->onRequestElements.connect([this]
{
addSome();
+57 -39
View File
@@ -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.mbid)
os << " (" << artist.mbid->getAsString() << ")";
if (artist.sortName)
os << " '" << *artist.sortName << "'";
@@ -42,12 +44,46 @@ 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.mbid)
os << " (" << release.mbid->getAsString() << ")" << std::endl;
if (release.mediumCount)
std::cout << "\tMediumCount: " << *release.mediumCount << std::endl;
for (const MetaData::Artist& artist : release.artists)
std::cout << "\tRelease artist: " << artist << std::endl;
return os;
}
static
std::ostream&
operator<<(std::ostream& os, const MetaData::Medium& medium)
{
if (!medium.name.empty())
os << medium.name;
os << std::endl;
if (medium.position)
os << "\tPosition: " << *medium.position << std::endl;
if (!medium.type.empty())
os << "\tType: " << medium.type << std::endl;
if (medium.trackCount)
std::cout << "\tTrackCount: " << *medium.trackCount << std::endl;
if (medium.replayGain)
std::cout << "\tReplay gain: " << *medium.replayGain << std::endl;
if (medium.release)
std::cout << "Release: " << *medium.release << std::endl;
return os;
}
@@ -69,14 +105,11 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
std::cout << "Parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() / 1000. << "ms" << std::endl;
std::cout << "Track metadata:" << std::endl;
std::cout << "Parsed metadata:" << std::endl;
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,42 +138,30 @@ 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->medium)
std::cout << "Medium: " << *track->medium;
std::cout << "Title: " << track->title << std::endl;
if (track->trackMBID)
std::cout << "track MBID = " << track->trackMBID->getAsString() << std::endl;
if (track->mbid)
std::cout << "Track MBID = " << track->mbid->getAsString() << std::endl;
if (track->recordingMBID)
std::cout << "recording MBID = " << track->recordingMBID->getAsString() << std::endl;
std::cout << "Recording MBID = " << track->recordingMBID->getAsString() << std::endl;
for (const auto& cluster : track->clusters)
for (const auto& [tag, values] : track->tags)
{
std::cout << "Cluster: " << cluster.first << std::endl;
for (const auto& name : cluster.second)
std::cout << "Tag: " << tag << std::endl;
for (const auto& value : values)
{
std::cout << "\t" << name << std::endl;
std::cout << "\t" << value << std::endl;
}
}
std::cout << "Duration: " << std::fixed << std::setprecision(2) << track->duration.count() / 1000. << "s" << std::endl;
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->position)
std::cout << "Position: " << *track->position << std::endl;
if (track->date.isValid())
std::cout << "Date: " << track->date.toString("yyyy-MM-dd") << std::endl;
@@ -153,11 +174,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;