Merge branch 'develop' for release v3.37.0
This commit is contained in:
+7
-7
@@ -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 \
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class TagLibParser : public IParser
|
||||
|
||||
private:
|
||||
std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) override;
|
||||
void processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug);
|
||||
void processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug);
|
||||
|
||||
const TagLib::AudioProperties::ReadStyle _readStyle;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
namespace MetaData::Utils
|
||||
{
|
||||
Wt::WDate
|
||||
parseDate(const std::string& dateStr)
|
||||
parseDate(std::string_view dateStr)
|
||||
{
|
||||
static constexpr const char* formats[]
|
||||
{
|
||||
@@ -39,7 +39,7 @@ namespace MetaData::Utils
|
||||
for (const char* format : formats)
|
||||
{
|
||||
std::tm tm = {};
|
||||
std::stringstream ss {dateStr};
|
||||
std::istringstream ss {std::string {dateStr}}; // TODO, remove extra copy here
|
||||
ss >> std::get_time(&tm, format);
|
||||
if (ss.fail())
|
||||
continue;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
namespace MetaData::Utils
|
||||
{
|
||||
Wt::WDate parseDate(const std::string& dateStr);
|
||||
Wt::WDate parseDate(std::string_view dateStr);
|
||||
std::string_view readStyleToString(ParserReadStyle readStyle);
|
||||
|
||||
struct PerformerArtist
|
||||
@@ -38,5 +38,6 @@ namespace MetaData::Utils
|
||||
|
||||
// format is "artist name (role)"
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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"))};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -132,10 +132,10 @@ stringTrim(std::string_view str, std::string_view whitespaces)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string
|
||||
std::string_view
|
||||
stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
return std::string {str.substr(0, str.find_last_not_of(whitespaces) + 1)};
|
||||
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
|
||||
}
|
||||
|
||||
std::string
|
||||
|
||||
@@ -48,7 +48,7 @@ std::string_view
|
||||
stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
std::string_view
|
||||
stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
|
||||
@@ -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() << "'";
|
||||
|
||||
@@ -127,6 +127,7 @@ class MediaPlayer : public Wt::WTemplate
|
||||
|
||||
Wt::WText* _title {};
|
||||
Wt::WAnchor* _release {};
|
||||
Wt::WText* _separator {};
|
||||
Wt::WAnchor* _artist {};
|
||||
Wt::WPushButton* _playQueue {};
|
||||
};
|
||||
|
||||
@@ -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); }};
|
||||
|
||||
|
||||
@@ -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([=]
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user