Merge branch 'develop' for release v3.53.0
This commit is contained in:
+1
-35
@@ -17,18 +17,12 @@ if(ENABLE_TESTS)
|
||||
find_package(GTest REQUIRED)
|
||||
endif()
|
||||
|
||||
# Common dependencies
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(Filesystem REQUIRED)
|
||||
find_package(Boost REQUIRED COMPONENTS system program_options iostreams)
|
||||
find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP)
|
||||
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||
pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
|
||||
pkg_check_modules(GraphicsMagick++ IMPORTED_TARGET GraphicsMagick++)
|
||||
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat)
|
||||
pkg_check_modules(Archive REQUIRED IMPORTED_TARGET libarchive)
|
||||
find_package(PAM)
|
||||
find_package(STB)
|
||||
|
||||
# WT
|
||||
if (NOT Wt_FOUND)
|
||||
@@ -47,34 +41,6 @@ if (NOT TARGET Wt::HTTP)
|
||||
message(FATAL_ERROR "Cannot find Wt::HTTP!")
|
||||
endif ()
|
||||
|
||||
# PAM
|
||||
option(USE_PAM "Use the PAM backend authentication API" ON)
|
||||
if (USE_PAM AND NOT PAM_FOUND)
|
||||
message(WARNING "PAM library not found: disabling")
|
||||
set(USE_PAM OFF)
|
||||
endif ()
|
||||
if (USE_PAM)
|
||||
message(STATUS "Using PAM authentication backend")
|
||||
else ()
|
||||
message(STATUS "NOT using PAM authentication backend")
|
||||
endif ()
|
||||
|
||||
# IMAGE
|
||||
if (STB_FOUND)
|
||||
set(IMAGE_LIBRARY STB CACHE STRING "STB library")
|
||||
elseif (GraphicsMagick++_FOUND)
|
||||
set(IMAGE_LIBRARY GraphicsMagick++ CACHE STRING "GraphicsMagick++")
|
||||
else ()
|
||||
message(FATAL_ERROR "Cannot find suitable image library")
|
||||
endif()
|
||||
set_property(CACHE IMAGE_LIBRARY PROPERTY STRINGS GraphicsMagick++ STB)
|
||||
if (IMAGE_LIBRARY STREQUAL GraphicsMagick++ AND NOT GraphicsMagick++_FOUND)
|
||||
message(FATAL_ERROR "GraphicsMagick++ not found")
|
||||
elseif (IMAGE_LIBRARY STREQUAL STB AND NOT STB_FOUND)
|
||||
message(FATAL_ERROR "STB not found")
|
||||
endif ()
|
||||
message(STATUS "IMAGE_LIBRARY set to ${IMAGE_LIBRARY}")
|
||||
|
||||
# Benchmark
|
||||
option(BUILD_BENCHMARKS "Build benchmarks" OFF)
|
||||
if (BUILD_BENCHMARKS)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
|
||||
FROM --platform=$BUILDPLATFORM alpine:3.18
|
||||
FROM --platform=$BUILDPLATFORM alpine:3.19
|
||||
|
||||
ARG BUILD_PACKAGES="\
|
||||
clang \
|
||||
@@ -36,7 +36,7 @@ ARG LMS_BUILD_TYPE="Release"
|
||||
RUN \
|
||||
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
xx-info is-cross && export BUILD_TESTS=OFF || export BUILD_TESTS=ON && \
|
||||
PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig cmake /tmp/lms/ -DCMAKE_INCLUDE_PATH=${PREFIX}/include -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} $(xx-clang --print-cmake-defines) -DCMAKE_PREFIX_PATH=/$(xx-info)/usr/lib/cmake -DBUILD_TESTING=${BUILD_TESTS} -DBUILD_BENCHMARKS=ON && \
|
||||
PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig cmake /tmp/lms/ -DCMAKE_INCLUDE_PATH=/$(xx-info)/usr/include -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} $(xx-clang --print-cmake-defines) -DCMAKE_PREFIX_PATH=/$(xx-info)/usr/lib/cmake -DBUILD_TESTING=${BUILD_TESTS} -DBUILD_BENCHMARKS=ON && \
|
||||
VERBOSE=1 make -j$(nproc) && \
|
||||
xx-verify src/lms/lms && \
|
||||
(xx-info is-cross || make test)
|
||||
|
||||
@@ -23,6 +23,6 @@ COPY . /tmp/lms/
|
||||
ARG LMS_BUILD_TYPE="Release"
|
||||
RUN \
|
||||
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_BENCHMARKS=ON && \
|
||||
cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX=/usr -DLMS_IMAGE_BACKEND=graphicsmagick -DBUILD_BENCHMARKS=ON && \
|
||||
VERBOSE=1 make -j$(nproc) && \
|
||||
make test
|
||||
|
||||
+22
-11
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.18 AS build
|
||||
FROM alpine:3.19 AS build
|
||||
|
||||
WORKDIR /tmp/workdir
|
||||
|
||||
@@ -30,14 +30,14 @@ ARG BUILD_PACKAGES=" \
|
||||
boost-dev \
|
||||
libarchive-dev \
|
||||
libconfig-dev \
|
||||
taglib-dev \
|
||||
utfcpp \
|
||||
sqlite-dev \
|
||||
gtest-dev"
|
||||
|
||||
RUN apk add --no-cache --update ${BUILD_PACKAGES}
|
||||
|
||||
# ffmpeg
|
||||
ARG FFMPEG_VERSION=4.3.5
|
||||
ARG FFMPEG_VERSION=6.1.1
|
||||
RUN \
|
||||
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
|
||||
@@ -67,9 +67,9 @@ RUN \
|
||||
--enable-libopus \
|
||||
--enable-libvorbis \
|
||||
--disable-everything \
|
||||
--enable-decoder=aac*,ac3*,alac,als,flac,mp3*,libopus,pcm*,libvorbis,wavpack,wma*,libopenjpg,png \
|
||||
--enable-decoder=aac*,ac3*,alac,als,dsd*,flac,mp3*,libopus,pcm*,libvorbis,wavpack,wma*,libopenjpg,png \
|
||||
--enable-encoder=libmp3lame,libopus,libvorbis \
|
||||
--enable-demuxer=aac,aiff,asf,flac,ipod,ogg,matroska,mov,mp3,mp4,wav,wv,webm \
|
||||
--enable-demuxer=aac,aiff,asf,dsf,flac,ipod,ogg,matroska,mov,mp3,mp4,wav,wv,webm \
|
||||
--enable-muxer=ogg,matroska,mp3,webm \
|
||||
--enable-protocol=file,pipe \
|
||||
--enable-filter=aresample \
|
||||
@@ -79,7 +79,7 @@ RUN \
|
||||
make distclean
|
||||
|
||||
# WT
|
||||
ARG WT_VERSION=4.10.2
|
||||
ARG WT_VERSION=4.10.4
|
||||
RUN \
|
||||
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://github.com/emweb/wt/archive/${WT_VERSION}.tar.gz && \
|
||||
@@ -92,7 +92,7 @@ RUN \
|
||||
make install
|
||||
|
||||
# STB
|
||||
ARG STB_VERSION=5736b15f7ea0ffb08dd38af21067c314d6a3aae9
|
||||
ARG STB_VERSION=ae721c50eaf761660b4f90cc590453cdb0c2acd0
|
||||
RUN \
|
||||
DIR=/tmp/stb && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://github.com/nothings/stb/archive/${STB_VERSION}.tar.gz && \
|
||||
@@ -100,6 +100,19 @@ RUN \
|
||||
mkdir -p ${PREFIX}/include/stb && \
|
||||
cp ./*.h ${PREFIX}/include/stb
|
||||
|
||||
# TAGLIB
|
||||
ARG TAGLIB_VERSION=v2.0.1
|
||||
RUN \
|
||||
DIR=/tmp/taglib && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://github.com/taglib/taglib/archive/${TAGLIB_VERSION}.tar.gz && \
|
||||
tar -x --strip-components=1 -f ${TAGLIB_VERSION}.tar.gz
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/taglib && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
CXXFLAGS="-I/usr/include/utf8cpp" cmake -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) && \
|
||||
make install
|
||||
|
||||
# LMS
|
||||
COPY . /tmp/lms/
|
||||
RUN \
|
||||
@@ -134,7 +147,7 @@ RUN \
|
||||
rm -rf /tmp/fakeroot/share/Wt/resources/themes
|
||||
|
||||
## Release Stage
|
||||
FROM alpine:3.18 AS release
|
||||
FROM alpine:3.19 AS release
|
||||
LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>"
|
||||
|
||||
ARG RUNTIME_PACKAGES=" \
|
||||
@@ -154,8 +167,7 @@ ARG RUNTIME_PACKAGES=" \
|
||||
boost-thread \
|
||||
libarchive \
|
||||
libconfig++ \
|
||||
sqlite-libs \
|
||||
taglib"
|
||||
sqlite-libs"
|
||||
|
||||
ARG LMS_USER=lms
|
||||
ARG LMS_GROUP=lms
|
||||
@@ -179,4 +191,3 @@ COPY --from=build /tmp/fakeroot/share/lms/lms.conf /etc/lms.conf
|
||||
EXPOSE 5082
|
||||
|
||||
ENTRYPOINT ["/usr/bin/lms"]
|
||||
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
|
||||
```
|
||||
__Notes__:
|
||||
* you can customize the installation directory using `-DCMAKE_INSTALL_PREFIX=path` (defaults to `/usr/local`).
|
||||
* you can customize the image library using `-DIMAGE_LIBRARY=<STB|GraphicsMagick++>`
|
||||
* you can customize the image library using `-DLMS_IMAGE_BACKEND=<stb|graphicsmagick>` (defaults to `stb`)
|
||||
```sh
|
||||
make
|
||||
```
|
||||
|
||||
+25
-25
@@ -3,31 +3,11 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Artists.template">
|
||||
<div class="mb-3">
|
||||
<ul class="nav nav-pills mb-2">
|
||||
<li class="nav-item">
|
||||
${random class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${starred class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${most-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-added class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${all class="nav-link"}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
${link-type class="form-select"}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
${link-type class="me-1"}
|
||||
<div class="d-flex">
|
||||
${search class="form-control form-control-sm me-1" type="search"}
|
||||
${sort-mode}
|
||||
</div>
|
||||
</div>
|
||||
${artists}
|
||||
@@ -42,4 +22,24 @@
|
||||
${loading-indicator}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Artists.template.track-artist-link-type-selector">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
${selected-item}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${link-type-all class="dropdown-item"}</li>
|
||||
<li>${link-type-artist class="dropdown-item"}</li>
|
||||
<li>${link-type-releaseartist class="dropdown-item"}</li>
|
||||
<li>${link-type-composer class="dropdown-item"}</li>
|
||||
<li>${link-type-conductor class="dropdown-item"}</li>
|
||||
<li>${link-type-lyricist class="dropdown-item"}</li>
|
||||
<li>${link-type-mixer class="dropdown-item"}</li>
|
||||
<li>${link-type-performer class="dropdown-item"}</li>
|
||||
<li>${link-type-producer class="dropdown-item"}</li>
|
||||
<li>${link-type-remixer class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -52,5 +52,20 @@
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.template.sort-mode-selector">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
${selected-item}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${random class="dropdown-item"}</li>
|
||||
<li>${starred class="dropdown-item"}</li>
|
||||
<li>${recently-played class="dropdown-item"}</li>
|
||||
<li>${most-played class="dropdown-item"}</li>
|
||||
<li>${recently-added class="dropdown-item"}</li>
|
||||
<li>${all class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
+3
-8
@@ -14,14 +14,11 @@
|
||||
<message id="Lms.main.template.navbar">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-3">
|
||||
<div class="container">
|
||||
<div class="d-flex">
|
||||
${search class="form-control" type="search"}
|
||||
</div>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto align-items-md-center mb-lg-0">
|
||||
<ul class="navbar-nav align-items-md-center me-md-2 mb-2 mb-md-0">
|
||||
<li class="nav-item">
|
||||
${releases class="nav-link"}
|
||||
</li>
|
||||
@@ -34,11 +31,9 @@
|
||||
<li class="nav-item">
|
||||
${tracklists class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${filters class="nav-link d-flex align-items-center"}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="navbar-nav align-items-md-center mb-lg-0">
|
||||
${filters class="d-flex align-items-center me-auto mb-2 mb-md-0"}
|
||||
<div class="navbar-nav align-items-md-center">
|
||||
${<if-is-admin>}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="${tr:Lms.administration}">
|
||||
|
||||
@@ -158,6 +158,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.media-library">Music Library</message>
|
||||
<message id="Lms.Explore.most-played">Most played</message>
|
||||
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
|
||||
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
|
||||
|
||||
@@ -158,6 +158,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.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>
|
||||
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
<message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Controllo file... {1}%</message>
|
||||
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message>
|
||||
<message id="Lms.Admin.ScannerController.step-discovering-files">File trovati: {1} files</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)...</message>
|
||||
@@ -157,6 +158,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.media-library">Libreria musicale</message>
|
||||
<message id="Lms.Explore.most-played">Più ascoltati</message>
|
||||
<message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message>
|
||||
<message id="Lms.Explore.musicbrainz-release">Album MusicBrainz</message>
|
||||
|
||||
@@ -158,6 +158,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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.notifications.template.entry">
|
||||
<div class="toast align-items-center ${bg-color} text-${text-color} border-0" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="${duration}">
|
||||
<div class="toast align-items-center ${bg-color} text-${text-color} border-0 m-1" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="${duration}">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
|
||||
+6
-22
@@ -3,28 +3,8 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.Releases.template">
|
||||
<div class="mb-3">
|
||||
<ul class="nav nav-pills mb-2">
|
||||
<li class="nav-item">
|
||||
${random class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${starred class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${most-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-added class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${all class="nav-link"}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="btn-group">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div class="btn-group me-1">
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
@@ -33,6 +13,10 @@
|
||||
<li>${play-shuffled class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
${search class="form-control form-control-sm me-1" type="search"}
|
||||
${sort-mode}
|
||||
</div>
|
||||
</div>
|
||||
${releases}
|
||||
</message>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Search.template">
|
||||
<div class="mb-3">
|
||||
<ul class="nav nav-pills mb-3">
|
||||
<li class="nav-item">
|
||||
${releases class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${artists class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${tracks class="nav-link"}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
${results}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Search.template.artists">
|
||||
<div class="row mb-3">
|
||||
<div class="col-lg-3">
|
||||
${link-type class="form-select"}
|
||||
</div>
|
||||
</div>
|
||||
${artists}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+14
-9
@@ -3,19 +3,24 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.TrackLists.template">
|
||||
<div class="mb-3">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
${recently-modified class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${all class="nav-link"}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex flex-row-reverse">
|
||||
${sort-mode}
|
||||
</div>
|
||||
${tracklists}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.TrackLists.template.sort-mode">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
${selected-item}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${all class="dropdown-item"}</li>
|
||||
<li>${recently-modified class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.TrackLists.template.entry">
|
||||
${name class="text-decoration-none link-secondary"}
|
||||
</message>
|
||||
|
||||
+6
-22
@@ -4,28 +4,8 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Tracks.template">
|
||||
<div class="mb-3">
|
||||
<ul class="nav nav-pills mb-2">
|
||||
<li class="nav-item">
|
||||
${random class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${starred class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${most-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-added class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${all class="nav-link"}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="btn-group">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div class="btn-group me-1">
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
@@ -34,6 +14,10 @@
|
||||
<li>${play-shuffled class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
${search class="form-control form-control-sm me-1" type="search"}
|
||||
${sort-mode}
|
||||
</div>
|
||||
</div>
|
||||
${tracks}
|
||||
</message>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
find_path(STB_INCLUDE_DIR stb_image.h PATH_SUFFIXES stb)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(
|
||||
STB
|
||||
REQUIRED_VARS STB_INCLUDE_DIR
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
find_path(STB_IMAGE_INCLUDE_DIR stb_image.h PATH_SUFFIXES stb)
|
||||
|
||||
find_path(STB_IMAGE_RESIZE2_INCLUDE_DIR stb_image_resize2.h PATH_SUFFIXES stb)
|
||||
if(STB_IMAGE_RESIZE2_INCLUDE_DIR)
|
||||
set(STB_IMAGE_RESIZE_VERSION 2)
|
||||
else()
|
||||
find_path(STB_IMAGE_RESIZE_INCLUDE_DIR stb_image_resize.h PATH_SUFFIXES stb)
|
||||
if(STB_IMAGE_RESIZE_INCLUDE_DIR)
|
||||
set(STB_IMAGE_RESIZE_VERSION 1)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(
|
||||
StbImage
|
||||
REQUIRED_VARS STB_IMAGE_INCLUDE_DIR STB_IMAGE_RESIZE_VERSION
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat)
|
||||
|
||||
add_library(lmsav SHARED
|
||||
impl/AudioFile.cpp
|
||||
@@ -27,4 +28,3 @@ target_link_libraries(lmsav PRIVATE
|
||||
)
|
||||
|
||||
install(TARGETS lmsav DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
|
||||
pkg_check_modules(Archive REQUIRED IMPORTED_TARGET libarchive)
|
||||
|
||||
add_library(lmscore SHARED
|
||||
impl/http/Client.cpp
|
||||
impl/http/SendQueue.cpp
|
||||
|
||||
@@ -43,17 +43,19 @@ namespace lms::db
|
||||
query.groupBy("c.id");
|
||||
|
||||
if (params.track.isValid() || params.release.isValid())
|
||||
{
|
||||
query.join("track_cluster t_c ON t_c.cluster_id = c.id");
|
||||
query.join("track t ON t.id = t_c.track_id");
|
||||
}
|
||||
|
||||
if (!params.clusterTypeName.empty())
|
||||
query.join("cluster_type c_t ON c_t.id = c.cluster_type_id");
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
query.where("t_c.track_id = ?").bind(params.track);
|
||||
|
||||
if (params.release.isValid())
|
||||
{
|
||||
query.join("track t ON t.id = t_c.track_id");
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
}
|
||||
|
||||
assert(!params.clusterType.isValid() || params.clusterTypeName.empty());
|
||||
if (params.clusterType.isValid())
|
||||
@@ -61,6 +63,17 @@ namespace lms::db
|
||||
else if (!params.clusterTypeName.empty())
|
||||
query.where("c_t.name = ?").bind(params.clusterTypeName);
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case ClusterSortMethod::None:
|
||||
break;
|
||||
case ClusterSortMethod::Name:
|
||||
query.orderBy("c.name COLLATE NOCASE");
|
||||
break;
|
||||
}
|
||||
|
||||
query.groupBy("c.id");
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -208,6 +221,12 @@ namespace lms::db
|
||||
return utils::execRangeQuery<ClusterTypeId>(query, range);
|
||||
}
|
||||
|
||||
void ClusterType::find(Session& session, const std::function<void(const pointer&)>& func)
|
||||
{
|
||||
auto query{ session.getDboSession()->find<ClusterType>() };
|
||||
return utils::forEachQueryResult(query, func);
|
||||
}
|
||||
|
||||
ClusterType::pointer ClusterType::find(Session& session, std::string_view name)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
@@ -32,9 +32,8 @@ namespace lms::db
|
||||
Wt::Dbo::Query<ArtistId> createArtistsQuery(Session& session, const Listen::ArtistStatsFindParameters& params)
|
||||
{
|
||||
auto query{ session.getDboSession()->query<ArtistId>("SELECT a.id from artist a")
|
||||
.join("track t ON t.id = t_a_l.track_id")
|
||||
.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id")
|
||||
.join("listen l ON l.track_id = t.id") };
|
||||
.join("listen l ON l.track_id = t_a_l.track_id") };
|
||||
|
||||
if (params.user.isValid())
|
||||
query.where("l.user_id = ?").bind(params.user);
|
||||
@@ -45,7 +44,10 @@ namespace lms::db
|
||||
assert(!params.artist.isValid()); // poor check
|
||||
|
||||
if (params.library.isValid())
|
||||
{
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
query.where("t.media_library_id = ?").bind(params.library);
|
||||
}
|
||||
|
||||
if (params.linkType)
|
||||
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||
@@ -69,6 +71,26 @@ namespace lms::db
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (!params.keywords.empty())
|
||||
{
|
||||
std::vector<std::string> clauses;
|
||||
std::vector<std::string> sortClauses;
|
||||
|
||||
for (const std::string_view keyword : params.keywords)
|
||||
{
|
||||
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
}
|
||||
|
||||
for (const std::string_view keyword : params.keywords)
|
||||
{
|
||||
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
}
|
||||
|
||||
query.where("(" + core::stringUtils::joinStrings(clauses, " AND ") + ") OR (" + core::stringUtils::joinStrings(sortClauses, " AND ") + ")");
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -114,6 +136,9 @@ namespace lms::db
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -157,6 +182,9 @@ namespace lms::db
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 57 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 59 };
|
||||
}
|
||||
|
||||
VersionInfo::VersionInfo()
|
||||
@@ -460,6 +460,18 @@ SELECT
|
||||
session.getDboSession()->execute("DROP INDEX " + indexName);
|
||||
}
|
||||
|
||||
void migrateFromV57(Session& session)
|
||||
{
|
||||
// useless index
|
||||
session.getDboSession()->execute("DROP INDEX cluster_name_idx");
|
||||
}
|
||||
|
||||
void migrateFromV58(Session& session)
|
||||
{
|
||||
// DSF support
|
||||
session.getDboSession()->execute("UPDATE scan_settings SET audio_file_extensions = audio_file_extensions || ' .dsf'");
|
||||
}
|
||||
|
||||
bool doDbMigration(Session& session)
|
||||
{
|
||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -494,6 +506,8 @@ SELECT
|
||||
{54, migrateFromV54},
|
||||
{55, migrateFromV55},
|
||||
{56, migrateFromV56},
|
||||
{57, migrateFromV57},
|
||||
{58, migrateFromV58},
|
||||
};
|
||||
|
||||
bool migrationPerformed{};
|
||||
|
||||
@@ -172,7 +172,6 @@ namespace lms::db
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)");
|
||||
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
|
||||
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
|
||||
|
||||
@@ -105,20 +105,6 @@ namespace lms::db
|
||||
});
|
||||
}
|
||||
|
||||
core::EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
const auto query{ session.getDboSession()->query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link") };
|
||||
|
||||
core::EnumSet<TrackArtistLinkType> res;
|
||||
utils::forEachQueryResult(query, [&](TrackArtistLinkType linkType)
|
||||
{
|
||||
res.insert(linkType);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
core::EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
@@ -35,46 +35,21 @@
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
||||
: _name{ name }
|
||||
, _type{ type }
|
||||
, _isPublic{ isPublic }
|
||||
, _creationDateTime{ utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
|
||||
, _lastModifiedDateTime{ utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
|
||||
, _user{ getDboPtr(user) }
|
||||
namespace
|
||||
{
|
||||
assert(user);
|
||||
}
|
||||
|
||||
TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const TrackList::FindParameters& params)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<TrackList> {new TrackList{ name, type, isPublic, user }});
|
||||
}
|
||||
auto query{ session.getDboSession()->query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM tracklist t_l") };
|
||||
|
||||
std::size_t TrackList::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
if (!params.clusters.empty() || params.mediaLibrary.isValid())
|
||||
query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id");
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM tracklist"));
|
||||
}
|
||||
if (params.mediaLibrary.isValid())
|
||||
query.join("track t ON t.id = t_l_e.track_id");
|
||||
|
||||
|
||||
TrackList::pointer TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
assert(userId.isValid());
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackList>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("type = ?").bind(type)
|
||||
.where("user_id = ?").bind(userId));
|
||||
}
|
||||
|
||||
RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<TrackListId>("SELECT DISTINCT t_l.id FROM tracklist t_l") };
|
||||
if (params.mediaLibrary.isValid())
|
||||
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
|
||||
|
||||
if (params.user.isValid())
|
||||
query.where("t_l.user_id = ?").bind(params.user);
|
||||
@@ -84,11 +59,8 @@ namespace lms::db
|
||||
|
||||
if (!params.clusters.empty())
|
||||
{
|
||||
query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id");
|
||||
query.join("track t ON t.id = t_l_e.track_id");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||
oss << "t_l_e.track_id IN (SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
||||
|
||||
@@ -117,9 +89,73 @@ namespace lms::db
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const TrackList::FindParameters& params)
|
||||
{
|
||||
std::string_view itemToSelect;
|
||||
|
||||
if constexpr (std::is_same_v<ResultType, TrackListId>)
|
||||
itemToSelect = "t_l.id";
|
||||
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<TrackList>>)
|
||||
itemToSelect = "t_l";
|
||||
else
|
||||
static_assert("Unhandled type");
|
||||
|
||||
return createQuery<ResultType>(session, itemToSelect, params);
|
||||
}
|
||||
}
|
||||
|
||||
TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
||||
: _name{ name }
|
||||
, _type{ type }
|
||||
, _isPublic{ isPublic }
|
||||
, _creationDateTime{ utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
|
||||
, _lastModifiedDateTime{ utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
assert(user);
|
||||
}
|
||||
|
||||
TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<TrackList> {new TrackList{ name, type, isPublic, user }});
|
||||
}
|
||||
|
||||
std::size_t TrackList::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM tracklist"));
|
||||
}
|
||||
|
||||
TrackList::pointer TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
assert(userId.isValid());
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackList>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("type = ?").bind(type)
|
||||
.where("user_id = ?").bind(userId));
|
||||
}
|
||||
|
||||
RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<TrackListId>(session, params) };
|
||||
return utils::execRangeQuery<TrackListId>(query, params.range);
|
||||
}
|
||||
|
||||
void TrackList::find(Session& session, const FindParameters& params, const std::function<void(const TrackList::pointer&)>& func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<TrackList>>(session, params) };
|
||||
utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
TrackList::pointer TrackList::find(Session& session, TrackListId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
@@ -61,6 +61,18 @@ namespace lms::db
|
||||
return utils::execRangeQuery<UserId>(query, params.range);
|
||||
}
|
||||
|
||||
void User::find(Session& session, const FindParameters& params, const std::function<void(const User::pointer&)>& func)
|
||||
{
|
||||
auto query{ session.getDboSession()->find<User>() };
|
||||
|
||||
if (params.scrobblingBackend)
|
||||
query.where("scrobbling_backend = ?").bind(*params.scrobblingBackend);
|
||||
if (params.feedbackBackend)
|
||||
query.where("feedback_backend = ?").bind(*params.feedbackBackend);
|
||||
|
||||
return utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
User::pointer User::findDemoUser(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -67,7 +68,7 @@ namespace lms::db
|
||||
ReleaseId release; // artists involved in this release
|
||||
MediaLibraryId mediaLibrary; // artists that belong to this library
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setClusters(std::span<const ClusterId> _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
|
||||
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||
|
||||
@@ -46,12 +46,14 @@ namespace lms::db
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
ClusterSortMethod sortMethod;
|
||||
ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
|
||||
std::string clusterTypeName; // if non empty, clusters that belong to this cluster type
|
||||
TrackId track; // if set, clusters involved in this track
|
||||
ReleaseId release; // if set, clusters involved in this release
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setSortMethod(ClusterSortMethod _method) { sortMethod = _method; return *this; }
|
||||
FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; }
|
||||
FindParameters& setClusterTypeName(std::string_view _name) { clusterTypeName = _name; return *this; }
|
||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
@@ -119,6 +121,7 @@ namespace lms::db
|
||||
// Getters
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<ClusterTypeId> findIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static void find(Session& session, const std::function<void(const pointer&)>& func);
|
||||
static pointer find(Session& session, std::string_view name);
|
||||
static pointer find(Session& session, ClusterTypeId id);
|
||||
static RangeResults<ClusterTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
@@ -70,6 +70,7 @@ namespace lms::db
|
||||
UserId user;
|
||||
std::optional<ScrobblingBackend> backend;
|
||||
std::vector<ClusterId> clusters; // if non empty, entities that belong to these clusters
|
||||
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;
|
||||
@@ -77,6 +78,7 @@ namespace lms::db
|
||||
StatsFindParameters& setUser(UserId _user) { user = _user; return *this; }
|
||||
StatsFindParameters& setScrobblingBackend(std::optional<ScrobblingBackend> _backend) { backend = _backend; return *this; }
|
||||
StatsFindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
StatsFindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
StatsFindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
StatsFindParameters& setArtist(ArtistId _artist) { artist = _artist; return *this; }
|
||||
StatsFindParameters& setMediaLibrary(MediaLibraryId _library) { library = _library; return *this; }
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -96,7 +97,7 @@ namespace lms::db
|
||||
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
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setClusters(std::span<const ClusterId> _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace lms::db
|
||||
Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
|
||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
||||
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
|
||||
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv .dsf" };
|
||||
std::string _extraTagsToScan;
|
||||
std::string _artistTagDelimiters;
|
||||
std::string _defaultTagDelimiters;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <filesystem>
|
||||
#include <ostream>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -80,7 +81,7 @@ namespace lms::db
|
||||
std::optional<int> discNumber; // matching this disc number
|
||||
MediaLibraryId mediaLibrary; // If set, tracks in this library
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setClusters(std::span<const ClusterId> _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setName(std::string_view _name) { name = _name; return *this; }
|
||||
FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; }
|
||||
|
||||
@@ -66,7 +66,6 @@ namespace lms::db
|
||||
static void find(Session& session, const FindParameters& parameters, const std::function<void(const TrackArtistLink::pointer&)>&);
|
||||
static pointer find(Session& session, TrackArtistLinkId linkId);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
|
||||
static core::EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
|
||||
static core::EnumSet<TrackArtistLinkType> findUsedTypes(Session& session, ArtistId _artist);
|
||||
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -28,6 +29,7 @@
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/ClusterId.hpp"
|
||||
#include "database/MediaLibraryId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/TrackListId.hpp"
|
||||
@@ -57,18 +59,21 @@ namespace lms::db
|
||||
std::optional<Range> range;
|
||||
std::optional<TrackListType> type;
|
||||
UserId user; // only tracklists owned by this user
|
||||
MediaLibraryId mediaLibrary; // only tracklists that have songs in this media library
|
||||
TrackListSortMethod sortMethod{ TrackListSortMethod::None };
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setClusters(std::span<const ClusterId> _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; }
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setType(TrackListType _type) { type = _type; return *this; }
|
||||
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
||||
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary) { mediaLibrary = _mediaLibrary; return *this; }
|
||||
FindParameters& setSortMethod(TrackListSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||
};
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId);
|
||||
static pointer find(Session& session, TrackListId tracklistId);
|
||||
static RangeResults<TrackListId> find(Session& session, const FindParameters& params);
|
||||
static void find(Session& session, const FindParameters& params, const std::function<void(const TrackList::pointer&)>& func);
|
||||
|
||||
// Accessors
|
||||
std::string_view getName() const { return _name; }
|
||||
|
||||
@@ -116,6 +116,12 @@ namespace lms::db
|
||||
StarredDateDesc,
|
||||
};
|
||||
|
||||
enum class ClusterSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
};
|
||||
|
||||
enum class ReleaseSortMethod
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -72,6 +72,7 @@ namespace lms::db
|
||||
static pointer find(Session& session, UserId id);
|
||||
static pointer find(Session& session, std::string_view loginName);
|
||||
static RangeResults<UserId> find(Session& session, const FindParameters& params);
|
||||
static void find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func);
|
||||
static pointer findDemoUser(Session& session);
|
||||
|
||||
// accessors
|
||||
|
||||
@@ -13,6 +13,7 @@ add_executable(test-database
|
||||
TrackBookmark.cpp
|
||||
TrackFeatures.cpp
|
||||
TrackList.cpp
|
||||
User.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test-database PRIVATE
|
||||
|
||||
@@ -137,11 +137,11 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setClusters({cluster1.getId()})) };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.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({ cluster2.getId() }));
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() }));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
@@ -237,6 +237,35 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, ClusterType)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
ClusterType::find(session, [&](const ClusterType::pointer&)
|
||||
{
|
||||
visited = true;
|
||||
});
|
||||
EXPECT_FALSE(visited);
|
||||
}
|
||||
|
||||
ScopedClusterType clusterType1{ session, "MyClusterType1" };
|
||||
ScopedClusterType clusterType2{ session, "MyClusterType2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
std::vector<ClusterTypeId> visitedClusterTypes;
|
||||
ClusterType::find(session, [&](const ClusterType::pointer& clusterType)
|
||||
{
|
||||
visitedClusterTypes.push_back(clusterType->getId());
|
||||
});
|
||||
ASSERT_EQ(visitedClusterTypes.size(), 2);
|
||||
EXPECT_EQ(visitedClusterTypes[0], clusterType1->getId());
|
||||
EXPECT_EQ(visitedClusterTypes[1], clusterType2->getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, ClusterType_singleTrack)
|
||||
{
|
||||
@@ -285,7 +314,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({ unusedCluster.getId() })).results.size(), 0);
|
||||
EXPECT_EQ(Release::find(session, Release::FindParameters{}.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);
|
||||
@@ -321,7 +350,7 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters(std::initializer_list<ClusterId>{cluster.getId()})) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
@@ -329,7 +358,7 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters({unusedCluster.getId()})) };
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters(std::initializer_list<ClusterId>{unusedCluster.getId()})) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
@@ -375,12 +404,12 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) };
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.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({ cluster2.getId() })).results.size(), 0);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster3.getId() })).results.size(), 0);
|
||||
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);
|
||||
|
||||
cluster2.get().modify()->addTrack(track.get());
|
||||
}
|
||||
@@ -388,19 +417,19 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) };
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.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({ cluster2.getId() }));
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.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({ cluster1.getId() }));
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.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({ cluster3.getId() })).results.size(), 0);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list<ClusterId>{ cluster3.getId() })).results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,7 +458,7 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters(std::initializer_list<ClusterId>{cluster.getId()})) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
@@ -581,7 +610,7 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters(std::initializer_list<ClusterId>{cluster.getId()})) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
@@ -589,7 +618,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({ cluster.getId() }));
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters(std::initializer_list<ClusterId>{ cluster.getId() }));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
@@ -620,7 +649,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({ cluster1.getId(), cluster2.getId() }));
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters(std::initializer_list<ClusterId>{ cluster1.getId(), cluster2.getId() }));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
|
||||
@@ -151,6 +151,46 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, TrackList_MediaLibrary)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrackList trackList1{ session, "MytrackList1", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
ScopedTrackList trackList2{ session, "MytrackList2", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedMediaLibrary library{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
session.create<TrackListEntry>(track1.get(), trackList1.get());
|
||||
session.create<TrackListEntry>(track2.get(), trackList2.get());
|
||||
track2.get().modify()->setMediaLibrary(library.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
std::vector<TrackListId> visitedTrackLists;
|
||||
TrackList::find(session, TrackList::FindParameters{}, [&](const TrackList::pointer& trackList)
|
||||
{
|
||||
visitedTrackLists.push_back(trackList->getId());
|
||||
});
|
||||
ASSERT_EQ(visitedTrackLists.size(), 2);
|
||||
EXPECT_EQ(visitedTrackLists[0], trackList1->getId());
|
||||
EXPECT_EQ(visitedTrackLists[1], trackList2->getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
std::vector<TrackListId> visitedTrackLists;
|
||||
TrackList::find(session, TrackList::FindParameters{}.setMediaLibrary(library->getId()), [&](const TrackList::pointer& trackList)
|
||||
{
|
||||
visitedTrackLists.push_back(trackList->getId());
|
||||
});
|
||||
ASSERT_EQ(visitedTrackLists.size(), 1);
|
||||
EXPECT_EQ(visitedTrackLists[0], trackList2->getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
@@ -163,7 +203,7 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters(std::initializer_list<ClusterId>{cluster.getId()})) };
|
||||
EXPECT_EQ(trackLists.results.size(), 0);
|
||||
}
|
||||
|
||||
@@ -177,7 +217,7 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters(std::initializer_list<ClusterId>{cluster.getId()})) };
|
||||
ASSERT_EQ(trackLists.results.size(), 1);
|
||||
EXPECT_EQ(trackLists.results.front(), trackList1.getId());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
TEST_F(DatabaseFixture, User)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
User::find(session, User::FindParameters{}, [&](const User::pointer&)
|
||||
{
|
||||
visited = true;
|
||||
});
|
||||
EXPECT_FALSE(visited);
|
||||
}
|
||||
|
||||
ScopedUser user1{ session, "MyUser1" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
std::vector<UserId> visitedUsers;
|
||||
User::find(session, User::FindParameters{}, [&](const User::pointer& user)
|
||||
{
|
||||
visitedUsers.push_back(user->getId());
|
||||
});
|
||||
EXPECT_EQ(visitedUsers.size(), 2);
|
||||
EXPECT_EQ(visitedUsers[0], user1->getId());
|
||||
EXPECT_EQ(visitedUsers[1], user2->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,22 +17,32 @@ target_link_libraries(lmsimage PUBLIC
|
||||
std::filesystem
|
||||
)
|
||||
|
||||
if (IMAGE_LIBRARY STREQUAL STB)
|
||||
set(LMS_IMAGE_BACKEND "stb" CACHE STRING "Image library")
|
||||
set_property(CACHE LMS_IMAGE_BACKEND PROPERTY STRINGS "stb" "graphicsmagick")
|
||||
|
||||
if (${LMS_IMAGE_BACKEND} STREQUAL "stb")
|
||||
find_package(StbImage REQUIRED)
|
||||
message(STATUS "Using stb (resize version ${STB_IMAGE_RESIZE_VERSION})")
|
||||
|
||||
target_sources(lmsimage PRIVATE
|
||||
impl/stb/JPEGImage.cpp
|
||||
impl/stb/RawImage.cpp
|
||||
)
|
||||
target_compile_options(lmsimage PRIVATE "-DLMS_SUPPORT_IMAGE_STB")
|
||||
target_include_directories(lmsimage PRIVATE ${STB_INCLUDE_DIR})
|
||||
elseif (IMAGE_LIBRARY STREQUAL GraphicsMagick++)
|
||||
target_compile_options(lmsimage PRIVATE "-DSTB_IMAGE_RESIZE_VERSION=${STB_IMAGE_RESIZE_VERSION}")
|
||||
target_include_directories(lmsimage PRIVATE ${STB_IMAGE_INCLUDE_DIR})
|
||||
|
||||
elseif (${LMS_IMAGE_BACKEND} STREQUAL "graphicsmagick")
|
||||
pkg_check_modules(GraphicsMagick++ REQUIRED IMPORTED_TARGET GraphicsMagick++)
|
||||
message(STATUS "Using graphicsmagick")
|
||||
|
||||
target_sources(lmsimage PRIVATE
|
||||
impl/graphicsmagick/JPEGImage.cpp
|
||||
impl/graphicsmagick/RawImage.cpp
|
||||
)
|
||||
target_compile_options(lmsimage PRIVATE "-DLMS_SUPPORT_IMAGE_GM")
|
||||
target_link_libraries(lmsimage PRIVATE PkgConfig::GraphicsMagick++)
|
||||
|
||||
else ()
|
||||
message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided")
|
||||
message(FATAL_ERROR "Invalid image library")
|
||||
endif ()
|
||||
|
||||
install(TARGETS lmsimage DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef LMS_SUPPORT_IMAGE_GM
|
||||
#error "Bad configuration"
|
||||
#endif
|
||||
|
||||
#include <Magick++.h>
|
||||
|
||||
#include "image/IEncodedImage.hpp"
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef LMS_SUPPORT_IMAGE_GM
|
||||
#error "Bad configuration"
|
||||
#endif
|
||||
|
||||
#include <Magick++.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
@@ -28,7 +28,13 @@
|
||||
#define STBI_FAILURE_USERMSG
|
||||
|
||||
#include <stb_image.h>
|
||||
#if STB_IMAGE_RESIZE_VERSION == 1
|
||||
#include <stb_image_resize.h>
|
||||
#elif STB_IMAGE_RESIZE_VERSION == 2
|
||||
#include <stb_image_resize2.h>
|
||||
#else
|
||||
#error "Unhandled STB image resize version"!
|
||||
#endif
|
||||
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
@@ -94,9 +100,17 @@ namespace lms::image::STB
|
||||
if (!resizedData)
|
||||
throw Exception{ "Cannot allocate memory for resized image!" };
|
||||
|
||||
#if STB_IMAGE_RESIZE_VERSION == 1
|
||||
if (::stbir_resize_uint8_srgb(reinterpret_cast<const unsigned char*>(_data.get()), _width, _height, 0,
|
||||
reinterpret_cast<unsigned char*>(resizedData.get()), width, height, 0,
|
||||
3, STBIR_ALPHA_CHANNEL_NONE, 0) == 0)
|
||||
#elif STB_IMAGE_RESIZE_VERSION == 2
|
||||
if (::stbir_resize_uint8_srgb(reinterpret_cast<const unsigned char*>(_data.get()), _width, _height, 0,
|
||||
reinterpret_cast<unsigned char*>(resizedData.get()), width, height, 0,
|
||||
STBIR_RGB) == 0)
|
||||
#else
|
||||
#error "Unhandled STB image resize version"!
|
||||
#endif
|
||||
{
|
||||
throw Exception{ "Failed to resize image:" + std::string{ ::stbi_failure_reason() } };
|
||||
}
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef LMS_SUPPORT_IMAGE_STB
|
||||
#error "Bad configuration"
|
||||
#endif
|
||||
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(test)
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
#include <taglib/apeproperties.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
#if TAGLIB_MAJOR_VERSION >= 2
|
||||
#include <taglib/dsffile.h>
|
||||
#endif
|
||||
#include <taglib/id3v2tag.h>
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/flacfile.h>
|
||||
@@ -328,6 +331,10 @@ namespace lms::metadata
|
||||
_audioProperties.bitsPerSample = mp4Properties->bitsPerSample();
|
||||
else if (const auto * wavePackProperties{ dynamic_cast<const TagLib::WavPack::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = wavePackProperties->bitsPerSample();
|
||||
#if TAGLIB_MAJOR_VERSION >= 2
|
||||
else if (const auto * dsfProperties{ dynamic_cast<const TagLib::DSF::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||
|
||||
@@ -28,6 +28,22 @@ target_link_libraries(lmsauth PUBLIC
|
||||
Wt::Wt
|
||||
)
|
||||
|
||||
# PAM
|
||||
option(USE_PAM "Use the PAM backend authentication API" ON)
|
||||
if (USE_PAM)
|
||||
find_package(PAM QUIET)
|
||||
if (USE_PAM AND NOT PAM_FOUND)
|
||||
message(WARNING "PAM library not found: disabling")
|
||||
set(USE_PAM OFF)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (USE_PAM)
|
||||
message(STATUS "Using PAM authentication backend")
|
||||
else ()
|
||||
message(STATUS "NOT using PAM authentication backend")
|
||||
endif ()
|
||||
|
||||
if (USE_PAM)
|
||||
target_compile_options(lmsauth PRIVATE "-DLMS_SUPPORT_PAM")
|
||||
target_sources(lmsauth PRIVATE impl/pam/PAMPasswordService.cpp)
|
||||
|
||||
@@ -37,7 +37,6 @@ namespace lms::db
|
||||
|
||||
namespace lms::auth
|
||||
{
|
||||
|
||||
class IAuthTokenService;
|
||||
|
||||
class IPasswordService
|
||||
|
||||
@@ -96,6 +96,7 @@ namespace lms::feedback
|
||||
Artist::FindParameters searchParams;
|
||||
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);
|
||||
@@ -136,6 +137,7 @@ namespace lms::feedback
|
||||
Release::FindParameters searchParams;
|
||||
searchParams.setStarringUser(params.user, *backend);
|
||||
searchParams.setClusters(params.clusters);
|
||||
searchParams.setKeywords(params.keywords);
|
||||
searchParams.setSortMethod(ReleaseSortMethod::StarredDateDesc);
|
||||
searchParams.setRange(params.range);
|
||||
searchParams.setMediaLibrary(params.library);
|
||||
@@ -175,6 +177,7 @@ namespace lms::feedback
|
||||
Track::FindParameters searchParams;
|
||||
searchParams.setStarringUser(params.user, *backend);
|
||||
searchParams.setClusters(params.clusters);
|
||||
searchParams.setKeywords(params.keywords);
|
||||
searchParams.setSortMethod(TrackSortMethod::StarredDateDesc);
|
||||
searchParams.setRange(params.range);
|
||||
searchParams.setMediaLibrary(params.library);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <boost/asio/io_service.hpp>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
@@ -53,11 +54,13 @@ namespace lms::feedback
|
||||
{
|
||||
db::UserId user;
|
||||
std::vector<db::ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
|
||||
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(const std::vector<db::ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setClusters(std::span<const db::ClusterId> _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setRange(std::optional<db::Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setMediaLibrary(db::MediaLibraryId _library) { library = _library; return *this; }
|
||||
};
|
||||
|
||||
@@ -441,7 +441,7 @@ namespace lms::scanner
|
||||
|
||||
_metadataScanQueue.wait();
|
||||
|
||||
while (_metadataScanQueue.popResults(scanResults, processMetaDataBatchSize) > 0)
|
||||
while (!_abortScan && _metadataScanQueue.popResults(scanResults, processMetaDataBatchSize) > 0)
|
||||
processMetaDataScanResults(context, scanResults, mediaLibrary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,11 +148,6 @@ namespace lms::scanner
|
||||
});
|
||||
}
|
||||
|
||||
void ScannerService::requestStop()
|
||||
{
|
||||
abortScan();
|
||||
}
|
||||
|
||||
void ScannerService::requestReload()
|
||||
{
|
||||
abortScan();
|
||||
@@ -295,16 +290,22 @@ namespace lms::scanner
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Completed scan step '" << scanStep->getStepName() << "'");
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock lock{ _statusMutex };
|
||||
|
||||
_curState = State::NotScheduled;
|
||||
_currentScanStepStats.reset(); // must be sync with _curState
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size());
|
||||
|
||||
if (!_abortScan)
|
||||
{
|
||||
stats.stopTime = Wt::WDateTime::currentDateTime();
|
||||
|
||||
{
|
||||
std::unique_lock lock{ _statusMutex };
|
||||
|
||||
_lastCompleteScanStats = stats;
|
||||
_currentScanStepStats.reset();
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Scan not aborted, scheduling next scan!");
|
||||
@@ -315,11 +316,6 @@ namespace lms::scanner
|
||||
else
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Scan aborted, not scheduling next scan!");
|
||||
|
||||
std::unique_lock lock{ _statusMutex };
|
||||
|
||||
_curState = State::NotScheduled;
|
||||
_currentScanStepStats.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ namespace lms::scanner
|
||||
ScannerService(const ScannerService&) = delete;
|
||||
ScannerService& operator=(const ScannerService&) = delete;
|
||||
|
||||
void requestStop() override;
|
||||
void requestReload() override;
|
||||
void requestImmediateScan(const ScanOptions& scanOptions) override;
|
||||
|
||||
|
||||
@@ -38,9 +38,7 @@ namespace lms::scanner
|
||||
virtual ~IScannerService() = default;
|
||||
|
||||
// Async requests
|
||||
virtual void requestStop() = 0;
|
||||
virtual void requestReload() = 0;
|
||||
|
||||
virtual void requestReload() = 0; // will stop/reschedule scan
|
||||
virtual void requestImmediateScan(const ScanOptions& options = {}) = 0;
|
||||
|
||||
enum class State
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace lms::scrobbling
|
||||
db::Listen::StatsFindParameters listenFindParams;
|
||||
listenFindParams.setUser(params.user);
|
||||
listenFindParams.setClusters(params.clusters);
|
||||
listenFindParams.setKeywords(params.keywords);
|
||||
listenFindParams.setRange(params.range);
|
||||
listenFindParams.setMediaLibrary(params.library);
|
||||
listenFindParams.setArtist(params.artist);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <boost/asio/io_service.hpp>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
@@ -60,12 +61,14 @@ namespace lms::scrobbling
|
||||
{
|
||||
db::UserId user;
|
||||
std::vector<db::ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
|
||||
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
|
||||
|
||||
FindParameters& setUser(const db::UserId _user) { user = _user; return *this; }
|
||||
FindParameters& setClusters(const std::vector<db::ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setClusters(std::span<const db::ClusterId> _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setRange(std::optional<db::Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setMediaLibrary(db::MediaLibraryId _library) { library = _library; return *this; }
|
||||
FindParameters& setArtist(db::ArtistId _artist) { artist = _artist; return *this; }
|
||||
|
||||
@@ -23,13 +23,14 @@
|
||||
|
||||
#include <Wt/Http/Request.h>
|
||||
|
||||
#include "database/UserId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "ClientInfo.hpp"
|
||||
#include "ProtocolVersion.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Session;
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace lms::api::subsonic
|
||||
@@ -38,7 +39,7 @@ namespace lms::api::subsonic
|
||||
{
|
||||
const Wt::Http::ParameterMap& parameters;
|
||||
db::Session& dbSession;
|
||||
db::UserId userId;
|
||||
const db::ObjectPtr<db::User> user;
|
||||
ClientInfo clientInfo;
|
||||
ProtocolVersion serverProtocolVersion;
|
||||
bool enableOpenSubsonic{ true };
|
||||
|
||||
@@ -135,13 +135,7 @@ namespace lms::api::subsonic
|
||||
|
||||
void checkUserTypeIsAllowed(RequestContext& context, core::EnumSet<db::UserType> allowedUserTypes)
|
||||
{
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
db::User::pointer currentUser{ db::User::find(context.dbSession, context.userId) };
|
||||
if (!currentUser)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
if (!allowedUserTypes.contains(currentUser->getType()))
|
||||
if (!allowedUserTypes.contains(context.user->getType()))
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
@@ -421,7 +415,17 @@ namespace lms::api::subsonic
|
||||
bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) };
|
||||
bool enableDefaultCover{ _defaultCoverClients.find(clientInfo.name) != std::cend(_openSubsonicDisabledClients) };
|
||||
|
||||
return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic, enableDefaultCover };
|
||||
db::User::pointer user;
|
||||
{
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
user = db::User::find(session, userId);
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
return { parameters, _db.getTLSSession(), user, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic, enableDefaultCover };
|
||||
}
|
||||
|
||||
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo)
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
#include "ClientInfo.hpp"
|
||||
#include "RequestContext.hpp"
|
||||
|
||||
|
||||
@@ -60,10 +60,6 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
if (type == "alphabeticalByName")
|
||||
{
|
||||
Release::FindParameters params;
|
||||
@@ -92,7 +88,7 @@ namespace lms::api::subsonic
|
||||
if (const Cluster::pointer cluster{ clusterType->getCluster(genre) })
|
||||
{
|
||||
Release::FindParameters params;
|
||||
params.setClusters({ cluster->getId() });
|
||||
params.setClusters(std::initializer_list<ClusterId>{ cluster->getId() });
|
||||
params.setSortMethod(ReleaseSortMethod::Name);
|
||||
params.setRange(range);
|
||||
params.setMediaLibrary(mediaLibraryId);
|
||||
@@ -117,7 +113,7 @@ namespace lms::api::subsonic
|
||||
else if (type == "frequent")
|
||||
{
|
||||
scrobbling::IScrobblingService::FindParameters params;
|
||||
params.setUser(context.userId);
|
||||
params.setUser(context.user->getId());
|
||||
params.setRange(range);
|
||||
params.setMediaLibrary(mediaLibraryId);
|
||||
|
||||
@@ -146,7 +142,7 @@ namespace lms::api::subsonic
|
||||
else if (type == "recent")
|
||||
{
|
||||
scrobbling::IScrobblingService::FindParameters params;
|
||||
params.setUser(context.userId);
|
||||
params.setUser(context.user->getId());
|
||||
params.setRange(range);
|
||||
params.setMediaLibrary(mediaLibraryId);
|
||||
|
||||
@@ -155,7 +151,7 @@ namespace lms::api::subsonic
|
||||
else if (type == "starred")
|
||||
{
|
||||
feedback::IFeedbackService::FindParameters params;
|
||||
params.setUser(context.userId);
|
||||
params.setUser(context.user->getId());
|
||||
params.setRange(range);
|
||||
params.setMediaLibrary(mediaLibraryId);
|
||||
|
||||
@@ -172,7 +168,7 @@ namespace lms::api::subsonic
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
{
|
||||
const Release::pointer release{ Release::find(context.dbSession, releaseId) };
|
||||
albumListNode.addArrayChild("album", createAlbumNode(context, release, user, id3));
|
||||
albumListNode.addArrayChild("album", createAlbumNode(context, release, context.user, id3));
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -185,10 +181,6 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& starredNode{ response.createNode(id3 ? Response::Node::Key{ "starred2" } : Response::Node::Key{ "starred" }) };
|
||||
|
||||
@@ -196,29 +188,29 @@ namespace lms::api::subsonic
|
||||
|
||||
{
|
||||
feedback::IFeedbackService::ArtistFindParameters artistFindParams;
|
||||
artistFindParams.setUser(context.userId);
|
||||
artistFindParams.setUser(context.user->getId());
|
||||
artistFindParams.setSortMethod(ArtistSortMethod::SortName);
|
||||
for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results)
|
||||
{
|
||||
if (auto artist{ Artist::find(context.dbSession, artistId) })
|
||||
starredNode.addArrayChild("artist", createArtistNode(context, artist, user, id3));
|
||||
starredNode.addArrayChild("artist", createArtistNode(context, artist, context.user, id3));
|
||||
}
|
||||
}
|
||||
|
||||
feedback::IFeedbackService::FindParameters findParameters;
|
||||
findParameters.setUser(context.userId);
|
||||
findParameters.setUser(context.user->getId());
|
||||
findParameters.setMediaLibrary(mediaLibrary);
|
||||
|
||||
for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results)
|
||||
{
|
||||
if (auto release{ Release::find(context.dbSession, releaseId) })
|
||||
starredNode.addArrayChild("album", createAlbumNode(context, release, user, id3));
|
||||
starredNode.addArrayChild("album", createAlbumNode(context, release, context.user, id3));
|
||||
}
|
||||
|
||||
for (const TrackId trackId : feedbackService.findStarredTracks(findParameters).results)
|
||||
{
|
||||
if (auto track{ Track::find(context.dbSession, trackId) })
|
||||
starredNode.addArrayChild("song", createSongNode(context, track, user));
|
||||
starredNode.addArrayChild("song", createSongNode(context, track, context.user));
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -243,15 +235,11 @@ namespace lms::api::subsonic
|
||||
if (size > defaultMaxCountSize)
|
||||
throw ParameterValueTooHighGenericError{ "size", defaultMaxCountSize };
|
||||
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& randomSongsNode{ response.createNode("randomSongs") };
|
||||
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
Track::FindParameters params;
|
||||
params.setSortMethod(TrackSortMethod::Random);
|
||||
params.setRange(Range{ 0, size });
|
||||
@@ -259,7 +247,7 @@ namespace lms::api::subsonic
|
||||
|
||||
Track::find(context.dbSession, params, [&](const Track::pointer& track)
|
||||
{
|
||||
randomSongsNode.addArrayChild("song", createSongNode(context, track, user));
|
||||
randomSongsNode.addArrayChild("song", createSongNode(context, track, context.user));
|
||||
});
|
||||
|
||||
return response;
|
||||
@@ -288,21 +276,17 @@ namespace lms::api::subsonic
|
||||
if (!cluster)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& songsByGenreNode{ response.createNode("songsByGenre") };
|
||||
|
||||
Track::FindParameters params;
|
||||
params.setClusters({ cluster->getId() });
|
||||
params.setClusters(std::initializer_list<ClusterId>{ cluster->getId() });
|
||||
params.setRange(Range{ offset, count });
|
||||
params.setMediaLibrary(mediaLibrary);
|
||||
|
||||
Track::find(context.dbSession, params, [&](const Track::pointer& track)
|
||||
{
|
||||
songsByGenreNode.addArrayChild("song", createSongNode(context, track, user));
|
||||
songsByGenreNode.addArrayChild("song", createSongNode(context, track, context.user));
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
@@ -36,11 +36,7 @@ namespace lms::api::subsonic
|
||||
{
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
const auto bookmarkIds{ TrackBookmark::find(context.dbSession, user->getId()) };
|
||||
const auto bookmarkIds{ TrackBookmark::find(context.dbSession, context.user->getId()) };
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& bookmarksNode{ response.createNode("bookmarks") };
|
||||
@@ -49,7 +45,7 @@ namespace lms::api::subsonic
|
||||
{
|
||||
const TrackBookmark::pointer bookmark{ TrackBookmark::find(context.dbSession, bookmarkId) };
|
||||
Response::Node bookmarkNode{ createBookmarkNode(bookmark) };
|
||||
bookmarkNode.addChild("entry", createSongNode(context, bookmark->getTrack(), user));
|
||||
bookmarkNode.addChild("entry", createSongNode(context, bookmark->getTrack(), context.user));
|
||||
bookmarksNode.addArrayChild("bookmark", std::move(bookmarkNode));
|
||||
}
|
||||
|
||||
@@ -65,18 +61,14 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
|
||||
const User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
// Replace any existing bookmark
|
||||
auto bookmark{ TrackBookmark::find(context.dbSession, user->getId(), trackId) };
|
||||
auto bookmark{ TrackBookmark::find(context.dbSession, context.user->getId(), trackId) };
|
||||
if (!bookmark)
|
||||
bookmark = context.dbSession.create<TrackBookmark>(user, track);
|
||||
bookmark = context.dbSession.create<TrackBookmark>(context.user, track);
|
||||
|
||||
bookmark.modify()->setOffset(std::chrono::milliseconds{ position });
|
||||
if (comment)
|
||||
@@ -92,7 +84,7 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
|
||||
auto bookmark{ TrackBookmark::find(context.dbSession, context.userId, trackId) };
|
||||
auto bookmark{ TrackBookmark::find(context.dbSession, context.user->getId(), trackId) };
|
||||
if (!bookmark)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
|
||||
@@ -75,15 +75,11 @@ namespace lms::api::subsonic
|
||||
{
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
for (const ArtistId similarArtistId : similarArtistsId)
|
||||
{
|
||||
const Artist::pointer similarArtist{ Artist::find(context.dbSession, similarArtistId) };
|
||||
if (similarArtist)
|
||||
artistInfoNode.addArrayChild("similarArtist", createArtistNode(context, similarArtist, user, id3));
|
||||
artistInfoNode.addArrayChild("similarArtist", createArtistNode(context, similarArtist, context.user, id3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,12 +101,8 @@ namespace lms::api::subsonic
|
||||
{
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
parameters.setSortMethod(ArtistSortMethod::SortName);
|
||||
switch (user->getSubsonicArtistListMode())
|
||||
switch (context.user->getSubsonicArtistListMode())
|
||||
{
|
||||
case SubsonicArtistListMode::AllArtists:
|
||||
break;
|
||||
@@ -165,12 +157,9 @@ namespace lms::api::subsonic
|
||||
for (const ArtistId artistId : artistIds)
|
||||
{
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
if (const Artist::pointer artist{ Artist::find(context.dbSession, artistId) })
|
||||
indexNode.addArrayChild("artist", createArtistNode(context, artist, user, id3));
|
||||
indexNode.addArrayChild("artist", createArtistNode(context, artist, context.user, id3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,16 +254,12 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& similarSongsNode{ response.createNode(id3 ? Response::Node::Key{ "similarSongs2" } : Response::Node::Key{ "similarSongs" }) };
|
||||
for (const TrackId trackId : tracks)
|
||||
{
|
||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||
similarSongsNode.addArrayChild("song", createSongNode(context, track, user));
|
||||
similarSongsNode.addArrayChild("song", createSongNode(context, track, context.user));
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -319,10 +304,6 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
if (root)
|
||||
{
|
||||
directoryNode.setAttribute("id", idToString(RootId{}));
|
||||
@@ -331,7 +312,7 @@ namespace lms::api::subsonic
|
||||
// TODO: this does not scale when a lot of artists are present
|
||||
Artist::find(context.dbSession, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::SortName), [&](const Artist::pointer& artist)
|
||||
{
|
||||
directoryNode.addArrayChild("child", createArtistNode(context, artist, user, false /* no id3 */));
|
||||
directoryNode.addArrayChild("child", createArtistNode(context, artist, context.user, false /* no id3 */));
|
||||
});
|
||||
}
|
||||
else if (artistId)
|
||||
@@ -346,7 +327,7 @@ namespace lms::api::subsonic
|
||||
|
||||
Release::find(context.dbSession, Release::FindParameters{}.setArtist(*artistId), [&](const Release::pointer& release)
|
||||
{
|
||||
directoryNode.addArrayChild("child", createAlbumNode(context, release, user, false /* no id3 */));
|
||||
directoryNode.addArrayChild("child", createAlbumNode(context, release, context.user, false /* no id3 */));
|
||||
});
|
||||
}
|
||||
else if (releaseId)
|
||||
@@ -361,7 +342,7 @@ namespace lms::api::subsonic
|
||||
|
||||
Track::find(context.dbSession, Track::FindParameters{}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release), [&](const Track::pointer& track)
|
||||
{
|
||||
directoryNode.addArrayChild("child", createSongNode(context, track, user));
|
||||
directoryNode.addArrayChild("child", createSongNode(context, track, context.user));
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -406,16 +387,12 @@ namespace lms::api::subsonic
|
||||
if (!artist)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node artistNode{ createArtistNode(context, artist, user, true /* id3 */) };
|
||||
Response::Node artistNode{ createArtistNode(context, artist, context.user, true /* id3 */) };
|
||||
|
||||
const auto releases{ Release::find(context.dbSession, Release::FindParameters {}.setArtist(artist->getId())) };
|
||||
for (const Release::pointer& release : releases.results)
|
||||
artistNode.addArrayChild("album", createAlbumNode(context, release, user, true /* id3 */));
|
||||
artistNode.addArrayChild("album", createAlbumNode(context, release, context.user, true /* id3 */));
|
||||
|
||||
response.addNode("artist", std::move(artistNode));
|
||||
|
||||
@@ -433,16 +410,12 @@ namespace lms::api::subsonic
|
||||
if (!release)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node albumNode{ createAlbumNode(context, release, user, true /* id3 */) };
|
||||
Response::Node albumNode{ createAlbumNode(context, release, context.user, true /* id3 */) };
|
||||
|
||||
const auto tracks{ Track::find(context.dbSession, Track::FindParameters{}.setRelease(id).setSortMethod(TrackSortMethod::Release)) };
|
||||
for (const Track::pointer& track : tracks.results)
|
||||
albumNode.addArrayChild("song", createSongNode(context, track, user));
|
||||
albumNode.addArrayChild("song", createSongNode(context, track, context.user));
|
||||
|
||||
response.addNode("album", std::move(albumNode));
|
||||
|
||||
@@ -460,12 +433,8 @@ namespace lms::api::subsonic
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
response.addNode("song", createSongNode(context, track, user));
|
||||
response.addNode("song", createSongNode(context, track, context.user));
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -504,15 +473,11 @@ namespace lms::api::subsonic
|
||||
if (artists.size() != 1)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& topSongs{ response.createNode("topSongs") };
|
||||
|
||||
scrobbling::IScrobblingService::FindParameters params;
|
||||
params.setUser(context.userId);
|
||||
params.setUser(context.user->getId());
|
||||
params.setRange(db::Range{ 0, count });
|
||||
params.setArtist(artists.front()->getId());
|
||||
|
||||
@@ -520,7 +485,7 @@ namespace lms::api::subsonic
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
if (Track::pointer track{ Track::find(context.dbSession, trackId) })
|
||||
topSongs.addArrayChild("song", createSongNode(context, track, user));
|
||||
topSongs.addArrayChild("song", createSongNode(context, track, context.user));
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/ReleaseId.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/feedback/IFeedbackService.hpp"
|
||||
#include "services/scrobbling/IScrobblingService.hpp"
|
||||
#include "core/Service.hpp"
|
||||
@@ -61,13 +62,13 @@ namespace lms::api::subsonic
|
||||
StarParameters params{ getStarParameters(context.parameters) };
|
||||
|
||||
for (const ArtistId id : params.artistIds)
|
||||
core::Service<feedback::IFeedbackService>::get()->star(context.userId, id);
|
||||
core::Service<feedback::IFeedbackService>::get()->star(context.user->getId(), id);
|
||||
|
||||
for (const ReleaseId id : params.releaseIds)
|
||||
core::Service<feedback::IFeedbackService>::get()->star(context.userId, id);
|
||||
core::Service<feedback::IFeedbackService>::get()->star(context.user->getId(), id);
|
||||
|
||||
for (const TrackId id : params.trackIds)
|
||||
core::Service<feedback::IFeedbackService>::get()->star(context.userId, id);
|
||||
core::Service<feedback::IFeedbackService>::get()->star(context.user->getId(), id);
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
@@ -77,13 +78,13 @@ namespace lms::api::subsonic
|
||||
StarParameters params{ getStarParameters(context.parameters) };
|
||||
|
||||
for (const ArtistId id : params.artistIds)
|
||||
core::Service<feedback::IFeedbackService>::get()->unstar(context.userId, id);
|
||||
core::Service<feedback::IFeedbackService>::get()->unstar(context.user->getId(), id);
|
||||
|
||||
for (const ReleaseId id : params.releaseIds)
|
||||
core::Service<feedback::IFeedbackService>::get()->unstar(context.userId, id);
|
||||
core::Service<feedback::IFeedbackService>::get()->unstar(context.user->getId(), id);
|
||||
|
||||
for (const TrackId id : params.trackIds)
|
||||
core::Service<feedback::IFeedbackService>::get()->unstar(context.userId, id);
|
||||
core::Service<feedback::IFeedbackService>::get()->unstar(context.user->getId(), id);
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
@@ -104,13 +105,13 @@ namespace lms::api::subsonic
|
||||
|
||||
if (!submission)
|
||||
{
|
||||
core::Service<scrobbling::IScrobblingService>::get()->listenStarted({ context.userId, ids.front() });
|
||||
core::Service<scrobbling::IScrobblingService>::get()->listenStarted({ context.user->getId(), ids.front() });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (times.empty())
|
||||
{
|
||||
core::Service<scrobbling::IScrobblingService>::get()->listenFinished({ context.userId, ids.front() });
|
||||
core::Service<scrobbling::IScrobblingService>::get()->listenFinished({ context.user->getId(), ids.front() });
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -118,7 +119,7 @@ namespace lms::api::subsonic
|
||||
{
|
||||
const TrackId trackId{ ids[i] };
|
||||
const unsigned long time{ times[i] };
|
||||
core::Service<scrobbling::IScrobblingService>::get()->addTimedListen({ {context.userId, trackId}, Wt::WDateTime::fromTime_t(static_cast<std::time_t>(time / 1000)) });
|
||||
core::Service<scrobbling::IScrobblingService>::get()->addTimedListen({ {context.user->getId(), trackId}, Wt::WDateTime::fromTime_t(static_cast<std::time_t>(time / 1000)) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +129,6 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
const User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
const auto track{ Track::find(context.dbSession, id) };
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError{};
|
||||
@@ -147,8 +143,8 @@ namespace lms::api::subsonic
|
||||
std::optional<av::transcoding::OutputFormat> requestedFormat{ subsonicStreamFormatToAvOutputFormat(format) };
|
||||
if (!requestedFormat)
|
||||
{
|
||||
if (user->getSubsonicEnableTranscodingByDefault())
|
||||
requestedFormat = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodingOutputFormat());
|
||||
if (context.user->getSubsonicEnableTranscodingByDefault())
|
||||
requestedFormat = userTranscodeFormatToAvFormat(context.user->getSubsonicDefaultTranscodingOutputFormat());
|
||||
}
|
||||
|
||||
if (!requestedFormat && (maxBitRate == 0 || track->getBitrate() <= maxBitRate ))
|
||||
@@ -172,9 +168,9 @@ namespace lms::api::subsonic
|
||||
}
|
||||
|
||||
if (!requestedFormat)
|
||||
requestedFormat = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodingOutputFormat());
|
||||
requestedFormat = userTranscodeFormatToAvFormat(context.user->getSubsonicDefaultTranscodingOutputFormat());
|
||||
if (!bitrate)
|
||||
bitrate = std::min<std::size_t>(user->getSubsonicDefaultTranscodingOutputBitrate(), maxBitRate);
|
||||
bitrate = std::min<std::size_t>(context.user->getSubsonicDefaultTranscodingOutputBitrate(), maxBitRate);
|
||||
|
||||
av::transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() };
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace lms::api::subsonic
|
||||
Response::Node& playlistsNode{ response.createNode("playlists") };
|
||||
|
||||
TrackList::FindParameters params;
|
||||
params.setUser(context.userId);
|
||||
params.setUser(context.user->getId());
|
||||
params.setType(TrackListType::Playlist);
|
||||
|
||||
auto tracklistIds{ TrackList::find(context.dbSession, params) };
|
||||
@@ -60,10 +60,6 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
TrackList::pointer tracklist{ TrackList::find(context.dbSession, trackListId) };
|
||||
if (!tracklist)
|
||||
throw RequestedDataNotFoundError{};
|
||||
@@ -73,7 +69,7 @@ namespace lms::api::subsonic
|
||||
|
||||
auto entries{ tracklist->getEntries() };
|
||||
for (const TrackListEntry::pointer& entry : entries.results)
|
||||
playlistNode.addArrayChild("entry", createSongNode(context, entry->getTrack(), user));
|
||||
playlistNode.addArrayChild("entry", createSongNode(context, entry->getTrack(), context.user));
|
||||
|
||||
response.addNode("playlist", std::move(playlistNode));
|
||||
|
||||
@@ -93,16 +89,12 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
TrackList::pointer tracklist;
|
||||
if (id)
|
||||
{
|
||||
tracklist = TrackList::find(context.dbSession, *id);
|
||||
if (!tracklist
|
||||
|| tracklist->getUser() != user
|
||||
|| tracklist->getUser() != context.user
|
||||
|| tracklist->getType() != TrackListType::Playlist)
|
||||
{
|
||||
throw RequestedDataNotFoundError{};
|
||||
@@ -113,7 +105,7 @@ namespace lms::api::subsonic
|
||||
}
|
||||
else
|
||||
{
|
||||
tracklist = context.dbSession.create<TrackList>(*name, TrackListType::Playlist, false, user);
|
||||
tracklist = context.dbSession.create<TrackList>(*name, TrackListType::Playlist, false, context.user);
|
||||
}
|
||||
|
||||
for (const TrackId trackId : trackIds)
|
||||
@@ -130,7 +122,7 @@ namespace lms::api::subsonic
|
||||
|
||||
auto entries{ tracklist->getEntries() };
|
||||
for (const TrackListEntry::pointer& entry : entries.results)
|
||||
playlistNode.addArrayChild("entry", createSongNode(context, entry->getTrack(), user));
|
||||
playlistNode.addArrayChild("entry", createSongNode(context, entry->getTrack(), context.user));
|
||||
|
||||
response.addNode("playlist", std::move(playlistNode));
|
||||
|
||||
@@ -151,13 +143,9 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) };
|
||||
if (!tracklist
|
||||
|| tracklist->getUser() != user
|
||||
|| tracklist->getUser() != context.user
|
||||
|| tracklist->getType() != TrackListType::Playlist)
|
||||
{
|
||||
throw RequestedDataNotFoundError{};
|
||||
@@ -200,13 +188,9 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) };
|
||||
if (!tracklist
|
||||
|| tracklist->getUser() != user
|
||||
|| tracklist->getUser() != context.user
|
||||
|| tracklist->getType() != TrackListType::Playlist)
|
||||
{
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
@@ -334,13 +334,9 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
findRequestedArtists(context, id3, keywords, mediaLibrary, user, searchResultNode);
|
||||
findRequestedAlbums(context, id3, keywords, mediaLibrary, user, searchResultNode);
|
||||
findRequestedTracks(context, keywords, mediaLibrary, user, searchResultNode);
|
||||
findRequestedArtists(context, id3, keywords, mediaLibrary, context.user, searchResultNode);
|
||||
findRequestedAlbums(context, id3, keywords, mediaLibrary, context.user, searchResultNode);
|
||||
findRequestedTracks(context, keywords, mediaLibrary, context.user, searchResultNode);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,7 @@ namespace lms::api::subsonic
|
||||
namespace {
|
||||
void checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username)
|
||||
{
|
||||
User::pointer currentUser{ User::find(context.dbSession, context.userId) };
|
||||
if (!currentUser)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
if (currentUser->getLoginName() != username && !currentUser->isAdmin())
|
||||
if (context.user->getLoginName() != username && !context.user->isAdmin())
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
}
|
||||
@@ -44,17 +40,14 @@ namespace lms::api::subsonic
|
||||
|
||||
Response handleGetUsersRequest(RequestContext& context)
|
||||
{
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& usersNode{ response.createNode("users") };
|
||||
|
||||
const auto userIds{ User::find(context.dbSession, User::FindParameters {}) };
|
||||
for (const UserId userId : userIds.results)
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
User::find(context.dbSession, User::FindParameters{}, [&](const User::pointer& user)
|
||||
{
|
||||
const User::pointer user{ User::find(context.dbSession, userId) };
|
||||
usersNode.addArrayChild("user", createUserNode(user));
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -77,7 +70,7 @@ namespace lms::api::subsonic
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
auto removeCreatedUser{ [&]()
|
||||
auto removeCreatedUser{ [&]
|
||||
{
|
||||
auto transaction {context.dbSession.createWriteTransaction()};
|
||||
User::pointer user{ User::find(context.dbSession, userId) };
|
||||
@@ -119,7 +112,7 @@ namespace lms::api::subsonic
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
// cannot delete ourself
|
||||
if (user->getId() == context.userId)
|
||||
if (user->getId() == context.user->getId())
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
user.remove();
|
||||
|
||||
@@ -42,7 +42,6 @@ add_executable(lms
|
||||
ui/explore/ReleasesView.cpp
|
||||
ui/explore/ReleaseView.cpp
|
||||
ui/explore/ReleaseTypes.cpp
|
||||
ui/explore/SearchView.cpp
|
||||
ui/explore/TrackCollector.cpp
|
||||
ui/explore/TrackListHelpers.cpp
|
||||
ui/explore/TrackListView.cpp
|
||||
|
||||
@@ -97,7 +97,6 @@ namespace lms::ui
|
||||
res->use(appRoot + "playqueue");
|
||||
res->use(appRoot + "release");
|
||||
res->use(appRoot + "releases");
|
||||
res->use(appRoot + "search");
|
||||
res->use(appRoot + "settings");
|
||||
res->use(appRoot + "tracklist");
|
||||
res->use(appRoot + "tracklists");
|
||||
@@ -139,7 +138,6 @@ namespace lms::ui
|
||||
{ "/artist", IdxExplore, false, std::nullopt },
|
||||
{ "/releases", IdxExplore, false, Wt::WString::tr("Lms.Explore.releases") },
|
||||
{ "/release", IdxExplore, false, std::nullopt },
|
||||
{ "/search", IdxExplore, false, Wt::WString::tr("Lms.Explore.search") },
|
||||
{ "/tracks", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracks") },
|
||||
{ "/tracklists", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracklists") },
|
||||
{ "/tracklist", IdxExplore, false, std::nullopt },
|
||||
@@ -435,9 +433,6 @@ namespace lms::ui
|
||||
logout->clicked().connect(this, &LmsApplication::logoutUser);
|
||||
}
|
||||
|
||||
Wt::WLineEdit* searchEdit{ navbar->bindNew<Wt::WLineEdit>("search") };
|
||||
searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
||||
|
||||
if (LmsApp->getUserType() == db::UserType::ADMIN)
|
||||
{
|
||||
navbar->setCondition("if-is-admin", true);
|
||||
@@ -462,17 +457,6 @@ namespace lms::ui
|
||||
_playQueue = mainStack->addWidget(std::move(playQueue));
|
||||
mainStack->addNew<SettingsView>();
|
||||
|
||||
searchEdit->enterPressed().connect([this]
|
||||
{
|
||||
setInternalPath("/search", true);
|
||||
});
|
||||
|
||||
searchEdit->textInput().connect([this, explore, searchEdit]
|
||||
{
|
||||
setInternalPath("/search", true);
|
||||
explore->search(searchEdit->text());
|
||||
});
|
||||
|
||||
// Admin stuff
|
||||
if (getUserType() == db::UserType::ADMIN)
|
||||
{
|
||||
|
||||
@@ -87,9 +87,7 @@ namespace lms::ui
|
||||
return trackListModel->getValue(*row);
|
||||
}
|
||||
|
||||
static
|
||||
std::shared_ptr<TrackListModel>
|
||||
createTrackListModel()
|
||||
static std::shared_ptr<TrackListModel> createTrackListModel()
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
@@ -102,12 +100,10 @@ namespace lms::ui
|
||||
params.setUser(LmsApp->getUserId());
|
||||
params.setSortMethod(TrackListSortMethod::Name);
|
||||
|
||||
auto tracklists{ TrackList::find(LmsApp->getDbSession(), params) };
|
||||
for (const TrackListId trackListId : tracklists.results)
|
||||
TrackList::find(LmsApp->getDbSession(), params, [&](const TrackList::pointer& trackList)
|
||||
{
|
||||
const TrackList::pointer trackList{ TrackList::find(LmsApp->getDbSession(), trackListId) };
|
||||
model->add(Wt::WString::fromUTF8(std::string{ trackList->getName() }), trackListId);
|
||||
}
|
||||
model->add(Wt::WString::fromUTF8(std::string{ trackList->getName() }), trackList->getId());
|
||||
});
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
+38
-34
@@ -80,7 +80,18 @@ namespace lms::ui::utils
|
||||
return cover;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WInteractWidget> createCluster(db::ClusterId clusterId, bool canDelete)
|
||||
std::unique_ptr<Wt::WInteractWidget> createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete)
|
||||
{
|
||||
auto res{ std::make_unique<Wt::WText>(Wt::WString{ canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "" } + name, Wt::TextFormat::UnsafeXHTML) };
|
||||
|
||||
res->setStyleClass("Lms-badge-cluster badge me-1 " + std::string{ colorStyleClass }); // HACK
|
||||
res->setToolTip(tooltip, Wt::TextFormat::Plain);
|
||||
res->setInline(true);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WInteractWidget> createFilterCluster(db::ClusterId clusterId, bool canDelete)
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
@@ -88,7 +99,7 @@ namespace lms::ui::utils
|
||||
if (!cluster)
|
||||
return {};
|
||||
|
||||
auto getStyleClass{ [](const db::Cluster::pointer& cluster) -> const char*
|
||||
auto getStyleClass{ [](const db::Cluster::pointer& cluster) -> std::string_view
|
||||
{
|
||||
switch (cluster->getType()->getId().getValue() % 8)
|
||||
{
|
||||
@@ -105,14 +116,33 @@ namespace lms::ui::utils
|
||||
return "bg-primary";
|
||||
} };
|
||||
|
||||
const std::string styleClass{ getStyleClass(cluster) };
|
||||
auto res{ std::make_unique<Wt::WText>(std::string {} + (canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "") + Wt::WString::fromUTF8(std::string{ cluster->getName() }), Wt::TextFormat::UnsafeXHTML) };
|
||||
return createFilter(Wt::WString::fromUTF8(std::string{ cluster->getName() }), Wt::WString::fromUTF8(std::string{ cluster->getType()->getName() }), getStyleClass(cluster), canDelete);
|
||||
}
|
||||
|
||||
res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK
|
||||
res->setToolTip(std::string{ cluster->getType()->getName() }, Wt::TextFormat::Plain);
|
||||
res->setInline(true);
|
||||
std::unique_ptr<Wt::WContainerWidget> createFilterClustersForTrack(db::Track::pointer track, Filters& filters)
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
return res;
|
||||
std::unique_ptr<Wt::WContainerWidget> clusterContainer{ std::make_unique<Wt::WContainerWidget>() };
|
||||
|
||||
// TODO: optimize this
|
||||
const auto clusterTypes{ ClusterType::findIds(LmsApp->getDbSession()).results };
|
||||
const auto clusterGroups{ track->getClusterGroups(clusterTypes, 3) };
|
||||
|
||||
for (const auto& clusters : clusterGroups)
|
||||
{
|
||||
for (const Cluster::pointer& cluster : clusters)
|
||||
{
|
||||
const ClusterId clusterId{ cluster->getId() };
|
||||
Wt::WInteractWidget* entry{ clusterContainer->addWidget(createFilterCluster(clusterId)) };
|
||||
entry->clicked().connect([&filters, clusterId]
|
||||
{
|
||||
filters.add(clusterId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return clusterContainer;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> createArtistAnchorList(const std::vector<db::ArtistId>& artistIds, std::string_view cssAnchorClass)
|
||||
@@ -278,30 +308,4 @@ namespace lms::ui::utils
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> createClustersForTrack(db::Track::pointer track, Filters& filters)
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> clusterContainer{ std::make_unique<Wt::WContainerWidget>() };
|
||||
|
||||
// TODO: optimize this
|
||||
const auto clusterTypes{ ClusterType::findIds(LmsApp->getDbSession()).results };
|
||||
const auto clusterGroups{ track->getClusterGroups(clusterTypes, 3) };
|
||||
|
||||
for (const auto& clusters : clusterGroups)
|
||||
{
|
||||
for (const Cluster::pointer& cluster : clusters)
|
||||
{
|
||||
const ClusterId clusterId{ cluster->getId() };
|
||||
Wt::WInteractWidget* entry{ clusterContainer->addWidget(createCluster(clusterId)) };
|
||||
entry->clicked().connect([&filters, clusterId]
|
||||
{
|
||||
filters.add(clusterId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return clusterContainer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WInteractWidget.h>
|
||||
#include <Wt/WString.h>
|
||||
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/ClusterId.hpp"
|
||||
@@ -56,7 +57,9 @@ namespace lms::ui::utils
|
||||
std::unique_ptr<Wt::WImage> createCover(db::ReleaseId releaseId, CoverResource::Size size);
|
||||
std::unique_ptr<Wt::WImage> createCover(db::TrackId trackId, CoverResource::Size size);
|
||||
|
||||
std::unique_ptr<Wt::WInteractWidget> createCluster(db::ClusterId clusterId, bool canDelete = false);
|
||||
std::unique_ptr<Wt::WInteractWidget> createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete = false);
|
||||
std::unique_ptr<Wt::WInteractWidget> createFilterCluster(db::ClusterId clusterId, bool canDelete = false);
|
||||
std::unique_ptr<Wt::WContainerWidget> createFilterClustersForTrack(db::ObjectPtr<db::Track> track, Filters& filters);
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> createArtistAnchorList(const std::vector<db::ArtistId>& artistIds, std::string_view cssAnchorClass = "link-success");
|
||||
std::unique_ptr<Wt::WContainerWidget> createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector<db::ArtistId>& artistIds, std::string_view cssAnchorClass = "link-success");
|
||||
@@ -67,6 +70,4 @@ namespace lms::ui::utils
|
||||
Wt::WLink createReleaseLink(db::ObjectPtr<db::Release> release);
|
||||
std::unique_ptr<Wt::WAnchor> createReleaseAnchor(db::ObjectPtr<db::Release> release, bool setText = true);
|
||||
std::unique_ptr<Wt::WAnchor> createTrackListAnchor(db::ObjectPtr<db::TrackList> trackList, bool setText = true);
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> createClustersForTrack(db::ObjectPtr<db::Track> track, Filters& filters);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace lms::ui
|
||||
}
|
||||
|
||||
// Don't want the scanner to go on with wrong settings
|
||||
core::Service<scanner::IScannerService>::get()->requestStop();
|
||||
core::Service<scanner::IScannerService>::get()->requestReload();
|
||||
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.MediaLibraries.media-libraries"), Wt::WString::tr("Lms.Admin.MediaLibrary.library-deleted"));
|
||||
|
||||
_libraries->removeWidget(libraryEntry);
|
||||
@@ -148,7 +148,7 @@ namespace lms::ui
|
||||
updateEntry(newMediaLibraryId, entry);
|
||||
|
||||
// Don't want the scanner to go on with wrong settings
|
||||
core::Service<scanner::IScannerService>::get()->requestStop();
|
||||
core::Service<scanner::IScannerService>::get()->requestReload();
|
||||
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.MediaLibraries.media-libraries"), Wt::WString::tr("Lms.settings-saved"));
|
||||
|
||||
LmsApp->getModalManager().dispose(mediaLibraryModalPtr);
|
||||
|
||||
@@ -269,7 +269,7 @@ namespace lms::ui
|
||||
|
||||
core::Service<recommendation::IRecommendationService>::get()->load();
|
||||
// Don't want the scanner to go on with wrong settings
|
||||
core::Service<scanner::IScannerService>::get()->requestStop();
|
||||
core::Service<scanner::IScannerService>::get()->requestReload();
|
||||
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.Database.database"), Wt::WString::tr("Lms.settings-saved"));
|
||||
}
|
||||
|
||||
|
||||
@@ -71,10 +71,10 @@ namespace lms::ui
|
||||
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const User::IdType currentUserId{ LmsApp->getUser() };
|
||||
for (const UserId userId : User::find(LmsApp->getDbSession(), User::FindParameters{}).results)
|
||||
const UserId currentUserId{ LmsApp->getUserId() };
|
||||
User::find(LmsApp->getDbSession(), User::FindParameters{}, [&](const User::pointer& user)
|
||||
{
|
||||
const User::pointer user{ User::find(LmsApp->getDbSession(), userId) };
|
||||
const UserId userId{ user->getId() };
|
||||
|
||||
Wt::WTemplate* entry{ _container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.Users.template.entry")) };
|
||||
|
||||
@@ -89,12 +89,12 @@ namespace lms::ui
|
||||
|
||||
// Don't edit ourself this way
|
||||
if (user->getId() == currentUserId)
|
||||
continue;
|
||||
return;
|
||||
|
||||
entry->setCondition("if-edit", true);
|
||||
Wt::WPushButton* editBtn = entry->bindNew<Wt::WPushButton>("edit-btn", Wt::WString::tr("Lms.template.edit-btn"), Wt::TextFormat::XHTML);
|
||||
editBtn->setToolTip(Wt::WString::tr("Lms.edit"));
|
||||
editBtn->clicked().connect([=]()
|
||||
editBtn->clicked().connect([userId]()
|
||||
{
|
||||
LmsApp->setInternalPath("/admin/user/" + userId.toString(), true);
|
||||
});
|
||||
@@ -131,6 +131,6 @@ namespace lms::ui
|
||||
|
||||
LmsApp->getModalManager().show(std::move(modal));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} // namespace lms::ui
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
|
||||
#include <Wt/WStringListModel.h>
|
||||
|
||||
namespace lms::ui {
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
// Helper class
|
||||
template <typename T>
|
||||
class ValueStringModel : public Wt::WStringListModel
|
||||
@@ -40,8 +40,7 @@ class ValueStringModel : public Wt::WStringListModel
|
||||
return Wt::cpp17::any_cast<Wt::WString>(data(index(static_cast<int>(row), 0), Wt::ItemDataRole::Display));
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
getRowFromString(const Wt::WString& value)
|
||||
std::optional<std::size_t> getRowFromString(const Wt::WString& value)
|
||||
{
|
||||
for (std::size_t i{}; i < static_cast<std::size_t>(rowCount()); ++i)
|
||||
{
|
||||
@@ -52,8 +51,7 @@ class ValueStringModel : public Wt::WStringListModel
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
getRowFromValue(const T& value)
|
||||
std::optional<std::size_t> getRowFromValue(const T& value)
|
||||
{
|
||||
for (std::size_t i{}; i < static_cast<std::size_t>(rowCount()); ++i)
|
||||
{
|
||||
@@ -64,19 +62,16 @@ class ValueStringModel : public Wt::WStringListModel
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
add(const Wt::WString& str, const T& value)
|
||||
void add(const Wt::WString& str, const T& value)
|
||||
{
|
||||
insertRows(rowCount(), 1);
|
||||
setData(rowCount() - 1, 0, value, Wt::ItemDataRole::User);
|
||||
setData(rowCount() - 1, 0, str, Wt::ItemDataRole::Display);
|
||||
}
|
||||
|
||||
void
|
||||
clear()
|
||||
void clear()
|
||||
{
|
||||
removeRows(0, rowCount());
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace lms::ui
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace lms::ui
|
||||
scrobbling::IScrobblingService& scrobblingService{ *core::Service<scrobbling::IScrobblingService>::get() };
|
||||
|
||||
const Range range{ getActualRange(requestedRange) };
|
||||
const Filters& filters{ getFilters() };
|
||||
|
||||
RangeResults<ArtistId> artists;
|
||||
|
||||
@@ -52,7 +53,9 @@ namespace lms::ui
|
||||
{
|
||||
feedback::IFeedbackService::ArtistFindParameters params;
|
||||
params.setUser(LmsApp->getUserId());
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(filters.getClusters());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setMediaLibrary(filters.getMediaLibrary());
|
||||
params.setLinkType(_linkType);
|
||||
params.setSortMethod(ArtistSortMethod::StarredDateDesc);
|
||||
params.setRange(range);
|
||||
@@ -64,7 +67,9 @@ namespace lms::ui
|
||||
{
|
||||
scrobbling::IScrobblingService::ArtistFindParameters params;
|
||||
params.setUser(LmsApp->getUserId());
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(filters.getClusters());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setMediaLibrary(filters.getMediaLibrary());
|
||||
params.setLinkType(_linkType);
|
||||
params.setRange(range);
|
||||
|
||||
@@ -76,7 +81,9 @@ namespace lms::ui
|
||||
{
|
||||
scrobbling::IScrobblingService::ArtistFindParameters params;
|
||||
params.setUser(LmsApp->getUserId());
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(filters.getClusters());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setMediaLibrary(filters.getMediaLibrary());
|
||||
params.setLinkType(_linkType);
|
||||
params.setRange(range);
|
||||
|
||||
@@ -87,7 +94,9 @@ namespace lms::ui
|
||||
case Mode::RecentlyAdded:
|
||||
{
|
||||
Artist::FindParameters params;
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(filters.getClusters());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setMediaLibrary(filters.getMediaLibrary());
|
||||
params.setLinkType(_linkType);
|
||||
params.setSortMethod(ArtistSortMethod::LastWritten);
|
||||
params.setRange(range);
|
||||
@@ -99,25 +108,12 @@ namespace lms::ui
|
||||
break;
|
||||
}
|
||||
|
||||
case Mode::Search:
|
||||
{
|
||||
Artist::FindParameters params;
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setLinkType(_linkType);
|
||||
params.setRange(range);
|
||||
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
artists = Artist::findIds(LmsApp->getDbSession(), params);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Mode::All:
|
||||
{
|
||||
Artist::FindParameters params;
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(filters.getClusters());
|
||||
params.setMediaLibrary(filters.getMediaLibrary());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setLinkType(_linkType);
|
||||
params.setSortMethod(ArtistSortMethod::SortName);
|
||||
params.setRange(range);
|
||||
@@ -143,7 +139,9 @@ namespace lms::ui
|
||||
if (!_randomArtists)
|
||||
{
|
||||
Artist::FindParameters params;
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(getFilters().getClusters());
|
||||
params.setMediaLibrary(getFilters().getMediaLibrary());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setLinkType(_linkType);
|
||||
params.setSortMethod(ArtistSortMethod::Random);
|
||||
params.setRange(Range{ 0, getMaxCount() });
|
||||
|
||||
@@ -18,62 +18,18 @@
|
||||
*/
|
||||
#include "ArtistListHelpers.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "core/EnumSet.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::ui::ArtistListHelpers
|
||||
{
|
||||
std::unique_ptr<Wt::WTemplate>
|
||||
createEntry(const db::ObjectPtr<db::Artist>& artist)
|
||||
std::unique_ptr<Wt::WTemplate> createEntry(const db::ObjectPtr<db::Artist>& artist)
|
||||
{
|
||||
auto res{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry")) };
|
||||
res->bindWidget("name", utils::createArtistAnchor(artist));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<ArtistLinkTypesModel>
|
||||
createArtistLinkTypesModel()
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
std::unique_ptr<ArtistLinkTypesModel> linkTypesModel {std::make_unique<ArtistLinkTypesModel>()};
|
||||
|
||||
core::EnumSet<TrackArtistLinkType> usedLinkTypes;
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createReadTransaction()};
|
||||
usedLinkTypes = TrackArtistLink::findUsedTypes(LmsApp->getDbSession());
|
||||
}
|
||||
|
||||
auto addTypeIfUsed {[&](TrackArtistLinkType linkType, std::string_view stringKey)
|
||||
{
|
||||
if (!usedLinkTypes.contains(linkType))
|
||||
return;
|
||||
|
||||
linkTypesModel->add(Wt::WString::trn(std::string {stringKey}, 2), linkType);
|
||||
}};
|
||||
|
||||
// add default one first (none)
|
||||
linkTypesModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), std::nullopt);
|
||||
|
||||
// TODO: sort by translated strings
|
||||
addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist");
|
||||
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
|
||||
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
|
||||
addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
|
||||
addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
|
||||
addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
|
||||
addTypeIfUsed(TrackArtistLinkType::Performer, "Lms.Explore.Artists.linktype-performer");
|
||||
addTypeIfUsed(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
|
||||
addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
|
||||
|
||||
return linkTypesModel;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ namespace lms::ui
|
||||
namespace ArtistListHelpers
|
||||
{
|
||||
std::unique_ptr<Wt::WTemplate> createEntry(const db::ObjectPtr<db::Artist>& artist);
|
||||
std::unique_ptr<ArtistLinkTypesModel> createArtistLinkTypesModel();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ namespace lms::ui
|
||||
for (const db::Cluster::pointer& cluster : clusters)
|
||||
{
|
||||
const db::ClusterId clusterId = cluster->getId();
|
||||
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createCluster(clusterId)) };
|
||||
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterCluster(clusterId)) };
|
||||
entry->clicked().connect([this, clusterId]
|
||||
{
|
||||
_filters.add(clusterId);
|
||||
@@ -198,7 +198,8 @@ namespace lms::ui
|
||||
_releaseContainers.clear();
|
||||
|
||||
Release::FindParameters params;
|
||||
params.setClusters(_filters.getClusterIds());
|
||||
params.setClusters(_filters.getClusters());
|
||||
params.setMediaLibrary(_filters.getMediaLibrary());
|
||||
params.setArtist(_artistId, { TrackArtistLinkType::ReleaseArtist }, {});
|
||||
params.setSortMethod(ReleaseSortMethod::OriginalDateDesc);
|
||||
|
||||
@@ -257,7 +258,8 @@ namespace lms::ui
|
||||
_appearsOnReleaseContainer = {};
|
||||
|
||||
Release::FindParameters params;
|
||||
params.setClusters(_filters.getClusterIds());
|
||||
params.setClusters(_filters.getClusters());
|
||||
params.setMediaLibrary(_filters.getMediaLibrary());
|
||||
params.setArtist(_artistId, types, { TrackArtistLinkType::ReleaseArtist });
|
||||
params.setSortMethod(ReleaseSortMethod::OriginalDateDesc);
|
||||
|
||||
@@ -343,7 +345,8 @@ namespace lms::ui
|
||||
const Range range{ static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize };
|
||||
|
||||
Track::FindParameters params;
|
||||
params.setClusters(_filters.getClusterIds());
|
||||
params.setClusters(_filters.getClusters());
|
||||
params.setMediaLibrary(_filters.getMediaLibrary());
|
||||
params.setArtist(_artistId);
|
||||
params.setRange(range);
|
||||
params.setSortMethod(TrackSortMethod::Name);
|
||||
|
||||
@@ -21,65 +21,48 @@
|
||||
|
||||
#include <Wt/WPushButton.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "common/InfiniteScrollingContainer.hpp"
|
||||
#include "ArtistListHelpers.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "SortModeSelector.hpp"
|
||||
#include "TrackArtistLinkTypeSelector.hpp"
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
Artists::Artists(Filters& filters)
|
||||
: Wt::WTemplate{ Wt::WString::tr("Lms.Explore.Artists.template") }
|
||||
, _artistCollector{ filters, _defaultMode, _maxCount }
|
||||
: Template{ Wt::WString::tr("Lms.Explore.Artists.template") }
|
||||
, _artistCollector{ filters, _defaultSortMode, _maxCount }
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
|
||||
auto bindMenuItem{ [this](const std::string& var, const Wt::WString& title, ArtistCollector::Mode mode)
|
||||
Wt::WLineEdit* searEdit{ bindNew<Wt::WLineEdit>("search") };
|
||||
searEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
||||
searEdit->textInput().connect([this, searEdit]
|
||||
{
|
||||
auto* menuItem {bindNew<Wt::WPushButton>(var, title)};
|
||||
menuItem->clicked().connect([=, this]
|
||||
{
|
||||
refreshView(mode);
|
||||
_currentActiveItem->removeStyleClass("active");
|
||||
menuItem->addStyleClass("active");
|
||||
_currentActiveItem = menuItem;
|
||||
refreshView(searEdit->text());
|
||||
});
|
||||
|
||||
if (mode == _defaultMode)
|
||||
SortModeSelector* sortModeSelector{ bindNew<SortModeSelector>("sort-mode", _defaultSortMode) };
|
||||
sortModeSelector->itemSelected.connect([this](ArtistCollector::Mode sortMode)
|
||||
{
|
||||
_currentActiveItem = menuItem;
|
||||
_currentActiveItem->addStyleClass("active");
|
||||
}
|
||||
} };
|
||||
refreshView(sortMode);
|
||||
});
|
||||
|
||||
bindMenuItem("random", Wt::WString::tr("Lms.Explore.random"), ArtistCollector::Mode::Random);
|
||||
bindMenuItem("starred", Wt::WString::tr("Lms.Explore.starred"), ArtistCollector::Mode::Starred);
|
||||
bindMenuItem("recently-played", Wt::WString::tr("Lms.Explore.recently-played"), ArtistCollector::Mode::RecentlyPlayed);
|
||||
bindMenuItem("most-played", Wt::WString::tr("Lms.Explore.most-played"), ArtistCollector::Mode::MostPlayed);
|
||||
bindMenuItem("recently-added", Wt::WString::tr("Lms.Explore.recently-added"), ArtistCollector::Mode::RecentlyAdded);
|
||||
bindMenuItem("all", Wt::WString::tr("Lms.Explore.all"), ArtistCollector::Mode::All);
|
||||
|
||||
_linkType = bindNew<Wt::WComboBox>("link-type");
|
||||
_linkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||
_linkType->changed().connect([this]
|
||||
TrackArtistLinkTypeSelector* linkTypeSelector{ bindNew<TrackArtistLinkTypeSelector>("link-type", _defaultLinkType) };
|
||||
linkTypeSelector->itemSelected.connect([this](std::optional<TrackArtistLinkType> linkType)
|
||||
{
|
||||
const std::optional<TrackArtistLinkType> linkType{ static_cast<ArtistLinkTypesModel*>(_linkType->model().get())->getValue(_linkType->currentIndex()) };
|
||||
refreshView(linkType);
|
||||
});
|
||||
|
||||
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const scanner::ScanStats& stats)
|
||||
{
|
||||
if (stats.nbChanges())
|
||||
_linkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||
});
|
||||
|
||||
_container = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
|
||||
_container->onRequestElements.connect([this]
|
||||
{
|
||||
@@ -112,6 +95,12 @@ namespace lms::ui
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void Artists::refreshView(const Wt::WString& searchText)
|
||||
{
|
||||
_artistCollector.setSearch(searchText.toUTF8());
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void Artists::addSome()
|
||||
{
|
||||
const auto artistIds{ _artistCollector.get(Range {static_cast<std::size_t>(_container->getCount()), _batchSize}) };
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "common/Template.hpp"
|
||||
#include "ArtistCollector.hpp"
|
||||
|
||||
namespace lms::ui
|
||||
@@ -33,7 +34,7 @@ namespace lms::ui
|
||||
class Filters;
|
||||
class InfiniteScrollingContainer;
|
||||
|
||||
class Artists : public Wt::WTemplate
|
||||
class Artists : public Template
|
||||
{
|
||||
public:
|
||||
Artists(Filters& filters);
|
||||
@@ -42,16 +43,17 @@ namespace lms::ui
|
||||
void refreshView();
|
||||
void refreshView(ArtistCollector::Mode mode);
|
||||
void refreshView(std::optional<db::TrackArtistLinkType> linkType);
|
||||
void refreshView(const Wt::WString& searchText);
|
||||
void addSome();
|
||||
|
||||
static constexpr std::size_t _batchSize{ 30 };
|
||||
static constexpr std::size_t _maxCount{ 8000 };
|
||||
|
||||
Wt::WWidget* _currentActiveItem {};
|
||||
Wt::WWidget* _currentLinkTypeActiveItem{};
|
||||
InfiniteScrollingContainer* _container{};
|
||||
ArtistCollector _artistCollector;
|
||||
Wt::WComboBox* _linkType {};
|
||||
static constexpr ArtistCollector::Mode _defaultMode {ArtistCollector::Mode::Random};
|
||||
static constexpr ArtistCollector::Mode _defaultSortMode{ ArtistCollector::Mode::Random };
|
||||
static constexpr std::optional<db::TrackArtistLinkType> _defaultLinkType{ std::nullopt };
|
||||
};
|
||||
} // namespace lms::ui
|
||||
|
||||
|
||||
@@ -59,7 +59,10 @@ namespace lms::ui
|
||||
void DatabaseCollectorBase::setSearch(std::string_view searchText)
|
||||
{
|
||||
_searchText = searchText;
|
||||
if (!searchText.empty())
|
||||
_searchKeywords = core::stringUtils::splitString(_searchText, ' ');
|
||||
else
|
||||
_searchKeywords.clear();
|
||||
}
|
||||
|
||||
} // ns UserInterface
|
||||
|
||||
@@ -45,7 +45,6 @@ namespace lms::ui
|
||||
RecentlyPlayed,
|
||||
RecentlyAdded,
|
||||
MostPlayed,
|
||||
Search,
|
||||
All
|
||||
};
|
||||
|
||||
@@ -58,7 +57,7 @@ namespace lms::ui
|
||||
protected:
|
||||
Range getActualRange(std::optional<Range> range) const;
|
||||
std::size_t getMaxCount() const;
|
||||
Filters& getFilters() { return _filters; }
|
||||
const Filters& getFilters() { return _filters; }
|
||||
const std::vector<std::string_view>& getSearchKeywords() const { return _searchKeywords; }
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 <string>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
#include <Wt/WText.h>
|
||||
#include <Wt/WSignal.h>
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
template <typename ItemType>
|
||||
class DropDownMenuSelector : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
// text is the template of the dropdown menu
|
||||
// "selected-item" is the key for the currently selected entry
|
||||
DropDownMenuSelector(const Wt::WString& text, ItemType defaultItem)
|
||||
: Wt::WTemplate{ text }
|
||||
, _defaultItem{ defaultItem }
|
||||
{
|
||||
_selectedItem = bindNew<Wt::WText>("selected-item");
|
||||
}
|
||||
|
||||
void bindItem(const std::string& var, const Wt::WString& title, ItemType item)
|
||||
{
|
||||
auto* menuItem{ bindNew<Wt::WPushButton>(var, title) };
|
||||
menuItem->clicked().connect([this, menuItem, title, item]
|
||||
{
|
||||
_currentActiveItem->removeStyleClass("active");
|
||||
menuItem->addStyleClass("active");
|
||||
_currentActiveItem = menuItem;
|
||||
_selectedItem->setText(title);
|
||||
|
||||
itemSelected.emit(item);
|
||||
});
|
||||
|
||||
if (item == _defaultItem)
|
||||
{
|
||||
_currentActiveItem = menuItem;
|
||||
_currentActiveItem->addStyleClass("active");
|
||||
_selectedItem->setText(title);
|
||||
}
|
||||
}
|
||||
|
||||
Wt::Signal<ItemType> itemSelected;
|
||||
|
||||
private:
|
||||
const ItemType _defaultItem;
|
||||
Wt::WWidget* _currentActiveItem{};
|
||||
Wt::WText* _selectedItem{};
|
||||
};
|
||||
}
|
||||
@@ -27,17 +27,15 @@
|
||||
#include "Filters.hpp"
|
||||
#include "ReleasesView.hpp"
|
||||
#include "ReleaseView.hpp"
|
||||
#include "SearchView.hpp"
|
||||
#include "TrackListView.hpp"
|
||||
#include "TrackListsView.hpp"
|
||||
#include "TracksView.hpp"
|
||||
|
||||
namespace lms::ui {
|
||||
|
||||
namespace {
|
||||
|
||||
void
|
||||
handleContentsPathChange(Wt::WStackedWidget* stack)
|
||||
namespace lms::ui
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void handleContentsPathChange(Wt::WStackedWidget* stack)
|
||||
{
|
||||
enum Idx
|
||||
{
|
||||
@@ -47,7 +45,6 @@ namespace lms::ui {
|
||||
IdxTrackList,
|
||||
IdxReleases,
|
||||
IdxRelease,
|
||||
IdxSearch,
|
||||
IdxTracks,
|
||||
};
|
||||
|
||||
@@ -59,7 +56,6 @@ namespace lms::ui {
|
||||
{ "/tracklist", IdxTrackList },
|
||||
{ "/releases", IdxReleases },
|
||||
{ "/release", IdxRelease },
|
||||
{ "/search", IdxSearch },
|
||||
{ "/tracks", IdxTracks },
|
||||
};
|
||||
|
||||
@@ -104,25 +100,14 @@ namespace lms::ui {
|
||||
auto release = std::make_unique<Release>(filters, _playQueueController);
|
||||
contentsStack->addWidget(std::move(release));
|
||||
|
||||
auto search = std::make_unique<SearchView>(filters, _playQueueController);
|
||||
_search = search.get();
|
||||
contentsStack->addWidget(std::move(search));
|
||||
|
||||
auto tracks = std::make_unique<Tracks>(filters, _playQueueController);
|
||||
contentsStack->addWidget(std::move(tracks));
|
||||
|
||||
wApp->internalPathChanged().connect(this, [=]
|
||||
wApp->internalPathChanged().connect(this, [contentsStack]
|
||||
{
|
||||
handleContentsPathChange(contentsStack);
|
||||
});
|
||||
|
||||
handleContentsPathChange(contentsStack);
|
||||
}
|
||||
|
||||
void
|
||||
Explore::search(const Wt::WString& searchText)
|
||||
{
|
||||
_search->refreshView(searchText);
|
||||
}
|
||||
|
||||
} // namespace lms::ui
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "PlayQueueController.hpp"
|
||||
|
||||
namespace lms::ui
|
||||
@@ -33,7 +34,6 @@ namespace lms::ui
|
||||
public:
|
||||
Explore(Filters& filters, PlayQueue& playQueue);
|
||||
|
||||
void search(const Wt::WString& searchText);
|
||||
PlayQueueController& getPlayQueueController() { return _playQueueController; }
|
||||
|
||||
private:
|
||||
|
||||
+149
-84
@@ -19,21 +19,81 @@
|
||||
|
||||
#include "Filters.hpp"
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include <Wt/WComboBox.h>
|
||||
#include <Wt/WDialog.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/MediaLibrary.hpp"
|
||||
#include "database/Session.hpp"
|
||||
|
||||
#include "common/ValueStringModel.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "ModalManager.hpp"
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
using namespace db;
|
||||
namespace
|
||||
{
|
||||
struct MediaLibraryTag {};
|
||||
|
||||
using TypeVariant = std::variant<db::ClusterTypeId, MediaLibraryTag>;
|
||||
using TypeModel = ValueStringModel<TypeVariant>;
|
||||
|
||||
std::unique_ptr<TypeModel> createTypeModel()
|
||||
{
|
||||
auto typeModel{ std::make_unique<TypeModel>() };
|
||||
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
db::ClusterType::find(LmsApp->getDbSession(), [&](const db::ClusterType::pointer& clusterType)
|
||||
{
|
||||
typeModel->add(Wt::WString::fromUTF8(std::string{ clusterType->getName() }), clusterType->getId());
|
||||
});
|
||||
}
|
||||
|
||||
typeModel->add(Wt::WString::tr("Lms.Explore.media-library"), MediaLibraryTag{});
|
||||
|
||||
return typeModel;
|
||||
}
|
||||
|
||||
using ValueVariant = std::variant<db::ClusterId, db::MediaLibraryId>;
|
||||
using ValueModel = ValueStringModel<ValueVariant>;
|
||||
|
||||
std::unique_ptr<ValueModel> createValueModel(TypeVariant type)
|
||||
{
|
||||
db::Session& session{ LmsApp->getDbSession() };
|
||||
|
||||
auto valueModel{ std::make_unique<ValueModel>() };
|
||||
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
if (std::holds_alternative<MediaLibraryTag>(type))
|
||||
{
|
||||
db::MediaLibrary::find(session, [&](const db::MediaLibrary::pointer& library)
|
||||
{
|
||||
valueModel->add(Wt::WString::fromUTF8(std::string{ library->getName() }), library->getId());
|
||||
});
|
||||
}
|
||||
else if (const db::ClusterTypeId * clusterTypeId{ std::get_if<db::ClusterTypeId>(&type) })
|
||||
{
|
||||
db::Cluster::FindParameters params;
|
||||
params.setClusterType(*clusterTypeId);
|
||||
params.setSortMethod(db::ClusterSortMethod::Name);
|
||||
|
||||
db::Cluster::find(session, params, [&](const db::Cluster::pointer& cluster)
|
||||
{
|
||||
valueModel->add(Wt::WString::fromUTF8(std::string{ cluster->getName() }), cluster->getId());
|
||||
});
|
||||
}
|
||||
|
||||
return valueModel;
|
||||
}
|
||||
}
|
||||
|
||||
void Filters::showDialog()
|
||||
{
|
||||
@@ -43,32 +103,27 @@ namespace lms::ui
|
||||
dialog->addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
|
||||
Wt::WComboBox* typeCombo{ dialog->bindNew<Wt::WComboBox>("type") };
|
||||
const std::shared_ptr<TypeModel> typeModel{ createTypeModel() };
|
||||
typeCombo->setModel(typeModel);
|
||||
|
||||
Wt::WComboBox* valueCombo{ dialog->bindNew<Wt::WComboBox>("value") };
|
||||
|
||||
Wt::WPushButton* addBtn{ dialog->bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.Explore.add-filter")) };
|
||||
addBtn->clicked().connect([this, typeCombo, valueCombo, dialogPtr]
|
||||
addBtn->clicked().connect([this, valueCombo, dialogPtr]
|
||||
{
|
||||
const std::string type{ typeCombo->valueText().toUTF8() };
|
||||
const std::string value{ valueCombo->valueText().toUTF8() };
|
||||
|
||||
// TODO use a model to store the cluster.id() values
|
||||
ClusterId clusterId{};
|
||||
const auto valueModel{ std::static_pointer_cast<ValueModel>(valueCombo->model()) };
|
||||
const ValueVariant value{ valueModel->getValue(valueCombo->currentIndex()) };
|
||||
|
||||
if (const db::MediaLibraryId * mediaLibraryId{ std::get_if<db::MediaLibraryId>(&value) })
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
ClusterType::pointer clusterType{ ClusterType::find(LmsApp->getDbSession(), type) };
|
||||
if (!clusterType)
|
||||
return;
|
||||
|
||||
Cluster::pointer cluster{ clusterType->getCluster(value) };
|
||||
if (!cluster)
|
||||
return;
|
||||
|
||||
clusterId = cluster->getId();
|
||||
set(*mediaLibraryId);
|
||||
}
|
||||
else if (const db::ClusterId * clusterId{ std::get_if<db::ClusterId>(&value) })
|
||||
{
|
||||
add(*clusterId);
|
||||
}
|
||||
|
||||
add(clusterId);
|
||||
// TODO
|
||||
LmsApp->getModalManager().dispose(dialogPtr);
|
||||
});
|
||||
|
||||
@@ -78,79 +133,20 @@ namespace lms::ui
|
||||
LmsApp->getModalManager().dispose(dialogPtr);
|
||||
});
|
||||
|
||||
// Populate data
|
||||
typeCombo->activated().connect([valueCombo, typeModel](int row)
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const auto clusterTypesIds{ ClusterType::findUsed(LmsApp->getDbSession()) };
|
||||
for (const ClusterTypeId clusterTypeId : clusterTypesIds.results)
|
||||
{
|
||||
const auto clusterType{ ClusterType::find(LmsApp->getDbSession(), clusterTypeId) };
|
||||
typeCombo->addItem(Wt::WString::fromUTF8(std::string{ clusterType->getName() }));
|
||||
}
|
||||
|
||||
if (!clusterTypesIds.results.empty())
|
||||
{
|
||||
const auto clusterType{ ClusterType::find(LmsApp->getDbSession(), clusterTypesIds.results.front()) };
|
||||
|
||||
for (const Cluster::pointer& cluster : clusterType->getClusters())
|
||||
{
|
||||
if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end())
|
||||
valueCombo->addItem(Wt::WString::fromUTF8(std::string{ cluster->getName() }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typeCombo->changed().connect([this, typeCombo, valueCombo]
|
||||
{
|
||||
const std::string name{ typeCombo->valueText().toUTF8() };
|
||||
const TypeVariant type{ typeModel->getValue(row) };
|
||||
|
||||
const std::shared_ptr<ValueModel> valueModel{ createValueModel(type) };
|
||||
valueCombo->clear();
|
||||
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
auto clusterType{ ClusterType::find(LmsApp->getDbSession(), name) };
|
||||
for (const Cluster::pointer& cluster : clusterType->getClusters())
|
||||
{
|
||||
if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end())
|
||||
valueCombo->addItem(Wt::WString::fromUTF8(std::string{ cluster->getName() }));
|
||||
}
|
||||
valueCombo->setModel(valueModel);
|
||||
});
|
||||
|
||||
typeCombo->activated().emit(0); // force emit to refresh the type combo model
|
||||
|
||||
LmsApp->getModalManager().show(std::move(dialog));
|
||||
}
|
||||
|
||||
void Filters::add(ClusterId clusterId)
|
||||
{
|
||||
if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), clusterId) != std::cend(_clusterIds))
|
||||
return;
|
||||
|
||||
Wt::WInteractWidget* filter{};
|
||||
|
||||
{
|
||||
auto cluster{ utils::createCluster(clusterId, true) };
|
||||
if (!cluster)
|
||||
return;
|
||||
|
||||
filter = _filters->addWidget(std::move(cluster));
|
||||
}
|
||||
|
||||
_clusterIds.push_back(clusterId);
|
||||
|
||||
filter->clicked().connect([this, filter, clusterId]
|
||||
{
|
||||
_filters->removeWidget(filter);
|
||||
_clusterIds.erase(std::remove_if(std::begin(_clusterIds), std::end(_clusterIds), [clusterId](ClusterId id) { return id == clusterId; }), std::end(_clusterIds));
|
||||
_sigUpdated.emit();
|
||||
});
|
||||
|
||||
LmsApp->notifyMsg(Notification::Type::Info,
|
||||
Wt::WString::tr("Lms.Explore.filters"),
|
||||
Wt::WString::tr("Lms.Explore.filter-added"), std::chrono::seconds{ 2 });
|
||||
|
||||
_sigUpdated.emit();
|
||||
}
|
||||
|
||||
Filters::Filters()
|
||||
: Wt::WTemplate{ Wt::WString::tr("Lms.Explore.template.filters") }
|
||||
{
|
||||
@@ -162,4 +158,73 @@ namespace lms::ui
|
||||
|
||||
_filters = bindNew<Wt::WContainerWidget>("clusters");
|
||||
}
|
||||
|
||||
void Filters::add(db::ClusterId clusterId)
|
||||
{
|
||||
if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), clusterId) != std::cend(_clusterIds))
|
||||
return;
|
||||
|
||||
Wt::WInteractWidget* filter{};
|
||||
|
||||
{
|
||||
auto cluster{ utils::createFilterCluster(clusterId, true) };
|
||||
if (!cluster)
|
||||
return;
|
||||
|
||||
filter = _filters->addWidget(std::move(cluster));
|
||||
}
|
||||
|
||||
_clusterIds.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));
|
||||
_sigUpdated.emit();
|
||||
});
|
||||
|
||||
emitFilterAddedNotification();
|
||||
}
|
||||
|
||||
void Filters::set(db::MediaLibraryId mediaLibraryId)
|
||||
{
|
||||
if (_mediaLibraryFilter)
|
||||
{
|
||||
_filters->removeWidget(_mediaLibraryFilter);
|
||||
_mediaLibraryFilter = nullptr;
|
||||
_mediaLibraryId = db::MediaLibraryId{};
|
||||
}
|
||||
|
||||
std::string libraryName;
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const auto library{ db::MediaLibrary::find(LmsApp->getDbSession(), mediaLibraryId) };
|
||||
if (!library)
|
||||
return;
|
||||
|
||||
libraryName = library->getName();
|
||||
}
|
||||
|
||||
_mediaLibraryId = 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{};
|
||||
_mediaLibraryFilter = nullptr;
|
||||
_sigUpdated.emit();
|
||||
});
|
||||
|
||||
emitFilterAddedNotification();
|
||||
}
|
||||
|
||||
void Filters::emitFilterAddedNotification()
|
||||
{
|
||||
LmsApp->notifyMsg(Notification::Type::Info,
|
||||
Wt::WString::tr("Lms.Explore.filters"),
|
||||
Wt::WString::tr("Lms.Explore.filter-added"), std::chrono::seconds{ 2 });
|
||||
|
||||
_sigUpdated.emit();
|
||||
}
|
||||
} // namespace lms::ui
|
||||
|
||||
@@ -19,12 +19,14 @@
|
||||
|
||||
#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 "Filters.hpp"
|
||||
|
||||
@@ -35,17 +37,24 @@ namespace lms::ui
|
||||
public:
|
||||
Filters();
|
||||
|
||||
const std::vector<db::ClusterId>& getClusterIds() const { return _clusterIds; }
|
||||
std::span<const db::ClusterId> getClusters() const { return _clusterIds; }
|
||||
db::MediaLibraryId getMediaLibrary() const { return _mediaLibraryId; }
|
||||
|
||||
void add(db::ClusterId clusterId);
|
||||
|
||||
Wt::Signal<>& updated() { return _sigUpdated; }
|
||||
|
||||
private:
|
||||
void showDialog();
|
||||
void set(db::MediaLibraryId mediaLibraryId);
|
||||
void emitFilterAddedNotification();
|
||||
|
||||
Wt::WContainerWidget* _filters;
|
||||
Wt::WContainerWidget* _filters{};
|
||||
Wt::Signal<> _sigUpdated;
|
||||
std::vector<db::ClusterId> _clusterIds;
|
||||
|
||||
Wt::WInteractWidget* _mediaLibraryFilter{};
|
||||
db::MediaLibraryId _mediaLibraryId;
|
||||
};
|
||||
} // namespace lms::ui
|
||||
|
||||
|
||||
@@ -28,10 +28,9 @@
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
std::vector<db::TrackId> getArtistsTracks(db::Session& session, const std::vector<db::ArtistId>& artistsId, const std::vector<db::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||
std::vector<db::TrackId> getArtistsTracks(db::Session& session, const std::vector<db::ArtistId>& artistsId, const Filters& filters, std::size_t maxTrackCount)
|
||||
{
|
||||
assert(maxTrackCount);
|
||||
|
||||
@@ -44,7 +43,8 @@ namespace lms::ui
|
||||
db::Track::FindParameters params;
|
||||
params.setArtist(artistId);
|
||||
params.setSortMethod(db::TrackSortMethod::DateDescAndRelease);
|
||||
params.setClusters(clusters);
|
||||
params.setClusters(filters.getClusters());
|
||||
params.setMediaLibrary(filters.getMediaLibrary());
|
||||
params.setRange(db::Range{ 0, maxTrackCount - res.size() });
|
||||
|
||||
const auto tracks{ db::Track::findIds(session, params) };
|
||||
@@ -59,7 +59,7 @@ namespace lms::ui
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<db::TrackId> getReleasesTracks(db::Session& session, const std::vector<db::ReleaseId>& releasesId, const std::vector<db::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||
std::vector<db::TrackId> getReleasesTracks(db::Session& session, const std::vector<db::ReleaseId>& releasesId, const Filters& filters, std::size_t maxTrackCount)
|
||||
{
|
||||
using namespace db;
|
||||
assert(maxTrackCount);
|
||||
@@ -73,7 +73,8 @@ namespace lms::ui
|
||||
db::Track::FindParameters params;
|
||||
params.setRelease(releaseId);
|
||||
params.setSortMethod(db::TrackSortMethod::Release);
|
||||
params.setClusters(clusters);
|
||||
params.setClusters(filters.getClusters());
|
||||
params.setMediaLibrary(filters.getMediaLibrary());
|
||||
params.setRange(db::Range{ 0, maxTrackCount - res.size() });
|
||||
|
||||
const auto tracks{ db::Track::findIds(session, params) };
|
||||
@@ -88,7 +89,7 @@ namespace lms::ui
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<db::TrackId> getDiscTracks(db::Session& session, const std::vector<PlayQueueController::Disc>& discs, const std::vector<db::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||
std::vector<db::TrackId> getDiscTracks(db::Session& session, const std::vector<PlayQueueController::Disc>& discs, const Filters& filters, std::size_t maxTrackCount)
|
||||
{
|
||||
using namespace db;
|
||||
assert(maxTrackCount);
|
||||
@@ -103,7 +104,8 @@ namespace lms::ui
|
||||
params.setRelease(disc.releaseId);
|
||||
params.setSortMethod(db::TrackSortMethod::Release);
|
||||
params.setDiscNumber(disc.discNumber);
|
||||
params.setClusters(clusters);
|
||||
params.setClusters(filters.getClusters());
|
||||
params.setMediaLibrary(filters.getMediaLibrary());
|
||||
params.setRange(db::Range{ 0, maxTrackCount - res.size() });
|
||||
|
||||
const auto tracks{ db::Track::findIds(session, params) };
|
||||
@@ -118,7 +120,7 @@ namespace lms::ui
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<db::TrackId> getTrackListTracks(db::Session& session, db::TrackListId trackListId, const std::vector<db::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||
std::vector<db::TrackId> getTrackListTracks(db::Session& session, db::TrackListId trackListId, const Filters& filters, std::size_t maxTrackCount)
|
||||
{
|
||||
using namespace db;
|
||||
assert(maxTrackCount);
|
||||
@@ -127,7 +129,8 @@ namespace lms::ui
|
||||
|
||||
db::Track::FindParameters params;
|
||||
params.setTrackList(trackListId);
|
||||
params.setClusters(clusters);
|
||||
params.setClusters(filters.getClusters());
|
||||
params.setMediaLibrary(filters.getMediaLibrary());
|
||||
params.setRange(db::Range{ 0, maxTrackCount });
|
||||
params.setSortMethod(TrackSortMethod::TrackList);
|
||||
|
||||
@@ -143,13 +146,13 @@ namespace lms::ui
|
||||
|
||||
void PlayQueueController::processCommand(Command command, const std::vector<db::ArtistId>& artistIds)
|
||||
{
|
||||
const std::vector<db::TrackId> tracks{ getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters.getClusterIds(), _maxTrackCountToEnqueue) };
|
||||
const std::vector<db::TrackId> tracks{ getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters, _maxTrackCountToEnqueue) };
|
||||
processCommand(command, tracks);
|
||||
}
|
||||
|
||||
void PlayQueueController::processCommand(Command command, const std::vector<db::ReleaseId>& releaseIds)
|
||||
{
|
||||
const std::vector<db::TrackId> tracks{ getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters.getClusterIds(), _maxTrackCountToEnqueue) };
|
||||
const std::vector<db::TrackId> tracks{ getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters, _maxTrackCountToEnqueue) };
|
||||
processCommand(command, tracks);
|
||||
}
|
||||
|
||||
@@ -175,13 +178,13 @@ namespace lms::ui
|
||||
|
||||
void PlayQueueController::processCommand(Command command, db::TrackListId trackListId)
|
||||
{
|
||||
const std::vector<db::TrackId> tracks{ getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters.getClusterIds(), _maxTrackCountToEnqueue) };
|
||||
const std::vector<db::TrackId> tracks{ getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters, _maxTrackCountToEnqueue) };
|
||||
processCommand(command, tracks);
|
||||
}
|
||||
|
||||
void PlayQueueController::processCommand(Command command, const std::vector<Disc>& discs)
|
||||
{
|
||||
const std::vector<db::TrackId> tracks{ getDiscTracks(LmsApp->getDbSession(), discs, _filters.getClusterIds(), _maxTrackCountToEnqueue) };
|
||||
const std::vector<db::TrackId> tracks{ getDiscTracks(LmsApp->getDbSession(), discs, _filters, _maxTrackCountToEnqueue) };
|
||||
processCommand(command, tracks);
|
||||
}
|
||||
|
||||
@@ -197,7 +200,7 @@ namespace lms::ui
|
||||
releaseId = track->getRelease()->getId();
|
||||
}
|
||||
|
||||
const std::vector<db::TrackId> tracks{ getReleasesTracks(LmsApp->getDbSession(), {releaseId}, _filters.getClusterIds(), _maxTrackCountToEnqueue) };
|
||||
const std::vector<db::TrackId> tracks{ getReleasesTracks(LmsApp->getDbSession(), {releaseId}, _filters, _maxTrackCountToEnqueue) };
|
||||
auto itTrack{ std::find(std::cbegin(tracks), std::cend(tracks), trackId) };
|
||||
if (itTrack == std::cend(tracks))
|
||||
return;
|
||||
|
||||
@@ -51,7 +51,9 @@ namespace lms::ui
|
||||
{
|
||||
feedback::IFeedbackService::FindParameters params;
|
||||
params.setUser(LmsApp->getUserId());
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(getFilters().getClusters());
|
||||
params.setMediaLibrary(getFilters().getMediaLibrary());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setRange(range);
|
||||
releases = feedbackService.findStarredReleases(params);
|
||||
break;
|
||||
@@ -61,7 +63,9 @@ namespace lms::ui
|
||||
{
|
||||
scrobbling::IScrobblingService::FindParameters params;
|
||||
params.setUser(LmsApp->getUserId());
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(getFilters().getClusters());
|
||||
params.setMediaLibrary(getFilters().getMediaLibrary());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setRange(range);
|
||||
|
||||
releases = scrobblingService.getRecentReleases(params);
|
||||
@@ -72,7 +76,9 @@ namespace lms::ui
|
||||
{
|
||||
scrobbling::IScrobblingService::FindParameters params;
|
||||
params.setUser(LmsApp->getUserId());
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(getFilters().getClusters());
|
||||
params.setMediaLibrary(getFilters().getMediaLibrary());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setRange(range);
|
||||
|
||||
releases = scrobblingService.getTopReleases(params);
|
||||
@@ -82,22 +88,10 @@ namespace lms::ui
|
||||
case Mode::RecentlyAdded:
|
||||
{
|
||||
Release::FindParameters params;
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setSortMethod(ReleaseSortMethod::LastWritten);
|
||||
params.setRange(range);
|
||||
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
releases = Release::findIds(LmsApp->getDbSession(), params);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Mode::Search:
|
||||
{
|
||||
Release::FindParameters params;
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(getFilters().getClusters());
|
||||
params.setMediaLibrary(getFilters().getMediaLibrary());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setSortMethod(ReleaseSortMethod::LastWritten);
|
||||
params.setRange(range);
|
||||
|
||||
{
|
||||
@@ -110,8 +104,10 @@ namespace lms::ui
|
||||
case Mode::All:
|
||||
{
|
||||
Release::FindParameters params;
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(getFilters().getClusters());
|
||||
params.setMediaLibrary(getFilters().getMediaLibrary());
|
||||
params.setSortMethod(ReleaseSortMethod::Name);
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setRange(range);
|
||||
|
||||
{
|
||||
@@ -135,7 +131,9 @@ namespace lms::ui
|
||||
if (!_randomReleases)
|
||||
{
|
||||
Release::FindParameters params;
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setClusters(getFilters().getClusters());
|
||||
params.setMediaLibrary(getFilters().getMediaLibrary());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setSortMethod(ReleaseSortMethod::Random);
|
||||
params.setRange(Range{ 0, getMaxCount() });
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ namespace lms::ui
|
||||
for (const db::Cluster::pointer& cluster : clusters)
|
||||
{
|
||||
const ClusterId clusterId{ cluster->getId() };
|
||||
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createCluster(clusterId)) };
|
||||
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterCluster(clusterId)) };
|
||||
entry->clicked().connect([this, clusterId]
|
||||
{
|
||||
_filters.add(clusterId);
|
||||
@@ -394,7 +394,8 @@ namespace lms::ui
|
||||
db::Track::FindParameters params;
|
||||
params.setRelease(_releaseId);
|
||||
params.setSortMethod(db::TrackSortMethod::Release);
|
||||
params.setClusters(_filters.getClusterIds());
|
||||
params.setClusters(_filters.getClusters());
|
||||
params.setMediaLibrary(_filters.getMediaLibrary());
|
||||
|
||||
db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "ReleasesView.hpp"
|
||||
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
|
||||
#include "database/Release.hpp"
|
||||
@@ -30,6 +31,7 @@
|
||||
#include "explore/PlayQueueController.hpp"
|
||||
#include "explore/ReleaseHelpers.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "SortModeSelector.hpp"
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
@@ -43,30 +45,18 @@ namespace lms::ui
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
|
||||
auto bindMenuItem{ [this](const std::string& var, const Wt::WString& title, ReleaseCollector::Mode mode)
|
||||
Wt::WLineEdit* searEdit{ bindNew<Wt::WLineEdit>("search") };
|
||||
searEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
||||
searEdit->textInput().connect([this, searEdit]
|
||||
{
|
||||
auto* menuItem {bindNew<Wt::WPushButton>(var, title)};
|
||||
menuItem->clicked().connect([this, mode, menuItem]
|
||||
{
|
||||
refreshView(mode);
|
||||
_currentActiveItem->removeStyleClass("active");
|
||||
menuItem->addStyleClass("active");
|
||||
_currentActiveItem = menuItem;
|
||||
refreshView(searEdit->text());
|
||||
});
|
||||
|
||||
if (mode == _defaultMode)
|
||||
SortModeSelector* sortMode{ bindNew<SortModeSelector>("sort-mode", _defaultMode) };
|
||||
sortMode->itemSelected.connect([this](ReleaseCollector::Mode sortMode)
|
||||
{
|
||||
_currentActiveItem = menuItem;
|
||||
_currentActiveItem->addStyleClass("active");
|
||||
}
|
||||
} };
|
||||
|
||||
bindMenuItem("random", Wt::WString::tr("Lms.Explore.random"), ReleaseCollector::Mode::Random);
|
||||
bindMenuItem("starred", Wt::WString::tr("Lms.Explore.starred"), ReleaseCollector::Mode::Starred);
|
||||
bindMenuItem("recently-played", Wt::WString::tr("Lms.Explore.recently-played"), ReleaseCollector::Mode::RecentlyPlayed);
|
||||
bindMenuItem("most-played", Wt::WString::tr("Lms.Explore.most-played"), ReleaseCollector::Mode::MostPlayed);
|
||||
bindMenuItem("recently-added", Wt::WString::tr("Lms.Explore.recently-added"), ReleaseCollector::Mode::RecentlyAdded);
|
||||
bindMenuItem("all", Wt::WString::tr("Lms.Explore.all"), ReleaseCollector::Mode::All);
|
||||
refreshView(sortMode);
|
||||
});
|
||||
|
||||
Wt::WPushButton* playBtn{ bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML) };
|
||||
playBtn->clicked().connect([this]
|
||||
@@ -116,6 +106,12 @@ namespace lms::ui
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void Releases::refreshView(const Wt::WString& searchText)
|
||||
{
|
||||
_releaseCollector.setSearch(searchText.toUTF8());
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void Releases::addSome()
|
||||
{
|
||||
const auto releaseIds{ _releaseCollector.get(Range {static_cast<std::size_t>(_container->getCount()), _batchSize}) };
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace lms::ui
|
||||
private:
|
||||
void refreshView();
|
||||
void refreshView(ReleaseCollector::Mode mode);
|
||||
void refreshView(const Wt::WString& searchText);
|
||||
|
||||
void addSome();
|
||||
std::vector<db::ReleaseId> getAllReleases();
|
||||
@@ -47,7 +48,6 @@ namespace lms::ui
|
||||
static constexpr std::size_t _maxCount{ _maxItemsPerLine * 500 };
|
||||
|
||||
PlayQueueController& _playQueueController;
|
||||
Wt::WWidget* _currentActiveItem {};
|
||||
InfiniteScrollingContainer* _container{};
|
||||
ReleaseCollector _releaseCollector;
|
||||
static constexpr ReleaseCollector::Mode _defaultMode{ ReleaseCollector::Mode::Random };
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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/>.
|
||||
*/
|
||||
|
||||
#include "SearchView.hpp"
|
||||
|
||||
#include <Wt/WPushButton.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
|
||||
#include "common/InfiniteScrollingContainer.hpp"
|
||||
#include "ArtistListHelpers.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "ReleaseHelpers.hpp"
|
||||
#include "TrackListHelpers.hpp"
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
SearchView::SearchView(Filters& filters, PlayQueueController& playQueueController)
|
||||
: Wt::WTemplate{ Wt::WString::tr("Lms.Explore.Search.template") }
|
||||
, _playQueueController{ playQueueController }
|
||||
, _filters{ filters }
|
||||
, _artistCollector{ filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist) }
|
||||
, _releaseCollector{ filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release) }
|
||||
, _trackCollector{ filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track) }
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_stack = bindNew<Wt::WStackedWidget>("results");
|
||||
_stack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
||||
|
||||
// releases
|
||||
_releases = _stack->addNew<InfiniteScrollingContainer>(Wt::WString::tr("Lms.Explore.Releases.template.container"));
|
||||
_releases->onRequestElements.connect([this] { addSomeReleases(); });
|
||||
|
||||
// artists
|
||||
{
|
||||
Wt::WTemplate* artistResults{ _stack->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Search.template.artists")) };
|
||||
|
||||
_artistLinkType = artistResults->bindNew<Wt::WComboBox>("link-type");
|
||||
_artistLinkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||
_artistLinkType->changed().connect([this]
|
||||
{
|
||||
const std::optional<TrackArtistLinkType> linkType{ static_cast<ArtistLinkTypesModel*>(_artistLinkType->model().get())->getValue(_artistLinkType->currentIndex()) };
|
||||
refreshView(linkType);
|
||||
});
|
||||
|
||||
_artists = artistResults->bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
|
||||
_artists->onRequestElements.connect([this] { addSomeArtists(); });
|
||||
}
|
||||
|
||||
// Tracks
|
||||
_tracks = _stack->addNew<InfiniteScrollingContainer>();
|
||||
_tracks->onRequestElements.connect([this] { addSomeTracks(); });
|
||||
|
||||
// Menu
|
||||
auto bindMenuItem{ [this](std::size_t index, const std::string& var, const Wt::WString& title)
|
||||
{
|
||||
Wt::WPushButton* menuItem {bindNew<Wt::WPushButton>(var, title)};
|
||||
menuItem->clicked().connect([this, menuItem, index]
|
||||
{
|
||||
_stack->setCurrentIndex(index);
|
||||
_currentActiveItem->removeStyleClass("active");
|
||||
menuItem->addStyleClass("active");
|
||||
_currentActiveItem = menuItem;
|
||||
});
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
_currentActiveItem = menuItem;
|
||||
_currentActiveItem->addStyleClass("active");
|
||||
}
|
||||
} };
|
||||
|
||||
bindMenuItem(0, "releases", Wt::WString::tr("Lms.Explore.releases"));
|
||||
bindMenuItem(1, "artists", Wt::WString::tr("Lms.Explore.artists"));
|
||||
bindMenuItem(2, "tracks", Wt::WString::tr("Lms.Explore.tracks"));
|
||||
|
||||
filters.updated().connect([this]
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
|
||||
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const scanner::ScanStats& stats)
|
||||
{
|
||||
if (stats.nbChanges())
|
||||
_artistLinkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t SearchView::getBatchSize(Mode mode) const
|
||||
{
|
||||
auto it{ _batchSizes.find(mode) };
|
||||
assert(it != _batchSizes.cend());
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::size_t SearchView::getMaxCount(Mode mode) const
|
||||
{
|
||||
auto it{ _maxCounts.find(mode) };
|
||||
assert(it != _maxCounts.cend());
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void SearchView::refreshView(std::optional<TrackArtistLinkType> linkType)
|
||||
{
|
||||
_artistCollector.setArtistLinkType(linkType);
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void SearchView::refreshView(const Wt::WString& searchText)
|
||||
{
|
||||
_releaseCollector.setSearch(searchText.toUTF8());
|
||||
_artistCollector.setSearch(searchText.toUTF8());
|
||||
_trackCollector.setSearch(searchText.toUTF8());
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void SearchView::refreshView()
|
||||
{
|
||||
_artists->reset();
|
||||
_releases->reset();
|
||||
_tracks->reset();
|
||||
}
|
||||
|
||||
void SearchView::addSomeArtists()
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
const Range range{ _artists->getCount(), getBatchSize(Mode::Artist) };
|
||||
const RangeResults<ArtistId> artistIds{ _artistCollector.get(range) };
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
for (const ArtistId artistId : artistIds.results)
|
||||
{
|
||||
const Artist::pointer artist{ Artist::find(LmsApp->getDbSession(), artistId) };
|
||||
if (artist)
|
||||
_artists->add(ArtistListHelpers::createEntry(artist));
|
||||
}
|
||||
}
|
||||
|
||||
_artists->setHasMore(artistIds.moreResults);
|
||||
}
|
||||
|
||||
void SearchView::addSomeReleases()
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
const Range range{ _releases->getCount(), getBatchSize(Mode::Release) };
|
||||
const RangeResults<ReleaseId> releaseIds{ _releaseCollector.get(range) };
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
for (const ReleaseId releaseId : releaseIds.results)
|
||||
{
|
||||
const Release::pointer release{ Release::find(LmsApp->getDbSession(), releaseId) };
|
||||
if (release)
|
||||
_releases->add(releaseListHelpers::createEntry(release));
|
||||
}
|
||||
}
|
||||
|
||||
_releases->setHasMore(releaseIds.moreResults);
|
||||
}
|
||||
|
||||
void SearchView::addSomeTracks()
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
const Range range{ _tracks->getCount(), getBatchSize(Mode::Track) };
|
||||
const RangeResults<TrackId> trackIds{ _trackCollector.get(range) };
|
||||
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
const Track::pointer track{ Track::find(LmsApp->getDbSession(), trackId) };
|
||||
if (track)
|
||||
_tracks->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
|
||||
}
|
||||
}
|
||||
|
||||
_tracks->setHasMore(trackIds.moreResults);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <Wt/WComboBox.h>
|
||||
#include <Wt/WStackedWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "ArtistCollector.hpp"
|
||||
#include "ReleaseCollector.hpp"
|
||||
#include "TrackCollector.hpp"
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
class InfiniteScrollingContainer;
|
||||
class Filters;
|
||||
class PlayQueueController;
|
||||
|
||||
class SearchView : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
SearchView(Filters& filters, PlayQueueController& playQueueController);
|
||||
|
||||
void refreshView(const Wt::WString& searchText);
|
||||
|
||||
private:
|
||||
// same order as in the menu
|
||||
enum class Mode
|
||||
{
|
||||
Artist,
|
||||
Release,
|
||||
Track,
|
||||
};
|
||||
|
||||
static constexpr Mode _defaultMode{ Mode::Release };
|
||||
static inline std::unordered_map<Mode, std::size_t> _batchSizes
|
||||
{
|
||||
{Mode::Artist, 6},
|
||||
{Mode::Release, 6},
|
||||
{Mode::Track, 6},
|
||||
};
|
||||
static inline std::unordered_map<Mode, std::size_t> _maxCounts
|
||||
{
|
||||
{Mode::Artist, 1000},
|
||||
{Mode::Release, 504},
|
||||
{Mode::Track, 500},
|
||||
};
|
||||
std::size_t getBatchSize(Mode mode) const;
|
||||
std::size_t getMaxCount(Mode mode) const;
|
||||
|
||||
void refreshView();
|
||||
void refreshView(std::optional<db::TrackArtistLinkType> linkType);
|
||||
void addSomeArtists();
|
||||
void addSomeReleases();
|
||||
void addSomeTracks();
|
||||
|
||||
PlayQueueController& _playQueueController;
|
||||
Filters& _filters;
|
||||
ArtistCollector _artistCollector;
|
||||
ReleaseCollector _releaseCollector;
|
||||
TrackCollector _trackCollector;
|
||||
Wt::WStackedWidget* _stack{};
|
||||
Wt::WWidget* _currentActiveItem {};
|
||||
InfiniteScrollingContainer* _artists{};
|
||||
InfiniteScrollingContainer* _releases{};
|
||||
InfiniteScrollingContainer* _tracks{};
|
||||
|
||||
Wt::WComboBox* _artistLinkType{};
|
||||
};
|
||||
|
||||
} // namespace lms::ui
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 "DropDownMenuSelector.hpp"
|
||||
|
||||
#include "DatabaseCollectorBase.hpp"
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
class SortModeSelector : public DropDownMenuSelector<DatabaseCollectorBase::Mode>
|
||||
{
|
||||
public:
|
||||
SortModeSelector(DatabaseCollectorBase::Mode defaultMode)
|
||||
: DropDownMenuSelector<DatabaseCollectorBase::Mode>{ Wt::WString::tr("Lms.Explore.template.sort-mode-selector"), defaultMode }
|
||||
{
|
||||
bindItem("random", Wt::WString::tr("Lms.Explore.random"), DatabaseCollectorBase::Mode::Random);
|
||||
bindItem("starred", Wt::WString::tr("Lms.Explore.starred"), DatabaseCollectorBase::Mode::Starred);
|
||||
bindItem("recently-played", Wt::WString::tr("Lms.Explore.recently-played"), DatabaseCollectorBase::Mode::RecentlyPlayed);
|
||||
bindItem("most-played", Wt::WString::tr("Lms.Explore.most-played"), DatabaseCollectorBase::Mode::MostPlayed);
|
||||
bindItem("recently-added", Wt::WString::tr("Lms.Explore.recently-added"), DatabaseCollectorBase::Mode::RecentlyAdded);
|
||||
bindItem("all", Wt::WString::tr("Lms.Explore.all"), DatabaseCollectorBase::Mode::All);
|
||||
}
|
||||
};
|
||||
} // namespace lms::ui
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user