Merge branch 'develop' for release v3.27.0
This commit is contained in:
@@ -0,0 +1 @@
|
||||
build
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
Build:
|
||||
strategy:
|
||||
matrix:
|
||||
BUILD_TYPE: [Release, Debug]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile-build
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
build-args: LMS_BUILD_TYPE=${{ matrix.BUILD_TYPE }}
|
||||
push: false
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
platforms: linux/amd64,linux/arm/v6
|
||||
+7
-6
@@ -6,21 +6,23 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
if (UNIX)
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--no-undefined")
|
||||
endif ()
|
||||
|
||||
include(CTest)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(Filesystem REQUIRED)
|
||||
find_package(FFMPEGAV REQUIRED)
|
||||
find_package(GTest REQUIRED)
|
||||
find_package(Boost REQUIRED COMPONENTS system program_options)
|
||||
find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP)
|
||||
find_package(PAM)
|
||||
find_package(STB)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||
pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
|
||||
pkg_check_modules(GraphicsMagick++ IMPORTED_TARGET GraphicsMagick++)
|
||||
find_package(GTest REQUIRED)
|
||||
pkg_check_modules(LIBAV IMPORTED_TARGET libavutil libavformat)
|
||||
find_package(PAM)
|
||||
find_package(STB)
|
||||
|
||||
# WT
|
||||
if (NOT Wt_FOUND)
|
||||
@@ -67,7 +69,6 @@ elseif (IMAGE_LIBRARY STREQUAL STB AND NOT STB_FOUND)
|
||||
endif ()
|
||||
message(STATUS "IMAGE_LIBRARY set to ${IMAGE_LIBRARY}")
|
||||
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
install(DIRECTORY approot DESTINATION share/lms)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
|
||||
FROM --platform=$BUILDPLATFORM alpine:3.14
|
||||
|
||||
ARG BUILD_PACKAGES="\
|
||||
clang \
|
||||
make \
|
||||
cmake \
|
||||
lld \
|
||||
curl \
|
||||
pkgconfig"
|
||||
|
||||
RUN apk add --no-cache ${BUILD_PACKAGES}
|
||||
|
||||
ARG LMS_BUILD_PACKAGES=" \
|
||||
gcc \
|
||||
g++ \
|
||||
musl-dev \
|
||||
boost-dev \
|
||||
ffmpeg-dev \
|
||||
libconfig-dev \
|
||||
taglib-dev \
|
||||
wt-dev \
|
||||
gtest-dev"
|
||||
|
||||
COPY --from=xx / /
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
RUN xx-apk add --no-scripts --no-cache ${LMS_BUILD_PACKAGES}
|
||||
|
||||
ARG PREFIX="/tmp/install"
|
||||
|
||||
# STB
|
||||
ARG STB_VERSION=b42009b3b9d4ca35bc703f5310eedc74f584be58
|
||||
RUN \
|
||||
DIR=/tmp/stb && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://github.com/nothings/stb/archive/${STB_VERSION}.tar.gz && \
|
||||
tar -x --strip-components=1 -f ${STB_VERSION}.tar.gz && \
|
||||
mkdir -p ${PREFIX}/include/stb && \
|
||||
cp ./*.h ${PREFIX}/include/stb
|
||||
|
||||
# LMS
|
||||
COPY . /tmp/lms/
|
||||
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} && \
|
||||
VERBOSE=1 make -j$(nproc) && \
|
||||
xx-verify src/lms/lms && \
|
||||
xx-info is-cross || make test
|
||||
@@ -1,9 +1,7 @@
|
||||
FROM alpine:3.13 AS build
|
||||
FROM alpine:3.14 AS build
|
||||
|
||||
WORKDIR /tmp/workdir
|
||||
|
||||
ARG MAKEFLAGS="-j2"
|
||||
|
||||
ARG PREFIX="/tmp/install"
|
||||
|
||||
ARG BUILD_PACKAGES=" \
|
||||
@@ -101,20 +99,15 @@ RUN \
|
||||
cp ./*.h ${PREFIX}/include/stb
|
||||
|
||||
# LMS
|
||||
ARG LMS_VERSION=v3.25.2
|
||||
COPY . /tmp/lms/
|
||||
RUN \
|
||||
DIR=/tmp/lms && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sL https://github.com/epoupon/lms/archive/${LMS_VERSION}.tar.gz -o ${LMS_VERSION}.tar.gz && \
|
||||
tar -x --strip-components=1 -f ${LMS_VERSION}.tar.gz
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/lms && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake . -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_PREFIX_PATH=${PREFIX} && \
|
||||
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_PREFIX_PATH=${PREFIX} && \
|
||||
LD_LIBRARY_PATH=${PREFIX}/lib VERBOSE=1 make && \
|
||||
LD_LIBRARY_PATH=${PREFIX}/lib make test && \
|
||||
make install && \
|
||||
mkdir -p ${PREFIX}/etc/ && \
|
||||
cp conf/lms.conf ${PREFIX}/etc
|
||||
cp /tmp/lms/conf/lms.conf ${PREFIX}/etc
|
||||
|
||||
# Now copy all the stuff installed in a new folder (/tmp/fakeroot/)
|
||||
RUN \
|
||||
@@ -134,9 +127,8 @@ RUN \
|
||||
rm -rf /tmp/fakeroot/share/man
|
||||
|
||||
## Release Stage
|
||||
FROM alpine:3.13 AS release
|
||||
FROM alpine:3.14 AS release
|
||||
LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>"
|
||||
LABEL version=${LMS_VERSION}
|
||||
|
||||
ARG RUNTIME_PACKAGES=" \
|
||||
libssl1.1 \
|
||||
@@ -1,6 +1,6 @@
|
||||
# LMS - Lightweight Music Server
|
||||
|
||||
 [](https://travis-ci.org/epoupon/lms) [](https://lgtm.com/projects/g/epoupon/lms/context:cpp)
|
||||
[](https://github.com/epoupon/lms/releases) [](https://github.com/epoupon/lms/actions) [](https://lgtm.com/projects/g/epoupon/lms/context:cpp)
|
||||
|
||||
_LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface!
|
||||
|
||||
@@ -12,13 +12,13 @@ A [demo instance](http://lms.demo.poupon.io) is available. Note the administrati
|
||||
* Audio transcode for maximum interoperability and low bandwith requirements
|
||||
* Multi-value tags: artists, genres, composers, lyricists, moods, ...
|
||||
* [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names
|
||||
* Scrobbling to [ListenBrainz](https://listenbrainz.org)
|
||||
* [ListenBrainz](https://listenbrainz.org) support for scrobbling and synchronizing listens
|
||||
* Compilation support
|
||||
* Disc subtitles support
|
||||
* ReplayGain support
|
||||
* Persistent play queue across sessions
|
||||
* _Systemd_ integration
|
||||
* User management, with several authentication backends
|
||||
* User management, with several authentication backends, see [Deployment](INSTALL.md#deployment)
|
||||
* Subsonic API, with the following additional features:
|
||||
* Playlists
|
||||
* Bookmarks
|
||||
@@ -48,13 +48,13 @@ __Notes on the self-organizing map__:
|
||||
* to enable the audio similarity source, you have to enable it first in the administration panel.
|
||||
|
||||
## Subsonic API
|
||||
The API version implemented is 1.12.0 and has been tested on _Android_ using the official application, _Ultrasonic_ and _DSub_.
|
||||
The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_ and _DSub_.
|
||||
|
||||
Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to navigate through the collection using the directory browsing commands.
|
||||
Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to navigate through the collection when using the directory browsing commands.
|
||||
|
||||
The Subsonic API is enabled by default.
|
||||
|
||||
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method defined from version 1.13.0.
|
||||
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method.
|
||||
|
||||
## About tags
|
||||
_LMS_ relies exclusively on tags to organize your music collection.
|
||||
|
||||
@@ -8,9 +8,16 @@
|
||||
${clusters}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
</div>
|
||||
${<if-has-release>}
|
||||
<div class="row">
|
||||
${releases class="Lms-explore-artist-entries-container"}
|
||||
</div>
|
||||
${</if-has-release>}
|
||||
${<if-has-non-release-track>}
|
||||
<div class="Lms-explore-artist-tracks-container">
|
||||
${tracks}
|
||||
</div>
|
||||
${</if-has-non-release-track>}
|
||||
<div class="row">
|
||||
${<if-has-similar-artists>}
|
||||
<div class="col-md-4">
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<message id="Lms.password-bad-login-combination">Bad login / password combination</message>
|
||||
<message id="Lms.password-client-throttled">Login throttled, please try again later</message>
|
||||
<message id="Lms.password-confirm">Confirm password</message>
|
||||
<message id="Lms.password-must-match-login">Password must match the login name!</message>
|
||||
<message id="Lms.password-new">New password</message>
|
||||
<message id="Lms.password-old">Old password</message>
|
||||
<message id="Lms.password-too-weak">Password too weak</message>
|
||||
@@ -94,7 +95,6 @@
|
||||
<!--User-->
|
||||
<message id="Lms.Admin.User.demo-account">Demo account</message>
|
||||
<message id="Lms.Admin.User.demo-account-already-exists">Demo account already exists!</message>
|
||||
<message id="Lms.Admin.User.demo-password-invalid">Demo password must be the login name!</message>
|
||||
<message id="Lms.Admin.User.last-login">Last login</message>
|
||||
<message id="Lms.Admin.User.user-already-exists">User already exists!</message>
|
||||
<message id="Lms.Admin.User.user-create">New user</message>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<message id="Lms.password-bad-login-combination">Mauvaise combinaison login / mot de passe</message>
|
||||
<message id="Lms.password-client-throttled">Trop de tentatives de connexion, veuillez réessayer plus tard</message>
|
||||
<message id="Lms.password-confirm">Confirmation du mot de passe</message>
|
||||
<message id="Lms.password-must-match-login">Le password doit être égal au login !</message>
|
||||
<message id="Lms.password-new">Nouveau mot de passe</message>
|
||||
<message id="Lms.password-old">Ancien mot de passe</message>
|
||||
<message id="Lms.password-too-weak">Mot de passe trop faible</message>
|
||||
@@ -94,7 +95,6 @@
|
||||
<!--User-->
|
||||
<message id="Lms.Admin.User.demo-account">Compte de démonstration</message>
|
||||
<message id="Lms.Admin.User.demo-account-already-exists">Le compte de démonstration existe déjà !</message>
|
||||
<message id="Lms.Admin.User.demo-password-invalid">Le password doit être égal au login !</message>
|
||||
<message id="Lms.Admin.User.last-login">Date du dernier login</message>
|
||||
<message id="Lms.Admin.User.user-already-exists">L'utilisateur existe déjà !</message>
|
||||
<message id="Lms.Admin.User.user-create">Nouvel utilisateur</message>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<message id="Lms.password-bad-login-combination">Errata combinazione di Login / Password </message>
|
||||
<message id="Lms.password-client-throttled">Superati i tentativi di accesso, riprova più tardi</message>
|
||||
<message id="Lms.password-confirm">Conferma la password</message>
|
||||
<message id="Lms.password-must-match-login">La password deve essere il nome utente!</message>
|
||||
<message id="Lms.password-new">Nuova password</message>
|
||||
<message id="Lms.password-old">Vecchia password</message>
|
||||
<message id="Lms.password-too-weak">La password è troppo debole</message>
|
||||
@@ -93,7 +94,6 @@
|
||||
<!--User-->
|
||||
<message id="Lms.Admin.User.demo-account">Account demo</message>
|
||||
<message id="Lms.Admin.User.demo-account-already-exists">L'account demo è già esistente!</message>
|
||||
<message id="Lms.Admin.User.demo-password-invalid">La password dell'account demo deve essere il nome utente!</message>
|
||||
<message id="Lms.Admin.User.last-login">Ultimo accesso</message>
|
||||
<message id="Lms.Admin.User.user-already-exists">Utente già esistente!</message>
|
||||
<message id="Lms.Admin.User.user-create">Crea utente</message>
|
||||
|
||||
+21
-20
@@ -17,6 +17,7 @@
|
||||
<message id="Lms.password-bad-login-combination">无效的登陆 / 密码组合</message>
|
||||
<message id="Lms.password-client-throttled">登录已被限制,请稍后再试</message>
|
||||
<message id="Lms.password-confirm">确认密码</message>
|
||||
<message id="Lms.password-must-match-login">演示密码必须是登录名!</message>
|
||||
<message id="Lms.password-new">新密码</message>
|
||||
<message id="Lms.password-old">旧密码</message>
|
||||
<message id="Lms.password-too-weak">密码太弱</message>
|
||||
@@ -34,31 +35,32 @@
|
||||
<message id="Lms.Error.user-not-found">未找到此用户</message>
|
||||
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.Database.daily">每日的</message>
|
||||
<message id="Lms.Admin.Database.daily">每日</message>
|
||||
<message id="Lms.Admin.Database.database">音乐收藏</message>
|
||||
<message id="Lms.Admin.Database.hourly">每小时的</message>
|
||||
<message id="Lms.Admin.Database.hourly">每小时</message>
|
||||
<message id="Lms.Admin.Database.immediate-scan">立即扫描!</message>
|
||||
<message id="Lms.Admin.Database.monthly">每月的</message>
|
||||
<message id="Lms.Admin.Database.monthly">每月</message>
|
||||
<message id="Lms.Admin.Database.menu-database"><i class="fa fa-fw fa-database" aria-hidden="true"></i> 音乐收藏</message>
|
||||
<message id="Lms.Admin.Database.never">从未</message>
|
||||
<message id="Lms.Admin.Database.never">从不</message>
|
||||
<message id="Lms.Admin.Database.path">媒体根目录</message>
|
||||
<message id="Lms.Admin.Database.path-help">包含 <code>.lmsignore</code> 文件的目录将会被跳过</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type">推荐引擎</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">基于标签</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type.features">基于音频分析</message>
|
||||
<message id="Lms.Admin.Database.scan-complete">扫描完成: {1} 总文件, {2} 附加文件, {3} 升级文件, {4} 已删除文件, {5} 副本文件, {6} 错误文件</message>
|
||||
<message id="Lms.Admin.Database.scan-complete">扫描完成:总文件 {1},添加文件 {2},更新文件 {3},删除文件 {4},副本文件 {5},错误文件 {6}</message>
|
||||
<message id="Lms.Admin.Database.scan-launched">扫描已完成!</message>
|
||||
<message id="Lms.Admin.Database.scan-options">扫描选项</message>
|
||||
<message id="Lms.Admin.Database.settings-saved">新设置已保存!</message>
|
||||
<message id="Lms.Admin.Database.tags">标签</message>
|
||||
<message id="Lms.Admin.Database.update-period">更新周期</message>
|
||||
<message id="Lms.Admin.Database.update-start-time">更新开始时间</message>
|
||||
<message id="Lms.Admin.Database.weekly">每周的</message>
|
||||
<message id="Lms.Admin.Database.weekly">每周</message>
|
||||
|
||||
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-parse-file">无法解析文件</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件</message>
|
||||
<message id="Lms.Admin.ScannerController.duplicates-header">{1} 备份文件:</message>
|
||||
<message id="Lms.Admin.ScannerController.errors-header">{1} 错误:</message>
|
||||
<message id="Lms.Admin.ScannerController.errors-header">{1} 错误:</message>
|
||||
<message id="Lms.Admin.ScannerController.force-scan-now">立即强制全盘扫描</message>
|
||||
<message id="Lms.Admin.ScannerController.get-report">生成报告</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan">最后扫描</message>
|
||||
@@ -75,7 +77,7 @@
|
||||
<message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">检查文件中... {1}%</message>
|
||||
<message id="Lms.Admin.ScannerController.step-discovering-files">检索文件中: {1} 文件</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 文件 ({3}%)...</message>
|
||||
|
||||
@@ -93,7 +95,6 @@
|
||||
<!--User-->
|
||||
<message id="Lms.Admin.User.demo-account">演示账号</message>
|
||||
<message id="Lms.Admin.User.demo-account-already-exists">演示账号已存在!</message>
|
||||
<message id="Lms.Admin.User.demo-password-invalid">演示密码必须是登录名!</message>
|
||||
<message id="Lms.Admin.User.last-login">最后登录</message>
|
||||
<message id="Lms.Admin.User.user-already-exists">用户已存在!</message>
|
||||
<message id="Lms.Admin.User.user-create">新建用户</message>
|
||||
@@ -119,7 +120,7 @@
|
||||
<message id="Lms.Explore.links">链接</message>
|
||||
<message id="Lms.Explore.most-played">播放最多</message>
|
||||
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message>
|
||||
<message id="Lms.Explore.musicbrainz-release">MusicBrainz 发布</message>
|
||||
<message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message>
|
||||
<message id="Lms.Explore.play-last">播放列表</message>
|
||||
<message id="Lms.Explore.play-shuffled">随机播放</message>
|
||||
<message id="Lms.Explore.random">随机</message>
|
||||
@@ -139,7 +140,7 @@
|
||||
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.Artists.linktype-all">所有歌手</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">追踪歌手</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">音轨歌手</message>
|
||||
<message id="Lms.Explore.Artists.linktype-composer">作曲</message>
|
||||
<message id="Lms.Explore.Artists.linktype-lyricist">作词</message>
|
||||
<message id="Lms.Explore.Artists.linktype-mixer">混响</message>
|
||||
@@ -156,14 +157,14 @@
|
||||
<message id="Lms.Explore.Search.search-placeholder">搜索中...</message>
|
||||
|
||||
<!--Player-->
|
||||
<message id="Lms.Player.transcoding-active">转码有效</message>
|
||||
<message id="Lms.Player.transcoding-active">转码已启用</message>
|
||||
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">清除</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks">{1} 追踪</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks">{1} 音轨</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks-added">
|
||||
<plural case="0">添加 {1} 追踪</plural>
|
||||
<plural case="1">已添加 {1} 追踪</plural>
|
||||
<plural case="0">添加 {1} 音轨</plural>
|
||||
<plural case="1">已添加 {1} 音轨</plural>
|
||||
</message>
|
||||
<message id="Lms.PlayQueue.playqueue">播放队列</message>
|
||||
<message id="Lms.PlayQueue.playqueue-full">播放队列已满!</message>
|
||||
@@ -177,13 +178,13 @@
|
||||
<!--Settings-->
|
||||
<message id="Lms.Settings.appearance">外观</message>
|
||||
<message id="Lms.Settings.audio">音频</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">这些音频设置取决于您的本地浏览器!</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
|
||||
<message id="Lms.Settings.change-password">更改密码</message>
|
||||
<message id="Lms.Settings.dark-mode">夜间模式</message>
|
||||
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
|
||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
|
||||
<message id="Lms.Settings.password-bad">无效密码</message>
|
||||
<message id="Lms.Settings.password-must-fill-old-password">旧密码必须填写</message>
|
||||
<message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message>
|
||||
<message id="Lms.Settings.replaygain-mode">ReplayGain 模式</message>
|
||||
<message id="Lms.Settings.replaygain-mode.none">没有 ReplayGain</message>
|
||||
<message id="Lms.Settings.replaygain-mode.auto">自动模式</message>
|
||||
@@ -194,10 +195,10 @@
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">追踪歌手</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">音轨歌手</message>
|
||||
<message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message>
|
||||
<message id="Lms.Settings.transcode">转码</message>
|
||||
<message id="Lms.Settings.transcode-bitrate">视频比特率</message>
|
||||
<message id="Lms.Settings.transcode-bitrate">转码比特率</message>
|
||||
<message id="Lms.Settings.transcode-enable">开启转码</message>
|
||||
<message id="Lms.Settings.transcode-format">转码格式</message>
|
||||
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
|
||||
@@ -215,5 +216,5 @@
|
||||
<message id="Wt.WMessageBox.Yes">是</message>
|
||||
<message id="Wt.WMessageBox.No">否</message>
|
||||
<message id="Wt.WValidator.Invalid">此字段不能为空</message>
|
||||
<message id="Wt.WDoubleValidator.BadRange">这个值应该在 {1} 和 {2}</message>
|
||||
<message id="Wt.WDoubleValidator.BadRange">这个值应该在 {1} 和 {2} 之间</message>
|
||||
</messages>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Quick and dirty av* includes discoverer
|
||||
|
||||
find_path(AVCODEC_INCLUDE_DIR NAMES libavcodec/avcodec.h PATH_SUFFIXES ffmpeg)
|
||||
find_library(AVCODEC_LIBRARY avcodec)
|
||||
|
||||
find_path(AVFORMAT_INCLUDE_DIR NAMES libavformat/avformat.h PATH_SUFFIXES ffmpeg)
|
||||
find_library(AVFORMAT_LIBRARY avformat)
|
||||
|
||||
find_path(AVUTIL_INCLUDE_DIR NAMES libavutil/avutil.h PATH_SUFFIXES ffmpeg)
|
||||
find_library(AVUTIL_LIBRARY avutil)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(
|
||||
FFMPEGAV
|
||||
FOUND_VAR FFMPEGAV_FOUND
|
||||
REQUIRED_VARS AVUTIL_LIBRARY AVFORMAT_LIBRARY
|
||||
)
|
||||
|
||||
mark_as_advanced(AVFORMAT_LIBRARY)
|
||||
mark_as_advanced(AVUTIL_LIBRARY)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ include(FindPackageHandleStandardArgs)
|
||||
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(
|
||||
STB
|
||||
FOUND_VAR STB_FOUND
|
||||
REQUIRED_VARS STB_INCLUDE_DIR
|
||||
)
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ login-throttler-max-entries = 10000;
|
||||
# API
|
||||
api-subsonic = true;
|
||||
|
||||
# Use this list to make the reported server version to 1.12.0 depending on the client's name
|
||||
# Main usage is to make auto detections for the 'p' (password) parameter work
|
||||
api-subsonic-report-old-server-protocol = ("DSub");
|
||||
|
||||
# Turn on this option to allow the demo account creation/use
|
||||
demo = false;
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
.Lms-explore-entry-list {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.Lms-explore-entry-list-text-container {
|
||||
@@ -229,6 +230,10 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.Lms-explore-artist-tracks-container {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.Lms-explore-mode-container {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
Database::IdType
|
||||
Database::UserId
|
||||
AuthServiceBase::getOrCreateUser(Database::Session& session, std::string_view loginName)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
@@ -33,19 +33,19 @@ namespace Auth
|
||||
Database::User::pointer user {Database::User::getByLoginName(session, loginName)};
|
||||
if (!user)
|
||||
{
|
||||
const Database::User::Type type {Database::User::getCount(session) == 0 ? Database::User::Type::ADMIN : Database::User::Type::REGULAR};
|
||||
const Database::UserType type {Database::User::getCount(session) == 0 ? Database::UserType::ADMIN : Database::UserType::REGULAR};
|
||||
|
||||
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == Database::User::Type::ADMIN);
|
||||
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == Database::UserType::ADMIN);
|
||||
|
||||
user = Database::User::create(session, loginName);
|
||||
user.modify()->setType(type);
|
||||
}
|
||||
|
||||
return user.id();
|
||||
return user->getId();
|
||||
}
|
||||
|
||||
void
|
||||
AuthServiceBase::onUserAuthenticated(Database::Session& session, Database::IdType userId)
|
||||
AuthServiceBase::onUserAuthenticated(Database::Session& session, Database::UserId userId)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Auth
|
||||
class AuthServiceBase
|
||||
{
|
||||
protected:
|
||||
Database::IdType getOrCreateUser(Database::Session& session, std::string_view loginName);
|
||||
void onUserAuthenticated(Database::Session& session, Database::IdType userId);
|
||||
Database::UserId getOrCreateUser(Database::Session& session, std::string_view loginName);
|
||||
void onUserAuthenticated(Database::Session& session, Database::UserId userId);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace Auth
|
||||
}
|
||||
|
||||
std::string
|
||||
AuthTokenService::createAuthToken(Database::Session& session, Database::IdType userId, const Wt::WDateTime& expiry)
|
||||
AuthTokenService::createAuthToken(Database::Session& session, Database::UserId userId, const Wt::WDateTime& expiry)
|
||||
{
|
||||
const std::string secret {Wt::WRandom::generateId(32)};
|
||||
const std::string secretHash {sha1Function.compute(secret, {})};
|
||||
@@ -86,7 +86,7 @@ namespace Auth
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
|
||||
|
||||
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser().id(), authToken->getExpiry()};
|
||||
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser()->getId(), authToken->getExpiry()};
|
||||
authToken.remove();
|
||||
|
||||
return res;
|
||||
@@ -123,7 +123,7 @@ namespace Auth
|
||||
}
|
||||
|
||||
void
|
||||
AuthTokenService::clearAuthTokens(Database::Session& session, Database::IdType userId)
|
||||
AuthTokenService::clearAuthTokens(Database::Session& session, Database::UserId userId)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ namespace Auth
|
||||
|
||||
private:
|
||||
AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
|
||||
std::string createAuthToken(Database::Session& session, Database::IdType userId, const Wt::WDateTime& expiry) override;
|
||||
void clearAuthTokens(Database::Session& session, Database::IdType userId) override;
|
||||
std::string createAuthToken(Database::Session& session, Database::UserId userId, const Wt::WDateTime& expiry) override;
|
||||
void clearAuthTokens(Database::Session& session, Database::UserId userId) override;
|
||||
|
||||
std::shared_mutex _mutex;
|
||||
LoginThrottler _loginThrottler;
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace Auth
|
||||
{
|
||||
_loginThrottler.onGoodClientAttempt(clientAddress);
|
||||
|
||||
const Database::IdType userId {getOrCreateUser(session, loginName)};
|
||||
const Database::UserId userId {getOrCreateUser(session, loginName)};
|
||||
onUserAuthenticated(session, userId);
|
||||
return {CheckResult::State::Granted, userId};
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Auth
|
||||
|
||||
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
|
||||
|
||||
const Database::IdType userId {getOrCreateUser(session, loginName)};
|
||||
const Database::UserId userId {getOrCreateUser(session, loginName)};
|
||||
onUserAuthenticated(session, userId);
|
||||
return {CheckResult::State::Granted, userId};
|
||||
}
|
||||
@@ -57,7 +57,7 @@ namespace Auth
|
||||
|
||||
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
|
||||
|
||||
const Database::IdType userId {getOrCreateUser(session, loginName)};
|
||||
const Database::UserId userId {getOrCreateUser(session, loginName)};
|
||||
onUserAuthenticated(session, userId);
|
||||
return {CheckResult::State::Granted, userId};
|
||||
}
|
||||
|
||||
@@ -80,25 +80,41 @@ namespace Auth
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
InternalPasswordService::isPasswordSecureEnough(std::string_view loginName, std::string_view password) const
|
||||
IPasswordService::PasswordAcceptabilityResult
|
||||
InternalPasswordService::checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const
|
||||
{
|
||||
return _validator.evaluateStrength(std::string {password}, std::string {loginName}, "").isValid();
|
||||
switch (context.userType)
|
||||
{
|
||||
case Database::UserType::ADMIN:
|
||||
case Database::UserType::REGULAR:
|
||||
return _validator.evaluateStrength(std::string {password}, context.loginName, "").isValid() ? PasswordAcceptabilityResult::OK : PasswordAcceptabilityResult::TooWeak;
|
||||
case Database::UserType::DEMO:
|
||||
return password == context.loginName ? PasswordAcceptabilityResult::OK : PasswordAcceptabilityResult::MustMatchLoginName;
|
||||
}
|
||||
|
||||
throw NotImplementedException {};
|
||||
}
|
||||
|
||||
void
|
||||
InternalPasswordService::setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword)
|
||||
InternalPasswordService::setPassword(Database::Session& session, Database::UserId userId, std::string_view newPassword)
|
||||
{
|
||||
const Database::User::PasswordHash passwordHash {hashPassword(newPassword)};
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
const Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
if (!user)
|
||||
throw Exception {"User not found!"};
|
||||
|
||||
if (!isPasswordSecureEnough(user->getLoginName(), newPassword))
|
||||
throw PasswordTooWeakException {};
|
||||
switch (checkPasswordAcceptability(newPassword, PasswordValidationContext {user->getLoginName(), user->getType()}))
|
||||
{
|
||||
case PasswordAcceptabilityResult::OK:
|
||||
break;
|
||||
case PasswordAcceptabilityResult::TooWeak:
|
||||
throw PasswordTooWeakException {};
|
||||
case PasswordAcceptabilityResult::MustMatchLoginName:
|
||||
throw PasswordMustMatchLoginNameException {};
|
||||
}
|
||||
|
||||
user.modify()->setPasswordHash(passwordHash);
|
||||
getAuthTokenService().clearAuthTokens(session, userId);
|
||||
|
||||
@@ -41,8 +41,8 @@ namespace Auth
|
||||
std::string_view password) override;
|
||||
|
||||
bool canSetPasswords() const override;
|
||||
bool isPasswordSecureEnough(std::string_view loginName, std::string_view password) const override;
|
||||
void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) override;
|
||||
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
|
||||
void setPassword(Database::Session& session, Database::UserId userId, std::string_view newPassword) override;
|
||||
|
||||
Database::User::PasswordHash hashPassword(std::string_view password) const;
|
||||
void hashRandomPassword() const;
|
||||
|
||||
@@ -186,14 +186,14 @@ namespace Auth
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
PAMPasswordService::isPasswordSecureEnough(std::string_view, std::string_view) const
|
||||
IPasswordService::PasswordAcceptabilityResult
|
||||
PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const
|
||||
{
|
||||
throw NotImplementedException {};
|
||||
}
|
||||
|
||||
void
|
||||
PAMPasswordService::setPassword(Database::Session&, Database::IdType, std::string_view)
|
||||
PAMPasswordService::setPassword(Database::Session&, Database::UserId, std::string_view)
|
||||
{
|
||||
throw NotImplementedException {};
|
||||
}
|
||||
|
||||
@@ -36,10 +36,9 @@ namespace Auth
|
||||
std::string_view password) override;
|
||||
|
||||
bool canSetPasswords() const override;
|
||||
bool isPasswordSecureEnough(std::string_view loginName,
|
||||
std::string_view password) const override;
|
||||
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
|
||||
void setPassword(Database::Session& session,
|
||||
Database::IdType userId,
|
||||
Database::UserId userId,
|
||||
std::string_view newPassword) override;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace Auth
|
||||
|
||||
struct AuthTokenInfo
|
||||
{
|
||||
Database::IdType userId;
|
||||
Database::UserId userId;
|
||||
Wt::WDateTime expiry;
|
||||
};
|
||||
|
||||
@@ -66,8 +66,8 @@ namespace Auth
|
||||
virtual AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
|
||||
|
||||
// Returns a one time token
|
||||
virtual std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry) = 0;
|
||||
virtual void clearAuthTokens(Database::Session& session, Database::IdType userid) = 0;
|
||||
virtual std::string createAuthToken(Database::Session& session, Database::UserId userid, const Wt::WDateTime& expiry) = 0;
|
||||
virtual void clearAuthTokens(Database::Session& session, Database::UserId userid) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntryCount);
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Auth
|
||||
};
|
||||
|
||||
State state {State::Denied};
|
||||
std::optional<Database::IdType> userId {};
|
||||
std::optional<Database::UserId> userId {};
|
||||
};
|
||||
|
||||
virtual CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) = 0;
|
||||
|
||||
@@ -53,23 +53,24 @@ namespace Auth
|
||||
Throttled,
|
||||
};
|
||||
State state {State::Denied};
|
||||
std::optional<Database::IdType> userId {};
|
||||
std::optional<Database::UserId> userId {};
|
||||
std::optional<Wt::WDateTime> expiry {};
|
||||
};
|
||||
virtual CheckResult checkUserPassword(Database::Session& session,
|
||||
const boost::asio::ip::address& clientAddress,
|
||||
std::string_view loginName,
|
||||
std::string_view password) = 0;
|
||||
virtual CheckResult checkUserPassword(Database::Session& session,
|
||||
const boost::asio::ip::address& clientAddress,
|
||||
std::string_view loginName,
|
||||
std::string_view password) = 0;
|
||||
|
||||
class PasswordTooWeakException : public Auth::Exception
|
||||
virtual bool canSetPasswords() const = 0;
|
||||
|
||||
enum class PasswordAcceptabilityResult
|
||||
{
|
||||
public:
|
||||
PasswordTooWeakException() : Auth::Exception {"Password too weak"} {}
|
||||
OK,
|
||||
TooWeak,
|
||||
MustMatchLoginName,
|
||||
};
|
||||
|
||||
virtual bool canSetPasswords() const = 0;
|
||||
virtual bool isPasswordSecureEnough(std::string_view username, std::string_view password) const = 0;
|
||||
virtual void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) = 0;
|
||||
virtual PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const = 0;
|
||||
virtual void setPassword(Database::Session& session, Database::UserId userId, std::string_view newPassword) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IPasswordService> createPasswordService(std::string_view authPasswordBackend, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService);
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
|
||||
namespace Auth
|
||||
@@ -33,5 +35,35 @@ namespace Auth
|
||||
public:
|
||||
NotImplementedException() : Auth::Exception {"Not implemented"} {}
|
||||
};
|
||||
|
||||
class UserNotFoundException : public Exception
|
||||
{
|
||||
public:
|
||||
UserNotFoundException() : Auth::Exception {"User not found"} {}
|
||||
};
|
||||
|
||||
struct PasswordValidationContext
|
||||
{
|
||||
std::string loginName;
|
||||
Database::UserType userType;
|
||||
};
|
||||
|
||||
class PasswordException : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
class PasswordTooWeakException : public PasswordException
|
||||
{
|
||||
public:
|
||||
PasswordTooWeakException() : PasswordException {"Password too weak"} {}
|
||||
};
|
||||
|
||||
class PasswordMustMatchLoginNameException : public PasswordException
|
||||
{
|
||||
public:
|
||||
PasswordMustMatchLoginNameException() : PasswordException {"Password must match login name"} {}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,7 @@ target_link_libraries(lmsav PUBLIC
|
||||
)
|
||||
|
||||
target_link_libraries(lmsav PRIVATE
|
||||
${AVFORMAT_LIBRARY}
|
||||
${AVUTIL_LIBRARY}
|
||||
PkgConfig::LIBAV
|
||||
)
|
||||
|
||||
install(TARGETS lmsav DESTINATION lib)
|
||||
|
||||
@@ -64,7 +64,6 @@ namespace Av
|
||||
|
||||
std::unique_ptr<IChildProcess> _childProcess;
|
||||
|
||||
bool _finished {};
|
||||
std::string _outputMimeType;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,11 +45,11 @@ namespace
|
||||
bool hasCover {};
|
||||
bool isMultiDisc {};
|
||||
std::filesystem::path trackPath;
|
||||
std::optional<Database::IdType> releaseId;
|
||||
std::optional<Database::ReleaseId> releaseId;
|
||||
};
|
||||
|
||||
std::optional<TrackInfo>
|
||||
getTrackInfo(Database::Session& dbSession, Database::IdType trackId)
|
||||
getTrackInfo(Database::Session& dbSession, Database::TrackId trackId)
|
||||
{
|
||||
std::optional<TrackInfo> res;
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace
|
||||
|
||||
if (const Database::Release::pointer& release {track->getRelease()})
|
||||
{
|
||||
res->releaseId = release.id();
|
||||
res->releaseId = release->getId();
|
||||
if (release->getTotalDisc() > 1)
|
||||
res->isMultiDisc = true;
|
||||
}
|
||||
@@ -75,7 +75,6 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
namespace CoverArt {
|
||||
|
||||
static
|
||||
@@ -101,7 +100,7 @@ Grabber::Grabber(const std::filesystem::path& execPath,
|
||||
: _defaultCoverPath {defaultCoverPath}
|
||||
, _maxCacheSize {maxCacheSize}
|
||||
, _maxFileSize {maxFileSize}
|
||||
, _jpegQuality {clamp<unsigned>(jpegQuality, 1, 100)}
|
||||
, _jpegQuality {Utils::clamp<unsigned>(jpegQuality, 1, 100)}
|
||||
{
|
||||
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
|
||||
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
|
||||
@@ -314,20 +313,17 @@ Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width)
|
||||
Grabber::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width)
|
||||
{
|
||||
return getFromTrack(dbSession, trackId, width, true /* allow release fallback*/);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width, bool allowReleaseFallback)
|
||||
Grabber::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Track, trackId, width};
|
||||
const CacheEntryDesc cacheEntryDesc {trackId, width};
|
||||
|
||||
std::shared_ptr<IEncodedImage> cover {loadFromCache(cacheEntryDesc)};
|
||||
if (cover)
|
||||
@@ -361,9 +357,9 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, Im
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, ImageSize width)
|
||||
Grabber::getFromRelease(Database::Session& session, Database::ReleaseId releaseId, ImageSize width)
|
||||
{
|
||||
const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Release, releaseId, width};
|
||||
const CacheEntryDesc cacheEntryDesc {releaseId, width};
|
||||
|
||||
std::shared_ptr<IEncodedImage> cover {loadFromCache(cacheEntryDesc)};
|
||||
if (cover)
|
||||
@@ -371,7 +367,7 @@ Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId,
|
||||
|
||||
struct ReleaseInfo
|
||||
{
|
||||
Database::IdType firstTrackId;
|
||||
Database::TrackId firstTrackId;
|
||||
std::filesystem::path releaseDirectory;
|
||||
};
|
||||
|
||||
@@ -386,7 +382,7 @@ Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId,
|
||||
if (const auto firstTrack {release->getFirstTrack()})
|
||||
{
|
||||
res = ReleaseInfo {};
|
||||
res->firstTrackId = firstTrack.id();
|
||||
res->firstTrackId = firstTrack->getId();
|
||||
res->releaseDirectory = firstTrack->getPath().parent_path();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <shared_mutex>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "cover/ICoverArtGrabber.hpp"
|
||||
@@ -46,20 +47,12 @@ namespace CoverArt
|
||||
{
|
||||
struct CacheEntryDesc
|
||||
{
|
||||
enum class Type
|
||||
{
|
||||
Track,
|
||||
Release,
|
||||
};
|
||||
|
||||
Type type;
|
||||
Database::IdType id;
|
||||
std::variant<Database::TrackId, Database::ReleaseId> id;
|
||||
std::size_t size;
|
||||
|
||||
bool operator==(const CacheEntryDesc& other) const
|
||||
{
|
||||
return type == other.type
|
||||
&& id == other.id
|
||||
return id == other.id
|
||||
&& size == other.size;
|
||||
}
|
||||
};
|
||||
@@ -75,8 +68,12 @@ namespace std
|
||||
public:
|
||||
size_t operator()(const CoverArt::CacheEntryDesc& e) const
|
||||
{
|
||||
size_t h = std::hash<int>()(static_cast<int>(e.type));
|
||||
h ^= std::hash<Database::IdType>()(e.id) << 1;
|
||||
size_t h {};
|
||||
std::visit([&](auto id)
|
||||
{
|
||||
using IdType = std::decay_t<decltype(id)>;
|
||||
h ^= std::hash<IdType>()(id);
|
||||
}, e.id);
|
||||
h ^= std::hash<std::size_t>()(e.size) << 1;
|
||||
return h;
|
||||
}
|
||||
@@ -101,11 +98,11 @@ namespace CoverArt
|
||||
Grabber& operator=(Grabber&&) = delete;
|
||||
|
||||
private:
|
||||
std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width) override;
|
||||
std::shared_ptr<IEncodedImage> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, ImageSize width) override;
|
||||
std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width) override;
|
||||
std::shared_ptr<IEncodedImage> getFromRelease(Database::Session& dbSession, Database::ReleaseId releaseId, ImageSize width) override;
|
||||
void flushCache() override;
|
||||
|
||||
std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width, bool allowReleaseFallback);
|
||||
std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback);
|
||||
std::unique_ptr<IEncodedImage> getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const;
|
||||
std::unique_ptr<IEncodedImage> getFromCoverFile(const std::filesystem::path& p, ImageSize width) const;
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ namespace CoverArt
|
||||
public:
|
||||
virtual ~IGrabber() = default;
|
||||
|
||||
virtual std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width) = 0;
|
||||
virtual std::shared_ptr<IEncodedImage> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, ImageSize width) = 0;
|
||||
virtual std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width) = 0;
|
||||
virtual std::shared_ptr<IEncodedImage> getFromRelease(Database::Session& dbSession, Database::ReleaseId releaseId, ImageSize width) = 0;
|
||||
|
||||
virtual void flushCache() = 0;
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "utils/Logger.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "Traits.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -37,7 +38,6 @@ Artist::Artist(const std::string& name, const std::optional<UUID>& MBID)
|
||||
_sortName {_name},
|
||||
_MBID {MBID ? MBID->getAsString() : ""}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
@@ -45,7 +45,7 @@ Artist::getByName(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.getDboSession().find<Artist>()
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>()
|
||||
.where("name = ?").bind(std::string {name, 0, _maxNameLength})
|
||||
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first
|
||||
|
||||
@@ -56,14 +56,14 @@ Artist::pointer
|
||||
Artist::getByMBID(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string {mbid.getAsString()});
|
||||
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue();
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
Artist::getById(Session& session, IdType id)
|
||||
Artist::getById(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<Artist>().where("id = ?").bind(id);
|
||||
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
@@ -82,7 +82,7 @@ static
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
const std::vector<std::string_view>& keywords,
|
||||
std::optional<TrackArtistLinkType> linkType)
|
||||
{
|
||||
@@ -125,7 +125,7 @@ createQuery(Session& session,
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const IdType clusterId : clusterIds)
|
||||
for (const ClusterId clusterId : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
@@ -145,7 +145,7 @@ Artist::getAll(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Artist>();
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>();
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ Artist::getAll(Session& session, SortMethod sortMethod)
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<pointer> res = query;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = query;
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ Artist::getAll(Session& session, SortMethod sortMethod, std::optional<Range> ran
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Artist::pointer>(session, "SELECT a FROM Artist a", {}, {}, std::nullopt)};
|
||||
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT a FROM Artist a", {}, {}, std::nullopt)};
|
||||
|
||||
switch (sortMethod)
|
||||
{
|
||||
@@ -191,11 +191,11 @@ Artist::getAll(Session& session, SortMethod sortMethod, std::optional<Range> ran
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
std::vector<Artist::pointer> res (collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
@@ -207,27 +207,27 @@ Artist::getAll(Session& session, SortMethod sortMethod, std::optional<Range> ran
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<ArtistId>
|
||||
Artist::getAllIds(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>("SELECT id FROM artist");
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<ArtistId> res = session.getDboSession().query<ArtistId>("SELECT id FROM artist");
|
||||
return std::vector<ArtistId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Artist::getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size)
|
||||
std::vector<ArtistId>
|
||||
Artist::getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<IdType>(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)};
|
||||
auto query {createQuery<ArtistId>(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)};
|
||||
|
||||
Wt::Dbo::collection<IdType> res = query
|
||||
Wt::Dbo::collection<ArtistId> res = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
return std::vector<ArtistId>(res.begin(), res.end());
|
||||
|
||||
}
|
||||
|
||||
@@ -240,22 +240,22 @@ Artist::getAllOrphans(Session& session)
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<ArtistId>
|
||||
Artist::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>
|
||||
Wt::Dbo::collection<ArtistId> res = session.getDboSession().query<ArtistId>
|
||||
("SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
return std::vector<ArtistId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getByClusters(Session& session, const std::set<IdType>& clusters, SortMethod sortMethod)
|
||||
Artist::getByClusters(Session& session, const std::vector<ClusterId>& clusters, SortMethod sortMethod)
|
||||
{
|
||||
assert(!clusters.empty());
|
||||
|
||||
@@ -266,7 +266,7 @@ Artist::getByClusters(Session& session, const std::set<IdType>& clusters, SortMe
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
const std::vector<std::string_view>& keywords,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
SortMethod sortMethod,
|
||||
@@ -275,7 +275,7 @@ Artist::getByFilter(Session& session,
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Artist::pointer>(session, "SELECT DISTINCT a from artist a", clusters, keywords, linkType)};
|
||||
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT DISTINCT a from artist a", clusters, keywords, linkType)};
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
@@ -288,11 +288,11 @@ Artist::getByFilter(Session& session,
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
std::vector<pointer> res (collection.begin(), collection.end());
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
@@ -308,23 +308,23 @@ Artist::getByFilter(Session& session,
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::set<IdType>& clusters,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Artist::pointer>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
|
||||
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
|
||||
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(*after);
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
std::vector<pointer> res (collection.begin(), collection.end());
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
@@ -340,14 +340,14 @@ Artist::getLastWritten(Session& session,
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getStarred(Session& session,
|
||||
User::pointer user,
|
||||
const std::set<IdType>& clusters,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Artist::pointer>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
|
||||
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
@@ -355,7 +355,7 @@ Artist::getStarred(Session& session,
|
||||
" INNER JOIN user_artist_starred uas ON uas.artist_id = a.id"
|
||||
" INNER JOIN user u ON u.id = uas.user_id WHERE u.id = ?)";
|
||||
|
||||
query.bind(user.id());
|
||||
query.bind(user->getId());
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
@@ -371,12 +371,12 @@ Artist::getStarred(Session& session,
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
|
||||
.groupBy("a.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
std::vector<pointer> res (collection.begin(), collection.end());
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
@@ -389,11 +389,9 @@ Artist::getStarred(Session& session,
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Release>>
|
||||
Artist::getReleases(const std::set<IdType>& clusterIds) const
|
||||
std::vector<Release::pointer>
|
||||
Artist::getReleases(const std::vector<ClusterId>& clusterIds) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
@@ -409,101 +407,113 @@ Artist::getReleases(const std::set<IdType>& clusterIds) const
|
||||
WhereClause clusterClause;
|
||||
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
where.And(WhereClause("a.id = ?")).bind(std::to_string(id()));
|
||||
where.And(WhereClause("a.id = ?")).bind(getId().toString());
|
||||
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size();
|
||||
|
||||
oss << " ORDER BY t.year DESC, r.name COLLATE NOCASE";
|
||||
oss << " ORDER BY t.date DESC, r.name COLLATE NOCASE";
|
||||
|
||||
Wt::Dbo::Query<Release::pointer> query = session()->query<Release::pointer>( oss.str() );
|
||||
auto query {session()->query<Wt::Dbo::ptr<Release>>(oss.str())};
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> res = query;
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<Release>>(res.begin(), res.end());
|
||||
auto res {query.resultList()};
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Artist::getReleaseCount() const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
int res = session()->query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id INNER JOIN track t ON t.release_id = r.id")
|
||||
.where("a.id = ?").bind(self()->id());
|
||||
.where("a.id = ?").bind(getId());
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>>
|
||||
std::vector<Track::pointer>
|
||||
Artist::getTracks(std::optional<TrackArtistLinkType> linkType) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Track>>("SELECT DISTINCT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||
.where("a.id = ?").bind(self()->id())
|
||||
.orderBy("t.year DESC,t.release_id,t.disc_number,t.track_number")};
|
||||
.where("a.id = ?").bind(getId())
|
||||
.orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number")};
|
||||
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> tracks {query.resultList()};
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<Track>>(tracks.begin(), tracks.end());
|
||||
auto tracks {query.resultList()};
|
||||
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>>
|
||||
Artist::getTracksWithRelease(std::optional<TrackArtistLinkType> linkType) const
|
||||
std::vector<Track::pointer>
|
||||
Artist::getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Track>>("SELECT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("a.id = ?").bind(self()->id())
|
||||
.orderBy("t.year,r.name,t.disc_number,t.track_number")};
|
||||
auto query {session()->query<Wt::Dbo::ptr<Track>>("SELECT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||
.where("a.id = ?").bind(getId())
|
||||
.where("t.release_id is NULL")
|
||||
.orderBy("t.name")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)};
|
||||
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> tracks {query.resultList()};
|
||||
std::vector<Track::pointer> res(tracks.begin(), tracks.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<Track>>(tracks.begin(), tracks.end());
|
||||
return res;
|
||||
}
|
||||
|
||||
bool
|
||||
Artist::hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType) const
|
||||
{
|
||||
auto query {session()->query<Wt::Dbo::ptr<Track>>("SELECT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||
.where("a.id = ?").bind(getId())
|
||||
.where("t.release_id is NULL")
|
||||
.orderBy("t.name")};
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>>
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
|
||||
return !query.resultList().empty();
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Artist::getRandomTracks(std::optional<std::size_t> count) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> tracks {session()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||
.where("a.id = ?").bind(self()->id())
|
||||
.where("a.id = ?").bind(getId())
|
||||
.orderBy("RANDOM()")
|
||||
.limit(count ? static_cast<int>(*count) : -1)};
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<Track>>(tracks.begin(), tracks.end());
|
||||
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>>
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
std::ostringstream oss;
|
||||
@@ -537,9 +547,9 @@ Artist::getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, std::opt
|
||||
oss << ")";
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<pointer> query {session()->query<pointer>(oss.str())
|
||||
.bind(self()->id())
|
||||
.bind(self()->id())
|
||||
Wt::Dbo::Query<Wt::Dbo::ptr<Artist>> query {session()->query<Wt::Dbo::ptr<Artist>>(oss.str())
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("a.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
||||
@@ -548,15 +558,13 @@ Artist::getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, std::opt
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
query.bind(type);
|
||||
|
||||
Wt::Dbo::collection<pointer> res = query;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res {query.resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>>
|
||||
std::vector<std::vector<Cluster::pointer>>
|
||||
Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
@@ -564,34 +572,34 @@ Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::si
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT c FROM cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN artist a ON t_a_l.artist_id = a.id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id";
|
||||
|
||||
where.And(WhereClause("a.id = ?")).bind(std::to_string(self()->id()));
|
||||
where.And(WhereClause("a.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(std::to_string(clusterType.id()));
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
|
||||
|
||||
Wt::Dbo::Query<Cluster::pointer> query = session()->query<Cluster::pointer>( oss.str() );
|
||||
Wt::Dbo::Query<Wt::Dbo::ptr<Cluster>> query = session()->query<Wt::Dbo::ptr<Cluster>>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
Wt::Dbo::collection<Cluster::pointer> queryRes = query;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
|
||||
|
||||
std::map<IdType, std::vector<Cluster::pointer>> clusters;
|
||||
for (auto cluster : queryRes)
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
for (const Cluster::pointer& cluster : queryRes)
|
||||
{
|
||||
if (clusters[cluster->getType().id()].size() < size)
|
||||
clusters[cluster->getType().id()].push_back(cluster);
|
||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (auto cluster_list : clusters)
|
||||
res.push_back(cluster_list.second);
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
res.push_back(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -25,21 +25,18 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Traits.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
Cluster::Cluster()
|
||||
{
|
||||
}
|
||||
|
||||
Cluster::Cluster(Wt::Dbo::ptr<ClusterType> type, std::string_view name)
|
||||
: _name(std::string {name, 0, _maxNameLength}),
|
||||
_clusterType {type}
|
||||
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
||||
: _name {std::string {name, 0, _maxNameLength}},
|
||||
_clusterType {getDboPtr(type)}
|
||||
{
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
Cluster::create(Session& session, Wt::Dbo::ptr<ClusterType> type, std::string_view name)
|
||||
Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
@@ -54,8 +51,7 @@ Cluster::getAll(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Cluster::pointer> res {session.getDboSession().find<Cluster>()};
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> res {session.getDboSession().find<Cluster>()};
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
@@ -63,67 +59,61 @@ std::vector<Cluster::pointer>
|
||||
Cluster::getAllOrphans(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
Wt::Dbo::collection<Cluster::pointer> res {session.getDboSession().query<Cluster::pointer>("SELECT DISTINCT c FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)")};
|
||||
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Cluster>>("SELECT DISTINCT c FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)").resultList()};
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
Cluster::getById(Session& session, IdType id)
|
||||
Cluster::getById(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Cluster>().where("id = ?").bind(id);
|
||||
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
void
|
||||
Cluster::addTrack(Wt::Dbo::ptr<Track> track)
|
||||
Cluster::addTrack(ObjectPtr<Track> track)
|
||||
{
|
||||
_tracks.insert(track);
|
||||
_tracks.insert(getDboPtr(track));
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>>
|
||||
std::vector<Track::pointer>
|
||||
Cluster::getTracks(std::optional<std::size_t> offset, std::optional<std::size_t> limit) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> res
|
||||
{session()->query<Track::pointer>("SELECT t FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("c.id = ?").bind(self()->id())
|
||||
auto res {session()->query<Wt::Dbo::ptr<Track>>("SELECT t FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("c.id = ?").bind(getId())
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(limit ? static_cast<int>(*limit) : -1)};
|
||||
.limit(limit ? static_cast<int>(*limit) : -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<Track>>(res.begin(), res.end());
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::set<IdType>
|
||||
std::vector<TrackId>
|
||||
Cluster::getTrackIds() const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session()->query<IdType>("SELECT t_c.track_id FROM track_cluster t_c INNER JOIN cluster c ON c.id = t_c.cluster_id")
|
||||
.where("c.id = ?").bind(self()->id());
|
||||
|
||||
return std::set<IdType>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<TrackId> res = session()->query<TrackId>("SELECT t_c.track_id FROM track_cluster t_c INNER JOIN cluster c ON c.id = t_c.cluster_id")
|
||||
.where("c.id = ?").bind(getId());
|
||||
|
||||
return std::vector<TrackId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Cluster::getReleasesCount() const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
return session()->query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("c.id = ?").bind(self()->id());
|
||||
|
||||
.where("c.id = ?").bind(getId());
|
||||
}
|
||||
|
||||
|
||||
ClusterType::ClusterType(std::string name)
|
||||
: _name(name)
|
||||
ClusterType::ClusterType(std::string_view name)
|
||||
: _name {name}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -132,7 +122,7 @@ ClusterType::getAllOrphans(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
|
||||
"SELECT c_t from cluster_type c_t"
|
||||
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
|
||||
.where("c.id IS NULL");
|
||||
@@ -145,7 +135,7 @@ ClusterType::getAllUsed(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
|
||||
"SELECT DISTINCT c_t from cluster_type c_t")
|
||||
.join("cluster c ON c_t.id = c.cluster_type_id");
|
||||
|
||||
@@ -157,15 +147,15 @@ ClusterType::getByName(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<ClusterType>().where("name = ?").bind(name);
|
||||
return session.getDboSession().find<ClusterType>().where("name = ?").bind(name).resultValue();
|
||||
}
|
||||
|
||||
ClusterType::pointer
|
||||
ClusterType::getById(Session& session, IdType id)
|
||||
ClusterType::getById(Session& session, ClusterTypeId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<ClusterType>().where("id= ?").bind(id);
|
||||
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
std::vector<ClusterType::pointer>
|
||||
@@ -173,8 +163,7 @@ ClusterType::getAll(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<ClusterType>();
|
||||
|
||||
auto res {session.getDboSession().find<ClusterType>().resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
@@ -193,24 +182,23 @@ Cluster::pointer
|
||||
ClusterType::getCluster(const std::string& name) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
return session()->find<Cluster>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("cluster_type_id = ?").bind(self()->id());
|
||||
.where("cluster_type_id = ?").bind(getId()).resultValue();
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
ClusterType::getClusters() const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<Cluster::pointer> res = session()->find<Cluster>()
|
||||
.where("cluster_type_id = ?").bind(self()->id())
|
||||
.orderBy("name");
|
||||
auto res = session()->find<Cluster>()
|
||||
.where("cluster_type_id = ?").bind(getId())
|
||||
.orderBy("name")
|
||||
.resultList();
|
||||
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
+114
-119
@@ -28,6 +28,7 @@
|
||||
#include "database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Traits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
@@ -38,7 +39,7 @@ static
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
const std::vector<std::string_view>& keywords)
|
||||
{
|
||||
|
||||
@@ -57,7 +58,7 @@ createQuery(Session& session,
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const IdType clusterId : clusterIds)
|
||||
for (const ClusterId clusterId : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
@@ -76,7 +77,6 @@ Release::Release(const std::string& name, const std::optional<UUID>& MBID)
|
||||
: _name {std::string(name, 0 , _maxNameLength)},
|
||||
_MBID {MBID ? MBID->getAsString() : ""}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
@@ -84,7 +84,11 @@ Release::getByName(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> res = session.getDboSession().find<Release>().where("name = ?").bind( std::string(name, 0, _maxNameLength) );
|
||||
auto res {session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("name = ?").bind( std::string(name, 0, _maxNameLength) )
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
@@ -93,15 +97,21 @@ Release::getByMBID(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Release>().where("mbid = ?").bind(std::string {mbid.getAsString()});
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("mbid = ?").bind(std::string {mbid.getAsString()})
|
||||
.resultValue();;
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
Release::getById(Session& session, IdType id)
|
||||
Release::getById(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Release>().where("id = ?").bind(id);
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
@@ -120,8 +130,7 @@ Release::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> releases {session.getDboSession().find<Release>()};
|
||||
return releases.size();
|
||||
return session.getDboSession().find<Release>().resultList().size();
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
@@ -129,21 +138,22 @@ Release::getAll(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Release>()
|
||||
auto res {session.getDboSession().find<Release>()
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
||||
.orderBy("name COLLATE NOCASE");
|
||||
.orderBy("name COLLATE NOCASE")
|
||||
.resultList()};
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<ReleaseId>
|
||||
Release::getAllIds(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>("SELECT id FROM release");
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<ReleaseId> res = session.getDboSession().query<ReleaseId>("SELECT id FROM release");
|
||||
return std::vector<ReleaseId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
@@ -151,44 +161,45 @@ Release::getAllOrderedByArtist(Session& session, std::optional<std::size_t> offs
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().query<Wt::Dbo::ptr<Release>>(
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Release>>(
|
||||
"SELECT DISTINCT r FROM release r"
|
||||
" INNER JOIN track t ON r.id = t.release_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
||||
" INNER JOIN artist a ON t_a_l.artist_id = a.id")
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE");
|
||||
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")
|
||||
.resultList()};
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAllRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> size)
|
||||
Release::getAllRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Release::pointer>(session, "SELECT DISTINCT r from release r", clusterIds,{})};
|
||||
|
||||
Wt::Dbo::collection<pointer> res = query
|
||||
auto query {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT DISTINCT r from release r", clusterIds, {})};
|
||||
auto res {query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Release::getAllIdsRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> size)
|
||||
std::vector<ReleaseId>
|
||||
Release::getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<IdType>(session, "SELECT DISTINCT r.id from release r", clusterIds,{})};
|
||||
auto query {createQuery<ReleaseId>(session, "SELECT DISTINCT r.id from release r", clusterIds, {})};
|
||||
|
||||
Wt::Dbo::collection<IdType> res = query
|
||||
Wt::Dbo::collection<ReleaseId> res = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
return std::vector<ReleaseId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
@@ -197,31 +208,31 @@ Release::getAllOrphans(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> res = session.getDboSession().query<Wt::Dbo::ptr<Release>>("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL");
|
||||
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Release>>("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL").resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Release::pointer>(session, "SELECT r from release r", clusterIds, {})};
|
||||
auto query {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT r from release r", clusterIds, {})};
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(after);
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> collection = query
|
||||
auto collection {query
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.groupBy("r.id")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1);
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1)
|
||||
.resultList()};
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
@@ -236,13 +247,14 @@ Release::getLastWritten(Session& session,
|
||||
std::vector<Release::pointer>
|
||||
Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<Range> range)
|
||||
{
|
||||
Wt::Dbo::collection<Release::pointer> res = session.getDboSession().query<Release::pointer>
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Release>>
|
||||
("SELECT DISTINCT r from release r INNER JOIN track t ON r.id = t.release_id")
|
||||
.where("t.year >= ?").bind(yearFrom)
|
||||
.where("t.year <= ?").bind(yearTo)
|
||||
.orderBy("t.year, r.name COLLATE NOCASE")
|
||||
.where("t.date >= ?").bind(Wt::WDate {yearFrom, 1, 1})
|
||||
.where("t.date <= ?").bind(Wt::WDate {yearTo, 12, 31})
|
||||
.orderBy("t.date, r.name COLLATE NOCASE")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) : -1);
|
||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
@@ -250,30 +262,31 @@ Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<Ran
|
||||
std::vector<Release::pointer>
|
||||
Release::getStarred(Session& session,
|
||||
User::pointer user,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Release::pointer>(session, "SELECT r from release r", clusterIds, {})};
|
||||
auto query {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT r from release r", clusterIds, {})};
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN user_release_starred urs ON urs.release_id = r.id"
|
||||
" INNER JOIN user u ON u.id = urs.user_id WHERE u.id = ?)";
|
||||
|
||||
query.bind(user.id());
|
||||
query.bind(user->getId());
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> collection = query
|
||||
auto collection {query
|
||||
.groupBy("r.id")
|
||||
.orderBy("r.name COLLATE NOCASE")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1);
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1)
|
||||
.resultList()};
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
@@ -288,7 +301,7 @@ Release::getStarred(Session& session,
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByClusters(Session& session, const std::set<IdType>& clusters)
|
||||
Release::getByClusters(Session& session, const std::vector<ClusterId>& clusters)
|
||||
{
|
||||
assert(!clusters.empty());
|
||||
|
||||
@@ -300,21 +313,21 @@ Release::getByClusters(Session& session, const std::set<IdType>& clusters)
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByFilter(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
const std::vector<std::string_view>& keywords,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> collection = createQuery<Release::pointer>(session, "SELECT r from release r", clusterIds, keywords)
|
||||
auto collection {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT r from release r", clusterIds, keywords)
|
||||
.groupBy("r.id")
|
||||
.orderBy("r.name COLLATE NOCASE")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
@@ -326,18 +339,18 @@ Release::getByFilter(Session& session,
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<ReleaseId>
|
||||
Release::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>
|
||||
Wt::Dbo::collection<ReleaseId> res = session.getDboSession().query<ReleaseId>
|
||||
("SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
return std::vector<ReleaseId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
@@ -345,11 +358,10 @@ std::optional<std::size_t>
|
||||
Release::getTotalTrack(void) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
int res = session()->query<int>("SELECT COALESCE(MAX(total_track),0) FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.bind(this->id());
|
||||
.bind(getId());
|
||||
|
||||
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
|
||||
}
|
||||
@@ -358,11 +370,10 @@ std::optional<std::size_t>
|
||||
Release::getTotalDisc(void) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
int res = session()->query<int>("SELECT COALESCE(MAX(total_disc),0) FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.bind(this->id());
|
||||
.bind(getId());
|
||||
|
||||
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
|
||||
}
|
||||
@@ -372,24 +383,25 @@ Release::getReleaseYear(bool original) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
const std::string field {original ? "original_year" : "year"};
|
||||
const char* field {original ? "original_date" : "date"};
|
||||
|
||||
Wt::Dbo::collection<int> dates = session()->query<int>(
|
||||
auto dates {session()->query<Wt::WDate>(
|
||||
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy(field)
|
||||
.bind(this->id());
|
||||
.bind(getId())
|
||||
.resultList()};
|
||||
|
||||
// various dates => no date
|
||||
if (dates.empty() || dates.size() > 1)
|
||||
return std::nullopt;
|
||||
|
||||
auto date {dates.front()};
|
||||
auto date {dates.front().year()};
|
||||
|
||||
if (date > 0)
|
||||
return date;
|
||||
else
|
||||
return std::nullopt;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
@@ -401,7 +413,7 @@ Release::getCopyright() const
|
||||
("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy("copyright")
|
||||
.bind(this->id());
|
||||
.bind(getId());
|
||||
|
||||
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
||||
|
||||
@@ -421,7 +433,7 @@ Release::getCopyrightURL() const
|
||||
("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy("copyright_url")
|
||||
.bind(this->id());
|
||||
.bind(getId());
|
||||
|
||||
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
||||
|
||||
@@ -432,32 +444,29 @@ Release::getCopyrightURL() const
|
||||
return values.front();
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>>
|
||||
std::vector<Artist::pointer>
|
||||
Release::getArtists(TrackArtistLinkType linkType) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session()->query<Wt::Dbo::ptr<Artist>>(
|
||||
auto res {session()->query<Wt::Dbo::ptr<Artist>>(
|
||||
"SELECT DISTINCT a FROM artist a"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||
" INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?").bind(self()->id())
|
||||
.where("t_a_l.type = ?").bind(linkType);
|
||||
.where("r.id = ?").bind(getId())
|
||||
.where("t_a_l.type = ?").bind(linkType)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<Artist>>(res.begin(), res.end());
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::Query<pointer> query {session()->query<pointer>(
|
||||
auto res {session()->query<Wt::Dbo::ptr<Release>>(
|
||||
"SELECT r FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
@@ -465,14 +474,14 @@ Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std
|
||||
" t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)"
|
||||
" AND r.id <> ?"
|
||||
)
|
||||
.bind(self()->id())
|
||||
.bind(self()->id())
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("r.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(count ? static_cast<int>(*count) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)};
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
Wt::Dbo::collection<pointer> res = query;
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
@@ -483,11 +492,9 @@ Release::hasVariousArtists() const
|
||||
return getArtists().size() > 1;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>>
|
||||
Release::getTracks(const std::set<IdType>& clusterIds) const
|
||||
std::vector<Track::pointer>
|
||||
Release::getTracks(const std::vector<ClusterId>& clusterIds) const
|
||||
{
|
||||
assert(self());
|
||||
assert(self()->id() != Wt::Dbo::dbo_traits<Release>::invalidId() );
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
@@ -502,12 +509,12 @@ Release::getTracks(const std::set<IdType>& clusterIds) const
|
||||
WhereClause clusterClause;
|
||||
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
where.And(WhereClause("r.id = ?")).bind(std::to_string(id()));
|
||||
where.And(WhereClause("r.id = ?")).bind(getId().toString());
|
||||
|
||||
oss << " " << where.get();
|
||||
|
||||
@@ -516,16 +523,12 @@ Release::getTracks(const std::set<IdType>& clusterIds) const
|
||||
|
||||
oss << " ORDER BY t.disc_number,t.track_number";
|
||||
|
||||
Wt::Dbo::Query<Track::pointer> query = session()->query<Track::pointer>( oss.str() );
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Track>>(oss.str())};
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
{
|
||||
query.bind(bindArg);
|
||||
}
|
||||
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > res = query;
|
||||
|
||||
return std::vector< Wt::Dbo::ptr<Track> > (res.begin(), res.end());
|
||||
auto res {query.resultList()};
|
||||
return std::vector<Track::pointer> (res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::size_t
|
||||
@@ -534,31 +537,28 @@ Release::getTracksCount() const
|
||||
return _tracks.size();
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<Track>
|
||||
Track::pointer
|
||||
Release::getFirstTrack() const
|
||||
{
|
||||
assert(self());
|
||||
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId());
|
||||
assert(session());
|
||||
|
||||
return session()->query<Track::pointer>("SELECT t from track t")
|
||||
return session()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t")
|
||||
.join("release r ON t.release_id = r.id")
|
||||
.where("r.id = ?").bind(self()->id())
|
||||
.where("r.id = ?").bind(getId())
|
||||
.orderBy("t.disc_number,t.track_number")
|
||||
.limit(1);
|
||||
.limit(1)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
std::chrono::milliseconds
|
||||
Release::getDuration() const
|
||||
{
|
||||
assert(self());
|
||||
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId());
|
||||
assert(session());
|
||||
|
||||
using milli = std::chrono::duration<int, std::milli>;
|
||||
|
||||
Wt::Dbo::Query<milli> query {session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||
.where("r.id = ?").bind(self()->id())};
|
||||
.where("r.id = ?").bind(getId())};
|
||||
|
||||
return query.resultValue();
|
||||
}
|
||||
@@ -566,21 +566,17 @@ Release::getDuration() const
|
||||
Wt::WDateTime
|
||||
Release::getLastWritten() const
|
||||
{
|
||||
assert(self());
|
||||
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId());
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::Query<Wt::WDateTime> query {session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||
.where("r.id = ?").bind(self()->id())};
|
||||
.where("r.id = ?").bind(getId())};
|
||||
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>>
|
||||
Release::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
|
||||
std::vector<std::vector<Cluster::pointer>>
|
||||
Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
{
|
||||
assert(self());
|
||||
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId() );
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
@@ -589,33 +585,32 @@ Release::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::s
|
||||
|
||||
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id ";
|
||||
|
||||
where.And(WhereClause("r.id = ?")).bind(std::to_string(self()->id()));
|
||||
where.And(WhereClause("r.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(std::to_string(clusterType.id()));
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
|
||||
|
||||
Wt::Dbo::Query<Cluster::pointer> query = session()->query<Cluster::pointer>( oss.str() );
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Cluster>>(oss.str())};
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
Wt::Dbo::collection<Cluster::pointer> queryRes = query;
|
||||
auto queryRes {query.resultList()};
|
||||
|
||||
std::map<IdType, std::vector<Cluster::pointer>> clusters;
|
||||
for (auto cluster : queryRes)
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
|
||||
{
|
||||
if (clusters[cluster->getType().id()].size() < size)
|
||||
clusters[cluster->getType().id()].push_back(cluster);
|
||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (auto cluster_list : clusters)
|
||||
res.push_back(cluster_list.second);
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
res.push_back(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -60,14 +60,14 @@ ScanSettings::get(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<ScanSettings>();
|
||||
return session.getDboSession().find<ScanSettings>().resultValue();
|
||||
}
|
||||
|
||||
std::unordered_set<std::filesystem::path>
|
||||
std::vector<std::filesystem::path>
|
||||
ScanSettings::getAudioFileExtensions() const
|
||||
{
|
||||
auto extensions = StringUtils::splitString(_audioFileExtensions, " ");
|
||||
return std::unordered_set<std::filesystem::path>(std::cbegin(extensions), std::cend(extensions));
|
||||
const auto extensions {StringUtils::splitString(_audioFileExtensions, " ")};
|
||||
return std::vector<std::filesystem::path>(std::cbegin(extensions), std::cend(extensions));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -111,19 +111,19 @@ ScanSettings::setClusterTypes(Session& session, const std::set<std::string>& clu
|
||||
// Create any missing cluster type
|
||||
for (const std::string& clusterTypeName : clusterTypeNames)
|
||||
{
|
||||
auto clusterType {ClusterType::getByName(session, clusterTypeName)};
|
||||
ClusterType::pointer clusterType {ClusterType::getByName(session, clusterTypeName)};
|
||||
if (!clusterType)
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
|
||||
clusterType = ClusterType::create(session, clusterTypeName);
|
||||
_clusterTypes.insert(clusterType);
|
||||
_clusterTypes.insert(getDboPtr(clusterType));
|
||||
|
||||
needRescan = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete no longer existing cluster types
|
||||
for (ClusterType::pointer& clusterType : _clusterTypes)
|
||||
for (Wt::Dbo::ptr<ClusterType> clusterType : _clusterTypes)
|
||||
{
|
||||
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
|
||||
[clusterType](const std::string& name) { return name == clusterType->getName(); }))
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Database
|
||||
{
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION {30};
|
||||
static constexpr Version LMS_DATABASE_VERSION {31};
|
||||
|
||||
class VersionInfo
|
||||
{
|
||||
@@ -329,6 +329,37 @@ CREATE TABLE "user_backup" (
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 30)
|
||||
{
|
||||
// drop "year" and "original_year" (rescan needed to convert them into dates)
|
||||
_session.execute(R"(
|
||||
CREATE TABLE "track_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"scan_version" integer not null,
|
||||
"track_number" integer not null,
|
||||
"disc_number" integer not null,
|
||||
"name" text not null,
|
||||
"duration" integer,
|
||||
"date" integer text,
|
||||
"original_date" integer text,
|
||||
"file_path" text not null,
|
||||
"file_last_write" text,
|
||||
"file_added" text,
|
||||
"has_cover" boolean not null,
|
||||
"mbid" text not null,
|
||||
"copyright" text not null,
|
||||
"copyright_url" text not null,
|
||||
"release_id" bigint, total_disc INTEGER NOT NULL DEFAULT(0), total_track INTEGER NOT NULL DEFAULT(0), track_replay_gain REAL, release_replay_gain REAL, disc_subtitle TEXT NOT NULL DEFAULT '', recording_mbid TEXT,
|
||||
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
_session.execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, name, duration, \"1900-01-01\", \"1900-01-01\", file_path, file_last_write, file_added, has_cover, mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain, disc_subtitle, recording_mbid FROM track");
|
||||
_session.execute("DROP TABLE track");
|
||||
_session.execute("ALTER TABLE track_backup RENAME TO track");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||
@@ -442,8 +473,8 @@ Session::prepareTables()
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
|
||||
|
||||
+110
-108
@@ -27,10 +27,12 @@
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "SqlQuery.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Traits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
@@ -40,7 +42,7 @@ static
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
const std::vector<std::string_view>& keywords)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
@@ -58,7 +60,7 @@ createQuery(Session& session,
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const IdType clusterId : clusterIds)
|
||||
for (const ClusterId clusterId : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
@@ -91,48 +93,49 @@ Track::getAll(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> res {session.getDboSession().find<Track>()
|
||||
.limit(limit ? static_cast<int>(*limit) : -1)};
|
||||
auto res {session.getDboSession().find<Track>()
|
||||
.limit(limit ? static_cast<int>(*limit) : -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Track::pointer>(std::cbegin(res), std::cend(res));
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getAllRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> limit)
|
||||
Track::getAllRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> collection = query
|
||||
auto query {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, {})};
|
||||
auto collection {query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(limit ? static_cast<int>(*limit) + 1: -1);
|
||||
.limit(limit ? static_cast<int>(*limit) + 1: -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<pointer>(collection.begin(), collection.end());
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Track::getAllIdsRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> limit)
|
||||
std::vector<TrackId>
|
||||
Track::getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<IdType>(session, "SELECT t.id from track t", clusterIds, {})};
|
||||
auto query {createQuery<TrackId>(session, "SELECT t.id from track t", clusterIds, {})};
|
||||
|
||||
Wt::Dbo::collection<IdType> collection = query
|
||||
Wt::Dbo::collection<TrackId> collection = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(limit ? static_cast<int>(*limit) + 1: -1);
|
||||
|
||||
return std::vector<IdType>(collection.begin(), collection.end());
|
||||
return std::vector<TrackId>(collection.begin(), collection.end());
|
||||
}
|
||||
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<TrackId>
|
||||
Track::getAllIds(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>("SELECT id FROM track");
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<TrackId> res = session.getDboSession().query<TrackId>("SELECT id FROM track");
|
||||
return std::vector<TrackId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Track::pointer
|
||||
@@ -140,16 +143,17 @@ Track::getByPath(Session& session, const std::filesystem::path& p)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Track>().where("file_path = ?").bind(p.string());
|
||||
return session.getDboSession().find<Track>().where("file_path = ?").bind(p.string()).resultValue();
|
||||
}
|
||||
|
||||
Track::pointer
|
||||
Track::getById(Session& session, IdType id)
|
||||
Track::getById(Session& session, TrackId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Track>()
|
||||
.where("id = ?").bind(id);
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
@@ -157,8 +161,9 @@ Track::getByRecordingMBID(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> res = session.getDboSession().find<Track>()
|
||||
.where("recording_mbid = ?").bind(std::string {mbid.getAsString()});
|
||||
auto res {session.getDboSession().find<Track>()
|
||||
.where("recording_mbid = ?").bind(std::string {mbid.getAsString()})
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
}
|
||||
@@ -174,17 +179,17 @@ Track::create(Session& session, const std::filesystem::path& p)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<std::pair<IdType, std::filesystem::path>>
|
||||
std::vector<std::pair<TrackId, std::filesystem::path>>
|
||||
Track::getAllPaths(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
{
|
||||
using QueryResultType = std::tuple<IdType, std::string>;
|
||||
using QueryResultType = std::tuple<TrackId, std::string>;
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<QueryResultType> queryRes = session.getDboSession().query<QueryResultType>("SELECT id,file_path FROM track")
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
|
||||
std::vector<std::pair<IdType, std::filesystem::path>> result;
|
||||
std::vector<std::pair<TrackId, std::filesystem::path>> result;
|
||||
result.reserve(queryRes.size());
|
||||
|
||||
std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result),
|
||||
@@ -201,26 +206,29 @@ Track::getMBIDDuplicates(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().query<pointer>( "SELECT track FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)").orderBy("track.release_id,track.disc_number,track.track_number,track.mbid");
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>( "SELECT track FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)")
|
||||
.orderBy("track.release_id,track.disc_number,track.track_number,track.mbid")
|
||||
.resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults)
|
||||
Track::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
|
||||
auto query {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, {})};
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(after);
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> collection = query
|
||||
auto collection {query
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.groupBy("t.id")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1);
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1)
|
||||
.resultList()};
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
@@ -237,63 +245,65 @@ Track::getAllWithRecordingMBIDAndMissingFeatures(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().query<pointer>
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>
|
||||
("SELECT t FROM track t")
|
||||
.where("LENGTH(t.recording_mbid) > 0")
|
||||
.where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)");
|
||||
.where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)")
|
||||
.resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<TrackId>
|
||||
Track::getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>
|
||||
Wt::Dbo::collection<TrackId> res = session.getDboSession().query<TrackId>
|
||||
("SELECT t.id FROM track t")
|
||||
.where("EXISTS (SELECT * from track_features t_f WHERE t_f.track_id = t.id)")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
return std::vector<TrackId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<TrackId>
|
||||
Track::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>
|
||||
Wt::Dbo::collection<TrackId> res = session.getDboSession().query<TrackId>
|
||||
("SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
return std::vector<TrackId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getStarred(Session& session,
|
||||
Wt::Dbo::ptr<User> user,
|
||||
const std::set<IdType>& clusterIds,
|
||||
ObjectPtr<User> user,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
|
||||
auto query {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, {})};
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN user_track_starred uts ON uts.track_id = t.id"
|
||||
" INNER JOIN user u ON u.id = uts.user_id WHERE u.id = ?)";
|
||||
|
||||
query.bind(user.id());
|
||||
query.bind(user->getId().toString());
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> collection = query
|
||||
auto collection {query
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1);
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1)
|
||||
.resultList()};
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
@@ -308,43 +318,41 @@ Track::getStarred(Session& session,
|
||||
std::vector<Cluster::pointer>
|
||||
Track::getClusters() const
|
||||
{
|
||||
std::vector< Cluster::pointer > clusters;
|
||||
std::copy(_clusters.begin(), _clusters.end(), std::back_inserter(clusters));
|
||||
return clusters;
|
||||
return std::vector<Cluster::pointer>(_clusters.begin(), _clusters.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<ClusterId>
|
||||
Track::getClusterIds() const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session()->query<IdType>
|
||||
auto res {session()->query<ClusterId>
|
||||
("SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id")
|
||||
.where("t.id = ?").bind(self()->id());
|
||||
.where("t.id = ?").bind(getId())
|
||||
.resultList()};
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
return std::vector<ClusterId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
bool
|
||||
Track::hasTrackFeatures() const
|
||||
{
|
||||
return (_trackFeatures.lock() != Database::TrackFeatures::pointer());
|
||||
return (_trackFeatures.lock() != Wt::Dbo::ptr<Database::TrackFeatures> {});
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getByFilter(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
const std::vector<std::string_view>& keywords,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> collection = createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, keywords)
|
||||
auto collection {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, keywords)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && (res.size() == static_cast<std::size_t>(range->limit) + 1))
|
||||
@@ -362,17 +370,18 @@ std::vector<Track::pointer>
|
||||
Track::getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
Wt::Dbo::collection<pointer> collection = session.getDboSession().query<Track::pointer>("SELECT t from track t")
|
||||
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>("SELECT t from track t")
|
||||
.join("release r ON t.release_id = r.id")
|
||||
.where("t.name = ?").bind(trackName)
|
||||
.where("r.name = ?").bind(releaseName);
|
||||
|
||||
return std::vector<pointer>(collection.begin(), collection.end());
|
||||
.where("r.name = ?").bind(releaseName)
|
||||
.resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getSimilarTracks(Session& session,
|
||||
const std::unordered_set<IdType>& tracks,
|
||||
const std::vector<TrackId>& tracks,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size)
|
||||
{
|
||||
@@ -387,7 +396,7 @@ Track::getSimilarTracks(Session& session,
|
||||
oss << "?";
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<pointer> query {session.getDboSession().query<pointer>(
|
||||
auto query {session.getDboSession().query<Wt::Dbo::ptr<Track>>(
|
||||
"SELECT t FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" AND t_c.cluster_id IN (SELECT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN (" + oss.str() + "))"
|
||||
@@ -397,19 +406,18 @@ Track::getSimilarTracks(Session& session,
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)};
|
||||
|
||||
for (IdType trackId : tracks)
|
||||
query.bind(trackId );
|
||||
for (TrackId trackId : tracks)
|
||||
query.bind(trackId);
|
||||
|
||||
for (IdType trackId : tracks)
|
||||
query.bind(trackId );
|
||||
for (TrackId trackId : tracks)
|
||||
query.bind(trackId);
|
||||
|
||||
Wt::Dbo::collection<pointer> res = query;
|
||||
auto res {query.resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getByClusters(Session& session,
|
||||
const std::set<IdType>& clusters)
|
||||
Track::getByClusters(Session& session, const std::vector<ClusterId>& clusters)
|
||||
{
|
||||
assert(!clusters.empty());
|
||||
session.checkSharedLocked();
|
||||
@@ -429,23 +437,23 @@ Track::clearArtistLinks()
|
||||
}
|
||||
|
||||
void
|
||||
Track::addArtistLink(const Wt::Dbo::ptr<TrackArtistLink>& artistLink)
|
||||
Track::addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink)
|
||||
{
|
||||
_trackArtistLinks.insert(artistLink);
|
||||
_trackArtistLinks.insert(getDboPtr(artistLink));
|
||||
}
|
||||
|
||||
void
|
||||
Track::setClusters(const std::vector<Wt::Dbo::ptr<Cluster>>& clusters)
|
||||
Track::setClusters(const std::vector<ObjectPtr<Cluster>>& clusters)
|
||||
{
|
||||
_clusters.clear();
|
||||
for (const Wt::Dbo::ptr<Cluster>& cluster : clusters)
|
||||
_clusters.insert(cluster);
|
||||
for (const ObjectPtr<Cluster>& cluster : clusters)
|
||||
_clusters.insert(getDboPtr(cluster));
|
||||
}
|
||||
|
||||
void
|
||||
Track::setFeatures(const Wt::Dbo::ptr<TrackFeatures>& features)
|
||||
Track::setFeatures(const ObjectPtr<TrackFeatures>& features)
|
||||
{
|
||||
_trackFeatures = features;
|
||||
_trackFeatures = getDboPtr(features);
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
@@ -475,13 +483,13 @@ Track::getTotalDisc() const
|
||||
std::optional<int>
|
||||
Track::getYear() const
|
||||
{
|
||||
return (_year > 0) ? std::make_optional<int>(_year) : std::nullopt;
|
||||
return (_date.isValid() ? std::make_optional<int>(_date.year()) : std::nullopt);
|
||||
}
|
||||
|
||||
std::optional<int>
|
||||
Track::getOriginalYear() const
|
||||
{
|
||||
return (_originalYear > 0) ? std::make_optional<int>(_originalYear) : std::nullopt;
|
||||
return (_originalDate.isValid() ? std::make_optional<int>(_originalDate.year()) : std::nullopt);
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
@@ -496,11 +504,9 @@ Track::getCopyrightURL() const
|
||||
return _copyrightURL != "" ? std::make_optional<std::string>(_copyrightURL) : std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>>
|
||||
std::vector<Artist::pointer>
|
||||
Track::getArtists(EnumSet<TrackArtistLinkType> linkTypes) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
std::ostringstream oss;
|
||||
@@ -525,22 +531,20 @@ Track::getArtists(EnumSet<TrackArtistLinkType> linkTypes) const
|
||||
oss << ")";
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<Artist::pointer> query {session()->query<Artist::pointer>(oss.str())};
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Artist>>(oss.str())};
|
||||
for (TrackArtistLinkType type : linkTypes)
|
||||
query.bind(type);
|
||||
|
||||
query.where("t.id = ?").bind(self()->id());
|
||||
query.where("t.id = ?").bind(getId());
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> res = query;
|
||||
auto res {query.resultList()};
|
||||
return std::vector<Artist::pointer>(std::begin(res), std::end(res));
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<ArtistId>
|
||||
Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
std::ostringstream oss;
|
||||
@@ -565,33 +569,32 @@ Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
|
||||
oss << ")";
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<IdType> query {session()->query<IdType>(oss.str())
|
||||
.where("t.id = ?").bind(self()->id())};
|
||||
Wt::Dbo::Query<ArtistId> query {session()->query<ArtistId>(oss.str())
|
||||
.where("t.id = ?").bind(getId())};
|
||||
|
||||
for (TrackArtistLinkType type : linkTypes)
|
||||
query.bind(type);
|
||||
|
||||
Wt::Dbo::collection<IdType> res = query;
|
||||
return std::vector<IdType>(std::begin(res), std::end(res));
|
||||
Wt::Dbo::collection<ArtistId> res = query;
|
||||
return std::vector<ArtistId>(std::begin(res), std::end(res));
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<TrackArtistLink>>
|
||||
std::vector<TrackArtistLink::pointer>
|
||||
Track::getArtistLinks() const
|
||||
{
|
||||
return std::vector<Wt::Dbo::ptr<TrackArtistLink>>(_trackArtistLinks.begin(), _trackArtistLinks.end());
|
||||
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end());
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<TrackFeatures>
|
||||
ObjectPtr<TrackFeatures>
|
||||
Track::getTrackFeatures() const
|
||||
{
|
||||
return _trackFeatures.lock();
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>>
|
||||
Track::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
|
||||
Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
@@ -600,28 +603,27 @@ Track::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::siz
|
||||
|
||||
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id";
|
||||
|
||||
where.And(WhereClause("t.id = ?")).bind(std::to_string(self()->id()));
|
||||
where.And(WhereClause("t.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(std::to_string(clusterType.id()));
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
|
||||
|
||||
Wt::Dbo::Query<Cluster::pointer> query = session()->query<Cluster::pointer>( oss.str() );
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Cluster>>(oss.str())};
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
Wt::Dbo::collection<Cluster::pointer> queryRes = query;
|
||||
auto queryRes {query.resultList()};
|
||||
|
||||
std::map<IdType, std::vector<Cluster::pointer>> clusters;
|
||||
for (auto cluster : queryRes)
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clusters;
|
||||
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
|
||||
{
|
||||
if (clusters[cluster->getType().id()].size() < size)
|
||||
clusters[cluster->getType().id()].push_back(cluster);
|
||||
if (clusters[cluster->getType()->getId()].size() < size)
|
||||
clusters[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
|
||||
@@ -23,17 +23,19 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
|
||||
#include "Traits.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
TrackArtistLink::TrackArtistLink(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, TrackArtistLinkType type)
|
||||
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type)
|
||||
: _type {type},
|
||||
_track {track},
|
||||
_artist {artist}
|
||||
_track {getDboPtr(track)},
|
||||
_artist {getDboPtr(artist)}
|
||||
{
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer
|
||||
TrackArtistLink::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, TrackArtistLinkType type)
|
||||
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
@@ -48,9 +50,9 @@ TrackArtistLink::getUsedTypes(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackArtistLinkType> collection = session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link");
|
||||
auto res {session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList()};
|
||||
|
||||
return EnumSet<TrackArtistLinkType>(std::begin(collection), std::end(collection));
|
||||
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,18 +22,18 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "Traits.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
TrackBookmark::TrackBookmark(Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track)
|
||||
: _user {user},
|
||||
_track {track}
|
||||
TrackBookmark::TrackBookmark(ObjectPtr<User> user, ObjectPtr<Track> track)
|
||||
: _user {getDboPtr(user)},
|
||||
_track {getDboPtr(track)}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::create(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track)
|
||||
TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
@@ -48,42 +48,41 @@ TrackBookmark::getAll(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackBookmark::pointer> res {session.getDboSession().find<TrackBookmark>()};
|
||||
|
||||
auto res {session.getDboSession().find<TrackBookmark>().resultList()};
|
||||
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
|
||||
}
|
||||
|
||||
std::vector<TrackBookmark::pointer>
|
||||
TrackBookmark::getByUser(Session& session, Wt::Dbo::ptr<User> user)
|
||||
TrackBookmark::getByUser(Session& session, User::pointer user)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackBookmark::pointer> res
|
||||
{
|
||||
session.getDboSession().find<TrackBookmark>()
|
||||
.where("user_id = ?").bind(user.id())
|
||||
};
|
||||
auto res {session.getDboSession().find<TrackBookmark>()
|
||||
.where("user_id = ?").bind(user->getId())
|
||||
.resultList()};
|
||||
|
||||
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
|
||||
}
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::getByUser(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track)
|
||||
TrackBookmark::getByUser(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
.where("user_id = ?").bind(user.id())
|
||||
.where("track_id = ?").bind(track.id());
|
||||
.where("user_id = ?").bind(user->getId())
|
||||
.where("track_id = ?").bind(track->getId())
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::getById(Session& session, IdType id)
|
||||
TrackBookmark::getById(Session& session, TrackBookmarkId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
.where("id = ?").bind(id);
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
TrackFeatures::TrackFeatures(Wt::Dbo::ptr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
: _data(jsonEncodedFeatures),
|
||||
_track(track)
|
||||
TrackFeatures::TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
: _data {jsonEncodedFeatures},
|
||||
_track {getDboPtr(track)}
|
||||
{
|
||||
}
|
||||
|
||||
TrackFeatures::pointer
|
||||
TrackFeatures::create(Session& session, Wt::Dbo::ptr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
return session.getDboSession().add(std::make_unique<TrackFeatures>(track, jsonEncodedFeatures));
|
||||
|
||||
@@ -30,32 +30,33 @@
|
||||
#include "database/Track.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Traits.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
TrackList::TrackList(std::string_view name, Type type, bool isPublic, Wt::Dbo::ptr<User> user)
|
||||
TrackList::TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr<User> user)
|
||||
: _name {name},
|
||||
_type {type},
|
||||
_isPublic {isPublic},
|
||||
_user {user}
|
||||
_user {getDboPtr(user)}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
TrackList::pointer
|
||||
TrackList::create(Session& session, std::string_view name, Type type, bool isPublic, Wt::Dbo::ptr<User> user)
|
||||
TrackList::create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr<User> user)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
assert(user);
|
||||
|
||||
auto res = session.getDboSession().add( std::make_unique<TrackList>(name, type, isPublic, user) );
|
||||
TrackList::pointer res {session.getDboSession().add( std::make_unique<TrackList>(name, type, isPublic, user) )};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
TrackList::pointer
|
||||
TrackList::get(Session& session, std::string_view name, Type type, Wt::Dbo::ptr<User> user)
|
||||
TrackList::get(Session& session, std::string_view name, Type type, ObjectPtr<User> user)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
assert(user);
|
||||
@@ -63,49 +64,51 @@ TrackList::get(Session& session, std::string_view name, Type type, Wt::Dbo::ptr<
|
||||
return session.getDboSession().find<TrackList>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("type = ?").bind(type)
|
||||
.where("user_id = ?").bind(user.id());
|
||||
.where("user_id = ?").bind(user->getId()).resultValue();
|
||||
}
|
||||
|
||||
std::vector<TrackList::pointer>
|
||||
TrackList::getAll(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
Wt::Dbo::collection<TrackList::pointer> res = session.getDboSession().find<TrackList>();
|
||||
|
||||
auto res = session.getDboSession().find<TrackList>().resultList();
|
||||
return std::vector<TrackList::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<TrackList::pointer>
|
||||
TrackList::getAll(Session& session, ObjectPtr<User> user)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<TrackList>()
|
||||
.where("user_id = ?").bind(user->getId())
|
||||
.orderBy("name COLLATE NOCASE")
|
||||
.resultList()};
|
||||
|
||||
return std::vector<TrackList::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<TrackList::pointer>
|
||||
TrackList::getAll(Session& session, Wt::Dbo::ptr<User> user)
|
||||
TrackList::getAll(Session& session, ObjectPtr<User> user, Type type)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackList::pointer> res = session.getDboSession().find<TrackList>()
|
||||
.where("user_id = ?").bind(user.id())
|
||||
.orderBy("name COLLATE NOCASE");
|
||||
|
||||
return std::vector<TrackList::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<TrackList::pointer>
|
||||
TrackList::getAll(Session& session, Wt::Dbo::ptr<User> user, Type type)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackList::pointer> res = session.getDboSession().find<TrackList>()
|
||||
.where("user_id = ?").bind(user.id())
|
||||
auto res {session.getDboSession().find<TrackList>()
|
||||
.where("user_id = ?").bind(user->getId())
|
||||
.where("type = ?").bind(type)
|
||||
.orderBy("name COLLATE NOCASE");
|
||||
.orderBy("name COLLATE NOCASE")
|
||||
.resultList()};
|
||||
|
||||
return std::vector<TrackList::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
TrackList::pointer
|
||||
TrackList::getById(Session& session, IdType id)
|
||||
TrackList::getById(Session& session, TrackListId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<TrackList>().where("id = ?").bind(id);
|
||||
return session.getDboSession().find<TrackList>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -120,10 +123,10 @@ TrackList::getCount() const
|
||||
return _entries.size();
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<TrackListEntry>
|
||||
TrackListEntry::pointer
|
||||
TrackList::getEntry(std::size_t pos) const
|
||||
{
|
||||
Wt::Dbo::ptr<TrackListEntry> res;
|
||||
TrackListEntry::pointer res;
|
||||
|
||||
auto entries = getEntries(pos, 1);
|
||||
if (!entries.empty())
|
||||
@@ -132,39 +135,39 @@ TrackList::getEntry(std::size_t pos) const
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<TrackListEntry>>
|
||||
std::vector<TrackListEntry::pointer>
|
||||
TrackList::getEntries(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> entries =
|
||||
auto entries {
|
||||
session()->find<TrackListEntry>()
|
||||
.where("tracklist_id = ?").bind(self().id())
|
||||
.where("tracklist_id = ?").bind(getId())
|
||||
.orderBy("id")
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<TrackListEntry>>(entries.begin(), entries.end());
|
||||
return std::vector<TrackListEntry::pointer>(entries.begin(), entries.end());
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<TrackListEntry>
|
||||
TrackList::getEntryByTrackAndDateTime(Wt::Dbo::ptr<Track> track, const Wt::WDateTime& dateTime) const
|
||||
TrackListEntry::pointer
|
||||
TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
return session()->find<TrackListEntry>()
|
||||
.where("tracklist_id = ?").bind(self().id())
|
||||
.where("track_id = ?").bind(track.id())
|
||||
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()));
|
||||
.where("tracklist_id = ?").bind(getId())
|
||||
.where("track_id = ?").bind(track->getId())
|
||||
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Artist::pointer>
|
||||
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
||||
Wt::Dbo::Query<Wt::Dbo::ptr<Artist>>
|
||||
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
||||
{
|
||||
auto query {session.query<Artist::pointer>(queryStr)};
|
||||
auto query {session.query<Wt::Dbo::ptr<Artist>>(queryStr)};
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
|
||||
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
|
||||
@@ -201,10 +204,10 @@ createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdTyp
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Release::pointer>
|
||||
createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds)
|
||||
Wt::Dbo::Query<Wt::Dbo::ptr<Release>>
|
||||
createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds)
|
||||
{
|
||||
auto query {session.query<Release::pointer>(queryStr)};
|
||||
auto query {session.query<Wt::Dbo::ptr<Release>>(queryStr)};
|
||||
query.join("track t ON t.release_id = r.id");
|
||||
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
|
||||
query.join("tracklist p ON p.id = p_e.tracklist_id");
|
||||
@@ -220,7 +223,7 @@ createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdTy
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (auto id : clusterIds)
|
||||
for (ClusterId id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(id);
|
||||
@@ -236,10 +239,10 @@ createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdTy
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Track::pointer>
|
||||
createTracksQuery(Wt::Dbo::Session& session, IdType tracklistId, const std::set<IdType>& clusterIds)
|
||||
Wt::Dbo::Query<Wt::Dbo::ptr<Track>>
|
||||
createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds)
|
||||
{
|
||||
auto query {session.query<Track::pointer>("SELECT t from track t INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")};
|
||||
auto query {session.query<Wt::Dbo::ptr<Track>>("SELECT t from track t INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")};
|
||||
|
||||
query.where("p.id = ?").bind(tracklistId);
|
||||
|
||||
@@ -253,7 +256,7 @@ createTracksQuery(Wt::Dbo::Session& session, IdType tracklistId, const std::set<
|
||||
WhereClause clusterClause;
|
||||
for (auto id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
@@ -267,16 +270,16 @@ createTracksQuery(Wt::Dbo::Session& session, IdType tracklistId, const std::set<
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
TrackList::getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getArtistsReverse(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = createArtistsQuery(*session(), "SELECT a from artist a", self()->id(), clusterIds, linkType)
|
||||
auto collection {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)
|
||||
.groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)")
|
||||
.orderBy("p_e.date_time DESC")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
@@ -291,18 +294,18 @@ TrackList::getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<T
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
TrackList::getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getReleasesReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> collection = createReleasesQuery(*session(), "SELECT r from release r", self()->id(), clusterIds)
|
||||
auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)
|
||||
.groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)")
|
||||
.orderBy("p_e.date_time DESC")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
auto res {std::vector<Release::pointer>(collection.begin(), collection.end())};
|
||||
std::vector<Release::pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
@@ -315,18 +318,18 @@ TrackList::getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
TrackList::getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getTracksReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> collection = createTracksQuery(*session(), self()->id(), clusterIds)
|
||||
auto collection {createTracksQuery(*session(), getId(), clusterIds)
|
||||
.groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)")
|
||||
.orderBy("p_e.date_time DESC")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
auto res {std::vector<Track::pointer>(collection.begin(), collection.end())};
|
||||
std::vector<Track::pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
@@ -338,29 +341,28 @@ TrackList::getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Ra
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Cluster>>
|
||||
std::vector<Cluster::pointer>
|
||||
TrackList::getClusters() const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Cluster::pointer> res = session()->query<Cluster::pointer>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
|
||||
.where("p.id = ?").bind(self()->id())
|
||||
auto res {session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
|
||||
.where("p.id = ?").bind(getId())
|
||||
.groupBy("c.id")
|
||||
.orderBy("COUNT(c.id) DESC");
|
||||
.orderBy("COUNT(c.id) DESC")
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<Cluster>>(res.begin(), res.end());
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
bool
|
||||
TrackList::hasTrack(IdType trackId) const
|
||||
TrackList::hasTrack(TrackId trackId) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<TrackListEntry::pointer> res = session()->query<TrackListEntry::pointer>("SELECT p_e from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id")
|
||||
.where("p_e.track_id = ?").bind(trackId)
|
||||
.where("p.id = ?").bind(self()->id());
|
||||
.where("p.id = ?").bind(getId());
|
||||
|
||||
return res.size() > 0;
|
||||
}
|
||||
@@ -369,67 +371,64 @@ std::vector<Track::pointer>
|
||||
TrackList::getSimilarTracks(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::Query<Track::pointer> query {session()->query<Track::pointer>(
|
||||
auto res {session()->query<Wt::Dbo::ptr<Track>>(
|
||||
"SELECT t FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" WHERE "
|
||||
" (t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id WHERE p.id = ?)"
|
||||
" AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))"
|
||||
)
|
||||
.bind(self()->id())
|
||||
.bind(self()->id())
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("t.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)};
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> tracks = query;
|
||||
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<TrackId>
|
||||
TrackList::getTrackIds() const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session()->query<IdType>("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id")
|
||||
.where("p.id = ?").bind(self()->id());
|
||||
Wt::Dbo::collection<TrackId> res = session()->query<TrackId>("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id")
|
||||
.where("p.id = ?").bind(getId());
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
return std::vector<TrackId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::chrono::milliseconds
|
||||
TrackList::getDuration() const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
using milli = std::chrono::duration<int, std::milli>;
|
||||
|
||||
Wt::Dbo::Query<milli> query {session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id")
|
||||
.where("p_e.tracklist_id = ?").bind(self()->id())};
|
||||
.where("p_e.tracklist_id = ?").bind(getId())};
|
||||
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
TrackList::getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
auto query {createArtistsQuery(*session(), "SELECT a from artist a", self()->id(), clusterIds, linkType)};
|
||||
auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)};
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
auto collection {query
|
||||
.orderBy("COUNT(a.id) DESC")
|
||||
.groupBy("a.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
||||
std::vector<Artist::pointer> res(collection.begin(), collection.end());
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
@@ -439,26 +438,23 @@ TrackList::getTopArtists(const std::set<IdType>& clusterIds, std::optional<Track
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
TrackList::getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getTopReleases(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
auto query {createReleasesQuery(*session(), "SELECT r from release r", self()->id(), clusterIds)};
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> collection = query
|
||||
auto query {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)};
|
||||
auto collection {query
|
||||
.orderBy("COUNT(r.id) DESC")
|
||||
.groupBy("r.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<Release::pointer>(collection.begin(), collection.end())};
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<Release::pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
@@ -471,21 +467,19 @@ TrackList::getTopReleases(const std::set<IdType>& clusterIds, std::optional<Rang
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
TrackList::getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
auto query {createTracksQuery(*session(), self()->id(), clusterIds)};
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> collection = query
|
||||
auto query {createTracksQuery(*session(), getId(), clusterIds)};
|
||||
auto collection {query
|
||||
.orderBy("COUNT(t.id) DESC")
|
||||
.groupBy("t.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<Track::pointer>(collection.begin(), collection.end())};
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<Track::pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
@@ -497,16 +491,16 @@ TrackList::getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range>
|
||||
return res;
|
||||
}
|
||||
|
||||
TrackListEntry::TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||
TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||
: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())} // force second resolution
|
||||
, _track {track}
|
||||
, _tracklist {tracklist}
|
||||
, _track {getDboPtr(track)}
|
||||
, _tracklist {getDboPtr(tracklist)}
|
||||
{
|
||||
assert(_dateTime.isValid());
|
||||
}
|
||||
|
||||
TrackListEntry::pointer
|
||||
TrackListEntry::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||
TrackListEntry::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
assert(track);
|
||||
@@ -519,11 +513,11 @@ TrackListEntry::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr
|
||||
}
|
||||
|
||||
TrackListEntry::pointer
|
||||
TrackListEntry::getById(Session& session, IdType id)
|
||||
TrackListEntry::getById(Session& session, TrackListEntryId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<TrackListEntry>().where("id = ?").bind(id);
|
||||
return session.getDboSession().find<TrackListEntry>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 <type_traits>
|
||||
#include <Wt/Dbo/StdSqlTraits.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Wt::Dbo
|
||||
{
|
||||
|
||||
template<typename T>
|
||||
struct sql_value_traits<T, typename std::enable_if<std::is_base_of<Database::IdType, T>::value>::type>
|
||||
{
|
||||
static_assert(!std::is_same_v<Database::IdType, T>, "Cannot use IdType, use derived types");
|
||||
static const bool specialized = true;
|
||||
|
||||
static std::string type(SqlConnection *conn, int size)
|
||||
{
|
||||
return sql_value_traits<typename T::ValueType, void>::type(conn, size);
|
||||
}
|
||||
|
||||
static void bind(const T& v, SqlStatement *statement, int column, int size)
|
||||
{
|
||||
sql_value_traits<typename T::ValueType>::bind(v.getValue(), statement, column, size);
|
||||
}
|
||||
|
||||
static bool read(T& v, SqlStatement *statement, int column, int size)
|
||||
{
|
||||
typename T::ValueType value;
|
||||
if (sql_value_traits<typename T::ValueType>::read(value, statement, column, size))
|
||||
{
|
||||
v = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
v = {};
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,25 +26,25 @@
|
||||
#include "database/TrackList.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Traits.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
|
||||
AuthToken::AuthToken(const std::string& value, const Wt::WDateTime& expiry, Wt::Dbo::ptr<User> user)
|
||||
AuthToken::AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
: _value {value}
|
||||
, _expiry {expiry}
|
||||
, _user {user}
|
||||
, _user {getDboPtr(user)}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::create(Session& session, const std::string& value, const Wt::WDateTime& expiry, Wt::Dbo::ptr<User> user)
|
||||
AuthToken::create(Session& session, const std::string& value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
auto res {session.getDboSession().add(std::make_unique<AuthToken>(value, expiry, user))};
|
||||
|
||||
AuthToken::pointer res {session.getDboSession().add(std::make_unique<AuthToken>(value, expiry, user))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
@@ -65,7 +65,8 @@ AuthToken::getByValue(Session& session, const std::string& value)
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<AuthToken>()
|
||||
.where("value = ?").bind(value);
|
||||
.where("value = ?").bind(value)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
static const std::string queuedListName {"__queued_tracks__"};
|
||||
@@ -80,17 +81,17 @@ User::getAll(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<User>();
|
||||
auto res {session.getDboSession().find<User>().resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
std::vector<UserId>
|
||||
User::getAllIds(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>("SELECT id FROM user");
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
auto res {session.getDboSession().query<UserId>("SELECT id FROM user").resultList()};
|
||||
return std::vector<UserId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
User::pointer
|
||||
@@ -98,8 +99,7 @@ User::getDemo(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
pointer res = session.getDboSession().find<User>().where("type = ?").bind(Type::DEMO);
|
||||
return res;
|
||||
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
@@ -125,16 +125,17 @@ User::create(Session& session, std::string_view loginName)
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::getById(Session& session, IdType id)
|
||||
User::getById(Session& session, UserId id)
|
||||
{
|
||||
return session.getDboSession().find<User>().where("id = ?").bind( id );
|
||||
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::getByLoginName(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession().find<User>()
|
||||
.where("login_name = ?").bind(name);
|
||||
.where("login_name = ?").bind(name)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -150,7 +151,7 @@ User::clearAuthTokens()
|
||||
_authTokens.clear();
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<TrackList>
|
||||
TrackList::pointer
|
||||
User::getQueuedTrackList(Session& session) const
|
||||
{
|
||||
assert(self());
|
||||
@@ -160,63 +161,63 @@ User::getQueuedTrackList(Session& session) const
|
||||
}
|
||||
|
||||
void
|
||||
User::starArtist(Wt::Dbo::ptr<Artist> artist)
|
||||
User::starArtist(ObjectPtr<Artist> artist)
|
||||
{
|
||||
if (_starredArtists.count(artist) == 0)
|
||||
_starredArtists.insert(artist);
|
||||
if (_starredArtists.count(getDboPtr(artist)) == 0)
|
||||
_starredArtists.insert(getDboPtr(artist));
|
||||
}
|
||||
|
||||
void
|
||||
User::unstarArtist(Wt::Dbo::ptr<Artist> artist)
|
||||
User::unstarArtist(ObjectPtr<Artist> artist)
|
||||
{
|
||||
if (_starredArtists.count(artist) != 0)
|
||||
_starredArtists.erase(artist);
|
||||
if (_starredArtists.count(getDboPtr(artist)) != 0)
|
||||
_starredArtists.erase(getDboPtr(artist));
|
||||
}
|
||||
|
||||
bool
|
||||
User::hasStarredArtist(Wt::Dbo::ptr<Artist> artist) const
|
||||
User::hasStarredArtist(ObjectPtr<Artist> artist) const
|
||||
{
|
||||
return _starredArtists.count(artist) != 0;
|
||||
return _starredArtists.count(getDboPtr(artist)) != 0;
|
||||
}
|
||||
|
||||
void
|
||||
User::starRelease(Wt::Dbo::ptr<Release> release)
|
||||
User::starRelease(ObjectPtr<Release> release)
|
||||
{
|
||||
if (_starredReleases.count(release) == 0)
|
||||
_starredReleases.insert(release);
|
||||
if (_starredReleases.count(getDboPtr(release)) == 0)
|
||||
_starredReleases.insert(getDboPtr(release));
|
||||
}
|
||||
|
||||
void
|
||||
User::unstarRelease(Wt::Dbo::ptr<Release> release)
|
||||
User::unstarRelease(ObjectPtr<Release> release)
|
||||
{
|
||||
if (_starredReleases.count(release) != 0)
|
||||
_starredReleases.erase(release);
|
||||
if (_starredReleases.count(getDboPtr(release)) != 0)
|
||||
_starredReleases.erase(getDboPtr(release));
|
||||
}
|
||||
|
||||
bool
|
||||
User::hasStarredRelease(Wt::Dbo::ptr<Release> release) const
|
||||
User::hasStarredRelease(ObjectPtr<Release> release) const
|
||||
{
|
||||
return _starredReleases.count(release) != 0;
|
||||
return _starredReleases.count(getDboPtr(release)) != 0;
|
||||
}
|
||||
|
||||
void
|
||||
User::starTrack(Wt::Dbo::ptr<Track> track)
|
||||
User::starTrack(ObjectPtr<Track> track)
|
||||
{
|
||||
if (_starredTracks.count(track) == 0)
|
||||
_starredTracks.insert(track);
|
||||
if (_starredTracks.count(getDboPtr(track)) == 0)
|
||||
_starredTracks.insert(getDboPtr(track));
|
||||
}
|
||||
|
||||
void
|
||||
User::unstarTrack(Wt::Dbo::ptr<Track> track)
|
||||
User::unstarTrack(ObjectPtr<Track> track)
|
||||
{
|
||||
if (_starredTracks.count(track) != 0)
|
||||
_starredTracks.erase(track);
|
||||
if (_starredTracks.count(getDboPtr(track)) != 0)
|
||||
_starredTracks.erase(getDboPtr(track));
|
||||
}
|
||||
|
||||
bool
|
||||
User::hasStarredTrack(Wt::Dbo::ptr<Track> track) const
|
||||
User::hasStarredTrack(ObjectPtr<Track> track) const
|
||||
{
|
||||
return _starredTracks.count(track) != 0;
|
||||
return _starredTracks.count(getDboPtr(track)) != 0;
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -22,17 +22,15 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
@@ -44,10 +42,9 @@ class Track;
|
||||
class TrackArtistLink;
|
||||
class User;
|
||||
|
||||
class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
class Artist : public Object<Artist, ArtistId>
|
||||
{
|
||||
public:
|
||||
|
||||
enum class SortMethod
|
||||
{
|
||||
None,
|
||||
@@ -55,43 +52,41 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
BySortName,
|
||||
};
|
||||
|
||||
using pointer = Wt::Dbo::ptr<Artist>;
|
||||
|
||||
Artist() {}
|
||||
Artist() = default;
|
||||
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
// Accessors
|
||||
static pointer getByMBID(Session& session, const UUID& MBID);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getById(Session& session, ArtistId id);
|
||||
static std::vector<pointer> getByName(Session& session, const std::string& name); // exact match on name field
|
||||
static std::vector<pointer> getByClusters(Session& session,
|
||||
const std::set<IdType>& clusters, // at least one track that belongs to these clusters
|
||||
const std::vector<ClusterId>& clusters, // at least one track that belongs to these clusters
|
||||
SortMethod sortMethod
|
||||
);
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters, // if non empty, at least one artist that belongs to these clusters
|
||||
const std::vector<ClusterId>& clusters, // if non empty, at least one artist that belongs to these clusters
|
||||
const std::vector<std::string_view>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
|
||||
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod);
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // No track related
|
||||
static std::vector<pointer> getLastWritten(Session& session,
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod);
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<ArtistId> getAllIds(Session& session);
|
||||
static std::vector<ArtistId> getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // No track related
|
||||
static std::vector<pointer> getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::set<IdType>& clusters,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
std::optional<Range>,
|
||||
bool& moreResults);
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<ArtistId> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getStarred(Session& session,
|
||||
Wt::Dbo::ptr<User> user,
|
||||
const std::set<IdType>& clusters,
|
||||
ObjectPtr<User> user,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range>, bool& moreResults);
|
||||
@@ -101,11 +96,12 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Release>> getReleases(const std::set<IdType>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
|
||||
std::size_t getReleaseCount() const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracksWithRelease(std::optional<TrackArtistLinkType> linkType = {}) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getRandomTracks(std::optional<std::size_t> count) const;
|
||||
std::vector<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
|
||||
std::size_t getReleaseCount() const;
|
||||
std::vector<ObjectPtr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
|
||||
bool hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType = std::nullopt) const;
|
||||
std::vector<ObjectPtr<Track>> getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<ObjectPtr<Track>> getRandomTracks(std::optional<std::size_t> count) const;
|
||||
|
||||
// No artistLinkTypes means get them all
|
||||
std::vector<pointer> getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
|
||||
@@ -113,14 +109,14 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
// Get the cluster of the tracks made by this artist
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>> getClusterGroups(std::vector<Wt::Dbo::ptr<ClusterType>> clusterTypes, std::size_t size) const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ObjectPtr<ClusterType>> clusterTypes, std::size_t size) const;
|
||||
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
void setSortName(const std::string& sortName);
|
||||
|
||||
// Create
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
|
||||
@@ -24,10 +24,9 @@
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "Types.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -36,31 +35,29 @@ class ClusterType;
|
||||
class ScanSettings;
|
||||
class Session;
|
||||
|
||||
class Cluster : public Wt::Dbo::Dbo<Cluster>
|
||||
class Cluster : public Object<Cluster, ClusterId>
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<Cluster>;
|
||||
|
||||
Cluster();
|
||||
Cluster(Wt::Dbo::ptr<ClusterType> type, std::string_view name);
|
||||
Cluster() = default;
|
||||
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
||||
|
||||
// Find utility
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static std::vector<pointer> getAllOrphans(Session& session);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getById(Session& session, ClusterId id);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, Wt::Dbo::ptr<ClusterType> type, std::string_view name);
|
||||
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
Wt::Dbo::ptr<ClusterType> getType() const { return _clusterType; }
|
||||
ObjectPtr<ClusterType> getType() const { return _clusterType; }
|
||||
std::size_t getTracksCount() const { return _tracks.size(); }
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracks(std::optional<std::size_t> offset = {}, std::optional<std::size_t> limit = {}) const;
|
||||
std::set<IdType> getTrackIds() const;
|
||||
std::vector<ObjectPtr<Track>> getTracks(std::optional<std::size_t> offset = {}, std::optional<std::size_t> limit = {}) const;
|
||||
std::vector<TrackId> getTrackIds() const;
|
||||
std::size_t getReleasesCount() const;
|
||||
|
||||
void addTrack(Wt::Dbo::ptr<Track> track);
|
||||
void addTrack(ObjectPtr<Track> track);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
@@ -72,7 +69,6 @@ class Cluster : public Wt::Dbo::Dbo<Cluster>
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
@@ -82,19 +78,17 @@ class Cluster : public Wt::Dbo::Dbo<Cluster>
|
||||
};
|
||||
|
||||
|
||||
class ClusterType : public Wt::Dbo::Dbo<ClusterType>
|
||||
class ClusterType : public Object<ClusterType, ClusterTypeId>
|
||||
{
|
||||
public:
|
||||
ClusterType() = default;
|
||||
ClusterType(std::string_view name);
|
||||
|
||||
using pointer = Wt::Dbo::ptr<ClusterType>;
|
||||
|
||||
ClusterType() {}
|
||||
ClusterType(std::string name);
|
||||
|
||||
// Getters
|
||||
static std::vector<pointer> getAllOrphans(Session& session);
|
||||
static std::vector<pointer> getAllUsed(Session& session);
|
||||
static pointer getByName(Session& session, const std::string& name);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getById(Session& session, ClusterTypeId id);
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
|
||||
static pointer create(Session& session, const std::string& name);
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -39,54 +39,51 @@ class Session;
|
||||
class Track;
|
||||
class User;
|
||||
|
||||
class Release : public Wt::Dbo::Dbo<Release>
|
||||
class Release : public Object<Release, ReleaseId>
|
||||
{
|
||||
public:
|
||||
|
||||
using pointer = Wt::Dbo::ptr<Release>;
|
||||
|
||||
Release() {}
|
||||
Release() = default;
|
||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer getByMBID(Session& session, const UUID& MBID);
|
||||
static std::vector<pointer> getByName(Session& session, const std::string& name);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getById(Session& session, ReleaseId id);
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // no track related
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<ReleaseId> getAllIds(Session& session);
|
||||
static std::vector<pointer> getAllOrderedByArtist(Session& session, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getAllRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<ReleaseId> getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<Range> range = std::nullopt);
|
||||
static std::vector<pointer> getStarred(Session& session, Wt::Dbo::ptr<User> user, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getStarred(Session& session, ObjectPtr<User> user, const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
|
||||
static std::vector<pointer> getByClusters(Session& session, const std::set<IdType>& clusters);
|
||||
static std::vector<pointer> getByClusters(Session& session, const std::vector<ClusterId>& clusters);
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters, // if non empty, at least one release that belongs to these clusters
|
||||
const std::vector<ClusterId>& clusters, // if non empty, at least one release that belongs to these clusters
|
||||
const std::vector<std::string_view>& keywords, // if non empty, name must match all of these keywords
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<ReleaseId> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracks(const std::set<IdType>& clusters = std::set<IdType>()) const;
|
||||
std::size_t getTracksCount() const;
|
||||
Wt::Dbo::ptr<Track> getFirstTrack() const;
|
||||
std::vector<ObjectPtr<Track>> getTracks(const std::vector<ClusterId>& clusters = {}) const;
|
||||
std::size_t getTracksCount() const;
|
||||
ObjectPtr<Track> getFirstTrack() const;
|
||||
|
||||
// Get the cluster of the tracks that belong to this release
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>> getClusterGroups(std::vector<Wt::Dbo::ptr<ClusterType>> clusterTypes, std::size_t size) const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||
|
||||
// Create
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
// Utility functions
|
||||
std::optional<int> getReleaseYear(bool originalDate = false) const; // 0 if unknown or various
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
std::optional<int> getReleaseYear(bool originalDate = false) const;
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
@@ -97,8 +94,8 @@ class Release : public Wt::Dbo::Dbo<Release>
|
||||
Wt::WDateTime getLastWritten() const;
|
||||
|
||||
// Get the artists of this release
|
||||
std::vector<Wt::Dbo::ptr<Artist> > getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||
std::vector<Wt::Dbo::ptr<Artist> > getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
||||
std::vector<ObjectPtr<Artist> > getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||
std::vector<ObjectPtr<Artist> > getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
||||
bool hasVariousArtists() const;
|
||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||
|
||||
|
||||
@@ -19,23 +19,22 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <unordered_set>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WTime.h>
|
||||
|
||||
#include "utils/Path.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
class ClusterType;
|
||||
class Session;
|
||||
|
||||
class ScanSettings : public Wt::Dbo::Dbo<ScanSettings>
|
||||
class ScanSettings : public Object<ScanSettings, ScanSettingsId>
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<ScanSettings>;
|
||||
|
||||
// Do not modify values (just add)
|
||||
enum class UpdatePeriod {
|
||||
Never = 0,
|
||||
@@ -61,8 +60,8 @@ class ScanSettings : public Wt::Dbo::Dbo<ScanSettings>
|
||||
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::vector<Wt::Dbo::ptr<ClusterType>> getClusterTypes() const;
|
||||
std::unordered_set<std::filesystem::path> getAudioFileExtensions() const;
|
||||
std::vector<ObjectPtr<ClusterType>> getClusterTypes() const;
|
||||
std::vector<std::filesystem::path> getAudioFileExtensions() const;
|
||||
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; }
|
||||
|
||||
// Setters
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
#include "Types.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -49,46 +49,43 @@ class TrackListEntry;
|
||||
class TrackStats;
|
||||
class User;
|
||||
|
||||
class Track : public Wt::Dbo::Dbo<Track>
|
||||
class Track : public Object<Track, TrackId>
|
||||
{
|
||||
public:
|
||||
|
||||
using pointer = Wt::Dbo::ptr<Track>;
|
||||
|
||||
Track() {}
|
||||
Track() = default;
|
||||
Track(const std::filesystem::path& p);
|
||||
|
||||
// Find utility functions
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer getByPath(Session& session, const std::filesystem::path& p);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getById(Session& session, TrackId id);
|
||||
static std::vector<pointer> getByRecordingMBID(Session& session, const UUID& MBID);
|
||||
static std::vector<pointer> getSimilarTracks(Session& session,
|
||||
const std::unordered_set<IdType>& trackIds,
|
||||
const std::vector<TrackId>& trackIds,
|
||||
std::optional<std::size_t> offset = {},
|
||||
std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getByClusters(Session& session,
|
||||
const std::set<IdType>& clusters); // tracks that belong to these clusters
|
||||
const std::vector<ClusterId>& clusters); // tracks that belong to these clusters
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters, // if non empty, tracks that belong to these clusters
|
||||
const std::vector<ClusterId>& clusters, // if non empty, tracks that belong to these clusters
|
||||
const std::vector<std::string_view>& keywords, // if non empty, name must match all of these keywords
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
static std::vector<pointer> getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName);
|
||||
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<pointer> getAllRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<std::pair<IdType, std::filesystem::path>> getAllPaths(Session& session, std::optional<std::size_t> offset = std::nullopt, std::optional<std::size_t> size = std::nullopt);
|
||||
static std::vector<pointer> getAllRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<TrackId> getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<TrackId> getAllIds(Session& session);
|
||||
static std::vector<std::pair<TrackId, std::filesystem::path>> getAllPaths(Session& session, std::optional<std::size_t> offset = std::nullopt, std::optional<std::size_t> size = std::nullopt);
|
||||
static std::vector<pointer> getMBIDDuplicates(Session& session);
|
||||
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getAllWithRecordingMBIDAndMissingFeatures(Session& session);
|
||||
static std::vector<IdType> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<TrackId> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<TrackId> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getStarred(Session& session,
|
||||
Wt::Dbo::ptr<User> user,
|
||||
const std::set<IdType>& clusters,
|
||||
ObjectPtr<User> user,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
std::optional<Range> range, bool& hasMore);
|
||||
|
||||
// Create utility
|
||||
@@ -105,8 +102,8 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
||||
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
||||
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
|
||||
void setYear(int year) { _year = year; }
|
||||
void setOriginalYear(int year) { _originalYear = year; }
|
||||
void setDate(const Wt::WDate& date) { _date = date; }
|
||||
void setOriginalDate(const Wt::WDate& date) { _originalDate = date; }
|
||||
void setHasCover(bool hasCover) { _hasCover = hasCover; }
|
||||
void setTrackMBID(const std::optional<UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
|
||||
void setRecordingMBID(const std::optional<UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
|
||||
@@ -115,44 +112,44 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
void setTrackReplayGain(float replayGain) { _trackReplayGain = replayGain; }
|
||||
void setReleaseReplayGain(float replayGain) { _releaseReplayGain = replayGain; }
|
||||
void clearArtistLinks();
|
||||
void addArtistLink(const Wt::Dbo::ptr<TrackArtistLink>& artistLink);
|
||||
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
|
||||
void setClusters(const std::vector<Wt::Dbo::ptr<Cluster>>& clusters );
|
||||
void setFeatures(const Wt::Dbo::ptr<TrackFeatures>& features);
|
||||
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
||||
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
||||
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters );
|
||||
void setFeatures(const ObjectPtr<TrackFeatures>& features);
|
||||
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::optional<std::size_t> getTrackNumber() const;
|
||||
std::optional<std::size_t> getTotalTrack() const;
|
||||
std::optional<std::size_t> getDiscNumber() const;
|
||||
const std::string& getDiscSubtitle() const { return _discSubtitle; }
|
||||
std::optional<std::size_t> getTotalDisc() const;
|
||||
std::optional<std::size_t> getTrackNumber() const;
|
||||
std::optional<std::size_t> getTotalTrack() const;
|
||||
std::optional<std::size_t> getDiscNumber() const;
|
||||
const std::string& getDiscSubtitle() const { return _discSubtitle; }
|
||||
std::optional<std::size_t> getTotalDisc() const;
|
||||
std::string getName() const { return _name; }
|
||||
std::filesystem::path getPath() const { return _filePath; }
|
||||
std::chrono::milliseconds getDuration() const { return _duration; }
|
||||
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
|
||||
std::filesystem::path getPath() const { return _filePath; }
|
||||
std::chrono::milliseconds getDuration() const { return _duration; }
|
||||
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
|
||||
std::optional<int> getYear() const;
|
||||
std::optional<int> getOriginalYear() const;
|
||||
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; }
|
||||
Wt::WDateTime getAddedTime() const { return _fileAdded; }
|
||||
bool hasCover() const { return _hasCover; }
|
||||
std::optional<UUID> getTrackMBID() const { return UUID::fromString(_trackMBID); }
|
||||
std::optional<UUID> getRecordingMBID() const { return UUID::fromString(_recordingMBID); }
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||
bool hasCover() const { return _hasCover; }
|
||||
std::optional<UUID> getTrackMBID() const { return UUID::fromString(_trackMBID); }
|
||||
std::optional<UUID> getRecordingMBID() const { return UUID::fromString(_recordingMBID); }
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||
|
||||
// no artistLinkTypes means get all
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const;
|
||||
std::vector<IdType> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const;
|
||||
std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() const;
|
||||
Wt::Dbo::ptr<Release> getRelease() const { return _release; }
|
||||
std::vector<Wt::Dbo::ptr<Cluster>> getClusters() const;
|
||||
std::vector<IdType> getClusterIds() const;
|
||||
bool hasTrackFeatures() const;
|
||||
Wt::Dbo::ptr<TrackFeatures> getTrackFeatures() const;
|
||||
std::vector<ObjectPtr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const;
|
||||
std::vector<ArtistId> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const;
|
||||
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<ClusterId> getClusterIds() const;
|
||||
bool hasTrackFeatures() const;
|
||||
ObjectPtr<TrackFeatures> getTrackFeatures() const;
|
||||
|
||||
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>> getClusterGroups(std::vector<Wt::Dbo::ptr<ClusterType>> clusterTypes, std::size_t size) const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
@@ -163,10 +160,10 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
Wt::Dbo::field(a, _discSubtitle, "disc_subtitle");
|
||||
Wt::Dbo::field(a, _totalTrack, "total_track");
|
||||
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _duration, "duration");
|
||||
Wt::Dbo::field(a, _year, "year");
|
||||
Wt::Dbo::field(a, _originalYear, "original_year");
|
||||
Wt::Dbo::field(a, _date, "date");
|
||||
Wt::Dbo::field(a, _originalDate, "original_date");
|
||||
Wt::Dbo::field(a, _filePath, "file_path");
|
||||
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
|
||||
Wt::Dbo::field(a, _fileAdded, "file_added");
|
||||
@@ -201,8 +198,8 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
std::string _artistName;
|
||||
std::string _releaseName;
|
||||
std::chrono::duration<int, std::milli> _duration {};
|
||||
int _year {};
|
||||
int _originalYear {};
|
||||
Wt::WDate _date;
|
||||
Wt::WDate _originalDate;
|
||||
std::string _filePath;
|
||||
Wt::WDateTime _fileLastWrite;
|
||||
Wt::WDateTime _fileAdded;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "Types.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
namespace Database
|
||||
@@ -33,20 +33,18 @@ namespace Database
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
class TrackArtistLink
|
||||
class TrackArtistLink : public Object<TrackArtistLink, TrackArtistLinkId>
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<TrackArtistLink>;
|
||||
|
||||
TrackArtistLink() = default;
|
||||
TrackArtistLink(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, TrackArtistLinkType type);
|
||||
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
|
||||
|
||||
static pointer create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, TrackArtistLinkType type);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
|
||||
|
||||
static EnumSet<TrackArtistLinkType> getUsedTypes(Session& session);
|
||||
|
||||
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
||||
Wt::Dbo::ptr<Artist> getArtist() const { return _artist; }
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
TrackArtistLinkType getType() const { return _type; }
|
||||
|
||||
template<class Action>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "Types.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -31,22 +31,20 @@ class Session;
|
||||
class Track;
|
||||
class User;
|
||||
|
||||
class TrackBookmark : public Wt::Dbo::Dbo<TrackBookmark>
|
||||
class TrackBookmark : public Object<TrackBookmark, TrackBookmarkId>
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<TrackBookmark>;
|
||||
|
||||
TrackBookmark () = default;
|
||||
TrackBookmark(Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track);
|
||||
TrackBookmark(ObjectPtr<User> user, ObjectPtr<Track> track);
|
||||
|
||||
// utility
|
||||
static pointer create(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track);
|
||||
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
|
||||
|
||||
// Find utility functions
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static std::vector<pointer> getByUser(Session& session, Wt::Dbo::ptr<User> user);
|
||||
static pointer getByUser(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static std::vector<pointer> getByUser(Session& session, ObjectPtr<User> user);
|
||||
static pointer getByUser(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
|
||||
static pointer getById(Session& session, TrackBookmarkId id);
|
||||
|
||||
// Setters
|
||||
void setOffset(std::chrono::milliseconds offset) { _offset = offset; }
|
||||
@@ -55,8 +53,8 @@ class TrackBookmark : public Wt::Dbo::Dbo<TrackBookmark>
|
||||
// Getters
|
||||
std::chrono::milliseconds getOffset() const { return _offset; }
|
||||
std::string_view getComment() const { return _comment; }
|
||||
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
||||
Wt::Dbo::ptr<User> getUser() const { return _user; }
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "Types.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -37,17 +37,14 @@ using FeatureName = std::string;
|
||||
using FeatureValues = std::vector<double>;
|
||||
using FeatureValuesMap = std::unordered_map<FeatureName, FeatureValues>;
|
||||
|
||||
class TrackFeatures : public Wt::Dbo::Dbo<TrackFeatures>
|
||||
class TrackFeatures : public Object<TrackFeatures, TrackFeaturesId>
|
||||
{
|
||||
public:
|
||||
|
||||
using pointer = Wt::Dbo::ptr<TrackFeatures>;
|
||||
|
||||
TrackFeatures() = default;
|
||||
TrackFeatures(Wt::Dbo::ptr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, Wt::Dbo::ptr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
|
||||
FeatureValues getFeatureValues(const FeatureName& feature) const;
|
||||
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "Types.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -39,11 +39,9 @@ class Track;
|
||||
class TrackListEntry;
|
||||
class User;
|
||||
|
||||
class TrackList : public Wt::Dbo::Dbo<TrackList>
|
||||
class TrackList : public Object<TrackList, TrackListId>
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<TrackList>;
|
||||
|
||||
enum class Type
|
||||
{
|
||||
Playlist, // user controlled playlists
|
||||
@@ -51,28 +49,28 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
||||
};
|
||||
|
||||
TrackList() = default;
|
||||
TrackList(std::string_view name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
|
||||
TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr<User> user);
|
||||
|
||||
// Stats utility
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<ObjectPtr<Artist>> getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<ObjectPtr<Release>> getTopReleases(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<ObjectPtr<Track>> getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
|
||||
// Search utility
|
||||
static pointer get(Session& session, std::string_view name, Type type, Wt::Dbo::ptr<User> user);
|
||||
static pointer getById(Session& session, IdType tracklistId);
|
||||
static pointer get(Session& session, std::string_view name, Type type, ObjectPtr<User> user);
|
||||
static pointer getById(Session& session, TrackListId tracklistId);
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static std::vector<pointer> getAll(Session& session, Wt::Dbo::ptr<User> user);
|
||||
static std::vector<pointer> getAll(Session& session, Wt::Dbo::ptr<User> user, Type type);
|
||||
static std::vector<pointer> getAll(Session& session, ObjectPtr<User> user);
|
||||
static std::vector<pointer> getAll(Session& session, ObjectPtr<User> user, Type type);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, std::string_view name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
|
||||
static pointer create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr<User> user);
|
||||
|
||||
// Accessors
|
||||
std::string getName() const { return _name; }
|
||||
bool isPublic() const { return _isPublic; }
|
||||
Type getType() const { return _type; }
|
||||
Wt::Dbo::ptr<User> getUser() const { return _user; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
|
||||
// Modifiers
|
||||
void setName(const std::string& name) { _name = name; }
|
||||
@@ -80,29 +78,29 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
||||
void clear() { _entries.clear(); }
|
||||
|
||||
// Get tracks, ordered by position
|
||||
bool isEmpty() const;
|
||||
std::size_t getCount() const;
|
||||
Wt::Dbo::ptr<TrackListEntry> getEntry(std::size_t pos) const;
|
||||
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
Wt::Dbo::ptr<TrackListEntry> getEntryByTrackAndDateTime(Wt::Dbo::ptr<Track> track, const Wt::WDateTime& dateTime) const;
|
||||
bool isEmpty() const;
|
||||
std::size_t getCount() const;
|
||||
ObjectPtr<TrackListEntry> getEntry(std::size_t pos) const;
|
||||
std::vector<ObjectPtr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
ObjectPtr<TrackListEntry> getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const;
|
||||
|
||||
// Get track bya
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<ObjectPtr<Artist>> getArtistsReverse(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<ObjectPtr<Release>> getReleasesReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<ObjectPtr<Track>> getTracksReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
|
||||
std::vector<IdType> getTrackIds() const;
|
||||
std::vector<TrackId> getTrackIds() const;
|
||||
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
|
||||
// Get clusters, order by occurence
|
||||
std::vector<Wt::Dbo::ptr<Cluster>> getClusters() const;
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
|
||||
bool hasTrack(IdType trackId) const;
|
||||
bool hasTrack(TrackId trackId) const;
|
||||
|
||||
// Ordered from most clusters in common
|
||||
std::vector<Wt::Dbo::ptr<Track>> getSimilarTracks(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
std::vector<ObjectPtr<Track>> getSimilarTracks(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
@@ -122,27 +120,24 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
||||
bool _isPublic {false};
|
||||
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<TrackListEntry> > _entries;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _entries;
|
||||
|
||||
};
|
||||
|
||||
class TrackListEntry : public Wt::Dbo::Dbo<TrackListEntry>
|
||||
class TrackListEntry : public Object<TrackListEntry, TrackListEntryId>
|
||||
{
|
||||
public:
|
||||
|
||||
using pointer = Wt::Dbo::ptr<TrackListEntry>;
|
||||
|
||||
TrackListEntry() = default;
|
||||
TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist, const Wt::WDateTime& dateTime);
|
||||
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime);
|
||||
|
||||
// find utility
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getById(Session& session, TrackListEntryId id);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist, const Wt::WDateTime& dateTime = Wt::WDateTime::currentDateTime());
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime = Wt::WDateTime::currentDateTime());
|
||||
|
||||
// Accessors
|
||||
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
|
||||
template<class Action>
|
||||
|
||||
@@ -20,16 +20,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
using IdType = Wt::Dbo::dbo_default_traits::IdType;
|
||||
|
||||
static inline bool IdIsValid(IdType id)
|
||||
class IdType
|
||||
{
|
||||
return id != Wt::Dbo::dbo_default_traits::invalidId();
|
||||
}
|
||||
public:
|
||||
using ValueType = Wt::Dbo::dbo_default_traits::IdType;
|
||||
|
||||
IdType() = default;
|
||||
IdType(ValueType id) : _id {id} { assert(isValid()); }
|
||||
|
||||
bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); }
|
||||
std::string toString() const { assert(isValid()); return std::to_string(_id); }
|
||||
|
||||
ValueType getValue() const { return _id; }
|
||||
|
||||
bool operator==(IdType other) const { return other._id == _id; }
|
||||
bool operator!=(IdType other) const { return !(*this == other); }
|
||||
bool operator<(IdType other) const { return other._id < _id; }
|
||||
|
||||
private:
|
||||
Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()};
|
||||
};
|
||||
|
||||
struct Range
|
||||
{
|
||||
@@ -70,5 +86,91 @@ namespace Database
|
||||
Internal = 0,
|
||||
ListenBrainz = 1,
|
||||
};
|
||||
|
||||
// Do not change enum values!
|
||||
enum class UserType
|
||||
{
|
||||
REGULAR = 0,
|
||||
ADMIN = 1,
|
||||
DEMO = 2,
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class ObjectPtr
|
||||
{
|
||||
public:
|
||||
ObjectPtr() = default;
|
||||
ObjectPtr(Wt::Dbo::ptr<T> obj) : _obj {obj} {}
|
||||
|
||||
const T* operator->() const { return _obj.get(); }
|
||||
operator bool() const { return _obj.get(); }
|
||||
bool operator!() const { return !_obj.get(); }
|
||||
|
||||
auto modify() { return _obj.modify(); }
|
||||
void remove() { _obj.remove(); }
|
||||
|
||||
private:
|
||||
template <typename, typename> friend class Object;
|
||||
Wt::Dbo::ptr<T> _obj;
|
||||
};
|
||||
|
||||
template <typename T, typename ObjectIdType>
|
||||
class Object : public Wt::Dbo::Dbo<T>
|
||||
{
|
||||
static_assert(std::is_base_of_v<Database::IdType, ObjectIdType>);
|
||||
static_assert(!std::is_same_v<Database::IdType, ObjectIdType>);
|
||||
|
||||
public:
|
||||
using pointer = ObjectPtr<T>;
|
||||
using IdType = ObjectIdType;
|
||||
|
||||
IdType getId() const { return Wt::Dbo::Dbo<T>::self()->Wt::Dbo::template Dbo<T>::id(); }
|
||||
|
||||
// catch some misuses
|
||||
typename Wt::Dbo::dbo_traits<T>::IdType id() const = delete;
|
||||
|
||||
protected:
|
||||
// Can get raw dbo ptr only from Objects
|
||||
template <typename SomeObject>
|
||||
static
|
||||
Wt::Dbo::ptr<SomeObject> getDboPtr(ObjectPtr<SomeObject> ptr) { return ptr._obj; }
|
||||
};
|
||||
}
|
||||
|
||||
// TODO factorize hash with std::enable_if
|
||||
#define LMS_DECLARE_IDTYPE(name) \
|
||||
namespace Database { \
|
||||
class name : public IdType \
|
||||
{ \
|
||||
public: \
|
||||
using IdType::IdType; \
|
||||
};\
|
||||
} \
|
||||
namespace std \
|
||||
{ \
|
||||
template<> \
|
||||
class hash<Database::name> \
|
||||
{ \
|
||||
public: \
|
||||
size_t operator()(Database::name id) const \
|
||||
{ \
|
||||
return std::hash<Database::name::ValueType>()(id.getValue()); \
|
||||
} \
|
||||
}; \
|
||||
} // ns std
|
||||
|
||||
LMS_DECLARE_IDTYPE(ArtistId)
|
||||
LMS_DECLARE_IDTYPE(AuthTokenId)
|
||||
LMS_DECLARE_IDTYPE(ClusterId)
|
||||
LMS_DECLARE_IDTYPE(ClusterTypeId)
|
||||
LMS_DECLARE_IDTYPE(ReleaseId)
|
||||
LMS_DECLARE_IDTYPE(ScanSettingsId)
|
||||
LMS_DECLARE_IDTYPE(TrackArtistLinkId)
|
||||
LMS_DECLARE_IDTYPE(TrackBookmarkId)
|
||||
LMS_DECLARE_IDTYPE(TrackFeaturesId)
|
||||
LMS_DECLARE_IDTYPE(TrackId)
|
||||
LMS_DECLARE_IDTYPE(TrackListId)
|
||||
LMS_DECLARE_IDTYPE(TrackListEntryId)
|
||||
LMS_DECLARE_IDTYPE(UserId)
|
||||
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -39,24 +39,21 @@ class TrackList;
|
||||
class Track;
|
||||
|
||||
class User;
|
||||
class AuthToken
|
||||
class AuthToken : public Object<AuthToken, AuthTokenId>
|
||||
{
|
||||
public:
|
||||
|
||||
using pointer = Wt::Dbo::ptr<AuthToken>;
|
||||
|
||||
AuthToken() = default;
|
||||
AuthToken(const std::string& value, const Wt::WDateTime& expiry, Wt::Dbo::ptr<User> user);
|
||||
AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
|
||||
|
||||
// Utility
|
||||
static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, Wt::Dbo::ptr<User> user);
|
||||
static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, ObjectPtr<User> user);
|
||||
static void removeExpiredTokens(Session& session, const Wt::WDateTime& now);
|
||||
static pointer getByValue(Session& session, const std::string& value);
|
||||
static pointer getById(Session& session, IdType tokenId);
|
||||
static pointer getById(Session& session, AuthTokenId tokenId);
|
||||
|
||||
// Accessors
|
||||
const Wt::WDateTime& getExpiry() const { return _expiry; }
|
||||
Wt::Dbo::ptr<User> getUser() const { return _user; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
const std::string& getValue() const { return _value; }
|
||||
|
||||
template<class Action>
|
||||
@@ -75,20 +72,9 @@ class AuthToken
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
|
||||
class User : public Wt::Dbo::Dbo<User>
|
||||
class User : public Object<User, UserId>
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<User>;
|
||||
|
||||
|
||||
// Do not change enum values!
|
||||
enum class Type
|
||||
{
|
||||
REGULAR = 0,
|
||||
ADMIN = 1,
|
||||
DEMO = 2,
|
||||
};
|
||||
|
||||
struct PasswordHash
|
||||
{
|
||||
std::string salt;
|
||||
@@ -129,17 +115,16 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists};
|
||||
static inline const Scrobbler defaultScrobbler {Scrobbler::Internal};
|
||||
|
||||
|
||||
User() = default;
|
||||
User(std::string_view loginName);
|
||||
|
||||
// utility
|
||||
static pointer create(Session& session, std::string_view loginName);
|
||||
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getById(Session& session, UserId id);
|
||||
static pointer getByLoginName(Session& session, std::string_view loginName);
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<UserId> getAllIds(Session& session);
|
||||
static pointer getDemo(Session& session);
|
||||
static std::size_t getCount(Session& session);
|
||||
|
||||
@@ -152,7 +137,7 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
// write
|
||||
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
|
||||
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
|
||||
void setType(Type type) { _type = type; }
|
||||
void setType(UserType type) { _type = type; }
|
||||
void setSubsonicTranscodeEnable(bool value) { _subsonicTranscodeEnable = value; }
|
||||
void setSubsonicTranscodeFormat(AudioFormat encoding) { _subsonicTranscodeFormat = encoding; }
|
||||
void setSubsonicTranscodeBitrate(Bitrate bitrate);
|
||||
@@ -166,8 +151,9 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
void setListenBrainzToken(const std::optional<UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; }
|
||||
|
||||
// read
|
||||
bool isAdmin() const { return _type == Type::ADMIN; }
|
||||
bool isDemo() const { return _type == Type::DEMO; }
|
||||
bool isAdmin() const { return _type == UserType::ADMIN; }
|
||||
bool isDemo() const { return _type == UserType::DEMO; }
|
||||
UserType getType() const { return _type; }
|
||||
bool getSubsonicTranscodeEnable() const { return _subsonicTranscodeEnable; }
|
||||
AudioFormat getSubsonicTranscodeFormat() const { return _subsonicTranscodeFormat; }
|
||||
Bitrate getSubsonicTranscodeBitrate() const { return _subsonicTranscodeBitrate; }
|
||||
@@ -179,20 +165,20 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
|
||||
|
||||
Wt::Dbo::ptr<TrackList> getQueuedTrackList(Session& session) const;
|
||||
ObjectPtr<TrackList> getQueuedTrackList(Session& session) const;
|
||||
|
||||
void starArtist(Wt::Dbo::ptr<Artist> artist);
|
||||
void unstarArtist(Wt::Dbo::ptr<Artist> artist);
|
||||
bool hasStarredArtist(Wt::Dbo::ptr<Artist> artist) const;
|
||||
void starArtist(ObjectPtr<Artist> artist);
|
||||
void unstarArtist(ObjectPtr<Artist> artist);
|
||||
bool hasStarredArtist(ObjectPtr<Artist> artist) const;
|
||||
|
||||
void starRelease(Wt::Dbo::ptr<Release> release);
|
||||
void unstarRelease(Wt::Dbo::ptr<Release> release);
|
||||
bool hasStarredRelease(Wt::Dbo::ptr<Release> release) const;
|
||||
void starRelease(ObjectPtr<Release> release);
|
||||
void unstarRelease(ObjectPtr<Release> release);
|
||||
bool hasStarredRelease(ObjectPtr<Release> release) const;
|
||||
|
||||
// Stars
|
||||
void starTrack(Wt::Dbo::ptr<Track> track);
|
||||
void unstarTrack(Wt::Dbo::ptr<Track> track);
|
||||
bool hasStarredTrack(Wt::Dbo::ptr<Track> track) const;
|
||||
void starTrack(ObjectPtr<Track> track);
|
||||
void unstarTrack(ObjectPtr<Track> track);
|
||||
bool hasStarredTrack(ObjectPtr<Track> track) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
@@ -233,7 +219,7 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
std::string _listenbrainzToken; // Musicbrainz Identifier
|
||||
|
||||
// Admin defined settings
|
||||
Type _type {Type::REGULAR};
|
||||
UserType _type {UserType::REGULAR};
|
||||
|
||||
// User defined settings
|
||||
SubsonicArtistListMode _subsonicArtistListMode {defaultSubsonicArtistListMode};
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
add_library(lmsmetadata SHARED
|
||||
impl/AvFormatParser.cpp
|
||||
impl/TagLibParser.cpp
|
||||
impl/Utils.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsmetadata INTERFACE
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
@@ -186,12 +186,12 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
|| tag == "YEAR"
|
||||
|| tag == "WM/Year")
|
||||
{
|
||||
track.year = StringUtils::readAs<int>(value);
|
||||
track.date = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "TDOR" // Original release time (ID3v2 2.4)
|
||||
|| tag == "TORY") // Original release year
|
||||
{
|
||||
track.originalYear = StringUtils::readAs<int>(value);
|
||||
track.originalDate = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "ACOUSTID ID")
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
@@ -214,18 +214,26 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
||||
}
|
||||
}
|
||||
else if (tag == "DATE")
|
||||
track.year = StringUtils::readAs<int>(value);
|
||||
else if (tag == "ORIGINALDATE" && !track.originalYear)
|
||||
{
|
||||
// Lower priority than ORIGINALYEAR
|
||||
track.originalYear = StringUtils::readAs<int>(value);
|
||||
// Higher priority than YEAR
|
||||
if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid())
|
||||
track.date = date;
|
||||
}
|
||||
else if (tag == "ORIGINALYEAR")
|
||||
else if (tag == "YEAR" && !track.date.isValid())
|
||||
{
|
||||
// Higher priority than ORIGINALDATE
|
||||
auto originalYear = StringUtils::readAs<int>(value);
|
||||
if (originalYear)
|
||||
track.originalYear = originalYear;
|
||||
// lower priority than DATE
|
||||
track.date = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "ORIGINALDATE")
|
||||
{
|
||||
// Higher priority than ORIGINALYEAR
|
||||
if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid())
|
||||
track.originalDate = date;
|
||||
}
|
||||
else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid())
|
||||
{
|
||||
// Lower priority than ORIGINALDATE
|
||||
track.originalDate = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "METADATA_BLOCK_PICTURE")
|
||||
track.hasCover = true;
|
||||
@@ -260,7 +268,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
TagLib::FileRef f {p.string().c_str(),
|
||||
true, // read audio properties
|
||||
TagLib::AudioProperties::Fast};
|
||||
TagLib::AudioProperties::Fast}; // TODO parametrize this
|
||||
|
||||
if (f.isNull())
|
||||
{
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "Utils.hpp"
|
||||
#include <ctime>
|
||||
#include <string_view>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
namespace MetaData::Utils
|
||||
{
|
||||
Wt::WDate
|
||||
parseDate(const std::string& dateStr)
|
||||
{
|
||||
static constexpr const char* formats[]
|
||||
{
|
||||
"%Y-%m-%d",
|
||||
"%Y/%m/%d",
|
||||
};
|
||||
|
||||
for (const char* format : formats)
|
||||
{
|
||||
std::tm tm = {};
|
||||
std::stringstream ss {dateStr};
|
||||
ss >> std::get_time(&tm, format);
|
||||
if (ss.fail())
|
||||
continue;
|
||||
|
||||
const Wt::WDate res
|
||||
{
|
||||
tm.tm_year + 1900, // years since 1900
|
||||
tm.tm_mon + 1, // months since January – [0, 11]
|
||||
tm.tm_mday ? tm.tm_mday : 1 // day of the month – [1, 31]
|
||||
};
|
||||
if (!res.isValid())
|
||||
continue;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2016 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 <Wt/WDate.h>
|
||||
|
||||
namespace MetaData::Utils
|
||||
{
|
||||
Wt::WDate parseDate(const std::string& dateStr);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
namespace MetaData
|
||||
@@ -56,21 +57,21 @@ namespace MetaData
|
||||
|
||||
struct Track
|
||||
{
|
||||
std::vector<Artist> artists;
|
||||
std::vector<Artist> albumArtists;
|
||||
std::string title;
|
||||
std::optional<UUID> trackMBID;
|
||||
std::optional<UUID> recordingMBID;
|
||||
std::optional<Album> album;
|
||||
Clusters clusters;
|
||||
std::vector<Artist> artists;
|
||||
std::vector<Artist> albumArtists;
|
||||
std::string title;
|
||||
std::optional<UUID> trackMBID;
|
||||
std::optional<UUID> recordingMBID;
|
||||
std::optional<Album> album;
|
||||
Clusters clusters;
|
||||
std::chrono::milliseconds duration;
|
||||
std::optional<std::size_t> trackNumber;
|
||||
std::optional<std::size_t> totalTrack;
|
||||
std::optional<std::size_t> discNumber;
|
||||
std::optional<std::size_t> totalDisc;
|
||||
std::optional<int> year;
|
||||
std::optional<int> originalYear;
|
||||
bool hasCover {};
|
||||
Wt::WDate date;
|
||||
Wt::WDate originalDate;
|
||||
bool hasCover {};
|
||||
std::vector<AudioStream> audioStreams;
|
||||
std::optional<UUID> acoustID;
|
||||
std::string copyright;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
include(GoogleTest)
|
||||
|
||||
add_executable(test-metadata
|
||||
Metadata.cpp
|
||||
Utils.cpp
|
||||
)
|
||||
|
||||
target_include_directories(test-metadata PRIVATE
|
||||
../impl
|
||||
)
|
||||
|
||||
target_link_libraries(test-metadata PRIVATE
|
||||
lmsmetadata
|
||||
GTest::GTest
|
||||
)
|
||||
|
||||
gtest_discover_tests(test-metadata)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 <gtest/gtest.h>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2019 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 <gtest/gtest.h>
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
TEST(MetaData, parseDate)
|
||||
{
|
||||
using namespace MetaData::Utils;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string str;
|
||||
Wt::WDate result;
|
||||
} testCases []
|
||||
{
|
||||
{ "1995-05-09", Wt::WDate {1995, 5, 9} },
|
||||
{ "1995-01-01", Wt::WDate {1995, 1, 1} },
|
||||
{ "1900-01-01", Wt::WDate {1900, 1, 1} },
|
||||
{ "1899-01-01", Wt::WDate {1899, 1, 1} },
|
||||
{ "1899-12-31", Wt::WDate {1899, 12, 31} },
|
||||
{ "1899-11-30", Wt::WDate {1899, 11, 30} },
|
||||
{ "1500-11-30", Wt::WDate {1500, 11, 30} },
|
||||
{ "1000-11-30", Wt::WDate {1000, 11, 30} },
|
||||
{ "1899-11-31", Wt::WDate {} }, // invalid day
|
||||
{ "1899-13-01", Wt::WDate {} }, // invalid month
|
||||
{ "1899-11", Wt::WDate {1899, 11, 1} }, // missing day
|
||||
{ "1899", Wt::WDate {1899, 1, 1} }, // missing month and days
|
||||
{ "1600", Wt::WDate {1600, 1, 1} }, // missing month and days
|
||||
{ "1995/05/09", Wt::WDate {1995, 5, 9} },
|
||||
{ "1995/01/01", Wt::WDate {1995, 1, 1} },
|
||||
{ "1900/01/01", Wt::WDate {1900, 1, 1} },
|
||||
{ "1899/01/01", Wt::WDate {1899, 1, 1} },
|
||||
{ "1899/12/31", Wt::WDate {1899, 12, 31} },
|
||||
{ "1899/11/30", Wt::WDate {1899, 11, 30} },
|
||||
{ "1500/11/30", Wt::WDate {1500, 11, 30} },
|
||||
{ "1000/11/30", Wt::WDate {1000, 11, 30} },
|
||||
{ "1899/11/31", Wt::WDate {} }, // invalid day
|
||||
{ "1899/13/01", Wt::WDate {} }, // invalid month
|
||||
{ "1899/11", Wt::WDate {1899, 11, 1} }, // missing day
|
||||
{ "1899", Wt::WDate {1899, 1, 1} }, // missing month and days
|
||||
{ "1600", Wt::WDate {1600, 1, 1} }, // missing month and days
|
||||
{ "1995/05-09", Wt::WDate {} }, // invalid mixup separators
|
||||
{ "1995-05/09", Wt::WDate {} }, // invalid mixup separators
|
||||
};
|
||||
|
||||
|
||||
for (const TestCase& testCase : testCases)
|
||||
{
|
||||
const Wt::WDate parsed {parseDate(testCase.str)};
|
||||
|
||||
EXPECT_EQ(parsed.year(), testCase.result.year()) << " str was '" << testCase.str << "'";
|
||||
EXPECT_EQ(parsed.month(), testCase.result.month()) << " str was '" << testCase.str << "'";
|
||||
EXPECT_EQ(parsed.day(), testCase.result.day()) << " str was '" << testCase.str << "'";
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
add_library(lmsrecommendation SHARED
|
||||
impl/clusters/ClustersClassifier.cpp
|
||||
impl/features/FeaturesClassifierCache.cpp
|
||||
impl/features/FeaturesClassifier.cpp
|
||||
impl/features/FeaturesEngineCache.cpp
|
||||
impl/features/FeaturesEngine.cpp
|
||||
impl/features/FeaturesDefs.cpp
|
||||
impl/Engine.cpp
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "ClustersClassifierCreator.hpp"
|
||||
#include "FeaturesClassifierCreator.hpp"
|
||||
#include "FeaturesEngineCreator.hpp"
|
||||
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Session.hpp"
|
||||
@@ -45,7 +45,7 @@ createClassifier(ClassifierType type)
|
||||
break;
|
||||
|
||||
case ClassifierType::Features:
|
||||
return createFeaturesClassifier();
|
||||
return createFeaturesEngine();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -63,10 +63,10 @@ Engine::Engine(Database::Db& db)
|
||||
{
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount)
|
||||
Engine::TrackContainer
|
||||
Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId trackListId, std::size_t maxCount)
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
TrackContainer res;
|
||||
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
for (const auto& classifierName : _classifierPriorities)
|
||||
@@ -83,10 +83,10 @@ Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::IdTy
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
Engine::getSimilarTracks(Database::Session& dbSession, const std::unordered_set<Database::IdType>& trackIds, std::size_t maxCount)
|
||||
Engine::TrackContainer
|
||||
Engine::getSimilarTracks(Database::Session& dbSession, const std::vector<Database::TrackId>& trackIds, std::size_t maxCount)
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
TrackContainer res;
|
||||
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
for (ClassifierType classifierType : _classifierPriorities)
|
||||
@@ -107,10 +107,10 @@ Engine::getSimilarTracks(Database::Session& dbSession, const std::unordered_set<
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releaseId, std::size_t maxCount)
|
||||
Engine::ReleaseContainer
|
||||
Engine::getSimilarReleases(Database::Session& dbSession, Database::ReleaseId releaseId, std::size_t maxCount)
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
ReleaseContainer res;
|
||||
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
for (ClassifierType classifierType : _classifierPriorities)
|
||||
@@ -131,13 +131,10 @@ Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releas
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
Engine::getSimilarArtists(Database::Session& dbSession,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount)
|
||||
Engine::ArtistContainer
|
||||
Engine::getSimilarArtists(Database::Session& dbSession, Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount)
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
ArtistContainer res;
|
||||
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
for (ClassifierType classifierType : _classifierPriorities)
|
||||
|
||||
@@ -55,12 +55,13 @@ namespace Recommendation
|
||||
private:
|
||||
void load(bool forceReload, const ProgressCallback& progressCallback) override;
|
||||
void cancelLoad() override;
|
||||
void requestCancelLoad() override {};
|
||||
|
||||
ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override;
|
||||
ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) override;
|
||||
ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override;
|
||||
ResultContainer getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) override;
|
||||
ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) override;
|
||||
ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) override;
|
||||
ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
|
||||
Database::ArtistId artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) override;
|
||||
|
||||
@@ -79,7 +80,6 @@ namespace Recommendation
|
||||
using ClassifierContainer = std::unordered_map<ClassifierType, std::unique_ptr<IClassifier>>;
|
||||
ClassifierContainer _classifiers;
|
||||
std::vector<ClassifierType> _classifierPriorities; // ordered by priority
|
||||
|
||||
};
|
||||
|
||||
} // ns Recommendation
|
||||
|
||||
+1
-1
@@ -24,6 +24,6 @@
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
std::unique_ptr<IClassifier> createFeaturesClassifier();
|
||||
std::unique_ptr<IClassifier> createFeaturesEngine();
|
||||
}
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "recommendation/IRecommendation.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
namespace Database
|
||||
@@ -34,7 +35,7 @@ namespace Database
|
||||
namespace Recommendation
|
||||
{
|
||||
|
||||
class IClassifier
|
||||
class IClassifier : public IRecommendation
|
||||
{
|
||||
public:
|
||||
virtual ~IClassifier() = default;
|
||||
@@ -50,13 +51,14 @@ namespace Recommendation
|
||||
virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0;
|
||||
virtual void requestCancelLoad() = 0;
|
||||
|
||||
using ResultContainer = std::unordered_set<Database::IdType>;
|
||||
template <typename IdType>
|
||||
using ResultContainer = std::vector<IdType>;
|
||||
|
||||
virtual ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
virtual ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
|
||||
Database::ArtistId artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -33,23 +33,23 @@ std::unique_ptr<IClassifier> createClustersClassifier()
|
||||
return std::make_unique<ClusterClassifier>();
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::unordered_set<Database::IdType>& trackIds, std::size_t maxCount) const
|
||||
IClassifier::ResultContainer<Database::TrackId>
|
||||
ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
|
||||
{
|
||||
ResultContainer<Database::TrackId> res;
|
||||
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
|
||||
const auto tracks {Database::Track::getSimilarTracks(dbSession, trackIds, 0, maxCount)};
|
||||
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
|
||||
|
||||
std::unordered_set<Database::IdType> res;
|
||||
std::transform(std::cbegin(tracks), std::cend(tracks), std::inserter(res, std::end(res)),
|
||||
[](const auto& track) { return track.id(); });
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const
|
||||
IClassifier::ResultContainer<Database::TrackId>
|
||||
ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
ResultContainer<Database::TrackId> res;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
@@ -58,16 +58,15 @@ ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Dat
|
||||
return res;
|
||||
|
||||
const auto tracks {trackList->getSimilarTracks(0, maxCount)};
|
||||
std::transform(std::cbegin(tracks), std::cend(tracks), std::inserter(res, std::end(res)),
|
||||
[](const Database::Track::pointer& track) { return track.id(); });
|
||||
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::IdType releaseId, std::size_t maxCount) const
|
||||
IClassifier::ResultContainer<Database::ReleaseId>
|
||||
ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::ReleaseId releaseId, std::size_t maxCount) const
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
ResultContainer<Database::ReleaseId> res;
|
||||
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
|
||||
@@ -76,19 +75,18 @@ ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::Id
|
||||
return res;
|
||||
|
||||
const auto releases {release->getSimilarReleases(0, maxCount)};
|
||||
std::transform(std::cbegin(releases), std::cend(releases), std::inserter(res, std::end(res)),
|
||||
[](const auto& release) { return release.id(); });
|
||||
std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const auto& release) { return release->getId(); });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
IClassifier::ResultContainer<Database::ArtistId>
|
||||
ClusterClassifier::getSimilarArtists(Database::Session& dbSession,
|
||||
Database::IdType artistId,
|
||||
Database::ArtistId artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> artistLinkTypes,
|
||||
std::size_t maxCount) const
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
ResultContainer<Database::ArtistId> res;
|
||||
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
|
||||
@@ -97,8 +95,7 @@ ClusterClassifier::getSimilarArtists(Database::Session& dbSession,
|
||||
return res;
|
||||
|
||||
const auto artists {artist->getSimilarArtists(artistLinkTypes, Database::Range {0, maxCount})};
|
||||
std::transform(std::cbegin(artists), std::cend(artists), std::inserter(res, std::end(res)),
|
||||
[](const auto& artist) { return artist.id(); });
|
||||
std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(res), [](const auto& artist) { return artist->getId(); });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ namespace Recommendation
|
||||
bool load(Database::Session&, bool, const ProgressCallback&) override { return true; }
|
||||
void requestCancelLoad() override {}
|
||||
|
||||
ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override;
|
||||
ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const override;
|
||||
ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override;
|
||||
ResultContainer getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const override;
|
||||
ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
|
||||
ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
||||
ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
|
||||
Database::ArtistId artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) const override;
|
||||
};
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <functional>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "som/DataNormalizer.hpp"
|
||||
#include "som/Network.hpp"
|
||||
#include "FeaturesClassifierCache.hpp"
|
||||
#include "FeaturesDefs.hpp"
|
||||
#include "IClassifier.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
using FeatureWeight = double;
|
||||
|
||||
class FeaturesClassifier : public IClassifier
|
||||
{
|
||||
public:
|
||||
FeaturesClassifier() = default;
|
||||
FeaturesClassifier(const FeaturesClassifier&) = delete;
|
||||
FeaturesClassifier(FeaturesClassifier&&) = delete;
|
||||
FeaturesClassifier& operator=(const FeaturesClassifier&) = delete;
|
||||
FeaturesClassifier& operator=(FeaturesClassifier&&) = delete;
|
||||
|
||||
using FeaturesFetchFunc = std::function<std::optional<std::unordered_map<std::string, std::vector<double>>>(Database::IdType /*trackId*/, const std::unordered_set<std::string>& /*features*/)>;
|
||||
// Default is to retrieve the features from the database (may be slow).
|
||||
// Use this only if you want to train different searchers with some cached data
|
||||
static void setFeaturesFetchFunc(FeaturesFetchFunc func) { _featuresFetchFunc = func; }
|
||||
|
||||
static const FeatureSettingsMap& getDefaultTrainFeatureSettings();
|
||||
|
||||
private:
|
||||
|
||||
std::string_view getName() const override { return "Features"; }
|
||||
|
||||
bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) override;
|
||||
void requestCancelLoad() override;
|
||||
|
||||
std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override;
|
||||
std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const override;
|
||||
std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override;
|
||||
std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) const override;
|
||||
|
||||
bool loadFromCache(Database::Session& session, const FeaturesClassifierCache& cache);
|
||||
|
||||
// Use training (may be very slow)
|
||||
struct TrainSettings
|
||||
{
|
||||
std::size_t iterationCount {10};
|
||||
float sampleCountPerNeuron {4};
|
||||
FeatureSettingsMap featureSettingsMap;
|
||||
};
|
||||
bool loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback);
|
||||
|
||||
using ObjectPositions = std::unordered_map<Database::IdType, std::unordered_set<SOM::Position>>;
|
||||
using MatrixOfObjects = SOM::Matrix<std::unordered_set<Database::IdType>>;
|
||||
|
||||
bool load(Database::Session& session,
|
||||
SOM::Network network,
|
||||
const ObjectPositions& tracksPosition);
|
||||
|
||||
FeaturesClassifierCache toCache() const;
|
||||
|
||||
static std::unordered_set<SOM::Position> getMatchingRefVectorsPosition(const std::unordered_set<Database::IdType>& ids, const ObjectPositions& objectPositions);
|
||||
static std::unordered_set<Database::IdType> getObjectsIds(const std::unordered_set<SOM::Position>& positionSet, const MatrixOfObjects& objectsMap);
|
||||
|
||||
std::unordered_set<Database::IdType> getSimilarObjects(const std::unordered_set<Database::IdType>& ids,
|
||||
const SOM::Matrix<std::unordered_set<Database::IdType>>& objectsMap,
|
||||
const ObjectPositions& objectPosition,
|
||||
std::size_t maxCount) const;
|
||||
|
||||
bool _loadCancelled {};
|
||||
std::unique_ptr<SOM::Network> _network;
|
||||
double _networkRefVectorsDistanceMedian {};
|
||||
|
||||
ObjectPositions _artistPositions;
|
||||
std::unordered_map<Database::TrackArtistLinkType, MatrixOfObjects> _artistsMap;
|
||||
|
||||
MatrixOfObjects _releasesMap;
|
||||
ObjectPositions _releasePositions;
|
||||
|
||||
MatrixOfObjects _tracksMap;
|
||||
ObjectPositions _trackPositions;
|
||||
|
||||
static inline FeaturesFetchFunc _featuresFetchFunc;
|
||||
};
|
||||
|
||||
} // ns Recommendation
|
||||
+86
-177
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FeaturesClassifier.hpp"
|
||||
#include "FeaturesEngine.hpp"
|
||||
|
||||
#include <numeric>
|
||||
|
||||
@@ -35,13 +35,13 @@
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
std::unique_ptr<IClassifier> createFeaturesClassifier()
|
||||
std::unique_ptr<IClassifier> createFeaturesEngine()
|
||||
{
|
||||
return std::make_unique<FeaturesClassifier>();
|
||||
return std::make_unique<FeaturesEngine>();
|
||||
}
|
||||
|
||||
const FeatureSettingsMap&
|
||||
FeaturesClassifier::getDefaultTrainFeatureSettings()
|
||||
FeaturesEngine::getDefaultTrainFeatureSettings()
|
||||
{
|
||||
static const FeatureSettingsMap defaultTrainFeatureSettings
|
||||
{
|
||||
@@ -57,16 +57,16 @@ FeaturesClassifier::getDefaultTrainFeatureSettings()
|
||||
|
||||
static
|
||||
std::optional<FeatureValuesMap>
|
||||
getTrackFeatureValues(FeaturesClassifier::FeaturesFetchFunc func, Database::IdType trackId, const std::unordered_set<FeatureName>& featureNames)
|
||||
getTrackFeatureValues(FeaturesEngine::FeaturesFetchFunc func, Database::TrackId trackId, const std::unordered_set<FeatureName>& featureNames)
|
||||
{
|
||||
return func(trackId, featureNames);
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<FeatureValuesMap>
|
||||
getTrackFeatureValuesFromDb(Database::Session& session, Database::IdType trackId, const std::unordered_set<FeatureName>& featureNames)
|
||||
getTrackFeatureValuesFromDb(Database::Session& session, Database::TrackId trackId, const std::unordered_set<FeatureName>& featureNames)
|
||||
{
|
||||
auto func = [&](Database::IdType trackId, const std::unordered_set<FeatureName>& featureNames)
|
||||
auto func = [&](Database::TrackId trackId, const std::unordered_set<FeatureName>& featureNames)
|
||||
{
|
||||
std::optional<FeatureValuesMap> res;
|
||||
|
||||
@@ -128,7 +128,7 @@ getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t
|
||||
}
|
||||
|
||||
bool
|
||||
FeaturesClassifier::loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
|
||||
FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier...";
|
||||
|
||||
@@ -141,7 +141,7 @@ FeaturesClassifier::loadFromTraining(Database::Session& session, const TrainSett
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
|
||||
|
||||
std::vector<Database::IdType> trackIds;
|
||||
std::vector<Database::TrackId> trackIds;
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
@@ -151,13 +151,13 @@ FeaturesClassifier::loadFromTraining(Database::Session& session, const TrainSett
|
||||
}
|
||||
|
||||
std::vector<SOM::InputVector> samples;
|
||||
std::vector<Database::IdType> samplesTrackIds;
|
||||
std::vector<Database::TrackId> samplesTrackIds;
|
||||
|
||||
samples.reserve(trackIds.size());
|
||||
samplesTrackIds.reserve(trackIds.size());
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features...";
|
||||
for (Database::IdType trackId : trackIds)
|
||||
for (Database::TrackId trackId : trackIds)
|
||||
{
|
||||
if (_loadCancelled)
|
||||
return false;
|
||||
@@ -223,7 +223,7 @@ FeaturesClassifier::loadFromTraining(Database::Session& session, const TrainSett
|
||||
return false;
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
|
||||
ObjectPositions trackPositions;
|
||||
TrackPositions trackPositions;
|
||||
for (std::size_t i {}; i < samples.size(); ++i)
|
||||
{
|
||||
if (_loadCancelled)
|
||||
@@ -231,7 +231,7 @@ FeaturesClassifier::loadFromTraining(Database::Session& session, const TrainSett
|
||||
|
||||
const SOM::Position position {network.getClosestRefVectorPosition(samples[i])};
|
||||
|
||||
trackPositions[samplesTrackIds[i]].insert(position);
|
||||
trackPositions[samplesTrackIds[i]].push_back(position);
|
||||
}
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE";
|
||||
@@ -240,28 +240,25 @@ FeaturesClassifier::loadFromTraining(Database::Session& session, const TrainSett
|
||||
}
|
||||
|
||||
bool
|
||||
FeaturesClassifier::loadFromCache(Database::Session& session, const FeaturesClassifierCache& cache)
|
||||
FeaturesEngine::loadFromCache(Database::Session& session, const FeaturesEngineCache& cache)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache...";
|
||||
|
||||
return load(session, std::move(cache._network), cache._trackPositions);
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
FeaturesClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount) const
|
||||
IClassifier::ResultContainer<Database::TrackId>
|
||||
FeaturesEngine::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId trackListId, std::size_t maxCount) const
|
||||
{
|
||||
const std::unordered_set<Database::IdType> trackIds {[&]
|
||||
const std::vector<Database::TrackId> trackIds {[&]
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
std::vector<Database::TrackId> res;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)};
|
||||
if (trackList)
|
||||
{
|
||||
const std::vector<Database::IdType> orderedTrackIds {trackList->getTrackIds()};
|
||||
res = std::unordered_set<Database::IdType>(std::cbegin(orderedTrackIds), std::cend(orderedTrackIds));
|
||||
}
|
||||
res = trackList->getTrackIds();
|
||||
|
||||
return res;
|
||||
}()};
|
||||
@@ -269,72 +266,64 @@ FeaturesClassifier::getSimilarTracksFromTrackList(Database::Session& session, Da
|
||||
return getSimilarTracks(session, trackIds, maxCount);
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
FeaturesClassifier::getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksIds, std::size_t maxCount) const
|
||||
std::vector<Database::TrackId>
|
||||
FeaturesEngine::getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksIds, std::size_t maxCount) const
|
||||
{
|
||||
auto similarTrackIds {getSimilarObjects(tracksIds, _tracksMap, _trackPositions, maxCount)};
|
||||
if (!similarTrackIds.empty())
|
||||
auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)};
|
||||
|
||||
{
|
||||
// Report only existing ids
|
||||
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
for (auto it {std::begin(similarTrackIds)}; it != std::end(similarTrackIds);)
|
||||
{
|
||||
const Database::IdType trackId {*it};
|
||||
if (!Database::Track::getById(session, trackId))
|
||||
it = similarTrackIds.erase(it);
|
||||
else
|
||||
it++;
|
||||
}
|
||||
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
|
||||
[&](Database::TrackId trackId)
|
||||
{
|
||||
return Database::Track::getById(session, trackId); // TODO exists
|
||||
}), std::end(similarTrackIds));
|
||||
}
|
||||
|
||||
return similarTrackIds;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
FeaturesClassifier::getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const
|
||||
std::vector<Database::ReleaseId>
|
||||
FeaturesEngine::getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const
|
||||
{
|
||||
auto similarReleaseIds {getSimilarObjects({releaseId}, _releasesMap, _releasePositions, maxCount)};
|
||||
if (!similarReleaseIds.empty())
|
||||
auto similarReleaseIds {getSimilarObjects<Database::ReleaseId>({releaseId}, _releaseMatrix, _releasePositions, maxCount)};
|
||||
|
||||
{
|
||||
// Report only existing ids
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
for (auto it {std::begin(similarReleaseIds)}; it != std::end(similarReleaseIds);)
|
||||
{
|
||||
const Database::IdType similarReleaseId {*it};
|
||||
if (!Database::Release::getById(session, similarReleaseId))
|
||||
it = similarReleaseIds.erase(it);
|
||||
else
|
||||
it++;
|
||||
}
|
||||
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
|
||||
[&](Database::ReleaseId releaseId)
|
||||
{
|
||||
return Database::Release::getById(session, releaseId); // TODO exists
|
||||
}), std::end(similarReleaseIds));
|
||||
}
|
||||
|
||||
return similarReleaseIds;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
FeaturesClassifier::getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
std::vector<Database::ArtistId>
|
||||
FeaturesEngine::getSimilarArtists(Database::Session& session,
|
||||
Database::ArtistId artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) const
|
||||
{
|
||||
auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType)
|
||||
{
|
||||
std::unordered_set<Database::IdType> similarArtistIds;
|
||||
std::vector<Database::ArtistId> similarArtistIds;
|
||||
|
||||
const auto itArtists {_artistsMap.find(linkType)};
|
||||
if (itArtists == std::cend(_artistsMap))
|
||||
const auto itArtists {_artistMatrix.find(linkType)};
|
||||
if (itArtists == std::cend(_artistMatrix))
|
||||
{
|
||||
return similarArtistIds;
|
||||
}
|
||||
|
||||
similarArtistIds = getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
|
||||
|
||||
return similarArtistIds;
|
||||
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
|
||||
}};
|
||||
|
||||
std::unordered_set<Database::IdType> similarArtistIds;
|
||||
std::unordered_set<Database::ArtistId> similarArtistIds;
|
||||
|
||||
for (Database::TrackArtistLinkType linkType : linkTypes)
|
||||
{
|
||||
@@ -342,44 +331,42 @@ FeaturesClassifier::getSimilarArtists(Database::Session& session,
|
||||
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
|
||||
}
|
||||
|
||||
if (!similarArtistIds.empty())
|
||||
std::vector<Database::ArtistId> res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
|
||||
|
||||
{
|
||||
// Report only existing ids
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
for (auto it {std::begin(similarArtistIds)}; it != std::end(similarArtistIds);)
|
||||
{
|
||||
const Database::IdType similarArtistId {*it};
|
||||
if (!Database::Artist::getById(session, similarArtistId))
|
||||
it = similarArtistIds.erase(it);
|
||||
else
|
||||
it++;
|
||||
}
|
||||
res.erase(std::remove_if(std::begin(res), std::end(res),
|
||||
[&](Database::ArtistId artistId)
|
||||
{
|
||||
return Database::Artist::getById(session, artistId); // TODO exists
|
||||
}), std::end(res));
|
||||
}
|
||||
|
||||
while (similarArtistIds.size() > maxCount)
|
||||
similarArtistIds.erase(Random::pickRandom(similarArtistIds));
|
||||
while (res.size() > maxCount)
|
||||
res.erase(Random::pickRandom(res));
|
||||
|
||||
return similarArtistIds;
|
||||
return res;
|
||||
}
|
||||
|
||||
FeaturesClassifierCache
|
||||
FeaturesClassifier::toCache() const
|
||||
FeaturesEngineCache
|
||||
FeaturesEngine::toCache() const
|
||||
{
|
||||
return FeaturesClassifierCache {*_network, _trackPositions};
|
||||
return FeaturesEngineCache {*_network, _trackPositions};
|
||||
}
|
||||
|
||||
bool
|
||||
FeaturesClassifier::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback)
|
||||
FeaturesEngine::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback)
|
||||
{
|
||||
if (forceReload)
|
||||
|
||||
{
|
||||
FeaturesClassifierCache::invalidate();
|
||||
FeaturesEngineCache::invalidate();
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::optional<FeaturesClassifierCache> cache {FeaturesClassifierCache::read()};
|
||||
const std::optional<FeaturesEngineCache> cache {FeaturesEngineCache::read()};
|
||||
if (cache)
|
||||
return loadFromCache(session, *cache);
|
||||
}
|
||||
@@ -395,64 +382,65 @@ FeaturesClassifier::load(Database::Session& session, bool forceReload, const Pro
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesClassifier::requestCancelLoad()
|
||||
FeaturesEngine::requestCancelLoad()
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Requesting init cancellation";
|
||||
_loadCancelled = true;
|
||||
}
|
||||
|
||||
bool
|
||||
FeaturesClassifier::load(Database::Session& session,
|
||||
FeaturesEngine::load(Database::Session& session,
|
||||
SOM::Network network,
|
||||
const ObjectPositions& tracksPosition)
|
||||
const TrackPositions& trackPositions)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
_networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian;
|
||||
|
||||
const SOM::Coordinate width {network.getWidth()};
|
||||
const SOM::Coordinate height {network.getHeight()};
|
||||
|
||||
_releasesMap = MatrixOfObjects {width, height};
|
||||
_tracksMap = MatrixOfObjects {width, height};
|
||||
_releaseMatrix = ReleaseMatrix {width, height};
|
||||
_trackMatrix = TrackMatrix {width, height};
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
|
||||
|
||||
for (auto itTrackCoord : tracksPosition)
|
||||
for (const auto& [trackId, positions] : trackPositions)
|
||||
{
|
||||
if (_loadCancelled)
|
||||
return false;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
Database::IdType trackId {itTrackCoord.first};
|
||||
const std::unordered_set<SOM::Position>& positionSet {itTrackCoord.second};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(session, trackId)};
|
||||
const Track::pointer track {Database::Track::getById(session, trackId)};
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
for (const SOM::Position& position : positionSet)
|
||||
for (const SOM::Position& position : positions)
|
||||
{
|
||||
_tracksMap[position].insert(trackId);
|
||||
_trackPositions[trackId].insert(position);
|
||||
Utils::push_back_if_not_present(_trackPositions[trackId], position);
|
||||
Utils::push_back_if_not_present(_trackMatrix[position], trackId);
|
||||
|
||||
if (track->getRelease())
|
||||
if (Release::pointer release {track->getRelease()})
|
||||
{
|
||||
_releasePositions[track->getRelease().id()].insert(position);
|
||||
_releasesMap[position].insert(track->getRelease().id());
|
||||
const ReleaseId releaseId {release->getId()};
|
||||
Utils::push_back_if_not_present(_releasePositions[releaseId], position);
|
||||
Utils::push_back_if_not_present(_releaseMatrix[position], releaseId);
|
||||
}
|
||||
for (const auto& artistLink : track->getArtistLinks())
|
||||
for (const TrackArtistLink::pointer& artistLink : track->getArtistLinks())
|
||||
{
|
||||
_artistPositions[artistLink->getArtist().id()].insert(position);
|
||||
auto itArtists {_artistsMap.find(artistLink->getType())};
|
||||
if (itArtists == std::cend(_artistsMap))
|
||||
const ArtistId artistId {artistLink->getArtist()->getId()};
|
||||
|
||||
Utils::push_back_if_not_present(_artistPositions[artistId], position);
|
||||
auto itArtists {_artistMatrix.find(artistLink->getType())};
|
||||
if (itArtists == std::cend(_artistMatrix))
|
||||
{
|
||||
auto [it, inserted] = _artistsMap.try_emplace(artistLink->getType(), MatrixOfObjects {});
|
||||
auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix {width, height});
|
||||
assert(inserted);
|
||||
itArtists = it;
|
||||
itArtists->second = MatrixOfObjects {width, height};
|
||||
}
|
||||
itArtists->second[position].insert(artistLink->getArtist().id());
|
||||
Utils::push_back_if_not_present(itArtists->second[position], artistId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -464,83 +452,4 @@ FeaturesClassifier::load(Database::Session& session,
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unordered_set<SOM::Position>
|
||||
FeaturesClassifier::getMatchingRefVectorsPosition(const std::unordered_set<Database::IdType>& ids, const ObjectPositions& objectPositions)
|
||||
{
|
||||
std::unordered_set<SOM::Position> res;
|
||||
|
||||
if (ids.empty())
|
||||
return res;
|
||||
|
||||
for (auto id : ids)
|
||||
{
|
||||
auto it = objectPositions.find(id);
|
||||
if (it == objectPositions.end())
|
||||
continue;
|
||||
|
||||
for (const auto& position : it->second)
|
||||
res.insert(position);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
FeaturesClassifier::getObjectsIds(const std::unordered_set<SOM::Position>& positionSet, const MatrixOfObjects& objectsMap)
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
|
||||
for (const auto& position : positionSet)
|
||||
{
|
||||
for (auto id : objectsMap.get(position))
|
||||
res.insert(id);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
FeaturesClassifier::getSimilarObjects(const std::unordered_set<Database::IdType>& ids,
|
||||
const MatrixOfObjects& objectsMap,
|
||||
const ObjectPositions& objectPosition,
|
||||
std::size_t maxCount) const
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
|
||||
std::unordered_set<SOM::Position> searchedRefVectorsPosition {getMatchingRefVectorsPosition(ids, objectPosition)};
|
||||
if (searchedRefVectorsPosition.empty())
|
||||
return res;
|
||||
|
||||
while (1)
|
||||
{
|
||||
std::unordered_set<Database::IdType> closestObjectIds {getObjectsIds(searchedRefVectorsPosition, objectsMap)};
|
||||
|
||||
// Remove objects that are already in input or already reported
|
||||
for (auto id : ids)
|
||||
closestObjectIds.erase(id);
|
||||
|
||||
for (auto it {std::cbegin(closestObjectIds)}; it != std::cend(closestObjectIds); ++it)
|
||||
{
|
||||
if (res.size() == maxCount)
|
||||
break;
|
||||
|
||||
res.insert(*it);
|
||||
}
|
||||
|
||||
if (res.size() == maxCount)
|
||||
break;
|
||||
|
||||
// If there is not enough objects, try again with closest neighbour until there is too much distance
|
||||
const std::optional<SOM::Position> closestRefVectorPosition {_network->getClosestRefVectorPosition(searchedRefVectorsPosition, _networkRefVectorsDistanceMedian * 0.75)};
|
||||
if (!closestRefVectorPosition)
|
||||
break;
|
||||
|
||||
searchedRefVectorsPosition.insert(closestRefVectorPosition.value());
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // ns Recommendation
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <algorithm>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "som/DataNormalizer.hpp"
|
||||
#include "som/Network.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
#include "FeaturesEngineCache.hpp"
|
||||
#include "FeaturesDefs.hpp"
|
||||
#include "IClassifier.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
using FeatureWeight = double;
|
||||
|
||||
class FeaturesEngine : public IClassifier
|
||||
{
|
||||
public:
|
||||
FeaturesEngine() = default;
|
||||
FeaturesEngine(const FeaturesEngine&) = delete;
|
||||
FeaturesEngine(FeaturesEngine&&) = delete;
|
||||
FeaturesEngine& operator=(const FeaturesEngine&) = delete;
|
||||
FeaturesEngine& operator=(FeaturesEngine&&) = delete;
|
||||
|
||||
using FeaturesFetchFunc = std::function<std::optional<std::unordered_map<std::string, std::vector<double>>>(Database::TrackId, const std::unordered_set<std::string>& /*features*/)>;
|
||||
// Default is to retrieve the features from the database (may be slow).
|
||||
// Use this only if you want to train different searchers with some cached data
|
||||
static void setFeaturesFetchFunc(FeaturesFetchFunc func) { _featuresFetchFunc = func; }
|
||||
|
||||
static const FeatureSettingsMap& getDefaultTrainFeatureSettings();
|
||||
|
||||
private:
|
||||
|
||||
std::string_view getName() const override { return "Features"; }
|
||||
|
||||
bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) override;
|
||||
void requestCancelLoad() override;
|
||||
|
||||
ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const override;
|
||||
ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
|
||||
ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
||||
ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
|
||||
Database::ArtistId artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) const override;
|
||||
|
||||
bool loadFromCache(Database::Session& session, const FeaturesEngineCache& cache);
|
||||
|
||||
// Use training (may be very slow)
|
||||
struct TrainSettings
|
||||
{
|
||||
std::size_t iterationCount {10};
|
||||
float sampleCountPerNeuron {4};
|
||||
FeatureSettingsMap featureSettingsMap;
|
||||
};
|
||||
bool loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback);
|
||||
|
||||
template <typename IdType>
|
||||
using ObjectPositions = std::unordered_map<IdType, std::vector<SOM::Position>>;
|
||||
|
||||
using ArtistPositions = ObjectPositions<Database::ArtistId>;
|
||||
using ReleasePositions = ObjectPositions<Database::ReleaseId>;
|
||||
using TrackPositions = ObjectPositions<Database::TrackId>;
|
||||
|
||||
template <typename IdType>
|
||||
using ObjectMatrix = SOM::Matrix<std::vector<IdType>>;
|
||||
using ArtistMatrix = ObjectMatrix<Database::ArtistId>;
|
||||
using ReleaseMatrix = ObjectMatrix<Database::ReleaseId>;
|
||||
using TrackMatrix = ObjectMatrix<Database::TrackId>;
|
||||
|
||||
bool load(Database::Session& session, SOM::Network network, const TrackPositions& tracksPosition);
|
||||
|
||||
FeaturesEngineCache toCache() const;
|
||||
|
||||
template <typename IdType>
|
||||
static std::vector<SOM::Position> getMatchingRefVectorsPosition(const std::vector<IdType>& ids, const ObjectPositions<IdType>& objectPositions);
|
||||
|
||||
template <typename IdType>
|
||||
static std::vector<IdType> getObjectsIds(const std::vector<SOM::Position>& positions, const ObjectMatrix<IdType>& objectsMatrix);
|
||||
|
||||
template <typename IdType>
|
||||
std::vector<IdType> getSimilarObjects(const std::vector<IdType>& ids,
|
||||
const ObjectMatrix<IdType>& objectMatrix,
|
||||
const ObjectPositions<IdType>& objectPositions,
|
||||
std::size_t maxCount) const;
|
||||
|
||||
bool _loadCancelled {};
|
||||
std::unique_ptr<SOM::Network> _network;
|
||||
double _networkRefVectorsDistanceMedian {};
|
||||
|
||||
ArtistPositions _artistPositions;
|
||||
std::unordered_map<Database::TrackArtistLinkType, ArtistMatrix> _artistMatrix;
|
||||
|
||||
ReleasePositions _releasePositions;
|
||||
ReleaseMatrix _releaseMatrix;
|
||||
|
||||
TrackPositions _trackPositions;
|
||||
TrackMatrix _trackMatrix;
|
||||
|
||||
static inline FeaturesFetchFunc _featuresFetchFunc;
|
||||
};
|
||||
|
||||
template <typename IdType>
|
||||
std::vector<SOM::Position>
|
||||
FeaturesEngine::getMatchingRefVectorsPosition(const std::vector<IdType>& ids, const ObjectPositions<IdType>& objectPositions)
|
||||
{
|
||||
std::vector<SOM::Position> res;
|
||||
|
||||
if (ids.empty())
|
||||
return res;
|
||||
|
||||
for (const IdType id : ids)
|
||||
{
|
||||
auto it = objectPositions.find(id);
|
||||
if (it == objectPositions.end())
|
||||
continue;
|
||||
|
||||
for (const SOM::Position& position : it->second)
|
||||
Utils::push_back_if_not_present(res, position);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename IdType>
|
||||
std::vector<IdType>
|
||||
FeaturesEngine::getObjectsIds(const std::vector<SOM::Position>& positions, const ObjectMatrix<IdType>& objectMatrix)
|
||||
{
|
||||
std::vector<IdType> res;
|
||||
|
||||
for (const SOM::Position& position : positions)
|
||||
{
|
||||
for (const IdType id : objectMatrix.get(position))
|
||||
Utils::push_back_if_not_present(res, id);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename IdType>
|
||||
std::vector<IdType>
|
||||
FeaturesEngine::getSimilarObjects(const std::vector<IdType>& ids,
|
||||
const ObjectMatrix<IdType>& objectMatrix,
|
||||
const ObjectPositions<IdType>& objectPositions,
|
||||
std::size_t maxCount) const
|
||||
{
|
||||
std::vector<IdType> res;
|
||||
|
||||
std::vector<SOM::Position> searchedRefVectorsPosition {getMatchingRefVectorsPosition(ids, objectPositions)};
|
||||
if (searchedRefVectorsPosition.empty())
|
||||
return res;
|
||||
|
||||
while (1)
|
||||
{
|
||||
std::vector<IdType> closestObjectIds {getObjectsIds(searchedRefVectorsPosition, objectMatrix)};
|
||||
|
||||
// Remove objects that are already in input or already reported
|
||||
closestObjectIds.erase(std::remove_if(std::begin(closestObjectIds), std::end(closestObjectIds),
|
||||
[&](IdType id)
|
||||
{
|
||||
return std::find(std::cbegin(ids), std::cend(ids), id) != std::cend(ids);
|
||||
})
|
||||
, std::end(closestObjectIds));
|
||||
|
||||
for (IdType id : closestObjectIds)
|
||||
{
|
||||
if (res.size() == maxCount)
|
||||
break;
|
||||
|
||||
Utils::push_back_if_not_present(res, id);
|
||||
}
|
||||
|
||||
if (res.size() == maxCount)
|
||||
break;
|
||||
|
||||
// If there is not enough objects, try again with closest neighbour until there is too much distance
|
||||
const std::optional<SOM::Position> closestRefVectorPosition {_network->getClosestRefVectorPosition(searchedRefVectorsPosition, _networkRefVectorsDistanceMedian * 0.75)};
|
||||
if (!closestRefVectorPosition)
|
||||
break;
|
||||
|
||||
Utils::push_back_if_not_present(searchedRefVectorsPosition, closestRefVectorPosition.value());
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // ns Recommendation
|
||||
+17
-17
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FeaturesClassifierCache.hpp"
|
||||
#include "FeaturesEngineCache.hpp"
|
||||
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
@@ -90,7 +90,7 @@ networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
|
||||
}
|
||||
|
||||
std::optional<SOM::Network>
|
||||
FeaturesClassifierCache::createNetworkFromCacheFile(const std::filesystem::path& path)
|
||||
FeaturesEngineCache::createNetworkFromCacheFile(const std::filesystem::path& path)
|
||||
{
|
||||
if (!std::filesystem::exists(path))
|
||||
return std::nullopt;
|
||||
@@ -143,19 +143,19 @@ FeaturesClassifierCache::createNetworkFromCacheFile(const std::filesystem::path&
|
||||
}
|
||||
|
||||
bool
|
||||
FeaturesClassifierCache::objectPositionToCacheFile(const ObjectPositions& objectsPosition, const std::filesystem::path& path)
|
||||
FeaturesEngineCache::objectPositionToCacheFile(const TrackPositions& trackPositions, const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
boost::property_tree::ptree root;
|
||||
|
||||
for (const auto& objectPosition : objectsPosition)
|
||||
for (const auto& [id, positions] : trackPositions)
|
||||
{
|
||||
boost::property_tree::ptree node;
|
||||
|
||||
node.put("id", objectPosition.first);
|
||||
node.put("id", id.getValue());
|
||||
|
||||
for (const auto& position : objectPosition.second)
|
||||
for (const SOM::Position& position : positions)
|
||||
{
|
||||
boost::property_tree::ptree positionNode;
|
||||
positionNode.put("x", position.x);
|
||||
@@ -177,8 +177,8 @@ FeaturesClassifierCache::objectPositionToCacheFile(const ObjectPositions& object
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<FeaturesClassifierCache::ObjectPositions>
|
||||
FeaturesClassifierCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
|
||||
std::optional<FeaturesEngineCache::TrackPositions>
|
||||
FeaturesEngineCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -188,17 +188,17 @@ FeaturesClassifierCache::createObjectPositionsFromCacheFile(const std::filesyste
|
||||
|
||||
boost::property_tree::read_xml(path.string(), root);
|
||||
|
||||
ObjectPositions res;
|
||||
TrackPositions res;
|
||||
|
||||
for (const auto& object : root.get_child("objects"))
|
||||
{
|
||||
auto id = object.second.get<Database::IdType>("id");
|
||||
const Database::TrackId id {object.second.get<Database::IdType::ValueType>("id")};
|
||||
for (const auto& position : object.second.get_child("position"))
|
||||
{
|
||||
auto x = position.second.get<SOM::Coordinate>("x");
|
||||
auto y = position.second.get<SOM::Coordinate>("y");
|
||||
|
||||
res[id].insert({x, y});
|
||||
res[id].push_back({x, y});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,14 +214,14 @@ FeaturesClassifierCache::createObjectPositionsFromCacheFile(const std::filesyste
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesClassifierCache::invalidate()
|
||||
FeaturesEngineCache::invalidate()
|
||||
{
|
||||
std::filesystem::remove(getCacheNetworkFilePath());
|
||||
std::filesystem::remove(getCacheTrackPositionsFilePath());
|
||||
}
|
||||
|
||||
std::optional<FeaturesClassifierCache>
|
||||
FeaturesClassifierCache::read()
|
||||
std::optional<FeaturesEngineCache>
|
||||
FeaturesEngineCache::read()
|
||||
{
|
||||
auto network{createNetworkFromCacheFile(getCacheNetworkFilePath())};
|
||||
if (!network)
|
||||
@@ -231,11 +231,11 @@ FeaturesClassifierCache::read()
|
||||
if (!trackPositions)
|
||||
return std::nullopt;
|
||||
|
||||
return FeaturesClassifierCache {std::move(*network), std::move(*trackPositions)};
|
||||
return FeaturesEngineCache {std::move(*network), std::move(*trackPositions)};
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesClassifierCache::write() const
|
||||
FeaturesEngineCache::write() const
|
||||
{
|
||||
std::filesystem::create_directories(Service<IConfig>::get()->getPath("working-dir") / "cache" / "features");
|
||||
|
||||
@@ -246,7 +246,7 @@ FeaturesClassifierCache::write() const
|
||||
}
|
||||
}
|
||||
|
||||
FeaturesClassifierCache::FeaturesClassifierCache(SOM::Network network, ObjectPositions trackPositions)
|
||||
FeaturesEngineCache::FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions)
|
||||
: _network {std::move(network)},
|
||||
_trackPositions {std::move(trackPositions)}
|
||||
{
|
||||
+8
-8
@@ -28,27 +28,27 @@
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
class FeaturesClassifierCache
|
||||
class FeaturesEngineCache
|
||||
{
|
||||
public:
|
||||
static void invalidate();
|
||||
|
||||
static std::optional<FeaturesClassifierCache> read();
|
||||
static std::optional<FeaturesEngineCache> read();
|
||||
void write() const;
|
||||
|
||||
private:
|
||||
using ObjectPositions = std::unordered_map<Database::IdType, std::unordered_set<SOM::Position>>;
|
||||
using TrackPositions = std::unordered_map<Database::TrackId, std::vector<SOM::Position>>;
|
||||
|
||||
FeaturesClassifierCache(SOM::Network network, ObjectPositions trackPositions);
|
||||
FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions);
|
||||
|
||||
static std::optional<SOM::Network> createNetworkFromCacheFile(const std::filesystem::path& path);
|
||||
static std::optional<ObjectPositions> createObjectPositionsFromCacheFile(const std::filesystem::path& path);
|
||||
static bool objectPositionToCacheFile(const ObjectPositions& objectsPosition, const std::filesystem::path& path);
|
||||
static std::optional<TrackPositions> createObjectPositionsFromCacheFile(const std::filesystem::path& path);
|
||||
static bool objectPositionToCacheFile(const TrackPositions& trackPositions, const std::filesystem::path& path);
|
||||
|
||||
friend class FeaturesClassifier;
|
||||
friend class FeaturesEngine;
|
||||
|
||||
SOM::Network _network;
|
||||
ObjectPositions _trackPositions;
|
||||
TrackPositions _trackPositions;
|
||||
};
|
||||
|
||||
} // namespace Recommendation
|
||||
@@ -20,9 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <memory>
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
@@ -48,15 +46,20 @@ namespace Recommendation
|
||||
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
|
||||
virtual void cancelLoad() = 0;
|
||||
|
||||
using ResultContainer = std::unordered_set<Database::IdType>;
|
||||
template <typename IdType>
|
||||
using ResultContainer = std::vector<IdType>;
|
||||
|
||||
virtual ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0;
|
||||
virtual ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) = 0;
|
||||
virtual ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0;
|
||||
virtual ResultContainer getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) = 0;
|
||||
using ArtistContainer = ResultContainer<Database::ArtistId>;
|
||||
using ReleaseContainer = ResultContainer<Database::ReleaseId>;
|
||||
using TrackContainer = ResultContainer<Database::TrackId>;
|
||||
|
||||
virtual TrackContainer getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) = 0;
|
||||
virtual TrackContainer getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) = 0;
|
||||
virtual ReleaseContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) = 0;
|
||||
virtual ArtistContainer getSimilarArtists(Database::Session& session, Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) = 0;
|
||||
|
||||
protected:
|
||||
virtual void requestCancelLoad() = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IEngine> createEngine(Database::Db& db);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
class IRecommendation
|
||||
{
|
||||
};
|
||||
|
||||
} // ns Recommendation
|
||||
|
||||
@@ -111,7 +111,7 @@ createArtist(Session& session, const MetaData::Artist& artistInfo)
|
||||
|
||||
static
|
||||
void
|
||||
updateArtistIfNeeded(const Artist::pointer& artist, const MetaData::Artist& artistInfo)
|
||||
updateArtistIfNeeded(Artist::pointer artist, const MetaData::Artist& artistInfo)
|
||||
{
|
||||
// Name may have been updated
|
||||
if (artist->getName() != artistInfo.name)
|
||||
@@ -543,7 +543,7 @@ Scanner::scan(bool forceScan)
|
||||
}
|
||||
|
||||
bool
|
||||
Scanner::fetchTrackFeatures(Database::IdType trackId, const UUID& recordingMBID)
|
||||
Scanner::fetchTrackFeatures(Database::TrackId trackId, const UUID& recordingMBID)
|
||||
{
|
||||
std::map<std::string, double> features;
|
||||
|
||||
@@ -551,14 +551,14 @@ Scanner::fetchTrackFeatures(Database::IdType trackId, const UUID& recordingMBID)
|
||||
const std::string data {AcousticBrainz::extractLowLevelFeatures(recordingMBID)};
|
||||
if (data.empty())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR) << "Track " << trackId << ", recording MBID = '" << recordingMBID.getAsString() << "': cannot extract features using AcousticBrainz";
|
||||
LMS_LOG(DBUPDATER, ERROR) << "Track " << trackId.getValue() << ", recording MBID = '" << recordingMBID.getAsString() << "': cannot extract features using AcousticBrainz";
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
auto uniqueTransaction {_dbSession.createUniqueTransaction()};
|
||||
|
||||
Wt::Dbo::ptr<Database::Track> track {Database::Track::getById(_dbSession, trackId)};
|
||||
Database::Track::pointer track {Database::Track::getById(_dbSession, trackId)};
|
||||
if (!track)
|
||||
return false;
|
||||
|
||||
@@ -580,7 +580,7 @@ Scanner::fetchTrackFeatures(ScanStats& stats)
|
||||
|
||||
struct TrackInfo
|
||||
{
|
||||
Database::IdType id;
|
||||
Database::TrackId id;
|
||||
UUID recordingMBID;
|
||||
};
|
||||
|
||||
@@ -592,7 +592,7 @@ Scanner::fetchTrackFeatures(ScanStats& stats)
|
||||
|
||||
auto tracks {Database::Track::getAllWithRecordingMBIDAndMissingFeatures(_dbSession)};
|
||||
for (const auto& track : tracks)
|
||||
res.emplace_back(TrackInfo {track.id(), *track->getRecordingMBID()});
|
||||
res.emplace_back(TrackInfo {track->getId(), *track->getRecordingMBID()});
|
||||
|
||||
return res;
|
||||
}()};
|
||||
@@ -817,12 +817,12 @@ Scanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanSt
|
||||
track.modify()->setTotalDisc(trackInfo->totalDisc);
|
||||
if (!trackInfo->discSubtitle.empty())
|
||||
track.modify()->setDiscSubtitle(trackInfo->discSubtitle);
|
||||
track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0);
|
||||
track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0);
|
||||
track.modify()->setDate(trackInfo->date);
|
||||
track.modify()->setOriginalDate(trackInfo->originalDate);
|
||||
|
||||
// If a file has an OriginalYear but no Year, set it to ease filtering
|
||||
if (!trackInfo->year && trackInfo->originalYear)
|
||||
track.modify()->setYear(*trackInfo->originalYear);
|
||||
if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid())
|
||||
track.modify()->setDate(trackInfo->originalDate);
|
||||
|
||||
track.modify()->setRecordingMBID(trackInfo->recordingMBID);
|
||||
track.modify()->setTrackMBID(trackInfo->trackMBID);
|
||||
@@ -922,8 +922,8 @@ Scanner::removeMissingTracks(ScanStats& stats)
|
||||
stepStats.totalElems = trackCount;
|
||||
notifyInProgress(stepStats);
|
||||
|
||||
std::vector<std::pair<Database::IdType, std::filesystem::path>> trackPaths;
|
||||
std::vector<IdType> tracksToRemove;
|
||||
std::vector<std::pair<Database::TrackId, std::filesystem::path>> trackPaths;
|
||||
std::vector<TrackId> tracksToRemove;
|
||||
|
||||
for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i))
|
||||
{
|
||||
@@ -950,7 +950,7 @@ Scanner::removeMissingTracks(ScanStats& stats)
|
||||
{
|
||||
auto transaction {_dbSession.createUniqueTransaction()};
|
||||
|
||||
for (const IdType trackId : tracksToRemove)
|
||||
for (const TrackId trackId : tracksToRemove)
|
||||
{
|
||||
Track::pointer track {Track::getById(_dbSession, trackId)};
|
||||
if (track)
|
||||
@@ -1026,7 +1026,7 @@ Scanner::checkDuplicatedAudioFiles(ScanStats& stats)
|
||||
if (auto trackMBID {track->getTrackMBID()})
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Found duplicated Track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName();
|
||||
stats.duplicates.emplace_back(ScanDuplicate {track.id(), DuplicateReason::SameMBID});
|
||||
stats.duplicates.emplace_back(ScanDuplicate {track->getId(), DuplicateReason::SameMBID});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <chrono>
|
||||
#include <shared_mutex>
|
||||
#include <optional>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/WIOService.h>
|
||||
@@ -34,6 +35,7 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "metadata/IParser.hpp"
|
||||
#include "scanner/IScanner.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
|
||||
class UUID;
|
||||
|
||||
@@ -75,7 +77,7 @@ class Scanner : public IScanner
|
||||
void scan(bool force);
|
||||
|
||||
void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats);
|
||||
bool fetchTrackFeatures(Database::IdType trackId, const UUID& MBID);
|
||||
bool fetchTrackFeatures(Database::TrackId trackId, const UUID& MBID);
|
||||
void fetchTrackFeatures(ScanStats& stats);
|
||||
|
||||
// Helpers
|
||||
@@ -86,7 +88,6 @@ class Scanner : public IScanner
|
||||
void removeOrphanEntries();
|
||||
void checkDuplicatedAudioFiles(ScanStats& stats);
|
||||
void scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats);
|
||||
Database::IdType doScanAudioFile(const std::filesystem::path& file, ScanStats& stats);
|
||||
void notifyInProgressIfNeeded(const ScanStepStats& stats);
|
||||
void notifyInProgress(const ScanStepStats& stats);
|
||||
void reloadSimilarityEngine(ScanStats& stats);
|
||||
@@ -112,8 +113,8 @@ class Scanner : public IScanner
|
||||
std::size_t _scanVersion {};
|
||||
Wt::WTime _startTime;
|
||||
Database::ScanSettings::UpdatePeriod _updatePeriod {Database::ScanSettings::UpdatePeriod::Never};
|
||||
std::unordered_set<std::filesystem::path> _fileExtensions;
|
||||
std::filesystem::path _mediaDirectory;
|
||||
std::unordered_set<std::filesystem::path> _fileExtensions;
|
||||
std::filesystem::path _mediaDirectory;
|
||||
Database::ScanSettings::RecommendationEngineType _recommendationEngineType;
|
||||
};
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Scanner {
|
||||
|
||||
struct ScanDuplicate
|
||||
{
|
||||
Database::IdType trackId;
|
||||
Database::TrackId trackId;
|
||||
DuplicateReason reason;
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Session;
|
||||
class TrackList;
|
||||
class User;
|
||||
@@ -48,7 +47,7 @@ namespace Scrobbling
|
||||
|
||||
virtual void addTimedListen(const TimedListen& listen) = 0;
|
||||
|
||||
virtual Wt::Dbo::ptr<Database::TrackList> getListensTrackList(Database::Session& session, Wt::Dbo::ptr<Database::User> user) = 0;
|
||||
virtual Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace Scrobbling
|
||||
}
|
||||
|
||||
std::optional<Database::Scrobbler>
|
||||
Scrobbling::getUserScrobbler(Database::IdType userId)
|
||||
Scrobbling::getUserScrobbler(Database::UserId userId)
|
||||
{
|
||||
std::optional<Database::Scrobbler> scrobbler;
|
||||
|
||||
@@ -76,49 +76,49 @@ namespace Scrobbling
|
||||
return scrobbler;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Artist>>
|
||||
std::vector<Database::ObjectPtr<Database::Artist>>
|
||||
Scrobbling::getRecentArtists(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Wt::Dbo::ptr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Artist>> res;
|
||||
std::vector<Database::ObjectPtr<Database::Artist>> res;
|
||||
if (history)
|
||||
res = history->getArtistsReverse(clusterIds, linkType, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Release>>
|
||||
std::vector<Database::ObjectPtr<Database::Release>>
|
||||
Scrobbling::getRecentReleases(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Wt::Dbo::ptr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Release>> res;
|
||||
std::vector<Database::ObjectPtr<Database::Release>> res;
|
||||
if (history)
|
||||
res = history->getReleasesReverse(clusterIds, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Track>>
|
||||
std::vector<Database::ObjectPtr<Database::Track>>
|
||||
Scrobbling::getRecentTracks(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Wt::Dbo::ptr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Track>> res;
|
||||
std::vector<Database::ObjectPtr<Database::Track>> res;
|
||||
if (history)
|
||||
res = history->getTracksReverse(clusterIds, range, moreResults);
|
||||
|
||||
@@ -127,57 +127,57 @@ namespace Scrobbling
|
||||
|
||||
|
||||
// Top
|
||||
std::vector<Wt::Dbo::ptr<Database::Artist>>
|
||||
std::vector<Database::ObjectPtr<Database::Artist>>
|
||||
Scrobbling::getTopArtists(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Wt::Dbo::ptr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Artist>> res;
|
||||
std::vector<Database::ObjectPtr<Database::Artist>> res;
|
||||
if (history)
|
||||
res = history->getTopArtists(clusterIds, linkType, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Release>>
|
||||
std::vector<Database::ObjectPtr<Database::Release>>
|
||||
Scrobbling::getTopReleases(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Wt::Dbo::ptr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Release>> res;
|
||||
std::vector<Database::ObjectPtr<Database::Release>> res;
|
||||
if (history)
|
||||
res = history->getTopReleases(clusterIds, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Track>>
|
||||
std::vector<Database::ObjectPtr<Database::Track>>
|
||||
Scrobbling::getTopTracks(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Wt::Dbo::ptr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Track>> res;
|
||||
std::vector<Database::ObjectPtr<Database::Track>> res;
|
||||
if (history)
|
||||
res = history->getTopTracks(clusterIds, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<Database::TrackList>
|
||||
Scrobbling::getListensTrackList(Database::Session& session, Wt::Dbo::ptr<Database::User> user)
|
||||
Database::ObjectPtr<Database::TrackList>
|
||||
Scrobbling::getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user)
|
||||
{
|
||||
return _scrobblers[user->getScrobbler()]->getListensTrackList(session, user);
|
||||
}
|
||||
|
||||
@@ -38,47 +38,47 @@ namespace Scrobbling
|
||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||
void addTimedListen(const TimedListen& listen) override;
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Artist>> getRecentArtists(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
std::vector<Database::ObjectPtr<Database::Artist>> getRecentArtists(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Release>> getRecentReleases(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
std::vector<Database::ObjectPtr<Database::Release>> getRecentReleases(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Track>> getRecentTracks(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
std::vector<Database::ObjectPtr<Database::Track>> getRecentTracks(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Artist>> getTopArtists(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
std::vector<Database::ObjectPtr<Database::Artist>> getTopArtists(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Release>> getTopReleases(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
std::vector<Database::ObjectPtr<Database::Release>> getTopReleases(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Track>> getTopTracks(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
std::vector<Database::ObjectPtr<Database::Track>> getTopTracks(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
Wt::Dbo::ptr<Database::TrackList> getListensTrackList(Database::Session& session, Wt::Dbo::ptr<Database::User> user);
|
||||
Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user);
|
||||
|
||||
std::optional<Database::Scrobbler> getUserScrobbler(Database::IdType userId);
|
||||
std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId);
|
||||
|
||||
Database::Db& _db;
|
||||
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers;
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Scrobbling
|
||||
if (!user)
|
||||
return;
|
||||
|
||||
Wt::Dbo::ptr<Database::TrackList> tracklist {getListensTrackList(session, user)};
|
||||
Database::TrackList::pointer tracklist {getListensTrackList(session, user)};
|
||||
if (!tracklist)
|
||||
tracklist = Database::TrackList::create(session, historyTracklistName, Database::TrackList::Type::Internal, false, user);
|
||||
|
||||
@@ -72,8 +72,8 @@ namespace Scrobbling
|
||||
Database::TrackListEntry::create(session, track, getListensTrackList(session, user), listen.listenedAt);
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<Database::TrackList>
|
||||
InternalScrobbler::getListensTrackList(Database::Session& session, Wt::Dbo::ptr<Database::User> user)
|
||||
Database::TrackList::pointer
|
||||
InternalScrobbler::getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user)
|
||||
{
|
||||
return Database::TrackList::get(session, historyTracklistName, Database::TrackList::Type::Internal, user);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
|
||||
#include "IScrobbler.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
class InternalScrobbler final : public IScrobbler
|
||||
@@ -34,7 +39,7 @@ namespace Scrobbling
|
||||
|
||||
void addTimedListen(const TimedListen& listen) override;
|
||||
|
||||
Wt::Dbo::ptr<Database::TrackList> getListensTrackList(Database::Session& session, Wt::Dbo::ptr<Database::User> user) override;
|
||||
Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user) override;
|
||||
|
||||
Database::Db& _db;
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
namespace
|
||||
{
|
||||
bool
|
||||
canBeScrobbled(Database::Session& session, Database::IdType trackId, std::chrono::seconds duration)
|
||||
canBeScrobbled(Database::Session& session, Database::TrackId trackId, std::chrono::seconds duration)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Scrobbling::ListenBrainz
|
||||
void listenStarted(const Listen& listen) override;
|
||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||
void addTimedListen(const TimedListen& listen) override;
|
||||
Wt::Dbo::ptr<Database::TrackList> getListensTrackList(Database::Session& session, Wt::Dbo::ptr<Database::User> user) override;
|
||||
Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user) override;
|
||||
|
||||
// Submit listens
|
||||
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace
|
||||
std::vector<Scrobbling::TimedListen> matchedListens;
|
||||
};
|
||||
ParseGetListensResult
|
||||
parseGetListens(Database::Session& session, std::string_view msgBody, Database::IdType userId)
|
||||
parseGetListens(Database::Session& session, std::string_view msgBody, Database::UserId userId)
|
||||
{
|
||||
ParseGetListensResult result;
|
||||
|
||||
@@ -233,7 +233,7 @@ namespace
|
||||
result.oldestEntry = listenedAt;
|
||||
|
||||
if (const Database::Track::pointer track {tryMatchListen(session, metadata)})
|
||||
result.matchedListens.emplace_back(Scrobbling::TimedListen {userId, track.id(), listenedAt});
|
||||
result.matchedListens.emplace_back(Scrobbling::TimedListen {{userId, track->getId()}, listenedAt});
|
||||
}
|
||||
}
|
||||
catch (const Wt::WException& error)
|
||||
@@ -285,7 +285,7 @@ namespace Scrobbling::ListenBrainz
|
||||
}
|
||||
|
||||
ListensSynchronizer::UserContext&
|
||||
ListensSynchronizer::getUserContext(Database::IdType userId)
|
||||
ListensSynchronizer::getUserContext(Database::UserId userId)
|
||||
{
|
||||
auto itContext {_userContexts.find(userId)};
|
||||
if (itContext == std::cend(_userContexts))
|
||||
@@ -338,14 +338,14 @@ namespace Scrobbling::ListenBrainz
|
||||
|
||||
assert(!isFetching());
|
||||
|
||||
std::vector<Database::IdType> userIds;
|
||||
std::vector<Database::UserId> userIds;
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
userIds = Database::User::getAllIds(_db.getTLSSession());
|
||||
}
|
||||
|
||||
for (const Database::IdType userId : userIds)
|
||||
for (const Database::UserId userId : userIds)
|
||||
{
|
||||
if (Utils::getListenBrainzToken(_db.getTLSSession(), userId))
|
||||
startGetListens(getUserContext(userId));
|
||||
@@ -373,7 +373,7 @@ namespace Scrobbling::ListenBrainz
|
||||
{
|
||||
_strand.dispatch([this, &context]
|
||||
{
|
||||
LOG(DEBUG) << "Fetch done for user " << context.userId << ", fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount;
|
||||
LOG(DEBUG) << "Fetch done for user " << context.userId.getValue() << ", fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount;
|
||||
context.fetching = false;
|
||||
|
||||
if (!isFetching())
|
||||
@@ -473,7 +473,7 @@ namespace Scrobbling::ListenBrainz
|
||||
}
|
||||
|
||||
std::optional<SendQueue::RequestData>
|
||||
ListensSynchronizer::createValidateTokenRequestData(Database::IdType userId)
|
||||
ListensSynchronizer::createValidateTokenRequestData(Database::UserId userId)
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
|
||||
|
||||
@@ -50,14 +50,14 @@ namespace Scrobbling::ListenBrainz
|
||||
private:
|
||||
struct UserContext
|
||||
{
|
||||
UserContext(Database::IdType id) : userId {id} {}
|
||||
UserContext(Database::UserId id) : userId {id} {}
|
||||
|
||||
UserContext(const UserContext&) = delete;
|
||||
UserContext(UserContext&&) = delete;
|
||||
UserContext& operator=(const UserContext&) = delete;
|
||||
UserContext& operator=(UserContext&&) = delete;
|
||||
|
||||
const Database::IdType userId;
|
||||
const Database::UserId userId;
|
||||
bool fetching {};
|
||||
std::optional<std::size_t> listenCount {};
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace Scrobbling::ListenBrainz
|
||||
std::size_t importedListenCount{};
|
||||
};
|
||||
|
||||
UserContext& getUserContext(Database::IdType userId);
|
||||
UserContext& getUserContext(Database::UserId userId);
|
||||
bool isFetching() const;
|
||||
void scheduleGetListens(std::chrono::seconds fromNow);
|
||||
void startGetListens();
|
||||
@@ -78,7 +78,7 @@ namespace Scrobbling::ListenBrainz
|
||||
void enqueValidateToken(UserContext& context);
|
||||
void enqueGetListenCount(UserContext& context);
|
||||
void enqueGetListens(UserContext& context);
|
||||
std::optional<SendQueue::RequestData> createValidateTokenRequestData(Database::IdType userId);
|
||||
std::optional<SendQueue::RequestData> createValidateTokenRequestData(Database::UserId userId);
|
||||
std::optional<SendQueue::RequestData> createGetListensRequestData(std::string_view listenBrainzUserName, const Wt::WDateTime& maxDateTime);
|
||||
void processGetListensResponse(std::string_view body, UserContext& context);
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Scrobbling::ListenBrainz
|
||||
SendQueue& _sendQueue;
|
||||
boost::asio::steady_timer _getListensTimer {_ioContext};
|
||||
|
||||
std::unordered_map<Database::IdType, UserContext> _userContexts;
|
||||
std::unordered_map<Database::UserId, UserContext> _userContexts;
|
||||
|
||||
const std::size_t _maxSyncListenCount;
|
||||
const std::chrono::hours _syncListensPeriod;
|
||||
|
||||
@@ -30,7 +30,7 @@ static constexpr std::string_view historyTracklistName {"__scrobbler_listenbrain
|
||||
namespace Scrobbling::ListenBrainz::Utils
|
||||
{
|
||||
std::optional<UUID>
|
||||
getListenBrainzToken(Database::Session& session, Database::IdType userId)
|
||||
getListenBrainzToken(Database::Session& session, Database::UserId userId)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
@@ -32,7 +33,7 @@ namespace Database
|
||||
|
||||
namespace Scrobbling::ListenBrainz::Utils
|
||||
{
|
||||
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::IdType userId);
|
||||
Wt::Dbo::ptr<Database::TrackList> getOrCreateListensTrackList(Database::Session& session, Wt::Dbo::ptr<Database::User> user);
|
||||
Wt::Dbo::ptr<Database::TrackList> getListensTrackList(Database::Session& session, Wt::Dbo::ptr<Database::User> user);
|
||||
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId);
|
||||
Database::ObjectPtr<Database::TrackList> getOrCreateListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user);
|
||||
Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user);
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "scrobbling/Listen.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -57,42 +57,42 @@ namespace Scrobbling
|
||||
|
||||
// Stats
|
||||
// From most recent to oldest
|
||||
virtual std::vector<Wt::Dbo::ptr<Database::Artist>> getRecentArtists(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
virtual std::vector<Database::ObjectPtr<Database::Artist>> getRecentArtists(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
|
||||
virtual std::vector<Wt::Dbo::ptr<Database::Release>> getRecentReleases(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
virtual std::vector<Database::ObjectPtr<Database::Release>> getRecentReleases(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
|
||||
virtual std::vector<Wt::Dbo::ptr<Database::Track>> getRecentTracks(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
virtual std::vector<Database::ObjectPtr<Database::Track>> getRecentTracks(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
|
||||
// Top
|
||||
virtual std::vector<Wt::Dbo::ptr<Database::Artist>> getTopArtists(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
virtual std::vector<Database::ObjectPtr<Database::Artist>> getTopArtists(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
|
||||
virtual std::vector<Wt::Dbo::ptr<Database::Release>> getTopReleases(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
virtual std::vector<Database::ObjectPtr<Database::Release>> getTopReleases(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
|
||||
virtual std::vector<Wt::Dbo::ptr<Database::Track>> getTopTracks(Database::Session& session,
|
||||
Wt::Dbo::ptr<Database::User> user,
|
||||
const std::set<Database::IdType>& clusterIds,
|
||||
virtual std::vector<Database::ObjectPtr<Database::Track>> getTopTracks(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
};
|
||||
|
||||
@@ -27,8 +27,8 @@ namespace Scrobbling
|
||||
{
|
||||
struct Listen
|
||||
{
|
||||
Database::IdType userId {};
|
||||
Database::IdType trackId {};
|
||||
Database::UserId userId {};
|
||||
Database::TrackId trackId {};
|
||||
};
|
||||
|
||||
struct TimedListen : public Listen
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
add_library(lmssom STATIC
|
||||
add_library(lmssom SHARED
|
||||
impl/DataNormalizer.cpp
|
||||
impl/Network.cpp
|
||||
)
|
||||
@@ -18,3 +18,4 @@ target_link_libraries(lmssom PUBLIC
|
||||
|
||||
set_property(TARGET lmssom PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
install(TARGETS lmssom DESTINATION lib)
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Random.hpp"
|
||||
@@ -197,7 +198,7 @@ Network::getClosestRefVectorPosition(const InputVector& data, InputVector::Dista
|
||||
}
|
||||
|
||||
std::optional<Position>
|
||||
Network::getClosestRefVectorPosition(const std::unordered_set<Position>& refVectorsPosition, InputVector::Distance maxDistance) const
|
||||
Network::getClosestRefVectorPosition(const std::vector<Position>& refVectorsPosition, InputVector::Distance maxDistance) const
|
||||
{
|
||||
std::unordered_set<Position> neighboursPosition;
|
||||
for (const Position& refVectorPosition : refVectorsPosition)
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <functional>
|
||||
@@ -70,7 +69,7 @@ class Network
|
||||
Position getClosestRefVectorPosition(const InputVector& data) const;
|
||||
std::optional<Position> getClosestRefVectorPosition(const InputVector& data, InputVector::Distance maxDistance) const;
|
||||
|
||||
std::optional<Position> getClosestRefVectorPosition(const std::unordered_set<Position>& refVectorsPosition, InputVector::Distance maxDistance) const;
|
||||
std::optional<Position> getClosestRefVectorPosition(const std::vector<Position>& refVectorsPosition, InputVector::Distance maxDistance) const;
|
||||
|
||||
InputVector::Distance getRefVectorsDistance(const Position& position1, const Position& position2) const;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
add_library(lmssubsonic SHARED
|
||||
impl/ParameterParsing.cpp
|
||||
impl/ProtocolVersion.cpp
|
||||
impl/Scan.cpp
|
||||
impl/Stream.cpp
|
||||
impl/SubsonicId.cpp
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user