Merge branch 'develop' for release v3.64.0
This commit is contained in:
@@ -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
@@ -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++ \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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...");
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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{}));
|
||||
|
||||
@@ -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
|
||||
@@ -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 } });
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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" } },
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace lms::metadata
|
||||
AlbumArtists, // non standard
|
||||
AlbumArtistSortOrder,
|
||||
AlbumArtistsSortOrder, // non standard
|
||||
AlbumComment, // non standard
|
||||
AlbumSortOrder,
|
||||
Arranger,
|
||||
Artist,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() };
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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{ [=] {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user