Merge branch 'develop' for release v3.53.0

This commit is contained in:
emeric
2024-05-05 18:49:47 +02:00
108 changed files with 1474 additions and 1506 deletions
+1 -35
View File
@@ -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)
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+15
View File
@@ -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
View File
@@ -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}">
+1
View File
@@ -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>
+1
View File
@@ -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>
+2
View File
@@ -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>
+1
View File
@@ -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>
+1 -1
View File
@@ -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
View File
@@ -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>
-31
View File
@@ -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
View File
@@ -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
View File
@@ -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>
-10
View File
@@ -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
)
+18
View File
@@ -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 -1
View File
@@ -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})
+3
View File
@@ -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
+23 -4
View File
@@ -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();
+30 -2
View File
@@ -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;
}
}
+15 -1
View File
@@ -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{};
-1
View File
@@ -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();
+75 -39
View File
@@ -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();
+12
View File
@@ -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;
+2 -1
View File
@@ -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
+1
View File
@@ -13,6 +13,7 @@ add_executable(test-database
TrackBookmark.cpp
TrackFeatures.cpp
TrackList.cpp
User.cpp
)
target_link_libraries(test-database PRIVATE
+45 -16
View File
@@ -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());
}
+42 -2
View File
@@ -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());
}
+53
View File
@@ -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 -7
View File
@@ -17,23 +17,33 @@ 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")
endif()
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>
+14
View File
@@ -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() } };
}
-4
View File
@@ -19,10 +19,6 @@
#pragma once
#ifndef LMS_SUPPORT_IMAGE_STB
#error "Bad configuration"
#endif
#include <cstddef>
#include <filesystem>
+1
View File
@@ -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
+16
View File
@@ -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; }
+3 -2
View File
@@ -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 };
+12 -8
View File
@@ -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{};
+15 -50
View File
@@ -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)) };
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,10 +70,10 @@ namespace lms::api::subsonic
userId = user->getId();
}
auto removeCreatedUser{ [&]()
auto removeCreatedUser{ [&]
{
auto transaction {context.dbSession.createWriteTransaction()};
User::pointer user {User::find(context.dbSession, userId)};
User::pointer user{ User::find(context.dbSession, userId) };
if (user)
user.remove();
} };
@@ -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();
-1
View File
@@ -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
-16
View File
@@ -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)
{
+4 -8
View File
@@ -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
View File
@@ -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;
}
}
+4 -3
View File
@@ -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);
}
+2 -2
View File
@@ -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);
+1 -1
View File
@@ -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"));
}
+6 -6
View File
@@ -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
+10 -15
View File
@@ -23,12 +23,12 @@
#include <Wt/WStringListModel.h>
namespace lms::ui {
// Helper class
template <typename T>
class ValueStringModel : public Wt::WStringListModel
namespace lms::ui
{
// Helper class
template <typename T>
class ValueStringModel : public Wt::WStringListModel
{
public:
T getValue(std::size_t row) const
{
@@ -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
+19 -21
View File
@@ -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() });
+2 -46
View File
@@ -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"))};
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;
}
}
-1
View File
@@ -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();
}
}
+7 -4
View File
@@ -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);
+22 -33
View File
@@ -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}) };
+9 -7
View File
@@ -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};
static constexpr std::size_t _batchSize{ 30 };
static constexpr std::size_t _maxCount{ 8000 };
Wt::WWidget* _currentActiveItem {};
InfiniteScrollingContainer* _container {};
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
+1 -2
View File
@@ -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{};
};
}
+11 -26
View File
@@ -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 },
};
@@ -76,13 +72,13 @@ namespace lms::ui {
} // namespace
Explore::Explore(Filters& filters, PlayQueue& playQueue)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.template")}
, _playQueueController {filters, playQueue}
: Wt::WTemplate{ Wt::WString::tr("Lms.Explore.template") }
, _playQueueController{ filters, playQueue }
{
addFunction("tr", &Functions::tr);
// Contents
Wt::WStackedWidget* contentsStack {bindNew<Wt::WStackedWidget>("contents")};
Wt::WStackedWidget* contentsStack{ bindNew<Wt::WStackedWidget>("contents") };
contentsStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
// same order as enum Idx
@@ -92,8 +88,8 @@ namespace lms::ui {
auto artist = std::make_unique<Artist>(filters, _playQueueController);
contentsStack->addWidget(std::move(artist));
auto trackLists {std::make_unique<TrackLists>(filters)};
auto trackList {std::make_unique<TrackList>(filters, _playQueueController)};
auto trackLists{ std::make_unique<TrackLists>(filters) };
auto trackList{ std::make_unique<TrackList>(filters, _playQueueController) };
trackList->trackListDeleted.connect(trackLists.get(), &TrackLists::onTrackListDeleted);
contentsStack->addWidget(std::move(trackLists));
contentsStack->addWidget(std::move(trackList));
@@ -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
+2 -2
View File
@@ -20,6 +20,7 @@
#pragma once
#include <Wt/WTemplate.h>
#include "PlayQueueController.hpp"
namespace lms::ui
@@ -33,12 +34,11 @@ namespace lms::ui
public:
Explore(Filters& filters, PlayQueue& playQueue);
void search(const Wt::WString& searchText);
PlayQueueController& getPlayQueueController() { return _playQueueController; }
private:
PlayQueueController _playQueueController;
SearchView* _search {};
SearchView* _search{};
};
} // namespace lms::ui
+149 -84
View File
@@ -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
+11 -2
View File
@@ -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
+17 -14
View File
@@ -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;
+18 -20
View File
@@ -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() });
+3 -2
View File
@@ -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)
{
+16 -20
View File
@@ -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}) };
+6 -6
View File
@@ -38,19 +38,19 @@ namespace lms::ui
private:
void refreshView();
void refreshView(ReleaseCollector::Mode mode);
void refreshView(const Wt::WString& searchText);
void addSome();
std::vector<db::ReleaseId> getAllReleases();
static constexpr std::size_t _maxItemsPerLine {6};
static constexpr std::size_t _batchSize {_maxItemsPerLine};
static constexpr std::size_t _maxCount {_maxItemsPerLine * 500};
static constexpr std::size_t _maxItemsPerLine{ 6 };
static constexpr std::size_t _batchSize{ _maxItemsPerLine };
static constexpr std::size_t _maxCount{ _maxItemsPerLine * 500 };
PlayQueueController& _playQueueController;
Wt::WWidget* _currentActiveItem {};
InfiniteScrollingContainer* _container {};
InfiniteScrollingContainer* _container{};
ReleaseCollector _releaseCollector;
static constexpr ReleaseCollector::Mode _defaultMode {ReleaseCollector::Mode::Random};
static constexpr ReleaseCollector::Mode _defaultMode{ ReleaseCollector::Mode::Random };
};
} // namespace lms::ui
-207
View File
@@ -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);
}
}
-93
View File
@@ -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
+43
View File
@@ -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