Merge branch 'develop' for release v3.64.0

This commit is contained in:
emeric
2025-02-23 16:49:27 +01:00
100 changed files with 2173 additions and 864 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
FROM --platform=$BUILDPLATFORM alpine:3.19
FROM --platform=$BUILDPLATFORM alpine:3.21
ARG BUILD_PACKAGES="\
clang \
+18 -18
View File
@@ -1,4 +1,4 @@
FROM alpine:3.20 AS build
FROM alpine:3.21.3 AS build
WORKDIR /tmp/workdir
@@ -37,7 +37,7 @@ ARG BUILD_PACKAGES=" \
RUN apk add --no-cache --update ${BUILD_PACKAGES}
# ffmpeg
ARG FFMPEG_VERSION=6.1.1
ARG FFMPEG_VERSION=6.1.2
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
@@ -56,8 +56,8 @@ RUN \
--disable-postproc \
--disable-pixelutils \
--disable-network \
--disable-shared \
--enable-static \
--enable-shared \
--disable-static \
--enable-gpl \
--enable-small \
--enable-version3 \
@@ -75,12 +75,11 @@ RUN \
--enable-filter=aresample \
--enable-lto \
--extra-libs=-ldl && \
make -j$(nproc) && \
make install && \
make distclean
make -j$(nproc) install
# WT
ARG WT_VERSION=4.11.1
ARG WT_VERSION=4.11.3
ARG WT_DEBUG=OFF
RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://github.com/emweb/wt/archive/${WT_VERSION}.tar.gz && \
@@ -88,9 +87,8 @@ RUN \
RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DSHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON && \
make -j$(nproc) && \
make install
cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_CXX_STANDARD=17 -DSHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON -DDEBUG=${WT_DEBUG} && \
make -j$(nproc) install
# STB
ARG STB_VERSION=5c205738c191bcb0abc65c4febfa9bd25ff35234
@@ -110,18 +108,16 @@ RUN \
RUN \
DIR=/tmp/taglib && mkdir -p ${DIR} && cd ${DIR} && \
CXXFLAGS="-I/usr/include/utf8cpp" cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_SHARED_LIBS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DBUILD_TESTING=OFF -DTRACE_IN_RELEASE=OFF -DWITH_ZLIB=ON && \
make -j$(nproc) && \
make install
CXXFLAGS="-I/usr/include/utf8cpp" cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_SHARED_LIBS=ON -DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DBUILD_TESTING=OFF -DTRACE_IN_RELEASE=OFF -DWITH_ZLIB=ON && \
make -j$(nproc) install
# LMS
COPY . /tmp/lms/
RUN \
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_PREFIX_PATH=${PREFIX} && \
LD_LIBRARY_PATH=${PREFIX}/lib make -j$(nproc) && \
LD_LIBRARY_PATH=${PREFIX}/lib make -j$(nproc) install && \
LD_LIBRARY_PATH=${PREFIX}/lib make test && \
make install && \
mkdir -p ${PREFIX}/etc/ && \
cp /tmp/lms/conf/lms.conf ${PREFIX}/etc
@@ -133,6 +129,11 @@ RUN \
strip --strip-all $bin && \
cp $bin /tmp/fakeroot/bin/; \
done && \
for lib in ${PREFIX}/lib/*.so; \
do \
strip --strip-all $lib; \
done && \
cp -r ${PREFIX}/lib /tmp/fakeroot/lib && \
cp -r ${PREFIX}/share /tmp/fakeroot/share && \
rm -rf /tmp/fakeroot/share/doc && \
rm -rf /tmp/fakeroot/share/man
@@ -143,7 +144,7 @@ RUN \
rm -rf /tmp/fakeroot/share/Wt/resources/themes
## Release Stage
FROM alpine:3.20 AS release
FROM alpine:3.21.3 AS release
LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>"
ARG RUNTIME_PACKAGES=" \
@@ -159,7 +160,6 @@ ARG RUNTIME_PACKAGES=" \
boost-filesystem \
boost-iostreams \
boost-program_options \
boost-system \
boost-thread \
libarchive \
libconfig++ \
+27 -1
View File
@@ -11,6 +11,7 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat
* Multi-valued tags: `genre`, `mood`, `artists`, ...
* Artist relationships: `composer`, `conductor`, `lyricist`, `mixer`, `performer`, `producer`, `remixer`
* [Release types](https://musicbrainz.org/doc/Release_Group/Type): `album`, `single`, `EP`, `compilation`, `live`, ...
* [Release groups](https://musicbrainz.org/doc/Release_Group) support to show different versions of albums, such as remasters, reissues, etc.
* [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names
* [ListenBrainz](https://listenbrainz.org) support for:
* Scrobbling and synchronizing listens
@@ -51,7 +52,7 @@ __Note__: You can use the `lms-metadata` tool to get an idea of the tags parsed
_LMS_ works best when using the default [Picard](https://picard.musicbrainz.org/) settings, where the `artist` tag contains a single display-friendly value, and the `artists` tag holds the actual artist names. This ensures a cleaner, more organized representation of artist names, when multiple artists are involved.
### Multiple album artists
While LMS can manage multiple album artists using the `albumartist` tag, it works better when using the custom `albumartists` and `albumartistssort` tags, similar to how it handles regular artist tags.
While _LMS_ can manage multiple album artists using the `albumartist` tag, it works better when using the custom `albumartists` and `albumartistssort` tags, similar to how it handles regular artist tags.
__Note__: if you use Picard, add the following script to include these tags:
```
@@ -59,6 +60,29 @@ $setmulti(albumartists,%_albumartists%)
$setmulti(albumartistssort,%_albumartists_sort%)
```
### Album track grouping
The recommended way to group tracks within an album is to use the `MUSICBRAINZ_ALBUMID` tag.
When this tag is not present, _LMS_ will attempt to group them as best as possible: if the analyzed file contains a disc number and the total number of discs is greater than 1, sibling directories are also scanned to find a matching album.
Otherwise, _LMS_ will only consider albums within the current directory.
For an album to be considered a match, the following conditions must be met:
* Same name
* Same sort name
* Same total number of discs
* Identical 'compilation' flag value
* Same record labels
* Same barcode
## Artist image lookup
The recommended method is to name the artist image file using the artist's MusicBrainz ArtistID. This file can be placed anywhere within one of the scanned libraries.
If no file with the MusicBrainz ArtistID is found, _LMS_ will look for a file named `artist` (or another name configured in `lms.conf`) in the artist's directories:
1. Identify the artist's directory: _LMS_ selects all albums by the artist using the "AlbumArtist" link and determines the longest common path among them.
2. Scan for the image: the directory is scanned starting from this common path, moving upwards if needed, until the artist image file is found.
3. Fallback search: if no image is found, _LMS_ will then search within each individual album folder.
## Playlist support
_LMS_ supports playlist files in `m3u` and `m3u8` formats. These playlists are synced during the scan process and are available as public shared playlists.
@@ -71,6 +95,8 @@ _LMS_ supports lyrics in `lrc` files, `txt` files, and embedded track metadata.
* Next track: <kbd>Ctrl</kbd> + <kbd>Right</kbd>
* Decrease volume: <kbd>Ctrl</kbd> + <kbd>Down</kbd>
* Increase volume: <kbd>Ctrl</kbd> + <kbd>Up</kbd>
* Seek back by 5 seconds: <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Left</kbd>
* Seek forward by 5 seconds: <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Right</kbd>
## Installation
+2
View File
@@ -25,6 +25,7 @@ The following extra fields are implemented:
* `artists`
* `discTitles`: discs with no subtitle are omitted
* `displayArtist`
* `explicitStatus`
* `genres`
* `isCompilation`
* `played`
@@ -44,6 +45,7 @@ The following extra fields are implemented:
* `contributors`
* `displayAlbumArtist`
* `displayArtist`
* `explicitStatus`
* `genres`
* `mediaType`
* `moods`
+4 -1
View File
@@ -177,6 +177,7 @@
<message id="Lms.Explore.duration">Duration</message>
<message id="Lms.Explore.filter-added">Filter added</message>
<message id="Lms.Explore.filters">Filters</message>
<message id="Lms.Explore.label">Label</message>
<message id="Lms.Explore.media-library">Music Library</message>
<message id="Lms.Explore.most-played">Most played</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
@@ -191,6 +192,7 @@
<message id="Lms.Explore.recently-modified">Recently modified</message>
<message id="Lms.Explore.recently-played">Recently played</message>
<message id="Lms.Explore.release-info">Release information</message>
<message id="Lms.Explore.release-type">Release type</message>
<message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.search">Search</message>
<message id="Lms.Explore.star">Star</message>
@@ -248,9 +250,10 @@
</message>
<!--Explore:Release-->
<message id="Lms.Explore.Release.similar-releases">Similar albums</message>
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disc {1}</message>
<message id="Lms.Explore.Release.other-versions">Other versions</message>
<message id="Lms.Explore.Release.similar-releases">Similar albums</message>
<message id="Lms.Explore.Release.type">Type</message>
<message id="Lms.Explore.Release.type-primary-album">Album</message>
<message id="Lms.Explore.Release.type-primary-broadcast">Broadcast</message>
+4 -1
View File
@@ -177,6 +177,7 @@
<message id="Lms.Explore.duration">Durée</message>
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
<message id="Lms.Explore.filters">Filtres</message>
<message id="Lms.Explore.label">Label</message>
<message id="Lms.Explore.media-library">Bibliothèque musicale</message>
<message id="Lms.Explore.most-played">Plus joués</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
@@ -191,6 +192,7 @@
<message id="Lms.Explore.recently-modified">Modifiés récemment</message>
<message id="Lms.Explore.recently-played">Joués récemment</message>
<message id="Lms.Explore.release-info">Informations sur l'album</message>
<message id="Lms.Explore.release-type">Type de publication</message>
<message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.search">Rechercher</message>
<message id="Lms.Explore.star">Ajouter aux favoris</message>
@@ -248,9 +250,10 @@
</message>
<!--Explore:Release-->
<message id="Lms.Explore.Release.similar-releases">Albums similaires</message>
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disque {1}</message>
<message id="Lms.Explore.Release.other-versions">Autres versions</message>
<message id="Lms.Explore.Release.similar-releases">Albums similaires</message>
<message id="Lms.Explore.Release.type">Type</message>
<message id="Lms.Explore.Release.type-primary-album">Album</message>
<message id="Lms.Explore.Release.type-primary-broadcast">Diffusion</message>
+4 -1
View File
@@ -177,6 +177,7 @@
<message id="Lms.Explore.duration">Durata</message>
<message id="Lms.Explore.filter-added">Filtro aggiunto</message>
<message id="Lms.Explore.filters">Filtri</message>
<message id="Lms.Explore.label">Etichetta</message>
<message id="Lms.Explore.media-library">Libreria musicale</message>
<message id="Lms.Explore.most-played">Più ascoltati</message>
<message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message>
@@ -191,6 +192,7 @@
<message id="Lms.Explore.recently-modified">Modificati di recente</message>
<message id="Lms.Explore.recently-played">Riprodotti di recente</message>
<message id="Lms.Explore.release-info">Informazioni sull'album</message>
<message id="Lms.Explore.release-type">Tipo di pubblicazione</message>
<message id="Lms.Explore.releases">Album</message>
<message id="Lms.Explore.search">Ricerca</message>
<message id="Lms.Explore.star">Aggiungi ai preferiti</message>
@@ -248,9 +250,10 @@
</message>
<!--Explore:Release-->
<message id="Lms.Explore.Release.similar-releases">Album simili</message>
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disco {1}</message>
<message id="Lms.Explore.Release.other-versions">Altre versioni</message>
<message id="Lms.Explore.Release.similar-releases">Album simili</message>
<message id="Lms.Explore.Release.type">Tipo</message>
<message id="Lms.Explore.Release.type-primary-album">Album</message>
<message id="Lms.Explore.Release.type-primary-broadcast">Trasmissione</message>
+4 -1
View File
@@ -194,6 +194,7 @@
<message id="Lms.Explore.duration">Długość</message>
<message id="Lms.Explore.filter-added">Filtr dodany</message>
<message id="Lms.Explore.filters">Filtry</message>
<message id="Lms.Explore.label">Wytwórnia</message>
<message id="Lms.Explore.media-library">Biblioteka muzyczna</message>
<message id="Lms.Explore.most-played">Najczęściej odtwarzane</message>
<message id="Lms.Explore.musicbrainz-artist">Artysta w MusicBrainz</message>
@@ -208,6 +209,7 @@
<message id="Lms.Explore.recently-modified">Ostatnio zmienione</message>
<message id="Lms.Explore.recently-played">Ostatnio odtwarzane</message>
<message id="Lms.Explore.release-info">Informacje o wydaniu</message>
<message id="Lms.Explore.release-type">Typ wydania</message>
<message id="Lms.Explore.releases">Albumy</message>
<message id="Lms.Explore.search">Szukaj</message>
<message id="Lms.Explore.star">Wyróżnij</message>
@@ -274,9 +276,10 @@
</message>
<!--Explore:Release-->
<message id="Lms.Explore.Release.similar-releases">Podobne albumy</message>
<message id="Lms.Explore.Release.copyright">Prawa autorskie</message>
<message id="Lms.Explore.Release.disc">Dysk {1}</message>
<message id="Lms.Explore.Release.other-versions">Inne wersje</message>
<message id="Lms.Explore.Release.similar-releases">Podobne albumy</message>
<message id="Lms.Explore.Release.type">Typ</message>
<message id="Lms.Explore.Release.type-primary-album">Album</message>
<message id="Lms.Explore.Release.type-primary-broadcast">Audycja</message>
+4 -1
View File
@@ -178,6 +178,7 @@
<message id="Lms.Explore.filter-added">过滤器已添加</message>
<message id="Lms.Explore.filters">过滤器</message>
<message id="Lms.Explore.most-played">播放最多</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message>
@@ -191,6 +192,7 @@
<message id="Lms.Explore.recently-modified">最近更改</message>
<message id="Lms.Explore.recently-played">最近播放</message>
<message id="Lms.Explore.releases">专辑</message>
<message id="Lms.Explore.search">搜索</message>
<message id="Lms.Explore.star">收藏</message>
@@ -248,10 +250,11 @@
<!--Explore:Release-->
<message id="Lms.Explore.Release.similar-releases">相似专辑</message>
<message id="Lms.Explore.Release.copyright">版权所有</message>
<message id="Lms.Explore.Release.disc">唱片 {1}</message>
<message id="Lms.Explore.Release.similar-releases">相似专辑</message>
+29 -3
View File
@@ -9,10 +9,10 @@
<message id="Lms.Explore.Release.template">
<div class="row mb-3 gy-3">
<div class="col-lg-3 col-md-4">
${cover}
${cover class="Lms-cursor-pointer rounded"}
</div>
<div class="col">
<h2>${name}</h2>
<h2>${name}${<if-has-release-comment>}<span class="ms-1 text-muted">[${comment}]</span>${</if-has-release-comment>}</h2>
${<if-has-release-artists>}${artists}${</if-has-release-artists>}
<div class="small text-muted">${<if-has-year>}${year} · ${</if-has-year>}${duration}</div>
${clusters class="mb-2"}
@@ -39,12 +39,28 @@
${</if-has-copyright>}
</div>
</div>
${<if-has-other-versions>}
<h3>${tr:Lms.Explore.Release.other-versions}</h3>
${other-versions class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 gx-2 gy-4"}
${</if-has-other-versions>}
${<if-has-similar-releases>}
<h3>${tr:Lms.Explore.Release.similar-releases}</h3>
${similar-releases class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 gx-2 gy-4"}
${similar-releases class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 gx-2 gy-4"}
${</if-has-similar-releases>}
</message>
<message id="Lms.Explore.Release.template.full-cover">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered Lms-cover-full-modal-dialog">
<div class="modal-content bg-transparent border-0">
<div class="modal-body text-center">
${cover-full class="Lms-cover-full"}
</div>
</div>
</div>
</div>
</message>
<message id="Lms.Explore.Release.template.entry-release-artist">
${artist class="text-decoration-none link-success"}
</message>
@@ -132,6 +148,16 @@
${<if-has-artist>}
${artist-table class="d-grid gap-2"}
${</if-has-artist>}
${<if-has-labels>}
<div class="row align-items-center">
<div class="col-4">
${tr:Lms.Explore.label}
</div>
<div class="col">
${release-labels}
</div>
</div>
${</if-has-labels>}
${<if-has-codec>}
<div class="row align-items-center">
<div class="col-4">
+3 -3
View File
@@ -27,15 +27,15 @@
<div class="Lms-responsive-square Lms-cover-container mb-1">
${cover class="shadow-sm"}
</div>
${release-name class="d-block text-truncate text-nowrap text-decoration-none link-success"}
${<if-has-artist>}${artist-name class="d-block text-truncate text-nowrap"}${</if-has-artist>}
${release-name class="Lms-multiline-clamp text-decoration-none link-success"}
${<if-has-artist>}${artist-name class="Lms-multiline-clamp"}${</if-has-artist>}
${<if-has-year>}<div class="small text-muted">${year}</div>${</if-has-year>}
</div>
</div>
</message>
<message id="Lms.Explore.Releases.template.container">
${elements class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 gx-2 gy-4"}
${elements class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 gx-2 gy-4"}
${loading-indicator}
</message>
+33
View File
@@ -87,6 +87,30 @@ body {
min-width: 64px;
}
.Lms-cover-full-modal-dialog {
width: auto;
max-width: none;
max-height: 90vh;
display: flex;
align-items: center;
justify-content: center;
}
.Lms-cover-full {
max-width: 100%;
max-height: 95vh;
width: auto;
height: auto;
display: block;
margin: auto;
cursor: pointer;
object-fit: contain;
}
.Lms-cursor-pointer {
cursor: pointer;
}
#lms-mp-progress {
transition: none;
}
@@ -103,6 +127,15 @@ body {
background-color: var(--bs-gray-900);
}
.Lms-multiline-clamp {
line-clamp: 2;
-webkit-line-clamp: 2;
overflow: hidden;
display: -webkit-box;
word-break: break-word;
-webkit-box-orient: vertical;
}
.Lms-notification-container {
position: fixed;
bottom: 78px;
+338 -319
View File
@@ -1,123 +1,253 @@
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
var LMS = LMS || {};
// Keep in sync with MediaPlayer::TranscodingMode cpp
const TranscodingMode = {
LMSTranscodingMode = {
Never: 0,
Always: 1,
IfFormatNotSupported: 2,
}
Object.freeze(LMSTranscodingMode);
const Mode = {
Transcoding: 1,
File: 2,
}
Object.freeze(Mode);
class LMSMediaPlayer {
// How much to increase / decrease volume when adjusting it with keyboard shortcuts
static #volumeStepAmount = 0.05;
// How much to increase / decrease volume when adjusting it with keyboard shortcuts
const volumeStepAmount = 0.05;
// How much to seek back / forward (in seconds) with keyboard shortcuts
static #seekAmount = 5;
LMS.mediaplayer = function () {
let _root = {};
let _elems = {};
let _offset = 0;
let _trackId = null;
let _duration = 0;
let _audioNativeSrc;
let _audioTranscodingSrc;
let _settings = {};
let _playedDuration = 0;
let _lastStartPlaying = null;
let _audioIsInit = false;
let _pendingTrackParameters = null;
let _gainNode = null;
let _audioCtx = null;
static #Mode = {
Transcoding: 1,
File: 2,
}
let _unlock = function() {
document.removeEventListener("touchstart", _unlock);
document.removeEventListener("touchend", _unlock);
document.removeEventListener("click", _unlock);
_initAudioCtx();
#root;
#elems;
#offset;
#trackId;
#duration;
#audioNativeSrc;
#audioTranscodingSrc;
#settings;
#playedDuration;
#lastStartPlaying;
#audioIsInit;
#pendingTrackParameters;
#gainNode;
#audioCtx;
constructor(root, defaultSettings) {
this.#root = root;
this.#elems = {};
this.#offset = 0;
this.#trackId = null;
this.#duration = 0;
this.#settings = {};
this.#playedDuration = 0;
this.#lastStartPlaying = null;
this.#audioIsInit = false;
this.#pendingTrackParameters = null;
this.#gainNode = null;
this.#audioCtx = null;
this.#elems.audio = document.getElementById("lms-mp-audio");
this.#elems.playpause = document.getElementById("lms-mp-playpause");
this.#elems.previous = document.getElementById("lms-mp-previous");
this.#elems.next = document.getElementById("lms-mp-next");
this.#elems.progress = document.getElementById("lms-mp-progress");
this.#elems.seek = document.getElementById("lms-mp-seek");
this.#elems.curtime = document.getElementById("lms-mp-curtime");
this.#elems.duration = document.getElementById("lms-mp-duration");
this.#elems.volume = document.getElementById("lms-mp-volume");
this.#elems.volumeslider = document.getElementById("lms-mp-volume-slider");
this.#elems.transcodingActive = document.getElementById("lms-transcoding-active");
this.#elems.playpause.addEventListener("click", () => {
this.#playPause();
});
this.#elems.previous.addEventListener("click", () => {
this.#playPrevious();
});
this.#elems.next.addEventListener("click", () => {
this.#playNext();
});
this.#elems.seek.addEventListener("change", () => {
this.#seekTo(parseInt(this.#elems.seek.value, 10));
});
this.#elems.audio.addEventListener("play", this.#updateControls.bind(this));
this.#elems.audio.addEventListener("playing", this.#updateControls.bind(this));
this.#elems.audio.addEventListener("pause", this.#updateControls.bind(this));
this.#elems.audio.addEventListener("play", this.#updateMediaSessionState.bind(this));
this.#elems.audio.addEventListener("playing", this.#updateMediaSessionState.bind(this));
this.#elems.audio.addEventListener("pause", this.#updateMediaSessionState.bind(this));
this.#elems.audio.addEventListener("pause", this.#pauseTimer.bind(this));
this.#elems.audio.addEventListener("playing", this.#startTimer.bind(this));
this.#elems.audio.addEventListener("waiting", this.#pauseTimer.bind(this));
this.#elems.audio.addEventListener("timeupdate", () => {
this.#elems.progress.style.width = "" + ((this.#offset + this.#elems.audio.currentTime) / this.#duration) * 100 + "%";
this.#elems.curtime.innerHTML = this.#durationToString(this.#offset + this.#elems.audio.currentTime);
});
this.#elems.audio.addEventListener("ended", () => {
this.#resetTimer();
Wt.emit(this.#root, "playbackEnded");
});
this.#elems.audio.addEventListener("canplay", () => {
if (this.#getAudioMode() == LMSMediaPlayer.#Mode.Transcoding) {
this.#elems.transcodingActive.style.display = "inline";
}
else {
this.#elems.transcodingActive.style.display = "none";
}
});
this.#initVolume();
this.#initDefaultSettings(defaultSettings);
this.#elems.volumeslider.addEventListener("input", () => {
this.#setVolume(this.#elems.volumeslider.value);
});
this.#elems.volume.addEventListener("click", () => {
if (this.#elems.audio.volume != 0) {
this.#setVolume(0);
}
else {
this.#setVolume(this.#elems.lastvolume);
}
});
document.addEventListener("keydown", (event) => {
let handled = false;
if (event.target instanceof HTMLInputElement)
return;
if (event.keyCode == 32) {
this.#playPause();
handled = true;
}
else if (event.ctrlKey && !event.shiftKey && event.keyCode == 37) {
this.#playPrevious();
handled = true;
}
else if (event.ctrlKey && !event.shiftKey && event.keyCode == 39) {
this.#playNext();
handled = true;
}
else if (event.ctrlKey && event.keyCode == 40) {
this.#stepVolumeDown();
handled = true;
}
else if (event.ctrlKey && event.keyCode == 38) {
this.#stepVolumeUp();
handled = true;
}
else if (event.ctrlKey && event.shiftKey && event.keyCode == 37) {
this.#seekBack();
handled = true;
}
else if (event.ctrlKey && event.shiftKey && event.keyCode == 39) {
this.#seekForward();
handled = true;
}
if (handled)
event.preventDefault();
});
document.addEventListener("touchstart", this.#unlock.bind(this));
document.addEventListener("touchend", this.#unlock.bind(this));
document.addEventListener("click", this.#unlock.bind(this));
}
#unlock() {
document.removeEventListener("touchstart", this.#unlock.bind(this));
document.removeEventListener("touchend", this.#unlock.bind(this));
document.removeEventListener("click", this.#unlock.bind(this));
this.#initAudioCtx();
};
let _initAudioCtx = function() {
if (_audioIsInit) {
_audioCtx.resume(); // not sure of this
#initAudioCtx() {
if (this.#audioIsInit)
return;
}
_audioIsInit = true;
this.#audioIsInit = true;
_audioCtx = new (window.AudioContext || window.webkitAudioContext)();
_gainNode = _audioCtx.createGain();
let source = _audioCtx.createMediaElementSource(_elems.audio);
source.connect(_gainNode);
_gainNode.connect(_audioCtx.destination);
_audioCtx.resume(); // not sure of this
this.#audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.#gainNode = this.#audioCtx.createGain();
let source = this.#audioCtx.createMediaElementSource(this.#elems.audio);
source.connect(this.#gainNode);
this.#gainNode.connect(this.#audioCtx.destination);
this.#audioCtx.resume(); // not sure of this
if ("mediaSession" in navigator) {
navigator.mediaSession.setActionHandler("play", function() {
_playPause();
navigator.mediaSession.setActionHandler("play", () => {
this.#playPause();
});
navigator.mediaSession.setActionHandler("pause", function() {
_playPause();
navigator.mediaSession.setActionHandler("pause", () => {
this.#playPause();
});
navigator.mediaSession.setActionHandler("previoustrack", function() {
_playPrevious();
navigator.mediaSession.setActionHandler("previoustrack", () => {
this.#playPrevious();
});
navigator.mediaSession.setActionHandler("nexttrack", function() {
_playNext();
navigator.mediaSession.setActionHandler("nexttrack", () => {
this.#playNext();
});
navigator.mediaSession.setActionHandler("seekto", function(e) {
_seekTo(e.seekTime);
navigator.mediaSession.setActionHandler("seekto", (e) => {
this.#seekTo(e.seekTime);
});
}
if (_pendingTrackParameters != null) {
_applyAudioTrackParameters(_pendingTrackParameters);
_pendingTrackParameters = null;
if (this.#pendingTrackParameters != null) {
this.#applyAudioTrackParameters(this.#pendingTrackParameters);
this.#pendingTrackParameters = null;
}
}
let _updateControls = function() {
#updateControls() {
const pauseClass = "fa-pause";
const playClass = "fa-play";
if (_elems.audio.paused) {
_elems.playpause.firstElementChild.classList.remove(pauseClass);
_elems.playpause.firstElementChild.classList.add(playClass);
if (this.#elems.audio.paused) {
this.#elems.playpause.firstElementChild.classList.remove(pauseClass);
this.#elems.playpause.firstElementChild.classList.add(playClass);
}
else {
_elems.playpause.firstElementChild.classList.remove(playClass);
_elems.playpause.firstElementChild.classList.add(pauseClass);
this.#elems.playpause.firstElementChild.classList.remove(playClass);
this.#elems.playpause.firstElementChild.classList.add(pauseClass);
}
}
let _startTimer = function() {
if (_lastStartPlaying == null)
Wt.emit(_root, "scrobbleListenNow", _trackId);
_lastStartPlaying = Date.now();
#startTimer() {
if (this.#lastStartPlaying == null)
Wt.emit(this.#root, "scrobbleListenNow", this.#trackId);
this.#lastStartPlaying = Date.now();
}
let _pauseTimer = function() {
if (_lastStartPlaying != null) {
_playedDuration += Date.now() - _lastStartPlaying;
_lastStartPlaying = null;
#pauseTimer() {
if (this.#lastStartPlaying != null) {
this.#playedDuration += Date.now() - this.#lastStartPlaying;
this.#lastStartPlaying = null;
}
}
let _resetTimer = function() {
if (_lastStartPlaying != null)
_pauseTimer();
#resetTimer() {
if (this.#lastStartPlaying != null)
this.#pauseTimer();
if (_playedDuration > 0) {
Wt.emit(_root, "scrobbleListenFinished", _trackId, _playedDuration);
_playedDuration = 0;
if (this.#playedDuration > 0) {
Wt.emit(this.#root, "scrobbleListenFinished", this.#trackId, this.#playedDuration);
this.#playedDuration = 0;
}
}
let _durationToString = function (duration) {
#durationToString(duration) {
const seconds = parseInt(duration, 10);
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
@@ -129,312 +259,173 @@ LMS.mediaplayer = function () {
].filter(Boolean).join(':');
}
let _playTrack = function() {
_elems.audio.play()
.then(_ => {})
#playTrack() {
this.#elems.audio.play()
.then(_ => { })
.catch(error => { console.log("Cannot play audio: " + error); });
}
let _playPause = function() {
_initAudioCtx();
#playPause() {
this.#initAudioCtx();
if (_elems.audio.paused && _elems.audio.children.length > 0) {
_playTrack();
if (this.#elems.audio.paused && this.#elems.audio.children.length > 0) {
this.#playTrack();
}
else
_elems.audio.pause();
this.#elems.audio.pause();
}
let _playPrevious = function() {
_initAudioCtx();
Wt.emit(_root, "playPrevious");
#playPrevious() {
this.#initAudioCtx();
Wt.emit(this.#root, "playPrevious");
}
let _playNext = function() {
_initAudioCtx();
Wt.emit(_root, "playNext");
#playNext() {
this.#initAudioCtx();
Wt.emit(this.#root, "playNext");
}
let _initVolume = function() {
if (typeof(Storage) !== "undefined" && localStorage.volume) {
_elems.volumeslider.value = Number(localStorage.volume);
#initVolume() {
if (typeof (Storage) !== "undefined" && localStorage.volume) {
this.#elems.volumeslider.value = Number(localStorage.volume);
}
_setVolume(_elems.volumeslider.value);
this.#setVolume(this.#elems.volumeslider.value);
}
let _initDefaultSettings = function(defaultSettings) {
if (typeof(Storage) !== "undefined" && localStorage.settings) {
_settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings));
#initDefaultSettings = function (defaultSettings) {
if (typeof (Storage) !== "undefined" && localStorage.settings) {
this.#settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings));
}
else {
_settings = defaultSettings;
this.#settings = defaultSettings;
}
Wt.emit(_root, "settingsLoaded", JSON.stringify(_settings));
Wt.emit(this.#root, "settingsLoaded", JSON.stringify(this.#settings));
}
let _setVolume = function(volume) {
_elems.lastvolume = _elems.audio.volume;
#setVolume(volume) {
this.#elems.lastvolume = this.#elems.audio.volume;
_elems.audio.volume = volume;
_elems.volumeslider.value = volume;
this.#elems.audio.volume = volume;
this.#elems.volumeslider.value = volume;
if (volume > 0.5) {
_elems.volume.classList.remove("fa-volume-off");
_elems.volume.classList.remove("fa-volume-down");
_elems.volume.classList.add("fa-volume-up");
this.#elems.volume.classList.remove("fa-volume-off");
this.#elems.volume.classList.remove("fa-volume-down");
this.#elems.volume.classList.add("fa-volume-up");
}
else if (volume > 0) {
_elems.volume.classList.remove("fa-volume-off");
_elems.volume.classList.remove("fa-volume-up");
_elems.volume.classList.add("fa-volume-down");
this.#elems.volume.classList.remove("fa-volume-off");
this.#elems.volume.classList.remove("fa-volume-up");
this.#elems.volume.classList.add("fa-volume-down");
}
else {
_elems.volume.classList.remove("fa-volume-up");
_elems.volume.classList.remove("fa-volume-down");
_elems.volume.classList.add("fa-volume-off");
this.#elems.volume.classList.remove("fa-volume-up");
this.#elems.volume.classList.remove("fa-volume-down");
this.#elems.volume.classList.add("fa-volume-off");
}
if (typeof(Storage) !== "undefined") {
if (typeof (Storage) !== "undefined") {
localStorage.volume = volume;
}
}
let _stepVolumeDown = function() {
let currentVolume = _elems.audio.volume;
let remainder = (currentVolume * 10) % (volumeStepAmount * 10);
let newVolume = remainder === 0 ? currentVolume - volumeStepAmount : currentVolume - (remainder / 10);
_setVolume(Math.max(newVolume, 0));
#stepVolumeDown() {
let currentVolume = this.#elems.audio.volume;
let remainder = (currentVolume * 10) % (LMSMediaPlayer.#volumeStepAmount * 10);
let newVolume = remainder === 0 ? currentVolume - LMSMediaPlayer.#volumeStepAmount : currentVolume - (remainder / 10);
this.#setVolume(Math.max(newVolume, 0));
}
let _stepVolumeUp = function() {
let currentVolume = _elems.audio.volume;
let remainder = (currentVolume * 10) % (volumeStepAmount * 10);
let newVolume = remainder === 0 ? currentVolume + volumeStepAmount : currentVolume + (volumeStepAmount - (remainder / 10));
_setVolume(Math.min(newVolume, 1));
#stepVolumeUp() {
let currentVolume = this.#elems.audio.volume;
let remainder = (currentVolume * 10) % (LMSMediaPlayer.#volumeStepAmount * 10);
let newVolume = remainder === 0 ? currentVolume + LMSMediaPlayer.#volumeStepAmount : currentVolume + (LMSMediaPlayer.#volumeStepAmount - (remainder / 10));
this.#setVolume(Math.min(newVolume, 1));
}
let _setReplayGain = function (replayGain) {
_gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20);
#setReplayGain(replayGain) {
this.#gainNode.gain.value = Math.pow(10, (this.#settings.replayGain.preAmpGain + replayGain) / 20);
}
let _seekTo = function(seekTime) {
_initAudioCtx();
let mode = _getAudioMode();
#seekTo(seekTime) {
this.#initAudioCtx();
let mode = this.#getAudioMode();
if (!mode)
return;
switch (mode) {
case Mode.Transcoding:
_offset = seekTime;
_removeAudioSources();
_addAudioSource(_audioTranscodingSrc + "&offset=" + _offset);
_elems.audio.load();
_elems.audio.currentTime = 0;
_playTrack();
case LMSMediaPlayer.#Mode.Transcoding:
this.#offset = seekTime;
this.#removeAudioSources();
this.#addAudioSource(this.#audioTranscodingSrc + "&offset=" + this.#offset);
this.#elems.audio.load();
this.#elems.audio.currentTime = 0;
this.#playTrack();
break;
case Mode.File:
_elems.audio.currentTime = seekTime;
_playTrack();
case LMSMediaPlayer.#Mode.File:
this.#elems.audio.currentTime = seekTime;
this.#playTrack();
break;
}
_updateMediaSessionState();
this.#updateMediaSessionState();
}
let _updateMediaSessionState = function() {
#seekBack() {
let currentPosition = this.#offset + this.#elems.audio.currentTime;
let newPosition = currentPosition - LMSMediaPlayer.#seekAmount;
this.#seekTo(Math.max(newPosition, 0));
}
#seekForward() {
let currentPosition = this.#offset + this.#elems.audio.currentTime;
let newPosition = currentPosition + LMSMediaPlayer.#seekAmount;
this.#seekTo(Math.min(newPosition, this.#duration));
}
#updateMediaSessionState() {
if ("mediaSession" in navigator) {
navigator.mediaSession.setPositionState({
duration: _duration,
duration: this.#duration,
playbackRate: 1,
position: Math.min(_offset + _elems.audio.currentTime, _duration),
position: Math.min(this.#offset + this.#elems.audio.currentTime, this.#duration),
});
if (_elems.audio.paused)
if (this.#elems.audio.paused)
navigator.mediaSession.playbackState = "paused";
else
navigator.mediaSession.playbackState = "playing";
}
}
let init = function(root, defaultSettings) {
_root = root;
_elems.audio = document.getElementById("lms-mp-audio");
_elems.playpause = document.getElementById("lms-mp-playpause");
_elems.previous = document.getElementById("lms-mp-previous");
_elems.next = document.getElementById("lms-mp-next");
_elems.progress = document.getElementById("lms-mp-progress");
_elems.seek = document.getElementById("lms-mp-seek");
_elems.curtime = document.getElementById("lms-mp-curtime");
_elems.duration = document.getElementById("lms-mp-duration");
_elems.volume = document.getElementById("lms-mp-volume");
_elems.volumeslider = document.getElementById("lms-mp-volume-slider");
_elems.transcodingActive = document.getElementById("lms-transcoding-active");
_elems.playpause.addEventListener("click", function() {
_playPause();
});
_elems.previous.addEventListener("click", function() {
_playPrevious();
});
_elems.next.addEventListener("click", function() {
_playNext();
});
_elems.seek.addEventListener("change", function() {
_seekTo(parseInt(_elems.seek.value, 10));
});
_elems.audio.addEventListener("play", _updateControls);
_elems.audio.addEventListener("playing", _updateControls);
_elems.audio.addEventListener("pause", _updateControls);
_elems.audio.addEventListener("play", _updateMediaSessionState);
_elems.audio.addEventListener("playing", _updateMediaSessionState);
_elems.audio.addEventListener("pause", _updateMediaSessionState);
_elems.audio.addEventListener("pause", _pauseTimer);
_elems.audio.addEventListener("playing", _startTimer);
_elems.audio.addEventListener("waiting", _pauseTimer);
_elems.audio.addEventListener("timeupdate", function() {
_elems.progress.style.width = "" + ((_offset + _elems.audio.currentTime) / _duration) * 100 + "%";
_elems.curtime.innerHTML = _durationToString(_offset + _elems.audio.currentTime);
});
_elems.audio.addEventListener("ended", function() {
_resetTimer();
Wt.emit(_root, "playbackEnded");
});
_elems.audio.addEventListener("canplay", function() {
if (_getAudioMode() == Mode.Transcoding) {
_elems.transcodingActive.style.display = "inline";
}
else {
_elems.transcodingActive.style.display = "none";
}
});
_initVolume();
_initDefaultSettings(defaultSettings);
_elems.volumeslider.addEventListener("input", function() {
_setVolume(_elems.volumeslider.value);
});
_elems.volume.addEventListener("click", function () {
if (_elems.audio.volume != 0) {
_setVolume(0);
}
else {
_setVolume(_elems.lastvolume);
}
});
document.addEventListener("keydown", function(event) {
let handled = false;
if (event.target instanceof HTMLInputElement)
return;
if (event.keyCode == 32) {
_playPause();
handled = true;
}
else if (event.ctrlKey && event.keyCode == 37) {
_playPrevious();
handled = true;
}
else if (event.ctrlKey && event.keyCode == 39) {
_playNext();
handled = true;
}
else if (event.ctrlKey && event.keyCode == 40) {
_stepVolumeDown();
handled = true;
}
else if (event.ctrlKey && event.keyCode == 38) {
_stepVolumeUp();
handled = true;
}
if (handled)
event.preventDefault();
});
document.addEventListener("touchstart", _unlock);
document.addEventListener("touchend", _unlock);
document.addEventListener("click", _unlock);
}
let _removeAudioSources = function() {
while ( _elems.audio.lastElementChild) {
_elems.audio.removeChild( _elems.audio.lastElementChild);
#removeAudioSources() {
while (this.#elems.audio.lastElementChild) {
this.#elems.audio.removeChild(this.#elems.audio.lastElementChild);
}
}
let _addAudioSource = function(audioSrc) {
#addAudioSource(audioSrc) {
let source = document.createElement('source');
source.src = audioSrc;
_elems.audio.appendChild(source);
this.#elems.audio.appendChild(source);
}
let _getAudioMode = function() {
if (_elems.audio.currentSrc) {
if (_elems.audio.currentSrc.includes("format"))
return Mode.Transcoding;
#getAudioMode() {
if (this.#elems.audio.currentSrc) {
if (this.#elems.audio.currentSrc.includes("format"))
return LMSMediaPlayer.#Mode.Transcoding;
else
return Mode.File;
return LMSMediaPlayer.#Mode.File;
}
else
return undefined;
}
let loadTrack = function(params, autoplay) {
_resetTimer();
_trackId = params.trackId;
_offset = 0;
_duration = params.duration;
_audioNativeSrc = params.nativeResource;
_audioTranscodingSrc = params.transcodingResource + "&bitrate=" + _settings.transcoding.bitrate + "&format=" + _settings.transcoding.format;
_elems.seek.max = _duration;
_removeAudioSources();
// ! order is important
if (_settings.transcoding.mode == TranscodingMode.Never || _settings.transcoding.mode == TranscodingMode.IfFormatNotSupported)
{
_addAudioSource(_audioNativeSrc);
}
if (_settings.transcoding.mode == TranscodingMode.Always || _settings.transcoding.mode == TranscodingMode.IfFormatNotSupported)
{
_addAudioSource(_audioTranscodingSrc);
}
_elems.audio.load();
_elems.curtime.innerHTML = _durationToString(_offset);
_elems.duration.innerHTML = _durationToString(_duration);
if (!_audioIsInit) {
_pendingTrackParameters = params;
return;
}
_applyAudioTrackParameters(params);
if (autoplay && _audioCtx.state == "running")
_playTrack();
}
let _applyAudioTrackParameters = function(params)
{
_setReplayGain(params.replayGain);
#applyAudioTrackParameters(params) {
this.#setReplayGain(params.replayGain);
if ("mediaSession" in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: params.title,
@@ -445,24 +436,52 @@ LMS.mediaplayer = function () {
}
}
let stop = function() {
_elems.audio.pause();
loadTrack(params, autoplay) {
this.#resetTimer();
this.#trackId = params.trackId;
this.#offset = 0;
this.#duration = params.duration;
this.#audioNativeSrc = params.nativeResource;
this.#audioTranscodingSrc = params.transcodingResource + "&bitrate=" + this.#settings.transcoding.bitrate + "&format=" + this.#settings.transcoding.format;
this.#elems.seek.max = this.#duration;
this.#removeAudioSources();
// ! order is important
if (this.#settings.transcoding.mode == LMSTranscodingMode.Never || this.#settings.transcoding.mode == LMSTranscodingMode.IfFormatNotSupported) {
this.#addAudioSource(this.#audioNativeSrc);
}
if (this.#settings.transcoding.mode == LMSTranscodingMode.Always || this.#settings.transcoding.mode == LMSTranscodingMode.IfFormatNotSupported) {
this.#addAudioSource(this.#audioTranscodingSrc);
}
this.#elems.audio.load();
this.#elems.curtime.innerHTML = this.#durationToString(this.#offset);
this.#elems.duration.innerHTML = this.#durationToString(this.#duration);
if (!this.#audioIsInit) {
this.#pendingTrackParameters = params;
return;
}
this.#applyAudioTrackParameters(params);
if (autoplay && this.#audioCtx.state == "running")
this.#playTrack();
}
let setSettings = function(settings) {
_settings = settings;
stop() {
this.#elems.audio.pause();
}
if (typeof(Storage) !== "undefined") {
localStorage.settings = JSON.stringify(_settings);
setSettings(settings) {
this.#settings = settings;
if (typeof (Storage) !== "undefined") {
localStorage.settings = JSON.stringify(this.#settings);
}
}
return {
init: init,
loadTrack: loadTrack,
stop: stop,
setSettings: setSettings,
};
}();
}
// @license-end
+3 -3
View File
@@ -21,13 +21,13 @@
#include <algorithm>
#include <cassert>
#include <cstring> // strerror
#include <fstream>
#include <archive.h>
#include <archive_entry.h>
#include "core/ILogger.hpp"
#include "core/String.hpp"
namespace lms::zip
{
@@ -45,7 +45,7 @@ namespace lms::zip
}
FileException(const std::filesystem::path& p, std::string_view message, int err)
: Exception{ "File '" + p.string() + "': " + std::string{ message } + ": " + ::strerror(err) }
: Exception{ "File '" + p.string() + "': " + std::string{ message } + ": " + core::stringUtils::systemErrorToString(err) }
{
}
};
@@ -75,7 +75,7 @@ namespace lms::zip
{
const int res{ ::archive_write_free(arch) };
if (res != ARCHIVE_OK)
LMS_LOG(UTILS, ERROR, "Failure while freeing archive control struct: " << std::string{ ::strerror(res) });
LMS_LOG(UTILS, ERROR, "Failure while freeing archive control struct: error code = " << res);
}
void ArchiveZipper::ArchiveEntryDeleter::operator()(struct ::archive_entry* archEntry)
+28 -13
View File
@@ -20,10 +20,9 @@
#include "ChildProcess.hpp"
#include <cerrno>
#include <cstring>
#include <cstddef>
#include <fcntl.h>
#include <signal.h>
#include <stdexcept>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -35,8 +34,8 @@
#include <boost/asio/buffer.hpp>
#include <boost/asio/read.hpp>
#include "core/Exception.hpp"
#include "core/ILogger.hpp"
#include "core/String.hpp"
namespace lms::core
{
@@ -46,7 +45,7 @@ namespace lms::core
{
public:
SystemException(int err, const std::string& errMsg)
: ChildProcessException{ errMsg + ": " + ::strerror(err) }
: ChildProcessException{ errMsg + ": " + stringUtils::systemErrorToString(err) }
{
}
@@ -86,14 +85,30 @@ namespace lms::core
throw SystemException{ errno, "fcntl failed to set FD_CLOEXEC!" };
#if defined(__linux__) && defined(F_SETPIPE_SZ)
for (const int fd : { pipefd[0], pipefd[1] })
{
// Just a hint here to prevent the writer from writing too many bytes ahead of the reader
constexpr std::size_t pipeSize{ 65536 * 4 };
if (fcntl(pipefd[0], F_SETPIPE_SZ, pipeSize) == -1)
throw SystemException{ errno, "fcntl failed!" };
if (fcntl(pipefd[1], F_SETPIPE_SZ, pipeSize) == -1)
throw SystemException{ errno, "fcntl failed!" };
constexpr std::size_t targetPipeSize{ static_cast<long>(65'536) * 4 };
std::size_t currentPipeSize{ 65'536 }; // common default value
#if defined(F_GETPIPE_SZ)
const int pipeSizeRes{ fcntl(fd, F_GETPIPE_SZ) };
if (pipeSizeRes == -1)
{
const int err{ errno };
LMS_LOG(CHILDPROCESS, DEBUG, "F_GETPIPE_SZ failed: " << stringUtils::systemErrorToString(err));
}
else
{
currentPipeSize = pipeSizeRes;
}
#endif
if (currentPipeSize < targetPipeSize)
{
if (fcntl(fd, F_SETPIPE_SZ, targetPipeSize) == -1)
{
const int err{ errno };
LMS_LOG(CHILDPROCESS, DEBUG, "F_SETPIPE_SZ failed: " << stringUtils::systemErrorToString(err));
}
}
}
#endif
@@ -153,7 +168,7 @@ namespace lms::core
// process may already have finished
LMS_LOG(CHILDPROCESS, DEBUG, "Killing child process...");
if (::kill(_childPID, SIGKILL) == -1)
LMS_LOG(CHILDPROCESS, DEBUG, "Kill failed: " << ::strerror(errno));
LMS_LOG(CHILDPROCESS, DEBUG, "Kill failed: " << stringUtils::systemErrorToString(errno));
}
bool ChildProcess::wait(bool block)
@@ -165,7 +180,7 @@ namespace lms::core
if (pid == -1)
throw SystemException{ errno, "waitpid failed!" };
else if (pid == 0)
if (pid == 0)
return false;
if (WIFEXITED(wstatus))
+1 -1
View File
@@ -28,7 +28,7 @@ namespace lms::core
{
IOContextRunner::IOContextRunner(boost::asio::io_context& ioContext, std::size_t threadCount, std::string_view name)
: _ioContext{ ioContext }
, _work{ ioContext }
, _work{ boost::asio::make_work_guard(ioContext) }
{
LMS_LOG(UTILS, INFO, "Starting IO context with " << threadCount << " threads...");
+16
View File
@@ -22,6 +22,10 @@
#include <limits>
#include <sstream>
#include <Wt/WDate.h>
#include <Wt/WDateTime.h>
#include <Wt/WTime.h>
namespace lms::core
{
PartialDateTime::PartialDateTime(int year)
@@ -84,6 +88,18 @@ namespace lms::core
return res;
}
PartialDateTime PartialDateTime::fromWtDateTime(const Wt::WDateTime& dateTime)
{
return core::PartialDateTime{
dateTime.date().year(),
static_cast<unsigned>(dateTime.date().month()),
static_cast<unsigned>(dateTime.date().day()),
static_cast<unsigned>(dateTime.time().hour()),
static_cast<unsigned>(dateTime.time().minute()),
static_cast<unsigned>(dateTime.time().second())
};
}
std::string PartialDateTime::toISO8601String() const
{
if (_precision == Precision::Invalid)
+23 -4
View File
@@ -20,6 +20,8 @@
#include "core/String.hpp"
#include <algorithm>
#include <array>
#include <cstring>
#include <iomanip>
#include <utility>
@@ -466,14 +468,24 @@ namespace lms::core::stringUtils
std::string toISO8601String(const Wt::WDateTime& dateTime)
{
// assume UTC
return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8();
if (dateTime.isValid())
{
// assume UTC
return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8();
}
return "";
}
std::string toISO8601String(const Wt::WDate& date)
{
// assume UTC
return date.toString("yyyy-MM-dd").toUTF8();
if (date.isValid())
{
// assume UTC
return date.toString("yyyy-MM-dd").toUTF8();
}
return "";
}
Wt::WDateTime fromISO8601String(std::string_view dateTime)
@@ -494,4 +506,11 @@ namespace lms::core::stringUtils
return "[" + std::to_string(mins) + ":" + (secs < 10 ? "0" : "") + std::to_string(secs) + "." + (millis < 100 ? (millis < 10 ? "00" : "0") : "") + std::to_string(millis) + "]";
}
std::string systemErrorToString(int err)
{
std::array<char, 128> buffer{};
::strerror_r(err, buffer.data(), buffer.size());
return std::string{ buffer.data() };
}
} // namespace lms::core::stringUtils
+3 -2
View File
@@ -21,6 +21,7 @@
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/post.hpp>
#include "core/Exception.hpp"
#include "core/ILogger.hpp"
@@ -64,9 +65,9 @@ namespace lms::core::http
, _baseUrl{ baseUrl }
{
_client.done().connect([this](Wt::AsioWrapper::error_code ec, const Wt::Http::Message& msg) {
_strand.dispatch([this, ec, msg = std::move(msg)] {
boost::asio::post(boost::asio::bind_executor(_strand, [this, ec, msg = std::move(msg)] {
onClientDone(ec, msg);
});
}));
});
}
@@ -21,7 +21,7 @@
#include <filesystem>
#include <memory>
#include <boost/asio/io_service.hpp>
#include <boost/asio/io_context.hpp>
#include "IChildProcess.hpp"
@@ -35,5 +35,5 @@ namespace lms::core
virtual std::unique_ptr<IChildProcess> spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) = 0;
};
std::unique_ptr<IChildProcessManager> createChildProcessManager(boost::asio::io_service& ioService);
std::unique_ptr<IChildProcessManager> createChildProcessManager(boost::asio::io_context& ioContext);
} // namespace lms::core
@@ -19,9 +19,9 @@
#pragma once
#include <optional>
#include <thread>
#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp>
namespace lms::core
@@ -39,7 +39,7 @@ namespace lms::core
private:
boost::asio::io_context& _ioContext;
std::optional<boost::asio::io_context::work> _work;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> _work;
std::vector<std::thread> _threads;
};
} // namespace lms::core
+22 -11
View File
@@ -23,6 +23,11 @@
#include <string>
#include <string_view>
namespace Wt
{
class WDateTime;
}
namespace lms::core
{
class PartialDateTime
@@ -35,13 +40,29 @@ namespace lms::core
PartialDateTime(int year, unsigned month, unsigned day, unsigned hour, unsigned min, unsigned sec);
static PartialDateTime fromString(std::string_view str);
static PartialDateTime fromWtDateTime(const Wt::WDateTime& dateTime);
std::string toISO8601String() const;
bool isValid() const { return _precision != Precision::Invalid; }
constexpr bool isValid() const { return _precision != Precision::Invalid; }
enum class Precision : std::uint8_t
{
Invalid,
Year,
Month,
Day,
Hour,
Min,
Sec,
};
constexpr Precision getPrecision() const { return _precision; }
constexpr std::optional<int> getYear() const { return (_precision >= Precision::Year ? std::make_optional(_year) : std::nullopt); }
constexpr std::optional<int> getMonth() const { return (_precision >= Precision::Month ? std::make_optional(_month) : std::nullopt); }
constexpr std::optional<int> getDay() const { return (_precision >= Precision::Day ? std::make_optional(_day) : std::nullopt); }
constexpr std::optional<int> getHour() const { return (_precision >= Precision::Hour ? std::make_optional(_hour) : std::nullopt); }
constexpr std::optional<int> getMin() const { return (_precision >= Precision::Min ? std::make_optional(_min) : std::nullopt); }
constexpr std::optional<int> getSec() const { return (_precision >= Precision::Sec ? std::make_optional(_sec) : std::nullopt); }
constexpr auto operator<=>(const PartialDateTime& other) const = default;
@@ -54,16 +75,6 @@ namespace lms::core
std::uint8_t _hour{}; // 0 to 23
std::uint8_t _min{}; // 0 to 59
std::uint8_t _sec{}; // 0 to 59
enum class Precision : std::uint8_t
{
Invalid,
Year,
Month,
Day,
Hour,
Min,
Sec,
};
Precision _precision{ Precision::Invalid };
};
} // namespace lms::core
+2 -1
View File
@@ -20,7 +20,6 @@
#pragma once
#include <chrono>
#include <initializer_list>
#include <optional>
#include <span>
#include <sstream>
@@ -120,4 +119,6 @@ namespace lms::core::stringUtils
// to "[minutes:seconds.milliseconds]"
std::string formatTimestamp(std::chrono::milliseconds timestamp);
std::string systemErrorToString(int err);
} // namespace lms::core::stringUtils
+23 -4
View File
@@ -301,14 +301,33 @@ namespace lms::core::stringUtils::tests
TEST(Stringutils, DateToString)
{
const Wt::WDate date{ 2020, 01, 03 };
EXPECT_EQ(toISO8601String(date), "2020-01-03");
{
const Wt::WDate date{ 2020, 01, 03 };
EXPECT_EQ(toISO8601String(date), "2020-01-03");
}
{
const Wt::WDate date;
EXPECT_EQ(toISO8601String(date), "");
}
}
TEST(Stringutils, DateTimeToString)
{
const Wt::WDateTime dateTime{ Wt::WDate{ 2020, 01, 03 }, Wt::WTime{ 9, 8, 11, 75 } };
EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T09:08:11.075");
{
const Wt::WDateTime dateTime{ Wt::WDate{ 2020, 01, 03 }, Wt::WTime{ 9, 8, 11, 75 } };
EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T09:08:11.075");
}
{
const Wt::WDateTime dateTime{ Wt::WDate{ 2020, 01, 03 } };
EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T00:00:00.000");
}
{
const Wt::WDateTime dateTime;
EXPECT_EQ(toISO8601String(dateTime), "");
}
}
TEST(Stringutils, DateTimeFromString)
+26 -11
View File
@@ -49,8 +49,10 @@ namespace lms::db
|| params.linkType
|| params.track.isValid()
|| params.release.isValid()
|| params.clusters.size() == 1
|| params.mediaLibrary.isValid())
|| params.filters.clusters.size() == 1
|| params.filters.mediaLibrary.isValid()
|| params.filters.label.isValid()
|| params.filters.releaseType.isValid())
{
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
}
@@ -59,7 +61,9 @@ namespace lms::db
|| params.sortMethod == ArtistSortMethod::AddedDesc
|| params.writtenAfter.isValid()
|| params.release.isValid()
|| params.mediaLibrary.isValid())
|| params.filters.mediaLibrary.isValid()
|| params.filters.label.isValid()
|| params.filters.releaseType.isValid())
{
query.join("track t ON t.id = t_a_l.track_id");
@@ -69,8 +73,20 @@ namespace lms::db
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
if (params.filters.label.isValid())
{
query.join("release_label r_l ON r_l.release_id = t.release_id");
query.where("r_l.label_id = ?").bind(params.filters.label);
}
if (params.filters.releaseType.isValid())
{
query.join("release_release_type r_r_t ON r_r_t.release_id = t.release_id");
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
}
}
if (params.linkType)
@@ -108,27 +124,27 @@ namespace lms::db
.bind(SyncState::PendingRemove);
}
if (params.clusters.size() == 1)
if (params.filters.clusters.size() == 1)
{
query.join("track_cluster t_c ON t_c.track_id = t_a_l.track_id")
.where("t_c.cluster_id = ?")
.bind(params.clusters.front());
.bind(params.filters.clusters.front());
}
else if (params.clusters.size() > 1)
else if (params.filters.clusters.size() > 1)
{
std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT t_a_l.artist_id FROM track_artist_link t_a_l"
" INNER JOIN track_cluster t_c ON t_c.track_id = t_a_l.track_id";
WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
for (const ClusterId clusterId : params.filters.clusters)
{
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
query.bind(clusterId);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t_a_l.track_id,t_a_l.artist_id HAVING COUNT(DISTINCT t_c.cluster_id) = " << params.clusters.size() << ")";
oss << " GROUP BY t_a_l.track_id,t_a_l.artist_id HAVING COUNT(DISTINCT t_c.cluster_id) = " << params.filters.clusters.size() << ")";
query.where(oss.str());
}
@@ -189,7 +205,6 @@ namespace lms::db
: _mbid{ mbid ? mbid->getAsString() : "" }
{
setName(name);
_sortName = _name;
}
Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<core::UUID>& mbid)
+55 -15
View File
@@ -42,30 +42,46 @@ namespace lms::db
assert(!params.artist.isValid()); // poor check
if (params.library.isValid())
if (params.filters.mediaLibrary.isValid()
|| params.filters.label.isValid()
|| params.filters.releaseType.isValid())
{
query.join("track t ON t.id = t_a_l.track_id");
query.where("t.media_library_id = ?").bind(params.library);
if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
if (params.filters.label.isValid())
{
query.join("release_label r_l ON r_l.release_id = t.release_id");
query.where("r_l.label_id = ?").bind(params.filters.label);
}
if (params.filters.releaseType.isValid())
{
query.join("release_release_type r_r_t ON r_r_t.release_id = t.release_id");
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
}
}
if (params.linkType)
query.where("t_a_l.type = ?").bind(*params.linkType);
if (!params.clusters.empty())
if (!params.filters.clusters.empty())
{
std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT t_a_l.artist_id FROM track_artist_link t_a_l"
" INNER JOIN track_cluster t_c ON t_c.track_id = t_a_l.track_id";
WhereClause clusterClause;
for (auto id : params.clusters)
for (const db::ClusterId id : params.filters.clusters)
{
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
query.bind(id);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t_a_l.track_id,t_a_l.artist_id HAVING COUNT(DISTINCT t_c.cluster_id) = " << params.clusters.size() << ")";
oss << " GROUP BY t_a_l.track_id,t_a_l.artist_id HAVING COUNT(DISTINCT t_c.cluster_id) = " << params.filters.clusters.size() << ")";
query.where(oss.str());
}
@@ -110,10 +126,22 @@ namespace lms::db
.bind(params.artist);
}
if (params.library.isValid())
query.where("t.media_library_id = ?").bind(params.library);
if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
if (!params.clusters.empty())
if (params.filters.label.isValid())
{
query.join("release_label r_l ON r_l.release_id = r.id");
query.where("r_l.label_id = ?").bind(params.filters.label);
}
if (params.filters.releaseType.isValid())
{
query.join("release_release_type r_r_t ON r_r_t.release_id = r.id");
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
}
if (!params.filters.clusters.empty())
{
std::ostringstream oss;
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
@@ -122,14 +150,14 @@ namespace lms::db
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (ClusterId id : params.clusters)
for (const ClusterId id : params.filters.clusters)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(id);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << params.filters.clusters.size() << ")";
query.where(oss.str());
}
@@ -157,10 +185,22 @@ namespace lms::db
.bind(params.artist);
}
if (params.library.isValid())
query.where("t.media_library_id = ?").bind(params.library);
if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
if (!params.clusters.empty())
if (params.filters.label.isValid())
{
query.join("release_label r_l ON r_l.release_id = t.release_id");
query.where("r_l.label_id = ?").bind(params.filters.label);
}
if (params.filters.releaseType.isValid())
{
query.join("release_release_type r_r_t ON r_r_t.release_id = t.release_id");
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
}
if (!params.filters.clusters.empty())
{
std::ostringstream oss;
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
@@ -168,14 +208,14 @@ namespace lms::db
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
WhereClause clusterClause;
for (auto id : params.clusters)
for (const ClusterId id : params.filters.clusters)
{
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
query.bind(id);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.filters.clusters.size() << ")";
query.where(oss.str());
}
+7
View File
@@ -78,6 +78,13 @@ namespace lms::db
});
}
bool MediaLibrary::isEmpty() const
{
assert(session());
auto query{ session()->query<bool>("SELECT EXISTS (SELECT 1 FROM track WHERE media_library_id = ? LIMIT 1) AS is_media_library_empty").bind(getId()) };
return !utils::fetchQuerySingleResult(query);
}
void MediaLibrary::setPath(const std::filesystem::path& p)
{
assert(p.is_absolute());
+39 -1
View File
@@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
static constexpr Version LMS_DATABASE_VERSION{ 80 };
static constexpr Version LMS_DATABASE_VERSION{ 82 };
}
VersionInfo::VersionInfo()
@@ -1063,6 +1063,42 @@ FROM tracklist)");
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
}
void migrateFromV80(Session& session)
{
// Add release comment support
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD COLUMN comment TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scan rescan everything
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
}
void migrateFromV81(Session& session)
{
// Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all
dropIndexes(session);
// Add country + release country
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "country" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null
))");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "release_country" (
"country_id" bigint,
"release_id" bigint,
primary key ("country_id", "release_id"),
constraint "fk_release_country_key1" foreign key ("country_id") references "country" ("id") on delete cascade deferrable initially deferred,
constraint "fk_release_country_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
))");
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "release_country_country" on "release_country" ("country_id"))");
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "release_country_release" on "release_country" ("release_id"))");
// Just increment the scan version of the settings to make the next scan rescan everything
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
}
bool doDbMigration(Session& session)
{
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -1119,6 +1155,8 @@ FROM tracklist)");
{ 77, migrateFromV77 },
{ 78, migrateFromV78 },
{ 79, migrateFromV79 },
{ 80, migrateFromV80 },
{ 81, migrateFromV81 },
};
bool migrationPerformed{};
+6
View File
@@ -81,6 +81,8 @@ namespace lms::db
std::vector<std::filesystem::path> PlayListFile::getFiles() const
{
std::vector<std::filesystem::path> files;
try
{
Wt::Json::Object root;
Wt::Json::parse(_entries, root);
@@ -90,6 +92,10 @@ namespace lms::db
for (const Wt::Json::Value& file : filesArray)
files.push_back(static_cast<std::string>(file.toString()));
}
catch (const Wt::Json::ParseError& e)
{
LMS_LOG(DB, ERROR, "Cannot parse files saved in " << _absoluteFilePath << ": corrupted data?");
}
return files;
}
+145 -13
View File
@@ -28,6 +28,7 @@
#include "database/Image.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/Types.hpp"
#include "database/User.hpp"
#include "EnumSetTraits.hpp"
@@ -59,8 +60,8 @@ namespace lms::db
|| params.writtenAfter.isValid()
|| params.dateRange
|| params.artist.isValid()
|| params.clusters.size() == 1
|| params.mediaLibrary.isValid()
|| params.filters.clusters.size() == 1
|| params.filters.mediaLibrary.isValid()
|| params.directory.isValid()
|| params.parentDirectory.isValid())
{
@@ -73,8 +74,20 @@ namespace lms::db
query.where("d.parent_directory_id = ?").bind(params.parentDirectory);
}
if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
if (params.filters.label.isValid())
{
query.join("release_label r_l ON r_l.release_id = r.id");
query.where("r_l.label_id = ?").bind(params.filters.label);
}
if (params.filters.releaseType.isValid())
{
query.join("release_release_type r_r_t ON r_r_t.release_id = r.id");
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
}
if (params.directory.isValid())
query.where("t.directory_id = ?").bind(params.directory);
@@ -167,31 +180,34 @@ namespace lms::db
}
}
if (params.clusters.size() == 1)
if (params.filters.clusters.size() == 1)
{
query.join("track_cluster t_c ON t_c.track_id = t.id")
.where("t_c.cluster_id = ?")
.bind(params.clusters.front());
.bind(params.filters.clusters.front());
}
else if (params.clusters.size() > 1)
else if (params.filters.clusters.size() > 1)
{
std::ostringstream oss;
oss << "r.id IN (SELECT DISTINCT t.release_id FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
for (const ClusterId clusterId : params.filters.clusters)
{
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
query.bind(clusterId);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.filters.clusters.size() << ")";
query.where(oss.str());
}
if (params.releaseGroupMBID)
query.where("group_mbid = ?").bind(params.releaseGroupMBID->getAsString());
switch (params.sortMethod)
{
case ReleaseSortMethod::None:
@@ -202,6 +218,9 @@ namespace lms::db
case ReleaseSortMethod::Name:
query.orderBy("r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::SortName:
query.orderBy("r.sort_name COLLATE NOCASE");
break;
case ReleaseSortMethod::ArtistNameThenName:
query.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE");
break;
@@ -253,6 +272,50 @@ namespace lms::db
} // namespace
Country::Country(std::string_view name)
: _name{ name }
{
// As we use the name to uniquely identoify release type, we must throw (and not truncate)
if (name.size() > _maxNameLength)
throw Exception{ "Country name is too long: " + std::string{ name } + "'" };
}
Country::pointer Country::create(Session& session, std::string_view name)
{
return session.getDboSession()->add(std::unique_ptr<Country>{ new Country{ name } });
}
std::size_t Country::getCount(Session& session)
{
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM country"));
}
Country::pointer Country::find(Session& session, CountryId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Country>>("SELECT c from country c").where("c.id = ?").bind(id));
}
Country::pointer Country::find(Session& session, std::string_view name)
{
session.checkReadTransaction();
if (name.size() > _maxNameLength)
throw Exception{ "Requeted Country name is too long: " + std::string{ name } + "'" };
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Country>>("SELECT c from country c").where("c.name = ?").bind(name));
}
RangeResults<CountryId> Country::findOrphanIds(Session& session, std::optional<Range> range)
{
session.checkReadTransaction();
// select the labels that have no releases
auto query{ session.getDboSession()->query<CountryId>("select c.id from country c LEFT OUTER JOIN release_country r_c ON c.id = r_c.country_id WHERE r_c.release_id IS NULL") };
return utils::execRangeQuery<CountryId>(query, range);
}
Label::Label(std::string_view name)
: _name{ name }
{
@@ -288,6 +351,24 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Label>>("SELECT l from label l").where("l.name = ?").bind(name));
}
void Label::find(Session& session, LabelSortMethod sortMethod, std::function<void(const Label::pointer& label)> func)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->find<Label>() };
switch (sortMethod)
{
case LabelSortMethod::None:
break;
case LabelSortMethod::Name:
query.orderBy("name COLLATE NOCASE");
}
utils::forEachQueryResult(query, [&](const Label::pointer& label) {
func(label);
});
}
RangeResults<LabelId> Label::findOrphanIds(Session& session, std::optional<Range> range)
{
session.checkReadTransaction();
@@ -332,6 +413,24 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ReleaseType>>("SELECT r_t from release_type r_t").where("r_t.name = ?").bind(name));
}
void ReleaseType::find(Session& session, ReleaseTypeSortMethod sortMethod, std::function<void(const ReleaseType::pointer& releaseType)> func)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->find<ReleaseType>() };
switch (sortMethod)
{
case ReleaseTypeSortMethod::None:
break;
case ReleaseTypeSortMethod::Name:
query.orderBy("name COLLATE NOCASE");
}
utils::forEachQueryResult(query, [&](const ReleaseType::pointer& releaseType) {
func(releaseType);
});
}
RangeResults<ReleaseTypeId> ReleaseType::findOrphanIds(Session& session, std::optional<Range> range)
{
session.checkReadTransaction();
@@ -612,6 +711,11 @@ namespace lms::db
_labels.clear();
}
void Release::clearCountries()
{
_countries.clear();
}
void Release::clearReleaseTypes()
{
_releaseTypes.clear();
@@ -622,6 +726,11 @@ namespace lms::db
_labels.insert(getDboPtr(label));
}
void Release::addCountry(ObjectPtr<Country> country)
{
_countries.insert(getDboPtr(country));
}
void Release::addReleaseType(ObjectPtr<ReleaseType> releaseType)
{
_releaseTypes.insert(getDboPtr(releaseType));
@@ -659,8 +768,22 @@ namespace lms::db
{
std::vector<std::string> res;
for (const auto& label : _labels)
auto query{ _labels.find() };
utils::forEachQueryResult(query, [&](const Label::pointer& label) {
res.push_back(std::string{ label->getName() });
});
return res;
}
std::vector<std::string> Release::getCountryNames() const
{
std::vector<std::string> res;
auto query{ _countries.find() };
utils::forEachQueryResult(query, [&](const Country::pointer& country) {
res.push_back(std::string{ country->getName() });
});
return res;
}
@@ -669,8 +792,10 @@ namespace lms::db
{
std::vector<std::string> res;
for (const auto& releaseType : _releaseTypes)
auto query{ _releaseTypes.find() };
utils::forEachQueryResult(query, [&](const ReleaseType::pointer& releaseType) {
res.push_back(std::string{ releaseType->getName() });
});
return res;
}
@@ -704,11 +829,18 @@ namespace lms::db
return utils::fetchQuerySingleResult(session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t").where("t.release_id = ?").bind(getId()));
}
Wt::WDateTime Release::getLastWritten() const
Wt::WDateTime Release::getLastWrittenTime() const
{
assert(session());
return utils::fetchQuerySingleResult(session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t").where("t.release_id = ?").bind(getId()));
return utils::fetchQuerySingleResult(session()->query<Wt::WDateTime>("SELECT MAX(file_last_write) FROM track t").where("t.release_id = ?").bind(getId()));
}
Wt::WDateTime Release::getAddedTime() const
{
assert(session());
return utils::fetchQuerySingleResult(session()->query<Wt::WDateTime>("SELECT MAX(file_added) FROM track t").where("t.release_id = ?").bind(getId()));
}
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
+13 -2
View File
@@ -102,6 +102,7 @@ namespace lms::db
_session.mapClass<AuthToken>("auth_token");
_session.mapClass<Cluster>("cluster");
_session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Country>("country");
_session.mapClass<Directory>("directory");
_session.mapClass<Image>("image");
_session.mapClass<Label>("label");
@@ -199,6 +200,9 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS country_id_idx ON country(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS country_name_idx ON country(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_id_idx ON directory(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_parent_directory_idx ON directory(parent_directory_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)");
@@ -210,7 +214,8 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_stem_idx ON image(stem COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS label_name_idx ON label(name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS label_id_idx ON label(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS label_name_idx ON label(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)");
@@ -230,10 +235,15 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_image_idx ON release(image_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_group_mbid_idx ON release(group_mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_sort_name_idx ON release(sort_name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_sort_name_nocase_idx ON release(sort_name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_type_id_idx ON release_type(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)");
@@ -249,6 +259,7 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_file_size_idx ON track(name, file_size)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
+23 -8
View File
@@ -79,28 +79,28 @@ namespace lms::db
.bind(SyncState::PendingRemove);
}
if (params.clusters.size() == 1)
if (params.filters.clusters.size() == 1)
{
// optim
query.join("track_cluster t_c ON t_c.track_id = t.id")
.where("t_c.cluster_id = ?")
.bind(params.clusters.front());
.bind(params.filters.clusters.front());
}
else if (params.clusters.size() > 1)
else if (params.filters.clusters.size() > 1)
{
std::ostringstream oss;
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
for (const ClusterId clusterId : params.filters.clusters)
{
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
query.bind(clusterId);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.filters.clusters.size() << ")";
query.where(oss.str());
}
@@ -159,8 +159,20 @@ namespace lms::db
if (params.discNumber)
query.where("t.disc_number = ?").bind(*params.discNumber);
if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
if (params.filters.label.isValid())
{
query.join("release_label r_l ON r_l.release_id = t.release_id");
query.where("r_l.label_id = ?").bind(params.filters.label);
}
if (params.filters.releaseType.isValid())
{
query.join("release_release_type r_r_t ON r_r_t.release_id = t.release_id");
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
}
if (params.directory.isValid())
query.where("t.directory_id = ?").bind(params.directory);
@@ -168,6 +180,9 @@ namespace lms::db
if (params.hasEmbeddedImage.has_value())
query.where("t.has_cover = ?").bind(params.hasEmbeddedImage.value());
if (params.fileSize.has_value())
query.where("t.file_size = ?").bind(static_cast<long long>(params.fileSize.value()));
switch (params.sortMethod)
{
case TrackSortMethod::None:
@@ -195,7 +210,7 @@ namespace lms::db
query.orderBy("t.file_name COLLATE NOCASE");
break;
case TrackSortMethod::DateDescAndRelease:
query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)) DESC,t.release_id,t.disc_number,t.track_number");
query.orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number");
break;
case TrackSortMethod::Release:
query.orderBy("t.disc_number,t.track_number");
+25 -7
View File
@@ -42,7 +42,9 @@ namespace lms::db
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM tracklist t_l") };
if (!params.clusters.empty() || params.mediaLibrary.isValid())
if (!params.filters.clusters.empty()
|| params.filters.mediaLibrary.isValid()
|| params.filters.label.isValid())
{
query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id");
query.groupBy("t_l.id");
@@ -51,11 +53,27 @@ namespace lms::db
for (std::string_view keyword : params.keywords)
query.where("t_l.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
if (params.mediaLibrary.isValid())
if (params.filters.mediaLibrary.isValid()
|| params.filters.label.isValid()
|| params.filters.releaseType.isValid())
{
query.join("track t ON t.id = t_l_e.track_id");
if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
if (params.filters.label.isValid())
{
query.join("release_label r_l ON r_l.release_id = t.release_id");
query.where("r_l.label_id = ?").bind(params.filters.label);
}
if (params.filters.releaseType.isValid())
{
query.join("release_release_type r_r_t ON r_r_t.release_id = t.release_id");
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
}
}
if (params.user.isValid())
query.where("t_l.user_id = ?").bind(params.user);
@@ -68,7 +86,7 @@ namespace lms::db
if (params.visibility)
query.where("t_l.visibility = ?").bind(*params.visibility);
if (!params.clusters.empty())
if (!params.filters.clusters.empty())
{
std::ostringstream oss;
oss << "t_l_e.track_id IN (SELECT DISTINCT t.id FROM track t"
@@ -76,14 +94,14 @@ namespace lms::db
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters)
for (const ClusterId clusterId : params.filters.clusters)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(clusterId);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.filters.clusters.size() << ")";
query.where(oss.str());
}
@@ -32,6 +32,7 @@
#include "core/UUID.hpp"
#include "database/ArtistId.hpp"
#include "database/ClusterId.hpp"
#include "database/Filters.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Object.hpp"
#include "database/ReleaseId.hpp"
@@ -57,7 +58,7 @@ namespace lms::db
public:
struct FindParameters
{
std::vector<ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
Filters filters;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
ArtistSortMethod sortMethod{ ArtistSortMethod::None };
@@ -67,11 +68,10 @@ namespace lms::db
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
TrackId track; // artists involved in this track
ReleaseId release; // artists involved in this release
MediaLibraryId mediaLibrary; // artists that belong to this library
FindParameters& setClusters(std::span<const ClusterId> _clusters)
FindParameters& setFilters(const Filters& _filters)
{
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
filters = _filters;
return *this;
}
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
@@ -115,11 +115,6 @@ namespace lms::db
release = _release;
return *this;
}
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary)
{
mediaLibrary = _mediaLibrary;
return *this;
}
};
Artist() = default;
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(CountryId)
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <span>
#include "database/ClusterId.hpp"
#include "database/LabelId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/ReleaseTypeId.hpp"
namespace lms::db
{
struct Filters
{
MediaLibraryId mediaLibrary; // tracks that belongs to this library
std::vector<ClusterId> clusters; // tracks that belong to all these clusters
LabelId label; // tracks which release has this label
ReleaseTypeId releaseType; // tracks which release has this type
Filters& setClusters(std::span<const ClusterId> _clusters)
{
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
return *this;
}
Filters& setMediaLibrary(MediaLibraryId _mediaLibrary)
{
mediaLibrary = _mediaLibrary;
return *this;
}
Filters& setLabel(LabelId _label)
{
label = _label;
return *this;
}
Filters& setReleaseType(ReleaseTypeId _releaseType)
{
releaseType = _releaseType;
return *this;
}
};
} // namespace lms::db
+4 -11
View File
@@ -25,9 +25,8 @@
#include <Wt/WDateTime.h>
#include "database/ArtistId.hpp"
#include "database/ClusterId.hpp"
#include "database/Filters.hpp"
#include "database/ListenId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Object.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackId.hpp"
@@ -85,11 +84,10 @@ namespace lms::db
{
UserId user;
std::optional<ScrobblingBackend> backend;
std::vector<ClusterId> clusters; // if non empty, entities that belong to these clusters
db::Filters filters;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::optional<Range> range;
ArtistId artist; // if set, matching this artist
MediaLibraryId library;
StatsFindParameters& setUser(UserId _user)
{
@@ -101,9 +99,9 @@ namespace lms::db
backend = _backend;
return *this;
}
StatsFindParameters& setClusters(const std::vector<ClusterId>& _clusters)
StatsFindParameters& setFilters(const db::Filters& _filters)
{
clusters = _clusters;
filters = _filters;
return *this;
}
StatsFindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
@@ -121,11 +119,6 @@ namespace lms::db
artist = _artist;
return *this;
}
StatsFindParameters& setMediaLibrary(MediaLibraryId _library)
{
library = _library;
return *this;
}
};
struct ArtistStatsFindParameters : public StatsFindParameters
@@ -50,6 +50,7 @@ namespace lms::db
// getters
std::string_view getName() const { return _name; }
const std::filesystem::path& getPath() const { return _path; }
bool isEmpty() const;
// setters
void setName(std::string_view name) { _name = name; }
+54 -11
View File
@@ -20,7 +20,6 @@
#pragma once
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
@@ -32,8 +31,9 @@
#include "core/PartialDateTime.hpp"
#include "core/UUID.hpp"
#include "database/ArtistId.hpp"
#include "database/ClusterId.hpp"
#include "database/CountryId.hpp"
#include "database/DirectoryId.hpp"
#include "database/Filters.hpp"
#include "database/LabelId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Object.hpp"
@@ -53,6 +53,37 @@ namespace lms::db
class Track;
class User;
class Country final : public Object<Country, CountryId>
{
public:
Country() = default;
static std::size_t getCount(Session& session);
static pointer find(Session& session, CountryId id);
static pointer find(Session& session, std::string_view name);
static RangeResults<CountryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// Accessors
std::string_view getName() const { return _name; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _releases, Wt::Dbo::ManyToMany, "release_country", "", Wt::Dbo::OnDeleteCascade);
}
private:
static constexpr std::size_t _maxNameLength{ 32 };
friend class Session;
Country(std::string_view name);
static pointer create(Session& session, std::string_view name);
std::string _name;
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> _releases; // releases that match this country
};
class Label final : public Object<Label, LabelId>
{
public:
@@ -61,6 +92,7 @@ namespace lms::db
static std::size_t getCount(Session& session);
static pointer find(Session& session, LabelId id);
static pointer find(Session& session, std::string_view name);
static void find(Session& session, LabelSortMethod sortMethod, std::function<void(const Label::pointer& label)> func);
static RangeResults<LabelId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// Accessors
@@ -92,6 +124,7 @@ namespace lms::db
static std::size_t getCount(Session& session);
static pointer find(Session& session, ReleaseTypeId id);
static pointer find(Session& session, std::string_view name);
static void find(Session& session, ReleaseTypeSortMethod sortMethod, std::function<void(const ReleaseType::pointer& releaseType)> func);
static RangeResults<ReleaseTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// Accessors
@@ -120,7 +153,7 @@ namespace lms::db
public:
struct FindParameters
{
std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters
Filters filters;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (cannot be set with keywords)
std::string name; // must match this name (cannot be set with keywords)
ReleaseSortMethod sortMethod{ ReleaseSortMethod::None };
@@ -132,14 +165,14 @@ namespace lms::db
ArtistId artist; // only releases that involved this user
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
core::EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
std::string releaseType; // If set, albums that has this release type
MediaLibraryId mediaLibrary; // If set, releases that has at least a track in this library
std::string releaseType; // If set, releases that has this release type
std::optional<core::UUID> releaseGroupMBID; // If set, releases that belong to this release group
DirectoryId directory; // if set, releases in this directory (cannot be set with parent directory)
DirectoryId parentDirectory; // if set, releases in this parent directory (cannot be set with directory)
FindParameters& setClusters(std::span<const ClusterId> _clusters)
FindParameters& setFilters(const Filters& _filters)
{
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
filters = _filters;
return *this;
}
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
@@ -190,9 +223,9 @@ namespace lms::db
releaseType = _releaseType;
return *this;
}
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary)
FindParameters& setReleaseGroupMBID(const std::optional<core::UUID>& _releaseGroupMBID)
{
mediaLibrary = _mediaLibrary;
releaseGroupMBID = _releaseGroupMBID;
return *this;
}
FindParameters& setDirectory(DirectoryId _directory)
@@ -244,16 +277,19 @@ namespace lms::db
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
std::vector<DiscInfo> getDiscs() const;
std::chrono::milliseconds getDuration() const;
Wt::WDateTime getLastWritten() const;
Wt::WDateTime getAddedTime() const;
Wt::WDateTime getLastWrittenTime() const;
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
bool isCompilation() const { return _isCompilation; }
std::size_t getTrackCount() const;
std::vector<ObjectPtr<ReleaseType>> getReleaseTypes() const;
std::vector<std::string> getLabelNames() const;
std::vector<std::string> getCountryNames() const;
std::vector<std::string> getReleaseTypeNames() const;
void visitLabels(const std::function<void(const Label::pointer& label)>& _func) const;
core::EnumSet<Advisory> getAdvisories() const;
std::string_view getBarcode() const { return _barcode; }
std::string_view getComment() const { return _comment; }
ObjectPtr<Image> getImage() const;
// Setters
@@ -265,10 +301,13 @@ namespace lms::db
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
void setCompilation(bool value) { _isCompilation = value; }
void clearLabels();
void clearCountries();
void clearReleaseTypes();
void addLabel(ObjectPtr<Label> releaseType);
void addLabel(ObjectPtr<Label> label);
void addCountry(ObjectPtr<Country> country);
void addReleaseType(ObjectPtr<ReleaseType> releaseType);
void setBarcode(std::string_view barcode) { _barcode = barcode; }
void setComment(std::string_view comment) { _comment = comment; }
void setImage(ObjectPtr<Image> image);
// Get the artists of this release
@@ -290,11 +329,13 @@ namespace lms::db
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
Wt::Dbo::field(a, _isCompilation, "is_compilation");
Wt::Dbo::field(a, _barcode, "barcode");
Wt::Dbo::field(a, _comment, "comment");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull);
Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _countries, Wt::Dbo::ManyToMany, "release_country", "", Wt::Dbo::OnDeleteCascade);
}
private:
@@ -315,11 +356,13 @@ namespace lms::db
std::string _artistDisplayName;
bool _isCompilation{}; // See https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#compilation-itunes-5
std::string _barcode;
std::string _comment;
Wt::Dbo::ptr<Image> _image;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
Wt::Dbo::collection<Wt::Dbo::ptr<Label>> _labels;
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes;
Wt::Dbo::collection<Wt::Dbo::ptr<Country>> _countries;
};
} // namespace lms::db
+14 -14
View File
@@ -23,7 +23,6 @@
#include <filesystem>
#include <optional>
#include <ostream>
#include <span>
#include <string>
#include <string_view>
#include <vector>
@@ -38,6 +37,7 @@
#include "database/ArtistId.hpp"
#include "database/ClusterId.hpp"
#include "database/DirectoryId.hpp"
#include "database/Filters.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Object.hpp"
#include "database/ReleaseId.hpp"
@@ -65,7 +65,7 @@ namespace lms::db
public:
struct FindParameters
{
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
Filters filters;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::string name; // if non empty, must match this name (title)
std::string fileStem; // if non empty, must match this file stem
@@ -84,13 +84,13 @@ namespace lms::db
TrackListId trackList; // matching this trackList
std::optional<int> trackNumber; // matching this track number
std::optional<int> discNumber; // matching this disc number
MediaLibraryId mediaLibrary; // If set, tracks in this library
DirectoryId directory; // if set, tracks in this directory
std::optional<bool> hasEmbeddedImage; // if set, tracks that have or not embedded images
std::optional<std::size_t> fileSize; // if set, tracks that match this file size
FindParameters& setClusters(std::span<const ClusterId> _clusters)
FindParameters& setFilters(const Filters& _filters)
{
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
filters = _filters;
return *this;
}
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
@@ -177,11 +177,6 @@ namespace lms::db
discNumber = _discNumber;
return *this;
}
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary)
{
mediaLibrary = _mediaLibrary;
return *this;
}
FindParameters& setDirectory(DirectoryId _directory)
{
directory = _directory;
@@ -192,6 +187,11 @@ namespace lms::db
hasEmbeddedImage = _hasEmbeddedImage;
return *this;
}
FindParameters& setFileSize(std::optional<std::size_t> _fileSize)
{
fileSize = _fileSize;
return *this;
}
};
Track() = default;
@@ -223,8 +223,8 @@ namespace lms::db
void setAbsoluteFilePath(const std::filesystem::path& filePath);
void setRelativeFilePath(const std::filesystem::path& filePath);
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
void setAddedTime(core::PartialDateTime time) { _fileAdded = time; }
void setLastWriteTime(const Wt::WDateTime& time) { _fileLastWrite = time; }
void setAddedTime(const Wt::WDateTime& time) { _fileAdded = time; }
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
void setBitsPerSample(std::size_t bitsPerSample) { _bitsPerSample = bitsPerSample; }
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
@@ -272,7 +272,7 @@ namespace lms::db
const core::PartialDateTime& getOriginalDate() const { return _originalDate; }
std::optional<int> getOriginalYear() const;
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
const core::PartialDateTime& getAddedTime() const { return _fileAdded; }
const Wt::WDateTime& getAddedTime() const { return _fileAdded; }
bool hasCover() const { return _hasCover; }
bool hasLyrics() const;
std::optional<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); }
@@ -367,7 +367,7 @@ namespace lms::db
std::filesystem::path _fileName;
long long _fileSize{};
Wt::WDateTime _fileLastWrite;
core::PartialDateTime _fileAdded;
Wt::WDateTime _fileAdded;
bool _hasCover{};
std::string _trackMBID;
std::string _recordingMBID;
@@ -20,7 +20,6 @@
#pragma once
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
@@ -28,8 +27,7 @@
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h>
#include "database/ClusterId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Filters.hpp"
#include "database/Object.hpp"
#include "database/TrackId.hpp"
#include "database/TrackListId.hpp"
@@ -62,19 +60,18 @@ namespace lms::db
// Search utility
struct FindParameters
{
std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
Filters filters;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
std::optional<Range> range;
std::optional<TrackListType> type;
UserId user; // only tracklists owned by this user
UserId excludedUser; // only tracklists *not* owned by this user
MediaLibraryId mediaLibrary; // only tracklists that have songs in this media library
UserId user; // only tracklists owned by this user
UserId excludedUser; // only tracklists *not* owned by this user
TrackListSortMethod sortMethod{ TrackListSortMethod::None };
std::optional<Visibility> visibility;
FindParameters& setClusters(std::span<const ClusterId> _clusters)
FindParameters& setFilters(const Filters& _filters)
{
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
filters = _filters;
return *this;
}
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
@@ -103,11 +100,6 @@ namespace lms::db
return *this;
}
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary)
{
mediaLibrary = _mediaLibrary;
return *this;
}
FindParameters& setSortMethod(TrackListSortMethod _sortMethod)
{
sortMethod = _sortMethod;
@@ -136,11 +136,18 @@ namespace lms::db
Name,
};
enum class LabelSortMethod
{
None,
Name,
};
enum class ReleaseSortMethod
{
None,
Id,
Name,
SortName,
ArtistNameThenName,
DateAsc,
DateDesc,
@@ -152,6 +159,12 @@ namespace lms::db
StarredDateDesc,
};
enum class ReleaseTypeSortMethod
{
None,
Name,
};
enum class TrackListSortMethod
{
None,
+7 -7
View File
@@ -324,13 +324,13 @@ namespace lms::db::tests
}
{
auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setMediaLibrary(library->getId())) };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
{
auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setMediaLibrary(otherLibrary->getId())) };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(otherLibrary->getId()))) };
EXPECT_EQ(artists.results.size(), 0);
}
}
@@ -751,11 +751,11 @@ namespace lms::db::tests
{
auto transaction{ session.createWriteTransaction() };
trackA1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 });
trackB1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 1 });
trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 });
trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 3 });
trackA2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 4 });
trackA1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } });
trackB1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 1 } });
trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } });
trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } });
trackA2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } });
TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Producer);
+20 -20
View File
@@ -201,11 +201,11 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() })) };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
tracks = Track::findIds(session, Track::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() }));
tracks = Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() })));
EXPECT_EQ(tracks.results.size(), 0);
}
@@ -235,7 +235,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) };
EXPECT_EQ(tracks.results.size(), 0);
}
@@ -248,7 +248,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) };
EXPECT_EQ(tracks.results.size(), 0);
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1);
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0);
@@ -263,7 +263,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1);
@@ -376,7 +376,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
ASSERT_EQ(Cluster::findOrphanIds(session).results.size(), 2);
EXPECT_EQ(Release::find(session, Release::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ unusedCluster.getId() })).results.size(), 0);
EXPECT_EQ(Release::find(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ unusedCluster.getId() }))).results.size(), 0);
EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1);
EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0);
EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0);
@@ -412,7 +412,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
const auto releases{ Release::findIds(session, Release::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() })) };
const auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
}
@@ -420,7 +420,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
const auto releases{ Release::findIds(session, Release::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ unusedCluster.getId() })) };
const auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ unusedCluster.getId() }))) };
EXPECT_EQ(releases.results.size(), 0);
}
@@ -466,12 +466,12 @@ namespace lms::db::tests
{
auto transaction{ session.createWriteTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() })) };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }))) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() })).results.size(), 0);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster3.getId() })).results.size(), 0);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() }))).results.size(), 0);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster3.getId() }))).results.size(), 0);
cluster2.get().modify()->addTrack(track.get());
}
@@ -479,19 +479,19 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() })) };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }))) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
artists = Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() }));
artists = Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() })));
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
artists = Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }));
artists = Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() })));
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster3.getId() })).results.size(), 0);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster3.getId() }))).results.size(), 0);
}
}
@@ -520,7 +520,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() })) };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
@@ -562,7 +562,7 @@ namespace lms::db::tests
std::vector<ClusterId> clusterIds;
std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); });
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setClusters(clusterIds)) };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
@@ -671,7 +671,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() })) };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
@@ -679,7 +679,7 @@ namespace lms::db::tests
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters(std::initializer_list<ClusterId>{ cluster.getId() }));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() })));
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
}
@@ -710,7 +710,7 @@ namespace lms::db::tests
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters(std::initializer_list<ClusterId>{ cluster1.getId(), cluster2.getId() }));
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId(), cluster2.getId() })));
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
}
+30 -30
View File
@@ -216,7 +216,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster->getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster->getId() });
auto artists{ Listen::getTopArtists(session, params) };
EXPECT_EQ(artists.results.size(), 0);
@@ -313,7 +313,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto artists{ Listen::getTopArtists(session, params) };
ASSERT_EQ(artists.results.size(), 0);
@@ -328,7 +328,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto artists{ Listen::getTopArtists(session, params) };
ASSERT_EQ(artists.results.size(), 1);
@@ -352,7 +352,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params;
params.setUser(user.getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setMediaLibrary(library.getId());
params.filters.setMediaLibrary(library.getId());
const auto artists{ Listen::getTopArtists(session, params) };
EXPECT_EQ(artists.results.size(), 0);
@@ -383,7 +383,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params;
params.setUser(user.getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setMediaLibrary(library.getId());
params.filters.setMediaLibrary(library.getId());
auto artists{ Listen::getTopArtists(session, params) };
ASSERT_EQ(artists.results.size(), 1);
@@ -395,7 +395,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params;
params.setUser(user.getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setMediaLibrary(otherLibrary.getId());
params.filters.setMediaLibrary(otherLibrary.getId());
auto artists{ Listen::getTopArtists(session, params) };
EXPECT_EQ(artists.results.size(), 0);
@@ -533,7 +533,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto releases{ Listen::getTopReleases(session, params) };
EXPECT_EQ(releases.results.size(), 0);
@@ -549,7 +549,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto releases{ Listen::getTopReleases(session, params) };
EXPECT_EQ(releases.results.size(), 1);
@@ -578,7 +578,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setMediaLibrary(library.getId());
params.filters.setMediaLibrary(library.getId());
auto releases{ Listen::getTopReleases(session, params) };
EXPECT_EQ(releases.moreResults, false);
@@ -593,7 +593,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setMediaLibrary(library.getId());
params.filters.setMediaLibrary(library.getId());
auto releases{ Listen::getTopReleases(session, params) };
EXPECT_EQ(releases.moreResults, false);
@@ -606,7 +606,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setMediaLibrary(otherLibrary.getId());
params.filters.setMediaLibrary(otherLibrary.getId());
auto releases{ Listen::getTopReleases(session, params) };
EXPECT_EQ(releases.moreResults, false);
@@ -780,7 +780,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto tracks{ Listen::getTopTracks(session, params) };
EXPECT_EQ(tracks.results.size(), 0);
@@ -796,7 +796,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto tracks{ Listen::getTopTracks(session, params) };
EXPECT_EQ(tracks.results.size(), 1);
@@ -818,7 +818,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user.getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setMediaLibrary(library.getId());
params.filters.setMediaLibrary(library.getId());
auto tracks{ Listen::getTopTracks(session, params) };
EXPECT_EQ(tracks.moreResults, false);
@@ -838,7 +838,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setMediaLibrary(library.getId());
params.filters.setMediaLibrary(library.getId());
auto tracks{ Listen::getTopTracks(session, params) };
EXPECT_EQ(tracks.moreResults, false);
@@ -851,7 +851,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setMediaLibrary(otherLibrary.getId());
params.filters.setMediaLibrary(otherLibrary.getId());
auto tracks{ Listen::getTopTracks(session, params) };
EXPECT_EQ(tracks.moreResults, false);
@@ -927,7 +927,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster->getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster->getId() });
auto artists{ Listen::getRecentArtists(session, params) };
EXPECT_EQ(artists.results.size(), 0);
@@ -1023,7 +1023,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto artists{ Listen::getRecentArtists(session, params) };
ASSERT_EQ(artists.results.size(), 0);
@@ -1038,7 +1038,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto artists{ Listen::getRecentArtists(session, params) };
ASSERT_EQ(artists.results.size(), 1);
@@ -1067,7 +1067,7 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() };
Listen::ArtistStatsFindParameters params;
params.setMediaLibrary(library.getId());
params.filters.setMediaLibrary(library.getId());
auto artists{ Listen::getRecentArtists(session, params) };
ASSERT_EQ(artists.results.size(), 1);
@@ -1077,7 +1077,7 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() };
Listen::ArtistStatsFindParameters params;
params.setMediaLibrary(otherLibrary.getId());
params.filters.setMediaLibrary(otherLibrary.getId());
auto artists{ Listen::getRecentArtists(session, params) };
EXPECT_EQ(artists.results.size(), 0);
@@ -1281,7 +1281,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto releases{ Listen::getRecentReleases(session, params) };
EXPECT_EQ(releases.results.size(), 0);
@@ -1296,7 +1296,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto releases{ Listen::getRecentReleases(session, params) };
EXPECT_EQ(releases.results.size(), 0);
@@ -1312,7 +1312,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto releases{ Listen::getRecentReleases(session, params) };
EXPECT_EQ(releases.results.size(), 1);
@@ -1341,7 +1341,7 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() };
Listen::StatsFindParameters params;
params.setMediaLibrary(library.getId());
params.filters.setMediaLibrary(library.getId());
auto releases{ Listen::getRecentReleases(session, params) };
EXPECT_EQ(releases.moreResults, false);
@@ -1352,7 +1352,7 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() };
Listen::StatsFindParameters params;
params.setMediaLibrary(otherLibrary.getId());
params.filters.setMediaLibrary(otherLibrary.getId());
auto releases{ Listen::getRecentReleases(session, params) };
EXPECT_EQ(releases.moreResults, false);
@@ -1425,7 +1425,7 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() };
Listen::StatsFindParameters params;
params.setMediaLibrary(library.getId());
params.filters.setMediaLibrary(library.getId());
auto tracks{ Listen::getRecentTracks(session, params) };
EXPECT_EQ(tracks.moreResults, false);
@@ -1437,7 +1437,7 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() };
Listen::StatsFindParameters params;
params.setMediaLibrary(otherLibrary.getId());
params.filters.setMediaLibrary(otherLibrary.getId());
auto tracks{ Listen::getRecentTracks(session, params) };
EXPECT_EQ(tracks.moreResults, false);
@@ -1655,7 +1655,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto tracks{ Listen::getRecentTracks(session, params) };
EXPECT_EQ(tracks.results.size(), 0);
@@ -1671,7 +1671,7 @@ namespace lms::db::tests
Listen::StatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setClusters({ cluster.getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster.getId() });
auto tracks{ Listen::getRecentTracks(session, params) };
EXPECT_EQ(tracks.results.size(), 1);
+1
View File
@@ -340,6 +340,7 @@ VALUES
EXPECT_FALSE(Artist::find(session, ArtistId{}));
EXPECT_FALSE(AuthToken::find(session, AuthTokenId{}));
EXPECT_FALSE(Country::find(session, CountryId{}));
EXPECT_FALSE(Cluster::find(session, ClusterId{}));
EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{}));
EXPECT_FALSE(Directory::find(session, DirectoryId{}));
+222 -7
View File
@@ -26,6 +26,7 @@ namespace lms::db::tests
{
using ScopedImage = ScopedEntity<db::Image>;
using ScopedLabel = ScopedEntity<db::Label>;
using ScopedCountry = ScopedEntity<db::Country>;
using ScopedReleaseType = ScopedEntity<db::ReleaseType>;
TEST_F(DatabaseFixture, Release)
@@ -245,13 +246,13 @@ namespace lms::db::tests
}
{
auto transaction{ session.createReadTransaction() };
auto releases{ Release::findIds(session, Release::FindParameters{}.setMediaLibrary(library->getId())) };
auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
}
{
auto transaction{ session.createReadTransaction() };
auto releases{ Release::findIds(session, Release::FindParameters{}.setMediaLibrary(otherLibrary->getId())) };
auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setMediaLibrary(otherLibrary->getId()))) };
EXPECT_EQ(releases.results.size(), 0);
}
}
@@ -808,6 +809,30 @@ namespace lms::db::tests
}
}
TEST_F(DatabaseFixture, Release_getLabelNames)
{
ScopedRelease release{ session, "MyRelease" };
ScopedLabel label{ session, "MyLabel" };
{
auto transaction{ session.createReadTransaction() };
const auto names{ release.get()->getLabelNames() };
EXPECT_EQ(names.size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
release.get().modify()->addLabel(label.get());
}
{
auto transaction{ session.createReadTransaction() };
const auto names{ release.get()->getLabelNames() };
ASSERT_EQ(names.size(), 1);
EXPECT_EQ(names[0], "MyLabel");
}
}
TEST_F(DatabaseFixture, Label_orphan)
{
ScopedLabel label{ session, "MyLabel" };
@@ -845,6 +870,78 @@ namespace lms::db::tests
}
}
TEST_F(DatabaseFixture, Country)
{
{
auto transaction{ session.createReadTransaction() };
Country::pointer res{ Country::find(session, "country") };
EXPECT_EQ(res, Country::pointer{});
}
ScopedCountry country{ session, "MyCountry" };
{
auto transaction{ session.createReadTransaction() };
Country::pointer res{ Country::find(session, "MyCountry") };
EXPECT_EQ(res, country.get());
}
}
TEST_F(DatabaseFixture, Release_getCountryNames)
{
ScopedCountry country{ session, "MyCountry" };
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createWriteTransaction() };
release.get().modify()->addCountry(country.get());
}
{
auto transaction{ session.createReadTransaction() };
const auto names{ release.get()->getCountryNames() };
ASSERT_EQ(names.size(), 1);
EXPECT_EQ(names[0], "MyCountry");
}
}
TEST_F(DatabaseFixture, Country_orphan)
{
ScopedCountry country{ session, "MyCountry" };
{
auto transaction{ session.createReadTransaction() };
auto countries{ Country::findOrphanIds(session) };
ASSERT_EQ(countries.results.size(), 1);
EXPECT_EQ(countries.results.front(), country.getId());
}
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createWriteTransaction() };
release.get().modify()->addCountry(country.get());
}
{
auto transaction{ session.createReadTransaction() };
auto countries{ Country::findOrphanIds(session) };
EXPECT_EQ(countries.results.size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
release.get().modify()->clearCountries();
}
{
auto transaction{ session.createReadTransaction() };
auto countries{ Country::findOrphanIds(session) };
ASSERT_EQ(countries.results.size(), 1);
EXPECT_EQ(countries.results.front(), country.getId());
}
}
TEST_F(DatabaseFixture, ReleaseType)
{
{
@@ -1138,11 +1235,11 @@ namespace lms::db::tests
{
auto transaction{ session.createWriteTransaction() };
trackA1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 });
trackB1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 1 });
trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 });
trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 3 });
trackA2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 4 });
trackA1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } });
trackB1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 1 } });
trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } });
trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } });
trackA2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } });
trackA1.get().modify()->setRelease(releaseA.get());
trackA2.get().modify()->setRelease(releaseA.get());
@@ -1201,4 +1298,122 @@ namespace lms::db::tests
EXPECT_EQ(releases.results[3], releaseC.getId());
}
}
TEST_F(DatabaseFixture, Release_LastWritten)
{
ScopedRelease release{ session, "relA" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
{
auto transaction{ session.createReadTransaction() };
Wt::WDateTime lastWritten{ release.get()->getLastWrittenTime() };
EXPECT_FALSE(lastWritten.isValid());
}
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } });
track2.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } });
track1.get().modify()->setRelease(release.get());
track2.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createReadTransaction() };
Wt::WDateTime lastWritten{ release.get()->getLastWrittenTime() };
ASSERT_TRUE(lastWritten.isValid());
EXPECT_EQ(lastWritten, track2.get()->getLastWriteTime());
}
}
TEST_F(DatabaseFixture, Release_AddedTime)
{
ScopedRelease release{ session, "relA" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
{
auto transaction{ session.createReadTransaction() };
const Wt::WDateTime addedTime{ release.get()->getAddedTime() };
EXPECT_FALSE(addedTime.isValid());
}
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } });
track2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } });
track1.get().modify()->setRelease(release.get());
track2.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createReadTransaction() };
const Wt::WDateTime addedTime{ release.get()->getAddedTime() };
ASSERT_TRUE(addedTime.isValid());
EXPECT_EQ(addedTime, track2.get()->getAddedTime());
}
}
TEST_F(DatabaseFixture, Release_groupMBID)
{
ScopedRelease release{ session, "relA" };
const std::optional<core::UUID> groupMBID{ core::UUID::fromString("1ad8f716-2fd6-4d09-8ada-39525947217c") };
{
auto transaction{ session.createReadTransaction() };
const auto releases{ Release::find(session, Release::FindParameters{}.setReleaseGroupMBID(groupMBID)) };
EXPECT_EQ(releases.results.size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
release.get().modify()->setGroupMBID(groupMBID);
}
{
auto transaction{ session.createReadTransaction() };
const auto releases{ Release::find(session, Release::FindParameters{}.setReleaseGroupMBID(groupMBID)) };
EXPECT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results[0]->getId(), release->getId());
}
}
TEST_F(DatabaseFixture, Release_sortName)
{
ScopedRelease release1{ session, "MyRelease1" };
ScopedRelease release2{ session, "MyRelease2" };
{
auto transaction{ session.createWriteTransaction() };
release1.get().modify()->setSortName("BB");
release2.get().modify()->setSortName("AA");
}
{
auto transaction{ session.createReadTransaction() };
const auto releases{ Release::find(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::Name)) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0]->getId(), release1->getId());
EXPECT_EQ(releases.results[1]->getId(), release2->getId());
}
{
auto transaction{ session.createReadTransaction() };
const auto releases{ Release::find(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::SortName)) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results[0]->getId(), release2->getId());
EXPECT_EQ(releases.results[1]->getId(), release1->getId());
}
}
} // namespace lms::db::tests
+19 -7
View File
@@ -151,20 +151,32 @@ namespace lms::db::tests
ScopedMediaLibrary library{ session, "MyLibrary", "/root" };
ScopedMediaLibrary otherLibrary{ session, "OtherLibrary", "/otherRoot" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(library->isEmpty());
EXPECT_TRUE(otherLibrary->isEmpty());
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setMediaLibrary(library.get());
}
{
auto transaction{ session.createWriteTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setMediaLibrary(library->getId())) };
auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(library->isEmpty());
EXPECT_TRUE(otherLibrary->isEmpty());
}
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
}
{
auto transaction{ session.createWriteTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setMediaLibrary(otherLibrary->getId())) };
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMediaLibrary(otherLibrary->getId()))) };
EXPECT_EQ(tracks.results.size(), 0);
}
}
@@ -387,9 +399,9 @@ namespace lms::db::tests
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 });
track2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 1 });
track4.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 });
track1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } });
track2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 1 } });
track4.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } });
}
{
+3 -3
View File
@@ -176,7 +176,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
std::vector<TrackListId> visitedTrackLists;
TrackList::find(session, TrackList::FindParameters{}.setMediaLibrary(library->getId()), [&](const TrackList::pointer& trackList) {
TrackList::find(session, TrackList::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId())), [&](const TrackList::pointer& trackList) {
visitedTrackLists.push_back(trackList->getId());
});
ASSERT_EQ(visitedTrackLists.size(), 1);
@@ -195,7 +195,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
auto trackLists{ TrackList::find(session, TrackList::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() })) };
auto trackLists{ TrackList::find(session, TrackList::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) };
EXPECT_EQ(trackLists.results.size(), 0);
}
@@ -209,7 +209,7 @@ namespace lms::db::tests
{
auto transaction{ session.createReadTransaction() };
auto trackLists{ TrackList::find(session, TrackList::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() })) };
auto trackLists{ TrackList::find(session, TrackList::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) };
ASSERT_EQ(trackLists.results.size(), 1);
EXPECT_EQ(trackLists.results.front(), trackList1.getId());
}
@@ -38,6 +38,7 @@ namespace lms::metadata
{ TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT", "TSO2" } },
{ TagType::AlbumArtists, { "ALBUMARTISTS" } },
{ TagType::AlbumArtistsSortOrder, { "ALBUMARTISTSSORT" } },
{ TagType::AlbumComment, { "ALBUMCOMMENT", "MUSICBRAINZ_ALBUMCOMMENT, MUSICBRAINZ ALBUM COMMENT", "MUSICBRAINZ/ALBUM COMMENT", "ALBUMVERSION", "VERSION" } },
{ TagType::AlbumSortOrder, { "ALBUMSORT", "ALBUM-SORT" } },
{ TagType::Arranger, { "ARRANGER" } },
{ TagType::Artist, { "ARTIST" } },
+1
View File
@@ -38,6 +38,7 @@ namespace lms::metadata
AlbumArtists, // non standard
AlbumArtistSortOrder,
AlbumArtistsSortOrder, // non standard
AlbumComment, // non standard
AlbumSortOrder,
Arranger,
Artist,
+53 -1
View File
@@ -266,6 +266,54 @@ namespace lms::metadata
return std::nullopt;
}
void fillInArtistsWithMbid(std::span<const Artist> artists, std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
{
for (const Artist& artist : artists)
{
if (artist.mbid.has_value())
{
// there may collisions, we don't want to replace
artistsWithMbid.emplace(artist.name, *artist.mbid);
}
}
}
void fillInMbids(std::span<Artist> artists, const std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
{
for (Artist& artist : artists)
{
if (!artist.mbid)
{
const auto it{ artistsWithMbid.find(artist.name) };
if (it != std::cend(artistsWithMbid))
artist.mbid = it->second;
}
}
}
void fillMissingMbids(Track& track)
{
// first pass: collect all artists that have mbids
std::unordered_map<std::string_view, core::UUID> artistsWithMbid;
// For now, mbids can only set in artist and album artist tags
// filling order is important: we estimate track-level artists are more likely
// to be set in other fields than album artists
fillInArtistsWithMbid(track.artists, artistsWithMbid);
if (track.medium && track.medium->release)
fillInArtistsWithMbid(track.medium->release->artists, artistsWithMbid);
// second pass: fill in all artists that have no mbid set with the same name
fillInMbids(track.conductorArtists, artistsWithMbid);
fillInMbids(track.composerArtists, artistsWithMbid);
fillInMbids(track.lyricistArtists, artistsWithMbid);
fillInMbids(track.mixerArtists, artistsWithMbid);
fillInMbids(track.producerArtists, artistsWithMbid);
fillInMbids(track.remixerArtists, artistsWithMbid);
for (auto& [role, artists] : track.performerArtists)
fillInMbids(artists, artistsWithMbid);
}
} // namespace
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle)
@@ -416,6 +464,8 @@ namespace lms::metadata
track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
fillMissingMbids(track);
// If a file has originalDate but no originalYear, set it
if (!track.originalYear)
track.originalYear = track.originalDate.getYear();
@@ -461,7 +511,7 @@ namespace lms::metadata
release.emplace();
release->name = std::move(*releaseName);
release->sortName = getTagValueAs<std::string>(tagReader, TagType::AlbumSortOrder).value_or("");
release->sortName = getTagValueAs<std::string>(tagReader, TagType::AlbumSortOrder).value_or(release->name);
release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _artistTagDelimiters, _defaultTagDelimiters);
release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs<std::string>(tagReader, TagType::AlbumArtist), _artistTagDelimiters);
release->mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseID);
@@ -470,6 +520,8 @@ namespace lms::metadata
release->isCompilation = getTagValueAs<bool>(tagReader, TagType::Compilation).value_or(false);
release->barcode = getTagValueAs<std::string>(tagReader, TagType::Barcode).value_or("");
release->labels = getTagValuesAs<std::string>(tagReader, TagType::RecordLabel, _defaultTagDelimiters);
release->comment = getTagValueAs<std::string>(tagReader, TagType::AlbumComment).value_or("");
release->countries = getTagValuesAs<std::string>(tagReader, TagType::ReleaseCountry, _defaultTagDelimiters);
if (!release->mediumCount)
{
// mediumCount may be encoded as "position/count"
+39 -12
View File
@@ -19,6 +19,7 @@
#include "TagLibTagReader.hpp"
#include <algorithm>
#include <unordered_map>
#include <taglib/aifffile.h>
@@ -69,6 +70,7 @@ namespace lms::metadata
{ TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT" } },
{ TagType::AlbumArtists, { "ALBUMARTISTS" } },
{ TagType::AlbumArtistsSortOrder, { "ALBUMARTISTSSORT" } },
{ TagType::AlbumComment, { "ALBUMCOMMENT", "MUSICBRAINZ_ALBUMCOMMENT, MUSICBRAINZ ALBUM COMMENT", "ALBUMVERSION", "VERSION" } },
{ TagType::AlbumSortOrder, { "ALBUMSORT" } },
{ TagType::Arranger, { "ARRANGER" } },
{ TagType::Artist, { "ARTIST" } },
@@ -190,6 +192,28 @@ namespace lms::metadata
}
}
void dedupTagValues(TagLib::PropertyMap& propertyMap, const std::filesystem::path& file)
{
for (auto& [key, values] : propertyMap)
{
if (values.size() <= 1)
continue;
TagLib::StringList newList;
for (const TagLib::String& value : values)
{
if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const TagLib::String& v) { return v == value; }))
newList.append(value);
}
if (values != newList)
{
LMS_LOG(METADATA, DEBUG, "File " << file << ": removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)");
values = newList;
}
}
}
TagLib::FileRef parseFile(const std::filesystem::path& p, ParserReadStyle parserReadStyle)
{
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
@@ -219,6 +243,18 @@ namespace lms::metadata
_propertyMap = _file.file()->properties();
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
for (const auto& [key, values] : _propertyMap)
{
for (const auto& value : values)
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value.to8Bit(true) << "'");
}
for (const auto& value : _propertyMap.unsupportedData())
LMS_LOG(METADATA, DEBUG, "Unknown value: '" << value.to8Bit(true) << "'");
}
// Some tags may not be known by TagLib
auto getAPETags = [&](const TagLib::APE::Tag* apeTag) {
if (!apeTag)
@@ -228,6 +264,9 @@ namespace lms::metadata
};
auto processID3v2Tags = [&](TagLib::ID3v2::Tag& id3v2Tags) {
// Dedup values for some tags that may be written in both a standard tag and in a custom tag
dedupTagValues(_propertyMap, p);
const auto& frameListMap{ id3v2Tags.frameListMap() };
// Not that good embedded pictures handling
@@ -405,18 +444,6 @@ namespace lms::metadata
if (wavFile->hasID3v2Tag())
processID3v2Tags(*wavFile->ID3v2Tag());
}
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
for (const auto& [key, values] : _propertyMap)
{
for (const auto& value : values)
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value.to8Bit(true) << "'");
}
for (const auto& value : _propertyMap.unsupportedData())
LMS_LOG(METADATA, DEBUG, "Unknown value: '" << value.to8Bit(true) << "'");
}
}
void TagLibTagReader::computeAudioProperties()
@@ -68,6 +68,8 @@ namespace lms::metadata
std::vector<std::string> releaseTypes;
bool isCompilation{};
std::string barcode;
std::string comment;
std::vector<std::string> countries;
auto operator<=>(const Release&) const = default;
};
+167 -21
View File
@@ -43,6 +43,7 @@ namespace lms::metadata
{ TagType::AlbumArtist, { "MyAlbumArtist1 & MyAlbumArtist2" } },
{ TagType::AlbumArtists, { "MyAlbumArtist1", "MyAlbumArtist2" } },
{ TagType::AlbumArtistsSortOrder, { "MyAlbumArtist1SortName", "MyAlbumArtist2SortName" } },
{ TagType::AlbumComment, { "MyAlbumComment" } },
{ TagType::Barcode, { "MyBarcode" } },
{ TagType::Comment, { "Comment1", "Comment2" } },
{ TagType::Compilation, { "1" } },
@@ -68,6 +69,7 @@ namespace lms::metadata
{ TagType::Producer, { "MyProducer1", "MyProducer2" } },
{ TagType::Remixer, { "MyRemixer1", "MyRemixer2" } },
{ TagType::RecordLabel, { "Label1", "Label2" } },
{ TagType::ReleaseCountry, { "MyCountry1", "MyCountry2" } },
{ TagType::Language, { "Language1", "Language2" } },
{ TagType::Lyricist, { "MyLyricist1", "MyLyricist2" } },
{ TagType::OriginalReleaseDate, { "2019/02/03" } },
@@ -202,28 +204,33 @@ namespace lms::metadata
// Release
ASSERT_TRUE(track->medium->release.has_value());
EXPECT_EQ(track->medium->release->artistDisplayName, "MyAlbumArtist1 & MyAlbumArtist2");
ASSERT_EQ(track->medium->release->artists.size(), 2);
EXPECT_EQ(track->medium->release->artists[0].name, "MyAlbumArtist1");
EXPECT_EQ(track->medium->release->artists[0].sortName, "MyAlbumArtist1SortName");
EXPECT_EQ(track->medium->release->artists[0].mbid, core::UUID::fromString("6fbf097c-1487-43e8-874b-50dd074398a7"));
EXPECT_EQ(track->medium->release->artists[1].name, "MyAlbumArtist2");
EXPECT_EQ(track->medium->release->artists[1].sortName, "MyAlbumArtist2SortName");
EXPECT_EQ(track->medium->release->artists[1].mbid, core::UUID::fromString("5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1"));
EXPECT_TRUE(track->medium->release->isCompilation);
EXPECT_EQ(track->medium->release->barcode, "MyBarcode");
ASSERT_EQ(track->medium->release->labels.size(), 2);
EXPECT_EQ(track->medium->release->labels[0], "Label1");
EXPECT_EQ(track->medium->release->labels[1], "Label2");
ASSERT_TRUE(track->medium->release->mbid.has_value());
EXPECT_EQ(track->medium->release->mbid.value(), core::UUID::fromString("3fa39992-b786-4585-a70e-85d5cc15ef69"));
EXPECT_EQ(track->medium->release->groupMBID.value(), core::UUID::fromString("5b1a5a44-8420-4426-9b86-d25dc8d04838"));
EXPECT_EQ(track->medium->release->mediumCount, 3);
EXPECT_EQ(track->medium->release->name, "MyAlbum");
EXPECT_EQ(track->medium->release->sortName, "MyAlbumSortName");
const Release& release{ track->medium->release.value() };
EXPECT_EQ(release.artistDisplayName, "MyAlbumArtist1 & MyAlbumArtist2");
ASSERT_EQ(release.artists.size(), 2);
EXPECT_EQ(release.artists[0].name, "MyAlbumArtist1");
EXPECT_EQ(release.artists[0].sortName, "MyAlbumArtist1SortName");
EXPECT_EQ(release.artists[0].mbid, core::UUID::fromString("6fbf097c-1487-43e8-874b-50dd074398a7"));
EXPECT_EQ(release.artists[1].name, "MyAlbumArtist2");
EXPECT_EQ(release.artists[1].sortName, "MyAlbumArtist2SortName");
EXPECT_EQ(release.artists[1].mbid, core::UUID::fromString("5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1"));
EXPECT_TRUE(release.isCompilation);
EXPECT_EQ(release.barcode, "MyBarcode");
ASSERT_EQ(release.labels.size(), 2);
EXPECT_EQ(release.labels[0], "Label1");
EXPECT_EQ(release.labels[1], "Label2");
ASSERT_TRUE(release.mbid.has_value());
EXPECT_EQ(release.mbid.value(), core::UUID::fromString("3fa39992-b786-4585-a70e-85d5cc15ef69"));
EXPECT_EQ(release.groupMBID.value(), core::UUID::fromString("5b1a5a44-8420-4426-9b86-d25dc8d04838"));
EXPECT_EQ(release.mediumCount, 3);
EXPECT_EQ(release.name, "MyAlbum");
EXPECT_EQ(release.sortName, "MyAlbumSortName");
EXPECT_EQ(release.comment, "MyAlbumComment");
ASSERT_EQ(release.countries.size(), 2);
EXPECT_EQ(release.countries[0], "MyCountry1");
EXPECT_EQ(release.countries[1], "MyCountry2");
{
std::vector<std::string> expectedReleaseTypes{ "Album", "Compilation" };
EXPECT_EQ(track->medium->release->releaseTypes, expectedReleaseTypes);
EXPECT_EQ(release.releaseTypes, expectedReleaseTypes);
}
}
@@ -584,6 +591,146 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
}
TEST(Parser, MBIDs_fallback)
{
TestTagReader testTags{
{
{ TagType::Artist, { "Artist1", "Artist2" } },
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtists, { "Artist3", "Artist4" } },
{ TagType::MusicBrainzArtistID, { "6643f584-5edc-45ce-927d-0a4ab25c2673", "481c5912-bf1a-47f7-b03c-d34e49711706" } },
{ TagType::MusicBrainzReleaseArtistID, { "ed42bcaf-e147-4f34-8f26-d74acc97670a", "6fc64a4b-26f5-441f-993c-fd511290233b" } },
{ TagType::Composer, { "Artist1", "Artist3" } },
{ TagType::Conductor, { "Artist1", "Artist3" } },
{ TagType::Lyricist, { "Artist1", "Artist3" } },
{ TagType::Mixer, { "Artist1", "Artist3" } },
{ TagType::Producer, { "Artist1", "Artist3" } },
{ TagType::Remixers, { "Artist1", "Artist3" } },
}
};
testTags.setPerformersTags({ { "RoleA", { "Artist1", "Artist3" } },
{ "RoleB", { "Artist2", "Artist4" } } });
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
ASSERT_TRUE(track->artists[0].mbid.has_value());
EXPECT_EQ(track->artists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
EXPECT_EQ(track->artists[1].name, "Artist2");
ASSERT_TRUE(track->artists[1].mbid.has_value());
EXPECT_EQ(track->artists[1].mbid.value(), core::UUID::fromString("481c5912-bf1a-47f7-b03c-d34e49711706"));
ASSERT_TRUE(track->medium.has_value());
ASSERT_TRUE(track->medium->release.has_value());
ASSERT_EQ(track->medium->release->artists.size(), 2);
EXPECT_EQ(track->medium->release->artists[0].name, "Artist3");
ASSERT_TRUE(track->medium->release->artists[0].mbid.has_value());
EXPECT_EQ(track->medium->release->artists[0].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
EXPECT_EQ(track->medium->release->artists[1].name, "Artist4");
ASSERT_TRUE(track->medium->release->artists[1].mbid.has_value());
EXPECT_EQ(track->medium->release->artists[1].mbid.value(), core::UUID::fromString("6fc64a4b-26f5-441f-993c-fd511290233b"));
ASSERT_EQ(track->composerArtists.size(), 2);
EXPECT_EQ(track->composerArtists[0].name, "Artist1");
ASSERT_TRUE(track->composerArtists[0].mbid.has_value());
EXPECT_EQ(track->composerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
EXPECT_EQ(track->composerArtists[1].name, "Artist3");
ASSERT_TRUE(track->composerArtists[1].mbid.has_value());
EXPECT_EQ(track->composerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
ASSERT_EQ(track->conductorArtists.size(), 2);
EXPECT_EQ(track->conductorArtists[0].name, "Artist1");
ASSERT_TRUE(track->conductorArtists[0].mbid.has_value());
EXPECT_EQ(track->conductorArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
EXPECT_EQ(track->conductorArtists[1].name, "Artist3");
ASSERT_TRUE(track->conductorArtists[1].mbid.has_value());
EXPECT_EQ(track->conductorArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
ASSERT_EQ(track->lyricistArtists.size(), 2);
EXPECT_EQ(track->lyricistArtists[0].name, "Artist1");
ASSERT_TRUE(track->lyricistArtists[0].mbid.has_value());
EXPECT_EQ(track->lyricistArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
EXPECT_EQ(track->lyricistArtists[1].name, "Artist3");
ASSERT_TRUE(track->lyricistArtists[1].mbid.has_value());
EXPECT_EQ(track->lyricistArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
ASSERT_EQ(track->mixerArtists.size(), 2);
EXPECT_EQ(track->mixerArtists[0].name, "Artist1");
ASSERT_TRUE(track->mixerArtists[0].mbid.has_value());
EXPECT_EQ(track->mixerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
EXPECT_EQ(track->mixerArtists[1].name, "Artist3");
ASSERT_TRUE(track->mixerArtists[1].mbid.has_value());
EXPECT_EQ(track->mixerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
ASSERT_EQ(track->producerArtists.size(), 2);
EXPECT_EQ(track->producerArtists[0].name, "Artist1");
ASSERT_TRUE(track->producerArtists[0].mbid.has_value());
EXPECT_EQ(track->producerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
EXPECT_EQ(track->producerArtists[1].name, "Artist3");
ASSERT_TRUE(track->producerArtists[1].mbid.has_value());
EXPECT_EQ(track->producerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
ASSERT_EQ(track->remixerArtists.size(), 2);
EXPECT_EQ(track->remixerArtists[0].name, "Artist1");
ASSERT_TRUE(track->remixerArtists[0].mbid.has_value());
EXPECT_EQ(track->remixerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
EXPECT_EQ(track->remixerArtists[1].name, "Artist3");
ASSERT_TRUE(track->remixerArtists[1].mbid.has_value());
EXPECT_EQ(track->remixerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
ASSERT_TRUE(track->performerArtists.contains("Rolea"));
ASSERT_EQ(track->performerArtists["Rolea"].size(), 2);
EXPECT_EQ(track->performerArtists["Rolea"][0].name, "Artist1");
ASSERT_TRUE(track->performerArtists["Rolea"][0].mbid.has_value());
EXPECT_EQ(track->performerArtists["Rolea"][0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
EXPECT_EQ(track->performerArtists["Rolea"][1].name, "Artist3");
ASSERT_TRUE(track->performerArtists["Rolea"][1].mbid.has_value());
EXPECT_EQ(track->performerArtists["Rolea"][1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
ASSERT_EQ(track->performerArtists["Roleb"].size(), 2);
EXPECT_EQ(track->performerArtists["Roleb"][0].name, "Artist2");
ASSERT_TRUE(track->performerArtists["Roleb"][0].mbid.has_value());
EXPECT_EQ(track->performerArtists["Roleb"][0].mbid.value(), core::UUID::fromString("481c5912-bf1a-47f7-b03c-d34e49711706"));
EXPECT_EQ(track->performerArtists["Roleb"][1].name, "Artist4");
ASSERT_TRUE(track->performerArtists["Roleb"][1].mbid.has_value());
EXPECT_EQ(track->performerArtists["Roleb"][1].mbid.value(), core::UUID::fromString("6fc64a4b-26f5-441f-993c-fd511290233b"));
}
TEST(Parser, MBIDs_fallback_priority)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "Artist1" } },
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtists, { "Artist1" } },
{ TagType::MusicBrainzArtistID, { "6643f584-5edc-45ce-927d-0a4ab25c2673" } },
{ TagType::MusicBrainzReleaseArtistID, { "ed42bcaf-e147-4f34-8f26-d74acc97670a" } },
{ TagType::Composer, { "Artist1" } },
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
ASSERT_EQ(track->composerArtists.size(), 1);
EXPECT_EQ(track->composerArtists[0].name, "Artist1");
ASSERT_TRUE(track->composerArtists[0].mbid.has_value());
EXPECT_EQ(track->composerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
}
TEST(Parser, release_sortNameFallback)
{
const TestTagReader testTags{
{
{ TagType::Album, { "MyAlbum" } },
// No AlbumSortOrder
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
ASSERT_TRUE(track->medium.has_value());
ASSERT_TRUE(track->medium->release.has_value());
EXPECT_EQ(track->medium->release->sortName, "MyAlbum");
}
TEST(Parser, advisory)
{
auto doTest = [](std::string_view value, std::optional<Track::Advisory> expectedValue) {
@@ -657,5 +804,4 @@ namespace lms::metadata
doTest("2020/01", core::PartialDateTime{ 2020, 1 });
doTest("2020", core::PartialDateTime{ 2020 });
}
} // namespace lms::metadata
@@ -97,13 +97,12 @@ namespace lms::feedback
return {};
Artist::FindParameters searchParams;
searchParams.setFilters(params.filters);
searchParams.setStarringUser(params.user, *backend);
searchParams.setClusters(params.clusters);
searchParams.setKeywords(params.keywords);
searchParams.setLinkType(params.linkType);
searchParams.setSortMethod(params.sortMethod);
searchParams.setRange(params.range);
searchParams.setMediaLibrary(params.library);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
@@ -149,11 +148,10 @@ namespace lms::feedback
Release::FindParameters searchParams;
searchParams.setStarringUser(params.user, *backend);
searchParams.setClusters(params.clusters);
searchParams.setFilters(params.filters);
searchParams.setKeywords(params.keywords);
searchParams.setSortMethod(ReleaseSortMethod::StarredDateDesc);
searchParams.setRange(params.range);
searchParams.setMediaLibrary(params.library);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
@@ -199,11 +197,10 @@ namespace lms::feedback
Track::FindParameters searchParams;
searchParams.setStarringUser(params.user, *backend);
searchParams.setClusters(params.clusters);
searchParams.setFilters(params.filters);
searchParams.setKeywords(params.keywords);
searchParams.setSortMethod(TrackSortMethod::StarredDateDesc);
searchParams.setRange(params.range);
searchParams.setMediaLibrary(params.library);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
@@ -24,6 +24,7 @@
#include <Wt/Json/Serializer.h>
#include <Wt/Json/Value.h>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/post.hpp>
#include "core/IConfig.hpp"
#include "core/Service.hpp"
@@ -132,9 +133,9 @@ namespace lms::feedback::listenBrainz
request.message.addHeader("Content-Type", "application/json");
request.onSuccessFunc = [this, type, starredTrackId](std::string_view /*msgBody*/) {
_strand.dispatch([this, type, starredTrackId] {
boost::asio::post(boost::asio::bind_executor(_strand, [this, type, starredTrackId] {
onFeedbackSent(type, starredTrackId);
});
}));
};
_client.sendPOSTRequest(std::move(request));
}
@@ -295,13 +296,13 @@ namespace lms::feedback::listenBrainz
void FeedbacksSynchronizer::onSyncEnded(UserContext& context)
{
_strand.dispatch([this, &context] {
boost::asio::post(boost::asio::bind_executor(_strand, [this, &context] {
LOG(INFO, "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount);
context.syncing = false;
if (!isSyncing())
scheduleSync(_syncFeedbacksPeriod);
});
}));
}
void FeedbacksSynchronizer::enqueValidateToken(UserContext& context)
@@ -344,7 +345,7 @@ namespace lms::feedback::listenBrainz
request.priority = core::http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context](std::string_view msgBody) {
std::string msgBodyCopy{ msgBody };
_strand.dispatch([this, msgBodyCopy, &context] {
boost::asio::post(boost::asio::bind_executor(_strand, [this, msgBodyCopy, &context] {
LOG(DEBUG, "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'");
const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy);
@@ -358,7 +359,7 @@ namespace lms::feedback::listenBrainz
enqueGetFeedbacks(context);
else
onSyncEnded(context);
});
}));
};
request.onFailureFunc = [this, &context] {
onSyncEnded(context);
@@ -376,7 +377,7 @@ namespace lms::feedback::listenBrainz
request.priority = core::http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context](std::string_view msgBody) {
std::string msgBodyCopy{ msgBody };
_strand.dispatch([this, msgBodyCopy, &context] {
boost::asio::post(boost::asio::bind_executor(_strand, [this, msgBodyCopy, &context] {
const std::size_t fetchedFeedbackCount{ processGetFeedbacks(msgBodyCopy, context) };
if (fetchedFeedbackCount == 0 // no more thing available on server
|| context.fetchedFeedbackCount >= context.feedbackCount // we may miss something, but we will get it next time
@@ -388,7 +389,7 @@ namespace lms::feedback::listenBrainz
{
enqueGetFeedbacks(context);
}
});
}));
};
request.onFailureFunc = [this, &context] {
onSyncEnded(context);
@@ -21,14 +21,12 @@
#include <memory>
#include <optional>
#include <span>
#include <Wt/WDateTime.h>
#include <boost/asio/io_service.hpp>
#include <boost/asio/io_context.hpp>
#include "database/ArtistId.hpp"
#include "database/ClusterId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Filters.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackId.hpp"
#include "database/Types.hpp"
@@ -53,19 +51,18 @@ namespace lms::feedback
struct FindParameters
{
db::UserId user;
std::vector<db::ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
db::Filters filters;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::optional<db::Range> range;
db::MediaLibraryId library;
FindParameters& setUser(const db::UserId _user)
{
user = _user;
return *this;
}
FindParameters& setClusters(std::span<const db::ClusterId> _clusters)
FindParameters& setFilters(const db::Filters& _filters)
{
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
filters = _filters;
return *this;
}
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
@@ -78,11 +75,6 @@ namespace lms::feedback
range = _range;
return *this;
}
FindParameters& setMediaLibrary(db::MediaLibraryId _library)
{
library = _library;
return *this;
}
};
// Artists
@@ -133,6 +125,6 @@ namespace lms::feedback
virtual std::optional<db::Rating> getRating(db::UserId userId, db::TrackId trackId) = 0;
};
std::unique_ptr<IFeedbackService> createFeedbackService(boost::asio::io_service& ioContext, db::Db& db);
std::unique_ptr<IFeedbackService> createFeedbackService(boost::asio::io_context& ioContext, db::Db& db);
} // namespace lms::feedback
@@ -29,6 +29,7 @@ namespace lms::scanner
{
db::MediaLibraryId id;
std::filesystem::path rootDirectory;
bool firstScan{};
auto operator<=>(const MediaLibraryInfo& other) const = default;
};
@@ -246,7 +246,7 @@ namespace lms::scanner
if (dateTime.isNull())
{
LMS_LOG(DBUPDATER, INFO, "Scheduling next scan right now");
_scheduleTimer.expires_from_now(std::chrono::seconds{ 0 });
_scheduleTimer.expires_after(std::chrono::seconds{ 0 });
_scheduleTimer.async_wait(cb);
}
else
@@ -395,7 +395,12 @@ namespace lms::scanner
newSettings.updatePeriod = scanSettings->getUpdatePeriod();
MediaLibrary::find(_db.getTLSSession(), [&](const MediaLibrary::pointer& mediaLibrary) {
newSettings.mediaLibraries.push_back(MediaLibraryInfo{ .id = mediaLibrary->getId(), .rootDirectory = mediaLibrary->getPath().lexically_normal() });
MediaLibraryInfo info;
info.firstScan = mediaLibrary->isEmpty();
info.id = mediaLibrary->getId();
info.rootDirectory = mediaLibrary->getPath().lexically_normal();
newSettings.mediaLibraries.push_back(info);
});
{
@@ -55,8 +55,8 @@ namespace lms::scanner
if (artistInfo.mbid)
artist.modify()->setMBID(artistInfo.mbid);
if (artistInfo.sortName)
artist.modify()->setSortName(*artistInfo.sortName);
artist.modify()->setSortName(artistInfo.sortName ? *artistInfo.sortName : artistInfo.name);
return artist;
}
@@ -141,6 +141,15 @@ namespace lms::scanner
return releaseType;
}
db::Country::pointer getOrCreateCountry(db::Session& session, std::string_view name)
{
db::Country::pointer country{ db::Country::find(session, name) };
if (!country)
country = session.create<db::Country>(name);
return country;
}
db::Label::pointer getOrCreateLabel(db::Session& session, std::string_view name)
{
db::Label::pointer label{ db::Label::find(session, name) };
@@ -166,13 +175,20 @@ namespace lms::scanner
release.modify()->setCompilation(releaseInfo.isCompilation);
if (release->getBarcode() != releaseInfo.barcode)
release.modify()->setBarcode(releaseInfo.barcode);
if (release->getComment() != releaseInfo.comment)
release.modify()->setComment(releaseInfo.comment);
if (release->getReleaseTypeNames() != releaseInfo.releaseTypes)
{
release.modify()->clearReleaseTypes();
for (std::string_view releaseType : releaseInfo.releaseTypes)
release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
}
if (release->getCountryNames() != releaseInfo.countries)
{
release.modify()->clearCountries();
for (std::string_view country : releaseInfo.countries)
release.modify()->addCountry(getOrCreateCountry(session, country));
}
if (release->getLabelNames() != releaseInfo.labels)
{
release.modify()->clearLabels();
@@ -329,6 +345,45 @@ namespace lms::scanner
return db::Advisory::UnSet;
}
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const FileInfo& fileInfo)
{
db::Track::FindParameters params;
// Add as many fields as possible to limit errors
params.setName(parsedTrack.title);
if (parsedTrack.medium)
{
if (parsedTrack.medium->position)
params.setDiscNumber(*parsedTrack.medium->position);
if (parsedTrack.medium->release)
params.setReleaseName(parsedTrack.medium->release->name);
}
if (parsedTrack.position)
params.setTrackNumber(*parsedTrack.position);
params.setHasEmbeddedImage(parsedTrack.hasCover);
params.setFileSize(fileInfo.fileSize);
bool error{};
db::Track::pointer res;
db::Track::find(session, params, [&](const db::Track::pointer& track) {
// Check that the track is truly no longer where it was during the last scan
std::error_code ec;
if (std::filesystem::exists(track->getAbsoluteFilePath(), ec))
return;
if (res)
{
LMS_LOG(DBUPDATER, DEBUG, "Found too many candidates for file move. New file = " << fileInfo.relativePath << ", candidate = " << track->getAbsoluteFilePath() << ", previous candidate = " << res->getAbsoluteFilePath());
error = true;
}
res = track;
});
if (error)
res = db::Track::pointer{};
return res;
}
class AudioFileScanOperation : public IFileScanOperation
{
public:
@@ -447,6 +502,17 @@ namespace lms::scanner
}
}
if (!track)
{
// maybe the file just moved?
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, *fileInfo);
if (track)
{
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << track->getAbsoluteFilePath());
track.modify()->setAbsoluteFilePath(_file);
}
}
// We estimate this is an audio file if the duration is not null
if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero())
{
@@ -479,19 +545,10 @@ namespace lms::scanner
if (!track)
{
track = dbSession.create<db::Track>();
track.modify()->setAbsoluteFilePath(_file);
const core::PartialDateTime addedTime{
fileInfo->lastWriteTime.date().year(),
static_cast<unsigned>(fileInfo->lastWriteTime.date().month()),
static_cast<unsigned>(fileInfo->lastWriteTime.date().day()),
static_cast<unsigned>(fileInfo->lastWriteTime.time().hour()),
static_cast<unsigned>(fileInfo->lastWriteTime.time().minute()),
static_cast<unsigned>(fileInfo->lastWriteTime.time().second())
};
track.modify()->setAddedTime(addedTime); // may be erased by encodingTime
added = true;
track.modify()->setAbsoluteFilePath(_file);
track.modify()->setAddedTime(_mediaLibrary.firstScan ? fileInfo->lastWriteTime : Wt::WDateTime::currentDateTime()); // may be erased by encodingTime
}
// Track related data
@@ -509,7 +566,18 @@ namespace lms::scanner
track.modify()->setLastWriteTime(fileInfo->lastWriteTime);
if (_parsedTrack->encodingTime.isValid())
track.modify()->setAddedTime(_parsedTrack->encodingTime);
{
const core::PartialDateTime& encodingTime{ _parsedTrack->encodingTime };
Wt::WDate date;
Wt::WTime time;
if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day)
date = Wt::WDate{ *encodingTime.getYear(), *encodingTime.getMonth(), *encodingTime.getDay() };
if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Sec)
time = Wt::WTime{ *encodingTime.getHour(), *encodingTime.getMin(), *encodingTime.getSec() };
if (date.isValid())
track.modify()->setAddedTime(time.isValid() ? Wt::WDateTime{ date, time } : Wt::WDateTime{ date });
}
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
track.modify()->setMediaLibrary(mediaLibrary);
@@ -38,6 +38,7 @@ namespace lms::scanner
removeOrphanedReleases(context);
removeOrphanedReleaseTypes(context);
removeOrphanedLabels(context);
removeOrphanedCountries(context);
removeOrphanedDirectories(context);
}
@@ -77,6 +78,12 @@ namespace lms::scanner
removeOrphanedEntries<db::Label>(context);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedCountries(ScanContext& context)
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned countries...");
removeOrphanedEntries<db::Country>(context);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedDirectories(ScanContext& context)
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned directories...");
@@ -39,6 +39,7 @@ namespace lms::scanner
void removeOrphanedReleases(ScanContext& context);
void removeOrphanedReleaseTypes(ScanContext& context);
void removeOrphanedLabels(ScanContext& context);
void removeOrphanedCountries(ScanContext& context);
void removeOrphanedDirectories(ScanContext& context);
template<typename T>
@@ -75,7 +75,7 @@ namespace lms::scanner
void ScanStepScanFiles::process(ScanContext& context, const MediaLibraryInfo& mediaLibrary)
{
const std::size_t scanQueueMaxScanRequestCount{ 100 * _fileScanQueue.getThreadCount() };
const std::size_t processFileResultsBatchSize{ 5 };
const std::size_t processFileResultsBatchSize{ 10 };
std::vector<std::unique_ptr<IFileScanOperation>> scanOperations;
@@ -41,10 +41,9 @@ namespace lms::scrobbling
{
db::Listen::StatsFindParameters listenFindParams;
listenFindParams.setUser(params.user);
listenFindParams.setClusters(params.clusters);
listenFindParams.setFilters(params.filters);
listenFindParams.setKeywords(params.keywords);
listenFindParams.setRange(params.range);
listenFindParams.setMediaLibrary(params.library);
listenFindParams.setArtist(params.artist);
return listenFindParams;
@@ -24,6 +24,7 @@
#include <Wt/Json/Serializer.h>
#include <Wt/Json/Value.h>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/post.hpp>
#include "ListensParser.hpp"
#include "core/IConfig.hpp"
@@ -245,14 +246,14 @@ namespace lms::scrobbling::listenBrainz
request.priority = core::http::ClientRequestParameters::Priority::Normal;
request.onSuccessFunc = [this, timedListen](std::string_view) {
_strand.dispatch([this, timedListen] {
boost::asio::post(boost::asio::bind_executor(_strand, [this, timedListen] {
if (saveListen(timedListen, db::SyncState::Synchronized))
{
UserContext& context{ getUserContext(timedListen.userId) };
if (context.listenCount)
(*context.listenCount)++;
}
});
}));
};
// on failure, this listen will be sent during the next sync
}
@@ -430,13 +431,13 @@ namespace lms::scrobbling::listenBrainz
void ListensSynchronizer::onSyncEnded(UserContext& context)
{
_strand.dispatch([this, &context] {
boost::asio::post(boost::asio::bind_executor(_strand, [this, &context] {
LOG(INFO, "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount);
context.syncing = false;
if (!isSyncing())
scheduleSync(_syncListensPeriod);
});
}));
}
void ListensSynchronizer::enqueValidateToken(UserContext& context)
@@ -479,7 +480,7 @@ namespace lms::scrobbling::listenBrainz
request.priority = core::http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context](std::string_view msgBody) {
const auto listenCount{ parseListenCount(msgBody) };
_strand.dispatch([this, listenCount, &context] {
boost::asio::post(boost::asio::bind_executor(_strand, [this, listenCount, &context] {
if (listenCount)
LOG(DEBUG, "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount);
@@ -494,7 +495,7 @@ namespace lms::scrobbling::listenBrainz
context.maxDateTime = Wt::WDateTime::currentDateTime();
enqueGetListens(context);
});
}));
};
request.onFailureFunc = [this, &context] {
onSyncEnded(context);
@@ -22,14 +22,12 @@
#include <chrono>
#include <memory>
#include <optional>
#include <span>
#include <Wt/WDateTime.h>
#include <boost/asio/io_service.hpp>
#include <boost/asio/io_context.hpp>
#include "database/ArtistId.hpp"
#include "database/ClusterId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Filters.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackId.hpp"
#include "database/Types.hpp"
@@ -61,20 +59,19 @@ namespace lms::scrobbling
struct FindParameters
{
db::UserId user;
std::vector<db::ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
db::Filters filters;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::optional<db::Range> range;
db::MediaLibraryId library; // if set, match this library
db::ArtistId artist; // if set, match this artist
db::ArtistId artist; // if set, match this artist
FindParameters& setUser(const db::UserId _user)
{
user = _user;
return *this;
}
FindParameters& setClusters(std::span<const db::ClusterId> _clusters)
FindParameters& setFilters(const db::Filters& _filters)
{
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
filters = _filters;
return *this;
}
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
@@ -87,11 +84,6 @@ namespace lms::scrobbling
range = _range;
return *this;
}
FindParameters& setMediaLibrary(db::MediaLibraryId _library)
{
library = _library;
return *this;
}
FindParameters& setArtist(db::ArtistId _artist)
{
artist = _artist;
+1 -1
View File
@@ -448,7 +448,7 @@ namespace lms::api::subsonic
if (!apiKey && !password)
throw RequiredParameterMissingError{ "apiKey" };
const auto clientAddress{ boost::asio::ip::address::from_string(request.clientAddress()) };
const auto clientAddress{ boost::asio::ip::make_address(request.clientAddress()) };
const std::string authToken{ apiKey ? *apiKey : decodePasswordIfNeeded(*password) };
const auto authResult{ core::Service<auth::IAuthTokenService>::get()->processAuthToken("subsonic", clientAddress, authToken) };
@@ -67,7 +67,7 @@ namespace lms::api::subsonic
Release::FindParameters params;
params.setSortMethod(ReleaseSortMethod::Name);
params.setRange(range);
params.setMediaLibrary(mediaLibraryId);
params.filters.setMediaLibrary(mediaLibraryId);
releases = Release::findIds(context.dbSession, params);
}
@@ -76,7 +76,7 @@ namespace lms::api::subsonic
Release::FindParameters params;
params.setSortMethod(ReleaseSortMethod::ArtistNameThenName);
params.setRange(range);
params.setMediaLibrary(mediaLibraryId);
params.filters.setMediaLibrary(mediaLibraryId);
releases = Release::findIds(context.dbSession, params);
}
@@ -90,10 +90,10 @@ namespace lms::api::subsonic
if (const Cluster::pointer cluster{ clusterType->getCluster(genre) })
{
Release::FindParameters params;
params.setClusters(std::initializer_list<ClusterId>{ cluster->getId() });
params.filters.setMediaLibrary(mediaLibraryId);
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster->getId() });
params.setSortMethod(ReleaseSortMethod::Name);
params.setRange(range);
params.setMediaLibrary(mediaLibraryId);
releases = Release::findIds(context.dbSession, params);
}
@@ -108,7 +108,7 @@ namespace lms::api::subsonic
params.setSortMethod(fromYear > toYear ? ReleaseSortMethod::DateDesc : ReleaseSortMethod::DateAsc);
params.setRange(range);
params.setDateRange(YearRange{ std::min(fromYear, toYear), std::max(fromYear, toYear) });
params.setMediaLibrary(mediaLibraryId);
params.filters.setMediaLibrary(mediaLibraryId);
releases = Release::findIds(context.dbSession, params);
}
@@ -117,7 +117,7 @@ namespace lms::api::subsonic
scrobbling::IScrobblingService::FindParameters params;
params.setUser(context.user->getId());
params.setRange(range);
params.setMediaLibrary(mediaLibraryId);
params.filters.setMediaLibrary(mediaLibraryId);
releases = scrobblingService.getTopReleases(params);
}
@@ -126,7 +126,7 @@ namespace lms::api::subsonic
Release::FindParameters params;
params.setSortMethod(ReleaseSortMethod::AddedDesc);
params.setRange(range);
params.setMediaLibrary(mediaLibraryId);
params.filters.setMediaLibrary(mediaLibraryId);
releases = Release::findIds(context.dbSession, params);
}
@@ -137,7 +137,7 @@ namespace lms::api::subsonic
Release::FindParameters params;
params.setSortMethod(ReleaseSortMethod::Random);
params.setRange(Range{ 0, size });
params.setMediaLibrary(mediaLibraryId);
params.filters.setMediaLibrary(mediaLibraryId);
releases = Release::findIds(context.dbSession, params);
}
@@ -146,7 +146,7 @@ namespace lms::api::subsonic
scrobbling::IScrobblingService::FindParameters params;
params.setUser(context.user->getId());
params.setRange(range);
params.setMediaLibrary(mediaLibraryId);
params.filters.setMediaLibrary(mediaLibraryId);
releases = scrobblingService.getRecentReleases(params);
}
@@ -155,7 +155,7 @@ namespace lms::api::subsonic
feedback::IFeedbackService::FindParameters params;
params.setUser(context.user->getId());
params.setRange(range);
params.setMediaLibrary(mediaLibraryId);
params.filters.setMediaLibrary(mediaLibraryId);
releases = feedbackService.findStarredReleases(params);
}
@@ -203,7 +203,7 @@ namespace lms::api::subsonic
feedback::IFeedbackService::FindParameters findParameters;
findParameters.setUser(context.user->getId());
findParameters.setMediaLibrary(mediaLibrary);
findParameters.filters.setMediaLibrary(mediaLibrary);
for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results)
{
@@ -247,7 +247,7 @@ namespace lms::api::subsonic
Track::FindParameters params;
params.setSortMethod(TrackSortMethod::Random);
params.setRange(Range{ 0, size });
params.setMediaLibrary(mediaLibraryId);
params.filters.setMediaLibrary(mediaLibraryId);
Track::find(context.dbSession, params, [&](const Track::pointer& track) {
randomSongsNode.addArrayChild("song", createSongNode(context, track, context.user));
@@ -283,9 +283,9 @@ namespace lms::api::subsonic
Response::Node& songsByGenreNode{ response.createNode("songsByGenre") };
Track::FindParameters params;
params.setClusters(std::initializer_list<ClusterId>{ cluster->getId() });
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster->getId() });
params.filters.setMediaLibrary(mediaLibrary);
params.setRange(Range{ offset, count });
params.setMediaLibrary(mediaLibrary);
Track::find(context.dbSession, params, [&](const Track::pointer& track) {
songsByGenreNode.addArrayChild("song", createSongNode(context, track, context.user));
@@ -398,7 +398,7 @@ namespace lms::api::subsonic
break;
}
}
parameters.setMediaLibrary(mediaLibrary);
parameters.filters.setMediaLibrary(mediaLibrary);
// This endpoint does not scale: make sort lived transactions in order not to block the whole application
@@ -159,9 +159,9 @@ namespace lms::api::subsonic
ArtistId lastRetrievedId;
auto findArtists{ [&] {
Artist::FindParameters params;
params.filters.setMediaLibrary(mediaLibrary);
params.setKeywords(keywords);
params.setRange(Range{ artistOffset, artistCount });
params.setMediaLibrary(mediaLibrary);
params.setSortMethod(ArtistSortMethod::Id); // must be consistent with both methods
Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) {
@@ -225,7 +225,7 @@ namespace lms::api::subsonic
Release::FindParameters params;
params.setKeywords(keywords);
params.setRange(Range{ albumOffset, albumCount });
params.setMediaLibrary(mediaLibrary);
params.filters.setMediaLibrary(mediaLibrary);
params.setSortMethod(ReleaseSortMethod::Id); // must be consistent with both methods
Release::find(context.dbSession, params, [&](const Release::pointer& release) {
@@ -289,7 +289,7 @@ namespace lms::api::subsonic
Track::FindParameters params;
params.setKeywords(keywords);
params.setRange(Range{ songOffset, songCount });
params.setMediaLibrary(mediaLibrary);
params.filters.setMediaLibrary(mediaLibrary);
params.setSortMethod(TrackSortMethod::Id); // must be consistent with both methods
Track::find(context.dbSession, params, [&](const Track::pointer& track) {
+1 -1
View File
@@ -85,7 +85,7 @@ namespace lms::api::subsonic
albumNode.setAttribute("isDir", true);
}
albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getLastWritten()));
albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getAddedTime()));
if (const auto image{ release->getImage() })
{
const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() };
+1 -1
View File
@@ -145,7 +145,7 @@ namespace lms::api::subsonic
trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count());
trackResponse.setAttribute("bitRate", (track->getBitrate() / 1000));
trackResponse.setAttribute("type", "music");
trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getLastWritten()));
trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getAddedTime()));
trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension()));
if (const auto rating{ core::Service<feedback::IFeedbackService>::get()->getRating(context.user->getId(), track->getId()) })
trackResponse.setAttribute("userRating", *rating);
+2 -2
View File
@@ -107,7 +107,7 @@ namespace lms::ui
if (field == PasswordField)
{
const auto checkResult{ _passwordService.checkUserPassword(
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
boost::asio::ip::make_address(LmsApp->environment().clientAddress()),
valueText(LoginNameField).toUTF8(),
valueText(PasswordField).toUTF8()) };
switch (checkResult.state)
@@ -153,7 +153,7 @@ namespace lms::ui
const std::string hashedCookie{ Wt::Auth::SHA1HashFunction{}.compute(*authCookie, authCookieSalt) };
const auto res{ core::Service<auth::IAuthTokenService>::get()->processAuthToken(authTokenDomain, boost::asio::ip::address::from_string(env.clientAddress()), hashedCookie) };
const auto res{ core::Service<auth::IAuthTokenService>::get()->processAuthToken(authTokenDomain, boost::asio::ip::make_address(env.clientAddress()), hashedCookie) };
switch (res.state)
{
case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied:
+4 -4
View File
@@ -213,7 +213,7 @@ namespace lms::ui
Settings defaultSettings;
std::ostringstream oss;
oss << "LMS.mediaplayer.init("
oss << jsRef() + ".mediaplayer = new LMSMediaPlayer("
<< jsRef()
<< ", defaultSettings = " << settingsToJSString(defaultSettings)
<< ")";
@@ -260,7 +260,7 @@ namespace lms::ui
// Update 'sizes' above to match this:
static_assert(static_cast<std::underlying_type_t<ArtworkResource::Size>>(ArtworkResource::Size::Small) == 128);
static_assert(static_cast<std::underlying_type_t<ArtworkResource::Size>>(ArtworkResource::Size::Large) == 512);
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
oss << jsRef() + ".mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
_title->setTextFormat(Wt::TextFormat::Plain);
_title->setText(Wt::WString::fromUTF8(track->getName()));
@@ -308,7 +308,7 @@ namespace lms::ui
void MediaPlayer::stop()
{
doJavaScript("LMS.mediaplayer.stop()");
doJavaScript(jsRef() + ".mediaplayer.stop()");
}
void MediaPlayer::setSettings(const Settings& settings)
@@ -317,7 +317,7 @@ namespace lms::ui
{
std::ostringstream oss;
oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")";
oss << jsRef() + ".mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")";
LMS_LOG(UI, DEBUG, "Running js = '" << oss.str() << "'");
doJavaScript(oss.str());
+9 -7
View File
@@ -44,22 +44,24 @@ namespace lms::ui
void ModalManager::show(std::unique_ptr<Wt::WWidget> modalWidget)
{
LMS_LOG(UI, DEBUG, "Want to show, id = " << modalWidget->id());
Wt::WWidget* modal{ modalWidget.get() };
addWidget(std::move(modalWidget));
std::ostringstream oss;
oss
<< R"({const modalElement = )" << jsRef() << R"(.getElementsByClassName('modal')[0];)"
<< R"(const modal = bootstrap.Modal.getOrCreateInstance(modalElement);)"
<< R"({const modalElementParent = document.getElementById(')" << modal->id() << R"(');)"
<< R"(const modalElement = modalElementParent.getElementsByClassName('modal')[0];)"
<< R"(const modal = bootstrap.Modal.getOrCreateInstance(modalElement,{backdrop:true, keyboard:true, focus:true});)"
<< R"(modal.show();)"
<< R"(modalElement.addEventListener('hidden.bs.modal', function () {)"
<< _closed.createCall({ "'" + modalWidget->id() + "'" })
<< _closed.createCall({ "'" + modal->id() + "'" })
<< R"(modal.dispose();)"
<< R"(});})";
LMS_LOG(UI, DEBUG, "Running JS '" << oss.str() << "'");
doJavaScript(oss.str());
addWidget(std::move(modalWidget));
// Execute in the modal's context to make sure the DOM is properly updated
modal->doJavaScript(oss.str());
}
void ModalManager::dispose(Wt::WWidget* modalWidget)
@@ -74,6 +76,6 @@ namespace lms::ui
LMS_LOG(UI, DEBUG, "Running JS '" << oss.str() << "'");
doJavaScript(oss.str());
modalWidget->doJavaScript(oss.str());
}
} // namespace lms::ui
+1 -1
View File
@@ -491,7 +491,7 @@ namespace lms::ui
}
auto image{ utils::createTrackImage(track->getId(), ArtworkResource::Size::Small) };
image->addStyleClass("Lms-cover-track");
image->addStyleClass("Lms-cover-track rounded");
if (const auto release{ track->getRelease() })
{
entry->setCondition("if-has-release", true);
+7 -1
View File
@@ -299,10 +299,16 @@ namespace lms::ui::utils
std::unique_ptr<Wt::WAnchor> createReleaseAnchor(db::Release::pointer release, bool setText)
{
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
if (setText)
{
std::string releaseName{ release->getName() };
if (std::string_view releaseComment{ release->getComment() }; !releaseComment.empty())
{
releaseName += " [";
releaseName += releaseComment;
releaseName += ']';
}
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(releaseName));
res->setToolTip(Wt::WString::fromUTF8(releaseName), Wt::TextFormat::Plain);
+12 -12
View File
@@ -218,8 +218,8 @@ namespace lms::ui
}
bool validate() { return _model->validate(); }
void updateModel() { Wt::WTemplateFormView::updateModel(_model.get()); }
void updateView() { Wt::WTemplateFormView::updateView(_model.get()); }
void refreshModel() { Wt::WTemplateFormView::updateModel(_model.get()); }
void refreshView() { Wt::WTemplateFormView::updateView(_model.get()); }
Wt::WString getValue() const
{
@@ -259,21 +259,21 @@ namespace lms::ui
return res;
}
void updateModels()
void refreshModels()
{
for (int i{}; i < count(); ++i)
{
LineEditEntryWidget* entry{ static_cast<LineEditEntryWidget*>(widget(i)) };
entry->updateModel();
entry->refreshModel();
}
}
void updateViews()
void refreshViews()
{
for (int i{}; i < count(); ++i)
{
LineEditEntryWidget* entry{ static_cast<LineEditEntryWidget*>(widget(i)) };
entry->updateView();
entry->refreshView();
}
}
@@ -392,16 +392,16 @@ namespace lms::ui
auto updateModels{ [=] {
t->updateModel(model.get());
extraTagsToScan->updateModels();
artistTagDelimiters->updateModels();
defaultTagDelimiters->updateModels();
extraTagsToScan->refreshModels();
artistTagDelimiters->refreshModels();
defaultTagDelimiters->refreshModels();
} };
auto updateViews{ [=] {
t->updateView(model.get());
extraTagsToScan->updateViews();
artistTagDelimiters->updateViews();
defaultTagDelimiters->updateViews();
extraTagsToScan->refreshViews();
artistTagDelimiters->refreshViews();
defaultTagDelimiters->refreshViews();
} };
auto loadInitialData{ [=] {
+1 -1
View File
@@ -100,7 +100,7 @@ namespace lms::ui
return Wt::WValidator::validate(input);
const auto checkResult{ _passwordService.checkUserPassword(
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
boost::asio::ip::make_address(LmsApp->environment().clientAddress()),
LmsApp->getUserLoginName(),
input.toUTF8()) };
switch (checkResult.state)
+7 -15
View File
@@ -40,7 +40,6 @@ namespace lms::ui
scrobbling::IScrobblingService& scrobblingService{ *core::Service<scrobbling::IScrobblingService>::get() };
const Range range{ getActualRange(requestedRange) };
const Filters& filters{ getFilters() };
RangeResults<ArtistId> artists;
@@ -53,10 +52,9 @@ namespace lms::ui
case Mode::Starred:
{
feedback::IFeedbackService::ArtistFindParameters params;
params.setFilters(getDbFilters());
params.setUser(LmsApp->getUserId());
params.setClusters(filters.getClusters());
params.setKeywords(getSearchKeywords());
params.setMediaLibrary(filters.getMediaLibrary());
params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::StarredDateDesc);
params.setRange(range);
@@ -68,9 +66,8 @@ namespace lms::ui
{
scrobbling::IScrobblingService::ArtistFindParameters params;
params.setUser(LmsApp->getUserId());
params.setClusters(filters.getClusters());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setMediaLibrary(filters.getMediaLibrary());
params.setLinkType(_linkType);
params.setRange(range);
@@ -82,9 +79,8 @@ namespace lms::ui
{
scrobbling::IScrobblingService::ArtistFindParameters params;
params.setUser(LmsApp->getUserId());
params.setClusters(filters.getClusters());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setMediaLibrary(filters.getMediaLibrary());
params.setLinkType(_linkType);
params.setRange(range);
@@ -95,9 +91,8 @@ namespace lms::ui
case Mode::RecentlyAdded:
{
Artist::FindParameters params;
params.setClusters(filters.getClusters());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setMediaLibrary(filters.getMediaLibrary());
params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::AddedDesc);
params.setRange(range);
@@ -112,9 +107,8 @@ namespace lms::ui
case Mode::RecentlyModified:
{
Artist::FindParameters params;
params.setClusters(filters.getClusters());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setMediaLibrary(filters.getMediaLibrary());
params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::LastWrittenDesc);
params.setRange(range);
@@ -129,8 +123,7 @@ namespace lms::ui
case Mode::All:
{
Artist::FindParameters params;
params.setClusters(filters.getClusters());
params.setMediaLibrary(filters.getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::SortName);
@@ -157,8 +150,7 @@ namespace lms::ui
if (!_randomArtists)
{
Artist::FindParameters params;
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::Random);
+1 -1
View File
@@ -34,7 +34,7 @@ namespace lms::ui::ArtistListHelpers
Wt::WAnchor* anchor{ entry->bindWidget("image", utils::createArtistAnchor(artist, false)) };
auto image{ utils::createArtistImage(artist->getId(), ArtworkResource::Size::Large) };
image->addStyleClass("Lms-cover-release Lms-cover-anchor");
image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack
anchor->setImage(std::move(image));
return entry;
+3 -6
View File
@@ -193,8 +193,7 @@ namespace lms::ui
_releaseContainers.clear();
Release::FindParameters params;
params.setClusters(_filters.getClusters());
params.setMediaLibrary(_filters.getMediaLibrary());
params.setFilters(_filters.getDbFilters());
params.setArtist(_artistId, { TrackArtistLinkType::ReleaseArtist }, {});
params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod());
@@ -251,8 +250,7 @@ namespace lms::ui
_appearsOnReleaseContainer = {};
Release::FindParameters params;
params.setClusters(_filters.getClusters());
params.setMediaLibrary(_filters.getMediaLibrary());
params.setFilters(_filters.getDbFilters());
params.setArtist(_artistId, types, { TrackArtistLinkType::ReleaseArtist });
params.setSortMethod(ReleaseSortMethod::OriginalDateDesc);
@@ -336,8 +334,7 @@ namespace lms::ui
const Range range{ static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize };
Track::FindParameters params;
params.setClusters(_filters.getClusters());
params.setMediaLibrary(_filters.getMediaLibrary());
params.setFilters(_filters.getDbFilters());
params.setArtist(_artistId);
params.setRange(range);
params.setSortMethod(TrackSortMethod::Name);
@@ -21,6 +21,8 @@
#include "core/String.hpp"
#include "explore/Filters.hpp"
namespace lms::ui
{
DatabaseCollectorBase::DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::size_t maxCount)
@@ -56,6 +58,11 @@ namespace lms::ui
return _maxCount;
}
const db::Filters& DatabaseCollectorBase::getDbFilters() const
{
return _filters.getDbFilters();
}
void DatabaseCollectorBase::setSearch(std::string_view searchText)
{
_searchText = searchText;
+6 -1
View File
@@ -26,6 +26,11 @@
#include "database/Types.hpp"
namespace lms::db
{
struct Filters;
}
namespace lms::ui
{
class Filters;
@@ -57,7 +62,7 @@ namespace lms::ui
protected:
Range getActualRange(std::optional<Range> range) const;
std::size_t getMaxCount() const;
const Filters& getFilters() { return _filters; }
const db::Filters& getDbFilters() const;
const std::vector<std::string_view>& getSearchKeywords() const { return _searchKeywords; }
private:
+114 -9
View File
@@ -27,7 +27,10 @@
#include <Wt/WTemplate.h>
#include "database/Cluster.hpp"
#include "database/LabelId.hpp"
#include "database/MediaLibrary.hpp"
#include "database/Release.hpp"
#include "database/ReleaseTypeId.hpp"
#include "database/Session.hpp"
#include "LmsApplication.hpp"
@@ -44,7 +47,15 @@ namespace lms::ui
{
};
using TypeVariant = std::variant<db::ClusterTypeId, MediaLibraryTag>;
struct LabelTag
{
};
struct ReleaseTypeTag
{
};
using TypeVariant = std::variant<db::ClusterTypeId, MediaLibraryTag, LabelTag, ReleaseTypeTag>;
using TypeModel = ValueStringModel<TypeVariant>;
std::unique_ptr<TypeModel> createTypeModel()
@@ -59,11 +70,13 @@ namespace lms::ui
}
typeModel->add(Wt::WString::tr("Lms.Explore.media-library"), MediaLibraryTag{});
typeModel->add(Wt::WString::tr("Lms.Explore.label"), LabelTag{});
typeModel->add(Wt::WString::tr("Lms.Explore.release-type"), ReleaseTypeTag{});
return typeModel;
}
using ValueVariant = std::variant<db::ClusterId, db::MediaLibraryId>;
using ValueVariant = std::variant<db::ClusterId, db::MediaLibraryId, db::LabelId, db::ReleaseTypeId>;
using ValueModel = ValueStringModel<ValueVariant>;
std::unique_ptr<ValueModel> createValueModel(TypeVariant type)
@@ -80,6 +93,18 @@ namespace lms::ui
valueModel->add(Wt::WString::fromUTF8(std::string{ library->getName() }), library->getId());
});
}
else if (std::holds_alternative<LabelTag>(type))
{
db::Label::find(session, db::LabelSortMethod::Name, [&](const db::Label::pointer& label) {
valueModel->add(Wt::WString::fromUTF8(std::string{ label->getName() }), label->getId());
});
}
else if (std::holds_alternative<ReleaseTypeTag>(type))
{
db::ReleaseType::find(session, db::ReleaseTypeSortMethod::Name, [&](const db::ReleaseType::pointer& releaseType) {
valueModel->add(Wt::WString::fromUTF8(std::string{ releaseType->getName() }), releaseType->getId());
});
}
else if (const db::ClusterTypeId * clusterTypeId{ std::get_if<db::ClusterTypeId>(&type) })
{
db::Cluster::FindParameters params;
@@ -118,6 +143,16 @@ namespace lms::ui
set(*mediaLibraryId);
state::writeValue<db::MediaLibraryId::ValueType>("filters_media_library_id", mediaLibraryId->getValue());
}
else if (const db::LabelId * labelId{ std::get_if<db::LabelId>(&value) })
{
set(*labelId);
state::writeValue<db::LabelId::ValueType>("filters_label_id", labelId->getValue());
}
else if (const db::ReleaseTypeId * releaseTypeId{ std::get_if<db::ReleaseTypeId>(&value) })
{
set(*releaseTypeId);
state::writeValue<db::ReleaseTypeId::ValueType>("filters_release_type_id", releaseTypeId->getValue());
}
else if (const db::ClusterId * clusterId{ std::get_if<db::ClusterId>(&value) })
{
add(*clusterId);
@@ -157,12 +192,16 @@ namespace lms::ui
_filters = bindNew<Wt::WContainerWidget>("clusters");
if (const std::optional<db::MediaLibraryId::ValueType> mediaLibraryId{ state::readValue<db::MediaLibraryId::ValueType>("filters_media_library_id") })
set(*mediaLibraryId);
set(db::MediaLibraryId{ *mediaLibraryId });
if (const std::optional<db::LabelId::ValueType> labelId{ state::readValue<db::LabelId::ValueType>("filters_label_id") })
set(db::LabelId{ *labelId });
if (const std::optional<db::ReleaseTypeId::ValueType> releaseTypeId{ state::readValue<db::ReleaseTypeId::ValueType>("filters_release_type_id") })
set(db::ReleaseTypeId{ *releaseTypeId });
}
void Filters::add(db::ClusterId clusterId)
{
if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), clusterId) != std::cend(_clusterIds))
if (std::find(std::cbegin(_dbFilters.clusters), std::cend(_dbFilters.clusters), clusterId) != std::cend(_dbFilters.clusters))
return;
Wt::WInteractWidget* filter{};
@@ -175,11 +214,11 @@ namespace lms::ui
filter = _filters->addWidget(std::move(cluster));
}
_clusterIds.push_back(clusterId);
_dbFilters.clusters.push_back(clusterId);
filter->clicked().connect([this, filter, clusterId] {
_filters->removeWidget(filter);
_clusterIds.erase(std::remove_if(std::begin(_clusterIds), std::end(_clusterIds), [clusterId](db::ClusterId id) { return id == clusterId; }), std::end(_clusterIds));
_dbFilters.clusters.erase(std::remove_if(std::begin(_dbFilters.clusters), std::end(_dbFilters.clusters), [clusterId](db::ClusterId id) { return id == clusterId; }), std::end(_dbFilters.clusters));
_sigUpdated.emit();
});
@@ -192,7 +231,7 @@ namespace lms::ui
{
_filters->removeWidget(_mediaLibraryFilter);
_mediaLibraryFilter = nullptr;
_mediaLibraryId = db::MediaLibraryId{};
_dbFilters.mediaLibrary = db::MediaLibraryId{};
}
std::string libraryName;
@@ -206,11 +245,11 @@ namespace lms::ui
libraryName = library->getName();
}
_mediaLibraryId = mediaLibraryId;
_dbFilters.mediaLibrary = mediaLibraryId;
_mediaLibraryFilter = _filters->addWidget(utils::createFilter(Wt::WString::fromUTF8(libraryName), Wt::WString::tr("Lms.Explore.media-library"), "bg-primary", true));
_mediaLibraryFilter->clicked().connect(_mediaLibraryFilter, [this] {
_filters->removeWidget(_mediaLibraryFilter);
_mediaLibraryId = db::MediaLibraryId{};
_dbFilters.mediaLibrary = db::MediaLibraryId{};
_mediaLibraryFilter = nullptr;
_sigUpdated.emit();
state::writeValue<db::MediaLibraryId::ValueType>("filters_media_library_id", std::nullopt);
@@ -219,6 +258,72 @@ namespace lms::ui
emitFilterAddedNotification();
}
void Filters::set(db::LabelId labelId)
{
if (_labelFilter)
{
_filters->removeWidget(_labelFilter);
_labelFilter = nullptr;
_dbFilters.label = db::LabelId{};
}
std::string name;
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const auto label{ db::Label::find(LmsApp->getDbSession(), labelId) };
if (!label)
return;
name = label->getName();
}
_dbFilters.label = labelId;
_labelFilter = _filters->addWidget(utils::createFilter(Wt::WString::fromUTF8(name), Wt::WString::tr("Lms.Explore.label"), "bg-secondary", true));
_labelFilter->clicked().connect(_labelFilter, [this] {
_filters->removeWidget(_labelFilter);
_dbFilters.label = db::LabelId{};
_labelFilter = nullptr;
_sigUpdated.emit();
state::writeValue<db::LabelId::ValueType>("filters_label_id", std::nullopt);
});
emitFilterAddedNotification();
}
void Filters::set(db::ReleaseTypeId releaseTypeId)
{
if (_releaseTypeFilter)
{
_filters->removeWidget(_releaseTypeFilter);
_releaseTypeFilter = nullptr;
_dbFilters.releaseType = db::ReleaseTypeId{};
}
std::string name;
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const auto releaseType{ db::ReleaseType::find(LmsApp->getDbSession(), releaseTypeId) };
if (!releaseType)
return;
name = releaseType->getName();
}
_dbFilters.releaseType = releaseTypeId;
_releaseTypeFilter = _filters->addWidget(utils::createFilter(Wt::WString::fromUTF8(name), Wt::WString::tr("Lms.Explore.release-type"), "bg-dark", true));
_releaseTypeFilter->clicked().connect(_releaseTypeFilter, [this] {
_filters->removeWidget(_releaseTypeFilter);
_dbFilters.releaseType = db::ReleaseTypeId{};
_releaseTypeFilter = nullptr;
_sigUpdated.emit();
state::writeValue<db::LabelId::ValueType>("filters_release_type_id", std::nullopt);
});
emitFilterAddedNotification();
}
void Filters::emitFilterAddedNotification()
{
LmsApp->notifyMsg(Notification::Type::Info,
+10 -11
View File
@@ -19,15 +19,11 @@
#pragma once
#include <span>
#include <vector>
#include <Wt/WContainerWidget.h>
#include <Wt/WSignal.h>
#include <Wt/WTemplate.h>
#include "database/ClusterId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Filters.hpp"
namespace lms::ui
{
@@ -36,8 +32,7 @@ namespace lms::ui
public:
Filters();
std::span<const db::ClusterId> getClusters() const { return _clusterIds; }
db::MediaLibraryId getMediaLibrary() const { return _mediaLibraryId; }
const db::Filters& getDbFilters() const { return _dbFilters; }
void add(db::ClusterId clusterId);
@@ -45,14 +40,18 @@ namespace lms::ui
private:
void showDialog();
void set(db::LabelId labelId);
void set(db::MediaLibraryId mediaLibraryId);
void set(db::ReleaseTypeId releaseTypeId);
void emitFilterAddedNotification();
Wt::WContainerWidget* _filters{};
Wt::Signal<> _sigUpdated;
std::vector<db::ClusterId> _clusterIds;
Wt::WInteractWidget* _mediaLibraryFilter{};
db::MediaLibraryId _mediaLibraryId;
Wt::WInteractWidget* _labelFilter{};
Wt::WInteractWidget* _releaseTypeFilter{};
Wt::Signal<> _sigUpdated;
db::Filters _dbFilters;
};
} // namespace lms::ui
+4 -8
View File
@@ -44,8 +44,7 @@ namespace lms::ui
db::Track::FindParameters params;
params.setArtist(artistId);
params.setSortMethod(db::TrackSortMethod::DateDescAndRelease);
params.setClusters(filters.getClusters());
params.setMediaLibrary(filters.getMediaLibrary());
params.setFilters(filters.getDbFilters());
params.setRange(db::Range{ 0, maxTrackCount - res.size() });
const auto tracks{ db::Track::findIds(session, params) };
@@ -74,8 +73,7 @@ namespace lms::ui
db::Track::FindParameters params;
params.setRelease(releaseId);
params.setSortMethod(db::TrackSortMethod::Release);
params.setClusters(filters.getClusters());
params.setMediaLibrary(filters.getMediaLibrary());
params.setFilters(filters.getDbFilters());
params.setRange(db::Range{ 0, maxTrackCount - res.size() });
const auto tracks{ db::Track::findIds(session, params) };
@@ -105,8 +103,7 @@ namespace lms::ui
params.setRelease(disc.releaseId);
params.setSortMethod(db::TrackSortMethod::Release);
params.setDiscNumber(disc.discNumber);
params.setClusters(filters.getClusters());
params.setMediaLibrary(filters.getMediaLibrary());
params.setFilters(filters.getDbFilters());
params.setRange(db::Range{ 0, maxTrackCount - res.size() });
const auto tracks{ db::Track::findIds(session, params) };
@@ -130,8 +127,7 @@ namespace lms::ui
db::Track::FindParameters params;
params.setTrackList(trackListId);
params.setClusters(filters.getClusters());
params.setMediaLibrary(filters.getMediaLibrary());
params.setFilters(filters.getDbFilters());
params.setRange(db::Range{ 0, maxTrackCount });
params.setSortMethod(TrackSortMethod::TrackList);
+8 -15
View File
@@ -52,8 +52,7 @@ namespace lms::ui
{
feedback::IFeedbackService::FindParameters params;
params.setUser(LmsApp->getUserId());
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setRange(range);
releases = feedbackService.findStarredReleases(params);
@@ -64,8 +63,7 @@ namespace lms::ui
{
scrobbling::IScrobblingService::FindParameters params;
params.setUser(LmsApp->getUserId());
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setRange(range);
@@ -77,8 +75,7 @@ namespace lms::ui
{
scrobbling::IScrobblingService::FindParameters params;
params.setUser(LmsApp->getUserId());
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setRange(range);
@@ -89,8 +86,7 @@ namespace lms::ui
case Mode::RecentlyAdded:
{
Release::FindParameters params;
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setSortMethod(ReleaseSortMethod::AddedDesc);
params.setRange(range);
@@ -105,8 +101,7 @@ namespace lms::ui
case Mode::RecentlyModified:
{
Release::FindParameters params;
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setSortMethod(ReleaseSortMethod::LastWrittenDesc);
params.setRange(range);
@@ -121,9 +116,8 @@ namespace lms::ui
case Mode::All:
{
Release::FindParameters params;
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setSortMethod(ReleaseSortMethod::Name);
params.setFilters(getDbFilters());
params.setSortMethod(ReleaseSortMethod::SortName);
params.setKeywords(getSearchKeywords());
params.setRange(range);
@@ -148,8 +142,7 @@ namespace lms::ui
if (!_randomReleases)
{
Release::FindParameters params;
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setSortMethod(ReleaseSortMethod::Random);
params.setRange(Range{ 0, getMaxCount() });
+34 -15
View File
@@ -23,6 +23,7 @@
#include <Wt/WImage.h>
#include <Wt/WText.h>
#include "core/EnumSet.hpp"
#include "database/Artist.hpp"
#include "database/Release.hpp"
@@ -34,9 +35,16 @@ namespace lms::ui::releaseListHelpers
namespace
{
std::unique_ptr<Wt::WTemplate> createEntryInternal(const Release::pointer& release, const std::string& templateKey, const Artist::pointer& artist, const bool showYear)
enum class ReleaseOptions
{
auto entry{ std::make_unique<Wt::WTemplate>(Wt::WString::tr(templateKey)) };
ShowArtist,
ShowYearAndOriginalYear,
ShowYear,
};
std::unique_ptr<Wt::WTemplate> createEntryInternal(const Release::pointer& release, const Artist::pointer& artist, core::EnumSet<ReleaseOptions> options)
{
auto entry{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry-grid")) };
entry->bindWidget("release-name", utils::createReleaseAnchor(release));
entry->addFunction("tr", &Wt::WTemplate::Functions::tr);
@@ -44,18 +52,21 @@ namespace lms::ui::releaseListHelpers
{
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
auto image{ utils::createReleaseCover(release->getId(), ArtworkResource::Size::Large) };
image->addStyleClass("Lms-cover-release Lms-cover-anchor"); // hack
image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack
anchor->setImage(std::move(image));
}
auto artistAnchors{ utils::createArtistsAnchorsForRelease(release, artist ? artist->getId() : ArtistId{}, "link-secondary") };
if (artistAnchors)
if (options.contains(ReleaseOptions::ShowArtist))
{
entry->setCondition("if-has-artist", true);
entry->bindWidget("artist-name", std::move(artistAnchors));
auto artistAnchors{ utils::createArtistsAnchorsForRelease(release, artist ? artist->getId() : ArtistId{}, "link-secondary") };
if (artistAnchors)
{
entry->setCondition("if-has-artist", true);
entry->bindWidget("artist-name", std::move(artistAnchors));
}
}
if (showYear)
if (options.contains(ReleaseOptions::ShowYearAndOriginalYear))
{
Wt::WString year{ releaseHelpers::buildReleaseYearString(release->getYear(), release->getOriginalYear()) };
if (!year.empty())
@@ -64,24 +75,32 @@ namespace lms::ui::releaseListHelpers
entry->bindString("year", year, Wt::TextFormat::Plain);
}
}
else if (options.contains(ReleaseOptions::ShowYear))
{
if (release->getYear())
{
entry->setCondition("if-has-year", true);
entry->bindInt("year", *release->getYear());
}
}
return entry;
}
} // namespace
std::unique_ptr<Wt::WTemplate> createEntry(const Release::pointer& release, const Artist::pointer& artist, bool showYear)
{
return createEntryInternal(release, "Lms.Explore.Releases.template.entry-grid", artist, showYear);
}
std::unique_ptr<Wt::WTemplate> createEntry(const Release::pointer& release)
{
return createEntry(release, Artist::pointer{}, false /*year*/);
return createEntryInternal(release, Artist::pointer{}, core::EnumSet<ReleaseOptions>{ ReleaseOptions::ShowArtist });
}
std::unique_ptr<Wt::WTemplate> createEntryForArtist(const db::Release::pointer& release, const db::Artist::pointer& artist)
{
return createEntry(release, artist, true);
return createEntryInternal(release, artist, core::EnumSet<ReleaseOptions>{ ReleaseOptions::ShowArtist, ReleaseOptions::ShowYearAndOriginalYear });
}
std::unique_ptr<Wt::WTemplate> createEntryForOtherVersions(const db::ObjectPtr<db::Release>& release)
{
return createEntryInternal(release, Artist::pointer{}, core::EnumSet<ReleaseOptions>{ ReleaseOptions::ShowYear });
}
} // namespace lms::ui::releaseListHelpers
+1
View File
@@ -40,6 +40,7 @@ namespace lms::ui::releaseListHelpers
{
std::unique_ptr<Wt::WTemplate> createEntry(const db::ObjectPtr<db::Release>& release);
std::unique_ptr<Wt::WTemplate> createEntryForArtist(const db::ObjectPtr<db::Release>& release, const db::ObjectPtr<db::Artist>& artist);
std::unique_ptr<Wt::WTemplate> createEntryForOtherVersions(const db::ObjectPtr<db::Release>& release);
} // namespace lms::ui::releaseListHelpers
namespace lms::ui::releaseHelpers
+61 -7
View File
@@ -24,8 +24,10 @@
#include <Wt/WAnchor.h>
#include <Wt/WImage.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplate.h>
#include "av/IAudioFile.hpp"
#include "core/String.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Release.hpp"
@@ -128,6 +130,13 @@ namespace lms::ui
}
}
// TODO make labels clickable to automatically add filters
if (const std::vector<std::string> labels{ release->getLabelNames() }; !labels.empty())
{
releaseInfo->setCondition("if-has-labels", true);
releaseInfo->bindString("release-labels", core::stringUtils::joinStrings(labels, " · "));
}
// TODO: save in DB and aggregate all this
for (const Track::pointer& track : Track::find(LmsApp->getDbSession(), Track::FindParameters{}.setRelease(releaseId).setRange(Range{ 0, 1 })).results)
{
@@ -216,7 +225,7 @@ namespace lms::ui
if (!releaseId)
throw ReleaseNotFoundException{};
auto similarReleasesIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarReleases(*releaseId, 6) };
auto similarReleasesIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarReleases(*releaseId, 5) };
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
@@ -229,9 +238,15 @@ namespace lms::ui
refreshCopyright(release);
refreshLinks(release);
refreshOtherVersions(release);
refreshSimilarReleases(similarReleasesIds);
bindString("name", Wt::WString::fromUTF8(std::string{ release->getName() }), Wt::TextFormat::Plain);
if (std::string_view comment{ release->getComment() }; !comment.empty())
{
setCondition("if-has-release-comment", true);
bindString("comment", Wt::WString::fromUTF8(std::string{ comment }), Wt::TextFormat::Plain);
}
Wt::WString year{ releaseHelpers::buildReleaseYearString(release->getYear(), release->getOriginalYear()) };
if (!year.empty())
@@ -244,7 +259,17 @@ namespace lms::ui
refreshReleaseArtists(release);
bindWidget<Wt::WImage>("cover", utils::createReleaseCover(release->getId(), ArtworkResource::Size::Large));
auto* image{ bindWidget<Wt::WImage>("cover", utils::createReleaseCover(release->getId(), ArtworkResource::Size::Large)) };
image->clicked().connect([=] {
auto fullCover{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.full-cover")) };
fullCover->bindNew<Wt::WImage>("cover-full", Wt::WLink{ LmsApp->getArtworkResource()->getReleaseCoverUrl(*releaseId) });
Wt::WTemplate* fullCoverPtr{ fullCover.get() };
fullCover->clicked().connect([=] {
LmsApp->getModalManager().dispose(fullCoverPtr);
});
LmsApp->getModalManager().show(std::move(fullCover));
});
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
{
@@ -384,8 +409,7 @@ namespace lms::ui
db::Track::FindParameters params;
params.setRelease(_releaseId);
params.setSortMethod(db::TrackSortMethod::Release);
params.setClusters(_filters.getClusters());
params.setMediaLibrary(_filters.getMediaLibrary());
params.setFilters(_filters.getDbFilters()); // TODO: do we really want to hide all tracks when a release does not match the current label filter?
db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track) {
const db::TrackId trackId{ track->getId() };
@@ -537,15 +561,45 @@ namespace lms::ui
}
}
void Release::refreshSimilarReleases(const std::vector<ReleaseId>& similarReleasesId)
void Release::refreshOtherVersions(const db::Release::pointer& release)
{
if (similarReleasesId.empty())
const auto groupMBID{ release->getGroupMBID() };
if (!groupMBID)
return;
db::Release::FindParameters params;
params.setReleaseGroupMBID(groupMBID);
params.setSortMethod(db::ReleaseSortMethod::DateAsc);
const auto releaseIds{ db::Release::findIds(LmsApp->getDbSession(), params) };
if (releaseIds.results.size() <= 1)
return;
setCondition("if-has-other-versions", true);
auto* container{ bindNew<Wt::WContainerWidget>("other-versions") };
for (const ReleaseId id : releaseIds.results)
{
if (id == _releaseId)
continue;
const db::Release::pointer otherVersionRelease{ db::Release::find(LmsApp->getDbSession(), id) };
if (!otherVersionRelease)
continue;
container->addWidget(releaseListHelpers::createEntryForOtherVersions(otherVersionRelease));
}
}
void Release::refreshSimilarReleases(const std::vector<ReleaseId>& similarReleaseIds)
{
if (similarReleaseIds.empty())
return;
setCondition("if-has-similar-releases", true);
auto* similarReleasesContainer{ bindNew<Wt::WContainerWidget>("similar-releases") };
for (const ReleaseId id : similarReleasesId)
for (const ReleaseId id : similarReleaseIds)
{
const db::Release::pointer similarRelease{ db::Release::find(LmsApp->getDbSession(), id) };
if (!similarRelease)
+2 -1
View File
@@ -44,7 +44,8 @@ namespace lms::ui
void refreshReleaseArtists(const db::ObjectPtr<db::Release>& release);
void refreshCopyright(const db::ObjectPtr<db::Release>& release);
void refreshLinks(const db::ObjectPtr<db::Release>& release);
void refreshSimilarReleases(const std::vector<db::ReleaseId>& similarReleasesId);
void refreshOtherVersions(const db::ObjectPtr<db::Release>& release);
void refreshSimilarReleases(const std::vector<db::ReleaseId>& similarReleaseIds);
Filters& _filters;
PlayQueueController& _playQueueController;
+7 -14
View File
@@ -52,8 +52,7 @@ namespace lms::ui
case Mode::Starred:
{
feedback::IFeedbackService::FindParameters params;
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setRange(range);
params.setUser(LmsApp->getUserId());
@@ -65,8 +64,7 @@ namespace lms::ui
{
scrobbling::IScrobblingService::FindParameters params;
params.setUser(LmsApp->getUserId());
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setRange(range);
@@ -78,8 +76,7 @@ namespace lms::ui
{
scrobbling::IScrobblingService::FindParameters params;
params.setUser(LmsApp->getUserId());
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setRange(range);
@@ -90,8 +87,7 @@ namespace lms::ui
case Mode::RecentlyAdded:
{
Track::FindParameters params;
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setSortMethod(TrackSortMethod::AddedDesc);
params.setRange(range);
@@ -106,8 +102,7 @@ namespace lms::ui
case Mode::RecentlyModified:
{
Track::FindParameters params;
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setSortMethod(TrackSortMethod::LastWrittenDesc);
params.setRange(range);
@@ -122,8 +117,7 @@ namespace lms::ui
case Mode::All:
{
Track::FindParameters params;
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setRange(range);
@@ -148,8 +142,7 @@ namespace lms::ui
if (!_randomTracks)
{
Track::FindParameters params;
params.setClusters(getFilters().getClusters());
params.setMediaLibrary(getFilters().getMediaLibrary());
params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords());
params.setSortMethod(TrackSortMethod::Random);
params.setRange(Range{ 0, getMaxCount() });
+1 -1
View File
@@ -220,7 +220,7 @@ namespace lms::ui::TrackListHelpers
}
auto image{ utils::createTrackImage(trackId, ArtworkResource::Size::Small) };
image->addStyleClass("Lms-cover-track");
image->addStyleClass("Lms-cover-track rounded");
if (track->getRelease())
{
entry->setCondition("if-has-release", true);
+1 -2
View File
@@ -187,8 +187,7 @@ namespace lms::ui
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
db::Track::FindParameters params;
params.setClusters(_filters.getClusters());
params.setMediaLibrary(_filters.getMediaLibrary());
params.setFilters(_filters.getDbFilters());
params.setTrackList(_trackListId);
params.setSortMethod(db::TrackSortMethod::TrackList);
params.setRange(db::Range{ static_cast<std::size_t>(_container->getCount()), _batchSize });
+1 -2
View File
@@ -120,8 +120,7 @@ namespace lms::ui
if (!_searchText.empty())
params.setKeywords(core::stringUtils::splitString(_searchText, ' '));
params.setClusters(_filters.getClusters());
params.setMediaLibrary(_filters.getMediaLibrary());
params.setFilters(_filters.getDbFilters());
params.setType(TrackListType::PlayList);
params.setRange(range);
+14 -7
View File
@@ -52,7 +52,7 @@ namespace lms::ui
beingDeleted();
}
std::string ArtworkResource::getArtistImageUrl(db::ArtistId artistId, Size size) const
std::string ArtworkResource::getArtistImageUrl(db::ArtistId artistId, std::optional<Size> size) const
{
std::string url;
@@ -73,7 +73,7 @@ namespace lms::ui
return url;
}
std::string ArtworkResource::getReleaseCoverUrl(db::ReleaseId releaseId, Size size) const
std::string ArtworkResource::getReleaseCoverUrl(db::ReleaseId releaseId, std::optional<Size> size) const
{
std::string url;
@@ -107,7 +107,7 @@ namespace lms::ui
return url;
}
std::string ArtworkResource::getTrackImageUrl(db::TrackId trackId, Size size) const
std::string ArtworkResource::getTrackImageUrl(db::TrackId trackId, std::optional<Size> size) const
{
std::string url;
@@ -135,14 +135,21 @@ namespace lms::ui
return url;
}
std::string ArtworkResource::getImageUrl(db::ImageId imageId, Size size, std::string_view type) const
std::string ArtworkResource::getImageUrl(db::ImageId imageId, std::optional<Size> size, std::string_view type) const
{
return url() + "&imageid=" + imageId.toString() + "&size=" + std::to_string(static_cast<std::size_t>(size)) + "&type=" + std::string{ type };
std::string res{ url() + "&imageid=" + imageId.toString() + "&type=" + std::string{ type } };
if (size)
res += "&size=" + std::to_string(static_cast<std::size_t>(*size));
return res;
}
std::string ArtworkResource::getImageUrl(db::TrackId trackId, Size size, std::string_view type) const
std::string ArtworkResource::getImageUrl(db::TrackId trackId, std::optional<Size> size, std::string_view type) const
{
return url() + "&trackid=" + trackId.toString() + "&size=" + std::to_string(static_cast<std::size_t>(size)) + "&type=" + std::string{ type };
std::string res{ url() + "&trackid=" + trackId.toString() + "&type=" + std::string{ type } };
if (size)
res += "&size=" + std::to_string(static_cast<std::size_t>(*size));
return res;
}
std::string ArtworkResource::getDefaultArtistImageUrl() const
+7 -5
View File
@@ -19,6 +19,8 @@
#pragma once
#include <optional>
#include <Wt/WResource.h>
#include "database/ArtistId.hpp"
@@ -42,13 +44,13 @@ namespace lms::ui
Large = 512,
};
std::string getArtistImageUrl(db::ArtistId artistId, Size size) const;
std::string getReleaseCoverUrl(db::ReleaseId releaseId, Size size) const;
std::string getTrackImageUrl(db::TrackId trackId, Size size) const;
std::string getArtistImageUrl(db::ArtistId artistId, std::optional<Size> size = std::nullopt) const;
std::string getReleaseCoverUrl(db::ReleaseId releaseId, std::optional<Size> size = std::nullopt) const;
std::string getTrackImageUrl(db::TrackId trackId, std::optional<Size> size = std::nullopt) const;
private:
std::string getImageUrl(db::ImageId imageId, Size size, std::string_view type) const;
std::string getImageUrl(db::TrackId trackId, Size size, std::string_view type) const;
std::string getImageUrl(db::ImageId imageId, std::optional<Size> size, std::string_view type) const;
std::string getImageUrl(db::TrackId trackId, std::optional<Size> size, std::string_view type) const;
std::string getDefaultArtistImageUrl() const;
std::string getDefaultReleaseCoverUrl() const;
+15 -9
View File
@@ -78,12 +78,12 @@ namespace lms::metadata
std::ostream& operator<<(std::ostream& os, const Release& release)
{
os << release.name;
if (!release.sortName.empty())
if (release.sortName != release.name)
os << " '" << release.sortName << "'";
os << std::endl;
for (std::string_view label : release.labels)
std::cout << "\tLabel: " << label << std::endl;
for (std::string_view releaseType : release.releaseTypes)
std::cout << "\tRelease type: " << releaseType << std::endl;
if (release.mbid)
os << "\tRelease MBID = " << release.mbid->getAsString() << std::endl;
@@ -94,20 +94,26 @@ namespace lms::metadata
if (release.mediumCount)
std::cout << "\tMediumCount: " << *release.mediumCount << std::endl;
if (!release.artistDisplayName.empty())
std::cout << "\tDisplay artist: " << release.artistDisplayName << std::endl;
std::cout << "\tIsCompilation: " << std::boolalpha << release.isCompilation << std::endl;
for (std::string_view label : release.labels)
std::cout << "\tLabel: " << label << std::endl;
for (std::string_view country : release.countries)
std::cout << "\tCountry: " << country << std::endl;
if (!release.barcode.empty())
std::cout << "\tBarcode: " << release.barcode << std::endl;
if (!release.comment.empty())
std::cout << "\tComment: " << release.comment << std::endl;
if (!release.artistDisplayName.empty())
std::cout << "\tDisplay artist: " << release.artistDisplayName << std::endl;
for (const Artist& artist : release.artists)
std::cout << "\tRelease artist: " << artist << std::endl;
for (std::string_view releaseType : release.releaseTypes)
std::cout << "\tRelease type: " << releaseType << std::endl;
return os;
}