From f1226051c47ae0e2cdb314e4685977e3fa3e18b0 Mon Sep 17 00:00:00 2001 From: Harry Cheng Date: Tue, 29 Jun 2021 11:12:57 +0800 Subject: [PATCH 01/25] Update Chinese translation --- approot/messages_zh.xml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 93cfa976..252233df 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -34,13 +34,13 @@ 未找到此用户 -每日的 +每日 音乐收藏 -每小时的 +每小时 立即扫描! -每月的 +每月 音乐收藏 -从未 +从不 媒体根目录 推荐引擎 基于标签 @@ -52,7 +52,7 @@ 标签 更新周期 更新开始时间 -每周的 +每周 无法获得音轨时间 无法解析文件 @@ -75,7 +75,7 @@ 扫描中: 阶段 {1}/{2} 检查文件中... {1}% 检索文件中: {1} 文件 -Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)... +从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)... 重载相似引擎中 {1}%... 扫描文件中: {1}/{2} 文件 ({3}%)... @@ -139,7 +139,7 @@ 所有歌手 -追踪歌手 +音轨歌手 作曲 作词 混响 @@ -156,14 +156,14 @@ 搜索中... -转码有效 +转码已启用 清除 -{1} 追踪 +{1} 音轨 - 添加 {1} 追踪 - 已添加 {1} 追踪 + 添加 {1} 音轨 + 已添加 {1} 音轨 播放队列 播放队列已满! @@ -177,13 +177,13 @@ 外观 音频 -这些音频设置取决于您的本地浏览器! +这些音频设置仅保存在您的本地浏览器! 更改密码 夜间模式 使用演示账号时不能保存! 设置 无效密码 -旧密码必须填写 +必须填写旧密码 ReplayGain 模式 没有 ReplayGain 自动模式 @@ -194,10 +194,10 @@ 歌手列表模式 所有歌手 专辑歌手 -追踪歌手 +音轨歌手 Subsonic 应用程序接口 转码 -视频比特率 +转码比特率 开启转码 转码格式 Matroska/Opus From dd7019886626f18f1543db645fc0291ac7cc30c0 Mon Sep 17 00:00:00 2001 From: Harry Cheng Date: Fri, 2 Jul 2021 10:25:03 +0800 Subject: [PATCH 02/25] Add Lms.Admin.Database.path-help With other improvements including symbols --- approot/messages_zh.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 252233df..5e914553 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -42,10 +42,11 @@ 音乐收藏 从不 媒体根目录 +包含 .lmsignore 文件的目录将会被跳过 推荐引擎 基于标签 基于音频分析 -扫描完成: {1} 总文件, {2} 附加文件, {3} 升级文件, {4} 已删除文件, {5} 副本文件, {6} 错误文件 +扫描完成:总文件 {1},添加文件 {2},更新文件 {3},删除文件 {4},副本文件 {5},错误文件 {6} 扫描已完成! 扫描选项 新设置已保存! @@ -58,7 +59,7 @@ 无法解析文件 无法读取文件 {1} 备份文件: -{1} 错误: +{1} 错误: 立即强制全盘扫描 生成报告 最后扫描 @@ -119,7 +120,7 @@ 链接 播放最多 MusicBrainz 歌手 -MusicBrainz 发布 +MusicBrainz 专辑 播放列表 随机播放 随机 @@ -215,5 +216,5 @@ 此字段不能为空 -这个值应该在 {1} 和 {2} +这个值应该在 {1} 和 {2} 之间 From 7d0dae1a0c9f568645a62dc591eb00741891b7b3 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 15 Aug 2021 21:35:27 +0200 Subject: [PATCH 03/25] Fixed build (hopefully) on arch, fixes #169 --- src/libs/utils/impl/ChildProcess.hpp | 1 + src/libs/utils/impl/NetAddress.cpp | 4 ++++ src/libs/utils/include/utils/NetAddress.hpp | 5 ++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libs/utils/impl/ChildProcess.hpp b/src/libs/utils/impl/ChildProcess.hpp index f92b6b25..46d7bcb0 100644 --- a/src/libs/utils/impl/ChildProcess.hpp +++ b/src/libs/utils/impl/ChildProcess.hpp @@ -24,6 +24,7 @@ #include #include +#include #include #include diff --git a/src/libs/utils/impl/NetAddress.cpp b/src/libs/utils/impl/NetAddress.cpp index 732565f8..b41b07ac 100644 --- a/src/libs/utils/impl/NetAddress.cpp +++ b/src/libs/utils/impl/NetAddress.cpp @@ -19,6 +19,8 @@ #include "utils/NetAddress.hpp" +#ifndef BOOST_ASIO_HAS_STD_HASH + namespace std { std::size_t hash::operator()(const boost::asio::ip::address& ipAddr) const @@ -40,3 +42,5 @@ namespace std return std::hash{}(ipAddr.to_string()); } } + +#endif // BOOST_ASIO_HAS_STD_HASH diff --git a/src/libs/utils/include/utils/NetAddress.hpp b/src/libs/utils/include/utils/NetAddress.hpp index 22bc8d7e..86575e1a 100644 --- a/src/libs/utils/include/utils/NetAddress.hpp +++ b/src/libs/utils/include/utils/NetAddress.hpp @@ -21,6 +21,9 @@ #include +#ifndef BOOST_ASIO_HAS_STD_HASH +#include + namespace std { template<> struct hash @@ -30,4 +33,4 @@ namespace std } - +#endif // BOOST_ASIO_HAS_STD_HASH From 6d2d72e007aa04e6213f7d1d43acce49d426bcfe Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 2 Sep 2021 12:37:35 +0200 Subject: [PATCH 04/25] Switched from Travis CI to github actions --- .dockerignore | 1 + .github/workflows/build.yml | 38 +++++++++++++++++++++ Dockerfile-build | 44 +++++++++++++++++++++++++ docker/Dockerfile => Dockerfile-release | 13 +++----- README.md | 2 +- 5 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/build.yml create mode 100644 Dockerfile-build rename docker/Dockerfile => Dockerfile-release (90%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +build diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..6d23fd62 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +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 QEMU + uses: docker/setup-qemu-action@v1 + + - 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 diff --git a/Dockerfile-build b/Dockerfile-build new file mode 100644 index 00000000..9f5b0824 --- /dev/null +++ b/Dockerfile-build @@ -0,0 +1,44 @@ +FROM alpine:3.13 AS build + +WORKDIR /tmp/workdir + +ARG MAKEFLAGS="-j2" + +ARG PREFIX="/tmp/install" + +ARG BUILD_PACKAGES=" \ + ca-certificates \ + curl \ + coreutils \ + bzip2 \ + pkgconfig \ + g++ \ + ffmpeg-dev \ + make \ + cmake \ + boost-dev \ + libconfig-dev \ + taglib-dev \ + wt-dev \ + gtest-dev" + +RUN apk add --no-cache --update ${BUILD_PACKAGES} + +# 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} && \ + CXXFLAGS="-I${PREFIX}/include" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} && \ + VERBOSE=1 make && \ + make test + diff --git a/docker/Dockerfile b/Dockerfile-release similarity index 90% rename from docker/Dockerfile rename to Dockerfile-release index 6528beac..a3ee5277 100644 --- a/docker/Dockerfile +++ b/Dockerfile-release @@ -101,20 +101,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 \ diff --git a/README.md b/README.md index 6dc0a0ef..98f14067 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # LMS - Lightweight Music Server -![GitHub release (latest by date)](https://img.shields.io/github/v/release/epoupon/lms) [![Build Status](https://travis-ci.org/epoupon/lms.svg?branch=master)](https://travis-ci.org/epoupon/lms) [![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/epoupon/lms.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/epoupon/lms/context:cpp) +[![Last Release](https://img.shields.io/github/v/release/epoupon/lms?logo=github&label=latest)](https://github.com/epoupon/lms/releases) [![Build](https://img.shields.io/github/workflow/status/epoupon/lms/Build?logo=github)](https://github.com/epoupon/lms/actions) [![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/epoupon/lms.svg?logo=lgtm&logoWidth=18)](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! From 9e370120905a2151ffdcb5cfedd0ab40a918d35f Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 2 Sep 2021 13:49:38 +0200 Subject: [PATCH 05/25] Added Docker workflow for tags --- .github/workflows/release.yml | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ebefc773 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,66 @@ +name: Docker +on: + push: + tags: + - "*.*.*" +jobs: + Docker: + name: Docker images + environment: + name: Docker + runs-on: ubuntu-latest + steps: + - name: Check Out Repo + uses: actions/checkout@v2 + + - name: Prepare + id: prep + run: | + DOCKER_IMAGE=epoupon/lms + VERSION=edge + if [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION=${GITHUB_REF#refs/tags/v} + fi + TAGS="${DOCKER_IMAGE}:${VERSION}" + if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + TAGS="$TAGS,${DOCKER_IMAGE}:latest" + fi + echo ::set-output name=tags::${TAGS} + + - 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: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./Dockerfile-release + builder: ${{ steps.buildx.outputs.name }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.prep.outputs.tags }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} + From 56a588a06b5ba85c4ec9dd47fe7eec095009c7d7 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 2 Sep 2021 21:05:13 +0200 Subject: [PATCH 06/25] Fixed demo account creation. fixes #167 --- src/libs/auth/impl/AuthServiceBase.cpp | 4 +- .../impl/internal/InternalPasswordService.cpp | 15 +- .../impl/internal/InternalPasswordService.hpp | 2 +- src/libs/auth/impl/pam/PAMPasswordService.cpp | 2 +- src/libs/auth/impl/pam/PAMPasswordService.hpp | 3 +- .../auth/include/auth/IPasswordService.hpp | 9 +- src/libs/auth/include/auth/Types.hpp | 14 ++ src/libs/database/impl/User.cpp | 2 +- src/libs/database/include/database/Types.hpp | 8 + src/libs/database/include/database/User.hpp | 18 +- src/libs/subsonic/impl/SubsonicResource.cpp | 173 +++++++++--------- src/libs/subsonic/impl/SubsonicResponse.hpp | 5 + src/lms/ui/LmsApplication.cpp | 27 +-- src/lms/ui/LmsApplication.hpp | 5 +- src/lms/ui/SettingsView.cpp | 2 +- src/lms/ui/admin/InitWizardView.cpp | 4 +- src/lms/ui/admin/UserView.cpp | 9 +- src/lms/ui/common/PasswordValidator.cpp | 23 ++- src/lms/ui/common/PasswordValidator.hpp | 9 +- 19 files changed, 172 insertions(+), 162 deletions(-) diff --git a/src/libs/auth/impl/AuthServiceBase.cpp b/src/libs/auth/impl/AuthServiceBase.cpp index a46280f2..f31f737e 100644 --- a/src/libs/auth/impl/AuthServiceBase.cpp +++ b/src/libs/auth/impl/AuthServiceBase.cpp @@ -33,9 +33,9 @@ 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); diff --git a/src/libs/auth/impl/internal/InternalPasswordService.cpp b/src/libs/auth/impl/internal/InternalPasswordService.cpp index a662a76f..2d17c044 100644 --- a/src/libs/auth/impl/internal/InternalPasswordService.cpp +++ b/src/libs/auth/impl/internal/InternalPasswordService.cpp @@ -81,9 +81,18 @@ namespace Auth } bool - InternalPasswordService::isPasswordSecureEnough(std::string_view loginName, std::string_view password) const + InternalPasswordService::isPasswordSecureEnough(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(); + case Database::UserType::DEMO: + return true; // no constraint + } + + throw NotImplementedException {}; } void @@ -97,7 +106,7 @@ namespace Auth if (!user) throw Exception {"User not found!"}; - if (!isPasswordSecureEnough(user->getLoginName(), newPassword)) + if (!isPasswordSecureEnough(newPassword, PasswordValidationContext {user->getLoginName(), user->getType()} )) throw PasswordTooWeakException {}; user.modify()->setPasswordHash(passwordHash); diff --git a/src/libs/auth/impl/internal/InternalPasswordService.hpp b/src/libs/auth/impl/internal/InternalPasswordService.hpp index 6cef56fb..67a5da08 100644 --- a/src/libs/auth/impl/internal/InternalPasswordService.hpp +++ b/src/libs/auth/impl/internal/InternalPasswordService.hpp @@ -41,7 +41,7 @@ namespace Auth std::string_view password) override; bool canSetPasswords() const override; - bool isPasswordSecureEnough(std::string_view loginName, std::string_view password) const override; + bool isPasswordSecureEnough(std::string_view loginName, const PasswordValidationContext& context) const override; void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) override; Database::User::PasswordHash hashPassword(std::string_view password) const; diff --git a/src/libs/auth/impl/pam/PAMPasswordService.cpp b/src/libs/auth/impl/pam/PAMPasswordService.cpp index 60623c28..7d8e1af2 100644 --- a/src/libs/auth/impl/pam/PAMPasswordService.cpp +++ b/src/libs/auth/impl/pam/PAMPasswordService.cpp @@ -187,7 +187,7 @@ namespace Auth } bool - PAMPasswordService::isPasswordSecureEnough(std::string_view, std::string_view) const + PAMPasswordService::isPasswordSecureEnough(std::string_view, const PasswordValidationContext&) const { throw NotImplementedException {}; } diff --git a/src/libs/auth/impl/pam/PAMPasswordService.hpp b/src/libs/auth/impl/pam/PAMPasswordService.hpp index 7b418f47..1b1bcc1c 100644 --- a/src/libs/auth/impl/pam/PAMPasswordService.hpp +++ b/src/libs/auth/impl/pam/PAMPasswordService.hpp @@ -36,8 +36,7 @@ namespace Auth std::string_view password) override; bool canSetPasswords() const override; - bool isPasswordSecureEnough(std::string_view loginName, - std::string_view password) const override; + bool isPasswordSecureEnough(std::string_view loginName, const PasswordValidationContext& context) const override; void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) override; diff --git a/src/libs/auth/include/auth/IPasswordService.hpp b/src/libs/auth/include/auth/IPasswordService.hpp index 896c4156..0c4b1cc5 100644 --- a/src/libs/auth/include/auth/IPasswordService.hpp +++ b/src/libs/auth/include/auth/IPasswordService.hpp @@ -61,14 +61,9 @@ namespace Auth std::string_view loginName, std::string_view password) = 0; - class PasswordTooWeakException : public Auth::Exception - { - public: - PasswordTooWeakException() : Auth::Exception {"Password too weak"} {} - }; - virtual bool canSetPasswords() const = 0; - virtual bool isPasswordSecureEnough(std::string_view username, std::string_view password) const = 0; + + virtual bool isPasswordSecureEnough(std::string_view password, const PasswordValidationContext& context) const = 0; virtual void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) = 0; }; diff --git a/src/libs/auth/include/auth/Types.hpp b/src/libs/auth/include/auth/Types.hpp index 9b79bab7..dec88e0a 100644 --- a/src/libs/auth/include/auth/Types.hpp +++ b/src/libs/auth/include/auth/Types.hpp @@ -19,6 +19,8 @@ #pragma once +#include +#include "database/Types.hpp" #include "utils/Exception.hpp" namespace Auth @@ -33,5 +35,17 @@ namespace Auth public: NotImplementedException() : Auth::Exception {"Not implemented"} {} }; + + struct PasswordValidationContext + { + std::string loginName; + Database::UserType userType; + }; + + class PasswordTooWeakException : public Exception + { + public: + PasswordTooWeakException() : Auth::Exception {"Password too weak"} {} + }; } diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp index b0dc3f01..9a6deb73 100644 --- a/src/libs/database/impl/User.cpp +++ b/src/libs/database/impl/User.cpp @@ -98,7 +98,7 @@ User::getDemo(Session& session) { session.checkSharedLocked(); - pointer res = session.getDboSession().find().where("type = ?").bind(Type::DEMO); + pointer res = session.getDboSession().find().where("type = ?").bind(UserType::DEMO); return res; } diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 080075f1..174bb65b 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -70,5 +70,13 @@ namespace Database Internal = 0, ListenBrainz = 1, }; + + // Do not change enum values! + enum class UserType + { + REGULAR = 0, + ADMIN = 1, + DEMO = 2, + }; } diff --git a/src/libs/database/include/database/User.hpp b/src/libs/database/include/database/User.hpp index 82980188..2bd593fb 100644 --- a/src/libs/database/include/database/User.hpp +++ b/src/libs/database/include/database/User.hpp @@ -80,15 +80,6 @@ class User : public Wt::Dbo::Dbo public: using pointer = Wt::Dbo::ptr; - - // Do not change enum values! - enum class Type - { - REGULAR = 0, - ADMIN = 1, - DEMO = 2, - }; - struct PasswordHash { std::string salt; @@ -152,7 +143,7 @@ class User : public Wt::Dbo::Dbo // 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 +157,9 @@ class User : public Wt::Dbo::Dbo void setListenBrainzToken(const std::optional& 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; } @@ -233,7 +225,7 @@ class User : public Wt::Dbo::Dbo std::string _listenbrainzToken; // Musicbrainz Identifier // Admin defined settings - Type _type {Type::REGULAR}; + UserType _type {UserType::REGULAR}; // User defined settings SubsonicArtistListMode _subsonicArtistListMode {defaultSubsonicArtistListMode}; diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 19727338..fa22a14b 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -180,7 +180,7 @@ std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap auto censorValue = [](const std::string& type, const std::string& value) -> std::string { if (type == "p" || type == "password") - return "*SENSIBLE DATA*"; + return "*REDACTED*"; else return value; }; @@ -221,24 +221,16 @@ checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username) static void -checkUserIsAdmin(RequestContext& context) +checkUserTypeIsAllowed(RequestContext& context, EnumSet allowedUserTypes) { - LMS_LOG(API_SUBSONIC, DEBUG) << "Check user is admin"; - auto transaction {context.dbSession.createSharedTransaction()}; User::pointer currentUser {User::getById(context.dbSession, context.userId)}; if (!currentUser) - { - LMS_LOG(API_SUBSONIC, DEBUG) << "NOT FOUND"; throw RequestedDataNotFoundError {}; - } - if (!currentUser->isAdmin()) - { - LMS_LOG(API_SUBSONIC, DEBUG) << "NOT ADMIN"; + if (!allowedUserTypes.contains(currentUser->getType())) throw UserNotAuthorizedError {}; - } } static @@ -567,7 +559,7 @@ handleChangePassword(RequestContext& context) Service::get()->setPassword(context.dbSession, userId, password); } - catch (Auth::IPasswordService::PasswordTooWeakException&) + catch (Auth::PasswordTooWeakException&) { throw PasswordTooWeakGenericError {}; } @@ -666,7 +658,7 @@ handleCreateUserRequest(RequestContext& context) { Service::get()->setPassword(context.dbSession, userId, password); } - catch (const Auth::IPasswordService::PasswordTooWeakException&) + catch (const Auth::PasswordTooWeakException&) { removeCreatedUser(); throw PasswordTooWeakGenericError {}; @@ -1719,7 +1711,7 @@ handleUpdateUserRequest(RequestContext& context) { Service<::Auth::IPasswordService>()->setPassword(context.dbSession, userId, decodePasswordIfNeeded(*password)); } - catch (const Auth::IPasswordService::PasswordTooWeakException&) + catch (const Auth::PasswordTooWeakException&) { throw PasswordTooWeakGenericError {}; } @@ -1922,114 +1914,114 @@ using RequestHandlerFunc = std::function; using CheckImplementedFunc = std::function; struct RequestEntryPointInfo { - RequestHandlerFunc func; - bool mustBeAdmin; - CheckImplementedFunc checkFunc {}; + RequestHandlerFunc func; + EnumSet allowedUserTypes {Database::UserType::DEMO, Database::UserType::REGULAR, Database::UserType::ADMIN}; + CheckImplementedFunc checkFunc {}; }; static std::unordered_map requestEntryPoints { // System - {"ping", {handlePingRequest, false}}, - {"getLicense", {handleGetLicenseRequest, false}}, + {"ping", {handlePingRequest}}, + {"getLicense", {handleGetLicenseRequest}}, // Browsing - {"getMusicFolders", {handleGetMusicFoldersRequest, false}}, - {"getIndexes", {handleGetIndexesRequest, false}}, - {"getMusicDirectory", {handleGetMusicDirectoryRequest, false}}, - {"getGenres", {handleGetGenresRequest, false}}, - {"getArtists", {handleGetArtistsRequest, false}}, - {"getArtist", {handleGetArtistRequest, false}}, - {"getAlbum", {handleGetAlbumRequest, false}}, - {"getSong", {handleNotImplemented, false}}, - {"getVideos", {handleNotImplemented, false}}, - {"getArtistInfo", {handleGetArtistInfoRequest, false}}, - {"getArtistInfo2", {handleGetArtistInfo2Request, false}}, - {"getAlbumInfo", {handleNotImplemented, false}}, - {"getAlbumInfo2", {handleNotImplemented, false}}, - {"getSimilarSongs", {handleGetSimilarSongsRequest, false}}, - {"getSimilarSongs2", {handleGetSimilarSongs2Request, false}}, - {"getTopSongs", {handleNotImplemented, false}}, + {"getMusicFolders", {handleGetMusicFoldersRequest}}, + {"getIndexes", {handleGetIndexesRequest}}, + {"getMusicDirectory", {handleGetMusicDirectoryRequest}}, + {"getGenres", {handleGetGenresRequest}}, + {"getArtists", {handleGetArtistsRequest}}, + {"getArtist", {handleGetArtistRequest}}, + {"getAlbum", {handleGetAlbumRequest}}, + {"getSong", {handleNotImplemented}}, + {"getVideos", {handleNotImplemented}}, + {"getArtistInfo", {handleGetArtistInfoRequest}}, + {"getArtistInfo2", {handleGetArtistInfo2Request}}, + {"getAlbumInfo", {handleNotImplemented}}, + {"getAlbumInfo2", {handleNotImplemented}}, + {"getSimilarSongs", {handleGetSimilarSongsRequest}}, + {"getSimilarSongs2", {handleGetSimilarSongs2Request}}, + {"getTopSongs", {handleNotImplemented}}, // Album/song lists - {"getAlbumList", {handleGetAlbumListRequest, false}}, - {"getAlbumList2", {handleGetAlbumList2Request, false}}, - {"getRandomSongs", {handleGetRandomSongsRequest, false}}, - {"getSongsByGenre", {handleGetSongsByGenreRequest, false}}, - {"getNowPlaying", {handleNotImplemented, false}}, - {"getStarred", {handleGetStarredRequest, false}}, - {"getStarred2", {handleGetStarred2Request, false}}, + {"getAlbumList", {handleGetAlbumListRequest}}, + {"getAlbumList2", {handleGetAlbumList2Request}}, + {"getRandomSongs", {handleGetRandomSongsRequest}}, + {"getSongsByGenre", {handleGetSongsByGenreRequest}}, + {"getNowPlaying", {handleNotImplemented}}, + {"getStarred", {handleGetStarredRequest}}, + {"getStarred2", {handleGetStarred2Request}}, // Searching - {"search", {handleNotImplemented, false}}, - {"search2", {handleSearch2Request, false}}, - {"search3", {handleSearch3Request, false}}, + {"search", {handleNotImplemented}}, + {"search2", {handleSearch2Request}}, + {"search3", {handleSearch3Request}}, // Playlists - {"getPlaylists", {handleGetPlaylistsRequest, false}}, - {"getPlaylist", {handleGetPlaylistRequest, false}}, - {"createPlaylist", {handleCreatePlaylistRequest, false}}, - {"updatePlaylist", {handleUpdatePlaylistRequest, false}}, - {"deletePlaylist", {handleDeletePlaylistRequest, false}}, + {"getPlaylists", {handleGetPlaylistsRequest}}, + {"getPlaylist", {handleGetPlaylistRequest}}, + {"createPlaylist", {handleCreatePlaylistRequest}}, + {"updatePlaylist", {handleUpdatePlaylistRequest}}, + {"deletePlaylist", {handleDeletePlaylistRequest}}, // Media retrieval - {"hls", {handleNotImplemented, false}}, - {"getCaptions", {handleNotImplemented, false}}, - {"getLyrics", {handleNotImplemented, false}}, - {"getAvatar", {handleNotImplemented, false}}, + {"hls", {handleNotImplemented}}, + {"getCaptions", {handleNotImplemented}}, + {"getLyrics", {handleNotImplemented}}, + {"getAvatar", {handleNotImplemented}}, // Media annotation - {"star", {handleStarRequest, false}}, - {"unstar", {handleUnstarRequest, false}}, - {"setRating", {handleNotImplemented, false}}, - {"scrobble", {handleScrobble, false}}, + {"star", {handleStarRequest}}, + {"unstar", {handleUnstarRequest}}, + {"setRating", {handleNotImplemented}}, + {"scrobble", {handleScrobble}}, // Sharing - {"getShares", {handleNotImplemented, false}}, - {"createShares", {handleNotImplemented, false}}, - {"updateShare", {handleNotImplemented, false}}, - {"deleteShare", {handleNotImplemented, false}}, + {"getShares", {handleNotImplemented}}, + {"createShares", {handleNotImplemented}}, + {"updateShare", {handleNotImplemented}}, + {"deleteShare", {handleNotImplemented}}, // Podcast - {"getPodcasts", {handleNotImplemented, false}}, - {"getNewestPodcasts", {handleNotImplemented, false}}, - {"refreshPodcasts", {handleNotImplemented, false}}, - {"createPodcastChannel", {handleNotImplemented, false}}, - {"deletePodcastChannel", {handleNotImplemented, false}}, - {"deletePodcastEpisode", {handleNotImplemented, false}}, - {"downloadPodcastEpisode", {handleNotImplemented, false}}, + {"getPodcasts", {handleNotImplemented}}, + {"getNewestPodcasts", {handleNotImplemented}}, + {"refreshPodcasts", {handleNotImplemented}}, + {"createPodcastChannel", {handleNotImplemented}}, + {"deletePodcastChannel", {handleNotImplemented}}, + {"deletePodcastEpisode", {handleNotImplemented}}, + {"downloadPodcastEpisode", {handleNotImplemented}}, // Jukebox - {"jukeboxControl", {handleNotImplemented, false}}, + {"jukeboxControl", {handleNotImplemented}}, // Internet radio - {"getInternetRadioStations", {handleNotImplemented, false}}, - {"createInternetRadioStation", {handleNotImplemented, false}}, - {"updateInternetRadioStation", {handleNotImplemented, false}}, - {"deleteInternetRadioStation", {handleNotImplemented, false}}, + {"getInternetRadioStations", {handleNotImplemented}}, + {"createInternetRadioStation", {handleNotImplemented}}, + {"updateInternetRadioStation", {handleNotImplemented}}, + {"deleteInternetRadioStation", {handleNotImplemented}}, // Chat - {"getChatMessages", {handleNotImplemented, false}}, - {"addChatMessages", {handleNotImplemented, false}}, + {"getChatMessages", {handleNotImplemented}}, + {"addChatMessages", {handleNotImplemented}}, // User management - {"getUser", {handleGetUserRequest, false}}, - {"getUsers", {handleGetUsersRequest, true}}, - {"createUser", {handleCreateUserRequest, true, &checkSetPasswordImplemented}}, - {"updateUser", {handleUpdateUserRequest, true}}, - {"deleteUser", {handleDeleteUserRequest, true}}, - {"changePassword", {handleChangePassword, false, &checkSetPasswordImplemented}}, + {"getUser", {handleGetUserRequest}}, + {"getUsers", {handleGetUsersRequest, {Database::UserType::ADMIN}}}, + {"createUser", {handleCreateUserRequest, {Database::UserType::ADMIN}, &checkSetPasswordImplemented}}, + {"updateUser", {handleUpdateUserRequest, {Database::UserType::ADMIN}}}, + {"deleteUser", {handleDeleteUserRequest, {Database::UserType::ADMIN}}}, + {"changePassword", {handleChangePassword, {Database::UserType::REGULAR, Database::UserType::ADMIN}, &checkSetPasswordImplemented}}, // Bookmarks - {"getBookmarks", {handleGetBookmarks, false}}, - {"createBookmark", {handleCreateBookmark, false}}, - {"deleteBookmark", {handleDeleteBookmark, false}}, - {"getPlayQueue", {handleNotImplemented, false}}, - {"savePlayQueue", {handleNotImplemented, false}}, + {"getBookmarks", {handleGetBookmarks}}, + {"createBookmark", {handleCreateBookmark}}, + {"deleteBookmark", {handleDeleteBookmark}}, + {"getPlayQueue", {handleNotImplemented}}, + {"savePlayQueue", {handleNotImplemented}}, // Media library scanning - {"getScanStatus", {Scan::handleGetScanStatus, true}}, - {"startScan", {Scan::handleStartScan, true}}, + {"getScanStatus", {Scan::handleGetScanStatus, {Database::UserType::ADMIN}}}, + {"startScan", {Scan::handleStartScan, {Database::UserType::ADMIN}}}, }; using MediaRetrievalHandlerFunc = std::function; @@ -2112,8 +2104,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp if (itEntryPoint->second.checkFunc) itEntryPoint->second.checkFunc(); - if (itEntryPoint->second.mustBeAdmin) - checkUserIsAdmin(requestContext); + checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes); Response resp {(itEntryPoint->second.func)(requestContext)}; diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 76f05a2d..bd7339c4 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -152,6 +152,11 @@ class PasswordTooWeakGenericError : public GenericError std::string getMessage() const override { return "Password too weak"; } }; +class DemoUserCannotChangePasswordGenericError : public GenericError +{ + std::string getMessage() const override { return "Demo user cannot change its password"; } +}; + class UserAlreadyExistsGenericError : public GenericError { std::string getMessage() const override { return "User already exists"; } diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index aba96fab..5a766550 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -116,20 +116,12 @@ LmsApplication::isUserAuthStrong() const return _authenticatedUser->strongAuth; } -bool -LmsApplication::isUserAdmin() +Database::UserType +LmsApplication::getUserType() { auto transaction {getDbSession().createSharedTransaction()}; - return getUser()->isAdmin(); -} - -bool -LmsApplication::isUserDemo() -{ - auto transaction {getDbSession().createSharedTransaction()}; - - return getUser()->isDemo(); + return getUser()->getType(); } std::string @@ -458,7 +450,7 @@ LmsApplication::onUserLoggedIn() // Only one active session by user if (otherApplication.getUserId() == getUserId()) { - if (!LmsApp->isUserDemo()) + if (LmsApp->getUserType() != Database::UserType::DEMO) { quit(Wt::WString::tr("Lms.quit-other-session")); } @@ -502,7 +494,7 @@ LmsApplication::createHome() Wt::WLineEdit* searchEdit {main->bindNew("search")}; searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder")); - if (isUserAdmin()) + if (LmsApp->getUserType() == Database::UserType::ADMIN) { main->setCondition("if-is-admin", true); main->bindNew("database", Wt::WLink {Wt::LinkType::InternalPath, "/admin/database"}, Wt::WString::tr("Lms.Admin.Database.menu-database")); @@ -530,7 +522,7 @@ LmsApplication::createHome() }); // Admin stuff - if (isUserAdmin()) + if (getUserType() == Database::UserType::ADMIN) { mainStack->addNew(); mainStack->addNew(); @@ -581,7 +573,8 @@ LmsApplication::createHome() _mediaPlayer->stop(); }); - if (isUserAdmin()) + const bool isAdmin {getUserType() == Database::UserType::ADMIN}; + if (isAdmin) { _scannerEvents.scanComplete.connect([=] (const Scanner::ScanStats& stats) { @@ -597,10 +590,10 @@ LmsApplication::createHome() internalPathChanged().connect([=] { - handlePathChange(*mainStack, isUserAdmin()); + handlePathChange(*mainStack, isAdmin); }); - handlePathChange(*mainStack, isUserAdmin()); + handlePathChange(*mainStack, isAdmin); } void diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 37d7cbf7..5a00c168 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -65,9 +65,8 @@ class LmsApplication : public Wt::WApplication Wt::Dbo::ptr getUser(); Database::IdType getUserId(); bool isUserAuthStrong() const; // user must be logged in prior this call - bool isUserAdmin(); // user must be logged in prior this call - bool isUserDemo(); // user must be logged in prior this call - std::string getUserLoginName(); // user must be logged in prior this call + Database::UserType getUserType(); // user must be logged in prior this call + std::string getUserLoginName(); // user must be logged in prior this call // Proxified scanner events Scanner::Events& getScannerEvents() { return _scannerEvents; } diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index f0a84ac7..59e0cbff 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -102,7 +102,7 @@ class SettingsModel : public Wt::WFormModel } addField(PasswordField); - setValidator(PasswordField, createPasswordStrengthValidator(LmsApp->getUserLoginName())); + setValidator(PasswordField, createPasswordStrengthValidator([] { return ::Auth::PasswordValidationContext {LmsApp->getUserLoginName(), LmsApp->getUserType()}; })); addField(PasswordConfirmField); } diff --git a/src/lms/ui/admin/InitWizardView.cpp b/src/lms/ui/admin/InitWizardView.cpp index be9561b3..c353dbf0 100644 --- a/src/lms/ui/admin/InitWizardView.cpp +++ b/src/lms/ui/admin/InitWizardView.cpp @@ -54,7 +54,7 @@ class InitWizardModel : public Wt::WFormModel addField(PasswordConfirmField); setValidator(AdminLoginField, createLoginNameValidator()); - setValidator(PasswordField, createPasswordStrengthValidator([this] { return valueText(AdminLoginField).toUTF8(); })); + setValidator(PasswordField, createPasswordStrengthValidator([this] { return ::Auth::PasswordValidationContext {valueText(AdminLoginField).toUTF8(), Database::UserType::ADMIN}; })); validator(PasswordField)->setMandatory(true); setValidator(PasswordConfirmField, createMandatoryValidator()); } @@ -69,7 +69,7 @@ class InitWizardModel : public Wt::WFormModel throw LmsException {"Admin user already created"}; Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8())}; - user.modify()->setType(Database::User::Type::ADMIN); + user.modify()->setType(Database::UserType::ADMIN); Service<::Auth::IPasswordService>::get()->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8()); } diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 2d4c7ab6..20ac8b43 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -66,7 +66,7 @@ class UserModel : public Wt::WFormModel if (authPasswordService) { addField(PasswordField); - setValidator(PasswordField, createPasswordStrengthValidator([this] { return getLoginName(); })); + setValidator(PasswordField, createPasswordStrengthValidator([this] { return ::Auth::PasswordValidationContext {getLoginName(), Wt::asNumber(value(DemoField)) ? UserType::DEMO : UserType::REGULAR}; })); if (!userId) validator(PasswordField)->setMandatory(true); } @@ -100,7 +100,7 @@ class UserModel : public Wt::WFormModel user = Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8()); if (Wt::asNumber(value(DemoField))) - user.modify()->setType(Database::User::Type::DEMO); + user.modify()->setType(Database::UserType::DEMO); if (_authPasswordService) _authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8()); @@ -159,6 +159,9 @@ class UserModel : public Wt::WFormModel } else if (field == PasswordField) { + if (Wt::asNumber(value(DemoField))) + setValidator(PasswordField, {}); + validatePassword(error); } else if (field == DemoField) @@ -172,7 +175,7 @@ class UserModel : public Wt::WFormModel if (error.empty()) return Wt::WFormModel::validateField(field); - setValidation(field, Wt::WValidator::Result( Wt::ValidationState::Invalid, error)); + setValidation(field, Wt::WValidator::Result {Wt::ValidationState::Invalid, error}); return false; } diff --git a/src/lms/ui/common/PasswordValidator.cpp b/src/lms/ui/common/PasswordValidator.cpp index 20559f5d..b923fe80 100644 --- a/src/lms/ui/common/PasswordValidator.cpp +++ b/src/lms/ui/common/PasswordValidator.cpp @@ -30,12 +30,14 @@ namespace UserInterface class PasswordStrengthValidator : public Wt::WValidator { public: - PasswordStrengthValidator(LoginNameGetFunc loginNameGetFunc) : _loginNameGetFunc {std::move(loginNameGetFunc)} {} - - Wt::WValidator::Result validate(const Wt::WString& input) const override; + PasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc) + : _passwordValidationContextGetFunc {std::move(passwordValidationContextGetFunc)} + {} private: - LoginNameGetFunc _loginNameGetFunc; + Wt::WValidator::Result validate(const Wt::WString& input) const override; + + PasswordValidationContextGetFunc _passwordValidationContextGetFunc; }; Wt::WValidator::Result @@ -44,21 +46,18 @@ namespace UserInterface if (input.empty()) return Wt::WValidator::validate(input); - if (Service<::Auth::IPasswordService>::get()->isPasswordSecureEnough(_loginNameGetFunc(), input.toUTF8())) + const ::Auth::PasswordValidationContext context {_passwordValidationContextGetFunc()}; + + if (Service<::Auth::IPasswordService>::get()->isPasswordSecureEnough(input.toUTF8(), context)) return Wt::WValidator::Result {Wt::ValidationState::Valid}; return Wt::WValidator::Result {Wt::ValidationState::Invalid, Wt::WString::tr("Lms.password-too-weak")}; } std::shared_ptr - createPasswordStrengthValidator(std::string_view loginName) + createPasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc) { - return std::make_shared([loginName = std::string {loginName}] { return loginName; }); - } - - std::shared_ptr createPasswordStrengthValidator(LoginNameGetFunc loginNameGetFunc) - { - return std::make_shared(std::move(loginNameGetFunc)); + return std::make_shared(std::move(passwordValidationContextGetFunc)); } class PasswordCheckValidator : public Wt::WValidator diff --git a/src/lms/ui/common/PasswordValidator.hpp b/src/lms/ui/common/PasswordValidator.hpp index 481d0270..d504dbc1 100644 --- a/src/lms/ui/common/PasswordValidator.hpp +++ b/src/lms/ui/common/PasswordValidator.hpp @@ -19,13 +19,16 @@ #pragma once +#include #include +#include "database/Types.hpp" +#include "auth/Types.hpp" + namespace UserInterface { - std::shared_ptr createPasswordStrengthValidator(std::string_view loginName); - using LoginNameGetFunc = std::function; - std::shared_ptr createPasswordStrengthValidator(LoginNameGetFunc loginNameGetFunc); + using PasswordValidationContextGetFunc = std::function<::Auth::PasswordValidationContext()>; + std::shared_ptr createPasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc); // Check current user password std::shared_ptr createPasswordCheckValidator(); From a261484bb8f8496466570e33504fb13901677c33 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 3 Sep 2021 13:41:02 +0200 Subject: [PATCH 07/25] Updated README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8bd31ab3..a20d26d8 100644 --- a/README.md +++ b/README.md @@ -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 From 7affe08fc47493228b16a313ed450cfba8880857 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 3 Sep 2021 13:41:30 +0200 Subject: [PATCH 08/25] Centralized demo account logic in the password auth service. ref #167 --- approot/messages.xml | 2 +- approot/messages_fr.xml | 2 +- approot/messages_it.xml | 2 +- approot/messages_zh.xml | 2 +- .../impl/internal/InternalPasswordService.cpp | 19 +++++++---- .../impl/internal/InternalPasswordService.hpp | 2 +- src/libs/auth/impl/pam/PAMPasswordService.cpp | 4 +-- src/libs/auth/impl/pam/PAMPasswordService.hpp | 2 +- .../auth/include/auth/IPasswordService.hpp | 20 +++++++---- src/libs/auth/include/auth/Types.hpp | 22 ++++++++++-- src/libs/subsonic/impl/SubsonicResource.cpp | 17 ++++++++-- src/libs/subsonic/impl/SubsonicResponse.hpp | 5 +++ src/lms/ui/SettingsView.cpp | 8 +++++ src/lms/ui/admin/UserView.cpp | 34 ++++++++----------- src/lms/ui/common/PasswordValidator.cpp | 13 +++++-- 15 files changed, 107 insertions(+), 47 deletions(-) diff --git a/approot/messages.xml b/approot/messages.xml index b599b76c..36d4bedd 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -17,6 +17,7 @@ Bad login / password combination Login throttled, please try again later Confirm password +Password must match the login name! New password Old password Password too weak @@ -94,7 +95,6 @@ Demo account Demo account already exists! -Demo password must be the login name! Last login User already exists! New user diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 5b688e0e..bf3ba440 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -17,6 +17,7 @@ Mauvaise combinaison login / mot de passe Trop de tentatives de connexion, veuillez réessayer plus tard Confirmation du mot de passe +Le password doit être égal au login ! Nouveau mot de passe Ancien mot de passe Mot de passe trop faible @@ -94,7 +95,6 @@ Compte de démonstration Le compte de démonstration existe déjà ! -Le password doit être égal au login ! Date du dernier login L'utilisateur existe déjà ! Nouvel utilisateur diff --git a/approot/messages_it.xml b/approot/messages_it.xml index ad690776..f285e9dc 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -17,6 +17,7 @@ Errata combinazione di Login / Password Superati i tentativi di accesso, riprova più tardi Conferma la password +La password deve essere il nome utente! Nuova password Vecchia password La password è troppo debole @@ -93,7 +94,6 @@ Account demo L'account demo è già esistente! -La password dell'account demo deve essere il nome utente! Ultimo accesso Utente già esistente! Crea utente diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 5e914553..e7f5dab7 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -17,6 +17,7 @@ 无效的登陆 / 密码组合 登录已被限制,请稍后再试 确认密码 +演示密码必须是登录名! 新密码 旧密码 密码太弱 @@ -94,7 +95,6 @@ 演示账号 演示账号已存在! -演示密码必须是登录名! 最后登录 用户已存在! 新建用户 diff --git a/src/libs/auth/impl/internal/InternalPasswordService.cpp b/src/libs/auth/impl/internal/InternalPasswordService.cpp index 2d17c044..82acd56b 100644 --- a/src/libs/auth/impl/internal/InternalPasswordService.cpp +++ b/src/libs/auth/impl/internal/InternalPasswordService.cpp @@ -80,16 +80,16 @@ namespace Auth return true; } - bool - InternalPasswordService::isPasswordSecureEnough(std::string_view password, const PasswordValidationContext& context) const + IPasswordService::PasswordAcceptabilityResult + InternalPasswordService::checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const { switch (context.userType) { case Database::UserType::ADMIN: case Database::UserType::REGULAR: - return _validator.evaluateStrength(std::string {password}, context.loginName, "").isValid(); + return _validator.evaluateStrength(std::string {password}, context.loginName, "").isValid() ? PasswordAcceptabilityResult::OK : PasswordAcceptabilityResult::TooWeak; case Database::UserType::DEMO: - return true; // no constraint + return password == context.loginName ? PasswordAcceptabilityResult::OK : PasswordAcceptabilityResult::MustMatchLoginName; } throw NotImplementedException {}; @@ -106,8 +106,15 @@ namespace Auth if (!user) throw Exception {"User not found!"}; - if (!isPasswordSecureEnough(newPassword, PasswordValidationContext {user->getLoginName(), user->getType()} )) - 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); diff --git a/src/libs/auth/impl/internal/InternalPasswordService.hpp b/src/libs/auth/impl/internal/InternalPasswordService.hpp index 67a5da08..6e599204 100644 --- a/src/libs/auth/impl/internal/InternalPasswordService.hpp +++ b/src/libs/auth/impl/internal/InternalPasswordService.hpp @@ -41,7 +41,7 @@ namespace Auth std::string_view password) override; bool canSetPasswords() const override; - bool isPasswordSecureEnough(std::string_view loginName, const PasswordValidationContext& context) const override; + PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override; void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) override; Database::User::PasswordHash hashPassword(std::string_view password) const; diff --git a/src/libs/auth/impl/pam/PAMPasswordService.cpp b/src/libs/auth/impl/pam/PAMPasswordService.cpp index 7d8e1af2..47b75932 100644 --- a/src/libs/auth/impl/pam/PAMPasswordService.cpp +++ b/src/libs/auth/impl/pam/PAMPasswordService.cpp @@ -186,8 +186,8 @@ namespace Auth return false; } - bool - PAMPasswordService::isPasswordSecureEnough(std::string_view, const PasswordValidationContext&) const + IPasswordService::PasswordAcceptabilityResult + PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const { throw NotImplementedException {}; } diff --git a/src/libs/auth/impl/pam/PAMPasswordService.hpp b/src/libs/auth/impl/pam/PAMPasswordService.hpp index 1b1bcc1c..b6213e17 100644 --- a/src/libs/auth/impl/pam/PAMPasswordService.hpp +++ b/src/libs/auth/impl/pam/PAMPasswordService.hpp @@ -36,7 +36,7 @@ namespace Auth std::string_view password) override; bool canSetPasswords() const override; - bool isPasswordSecureEnough(std::string_view loginName, const PasswordValidationContext& context) const override; + PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override; void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) override; diff --git a/src/libs/auth/include/auth/IPasswordService.hpp b/src/libs/auth/include/auth/IPasswordService.hpp index 0c4b1cc5..a5b9a0bc 100644 --- a/src/libs/auth/include/auth/IPasswordService.hpp +++ b/src/libs/auth/include/auth/IPasswordService.hpp @@ -56,15 +56,21 @@ namespace Auth std::optional userId {}; std::optional 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; - virtual bool canSetPasswords() const = 0; + virtual bool canSetPasswords() const = 0; - virtual bool isPasswordSecureEnough(std::string_view password, const PasswordValidationContext& context) const = 0; - virtual void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) = 0; + enum class PasswordAcceptabilityResult + { + OK, + TooWeak, + MustMatchLoginName, + }; + virtual PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const = 0; + virtual void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) = 0; }; std::unique_ptr createPasswordService(std::string_view authPasswordBackend, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService); diff --git a/src/libs/auth/include/auth/Types.hpp b/src/libs/auth/include/auth/Types.hpp index dec88e0a..8b97df6e 100644 --- a/src/libs/auth/include/auth/Types.hpp +++ b/src/libs/auth/include/auth/Types.hpp @@ -36,16 +36,34 @@ namespace Auth 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 PasswordTooWeakException : public Exception + class PasswordException : public Exception { public: - PasswordTooWeakException() : Auth::Exception {"Password too weak"} {} + 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"} {} }; } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index fa22a14b..2c0a7e1a 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -559,11 +559,15 @@ handleChangePassword(RequestContext& context) Service::get()->setPassword(context.dbSession, userId, password); } - catch (Auth::PasswordTooWeakException&) + catch (const Auth::PasswordMustMatchLoginNameException&) + { + throw PasswordMustMatchLoginNameGenericError {}; + } + catch (const Auth::PasswordTooWeakException&) { throw PasswordTooWeakGenericError {}; } - catch (Auth::Exception& authException) + catch (const Auth::Exception& authException) { throw UserNotAuthorizedError {}; } @@ -658,6 +662,11 @@ handleCreateUserRequest(RequestContext& context) { Service::get()->setPassword(context.dbSession, userId, password); } + catch (const Auth::PasswordMustMatchLoginNameException&) + { + removeCreatedUser(); + throw PasswordMustMatchLoginNameGenericError {}; + } catch (const Auth::PasswordTooWeakException&) { removeCreatedUser(); @@ -1711,6 +1720,10 @@ handleUpdateUserRequest(RequestContext& context) { Service<::Auth::IPasswordService>()->setPassword(context.dbSession, userId, decodePasswordIfNeeded(*password)); } + catch (const Auth::PasswordMustMatchLoginNameException&) + { + throw PasswordMustMatchLoginNameGenericError {}; + } catch (const Auth::PasswordTooWeakException&) { throw PasswordTooWeakGenericError {}; diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index bd7339c4..a19c195d 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -152,6 +152,11 @@ class PasswordTooWeakGenericError : public GenericError std::string getMessage() const override { return "Password too weak"; } }; +class PasswordMustMatchLoginNameGenericError : public GenericError +{ + std::string getMessage() const override { return "Password must match login name"; } +}; + class DemoUserCannotChangePasswordGenericError : public GenericError { std::string getMessage() const override { return "Demo user cannot change its password"; } diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 59e0cbff..fc446feb 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -276,6 +276,14 @@ class SettingsModel : public Wt::WFormModel validator(SettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz); } } + if (_authPasswordService) + { + if (_withOldPassword) + setValue(PasswordOldField, ""); + + setValue(PasswordField, ""); + setValue(PasswordConfirmField, ""); + } } private: diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 20ac8b43..e0467939 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -66,7 +66,7 @@ class UserModel : public Wt::WFormModel if (authPasswordService) { addField(PasswordField); - setValidator(PasswordField, createPasswordStrengthValidator([this] { return ::Auth::PasswordValidationContext {getLoginName(), Wt::asNumber(value(DemoField)) ? UserType::DEMO : UserType::REGULAR}; })); + setValidator(PasswordField, createPasswordStrengthValidator([this] { return ::Auth::PasswordValidationContext {getLoginName(), getUserType()}; })); if (!userId) validator(PasswordField)->setMandatory(true); } @@ -122,6 +122,19 @@ class UserModel : public Wt::WFormModel throw UserNotAllowedException {}; } + Database::UserType getUserType() const + { + if (_userId) + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; + return user->getType(); + } + + return Wt::asNumber(value(DemoField)) ? UserType::DEMO : UserType::REGULAR; + } + std::string getLoginName() const { if (_userId) @@ -131,18 +144,8 @@ class UserModel : public Wt::WFormModel const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; return user->getLoginName(); } - else - return valueText(LoginField).toUTF8(); - } - void validatePassword(Wt::WString& error) const - { - if (!valueText(PasswordField).empty() && Wt::asNumber(value(DemoField))) - { - // Demo account: password must be the same as the login name - if (valueText(PasswordField) != getLoginName()) - error = Wt::WString::tr("Lms.Admin.User.demo-password-invalid"); - } + return valueText(LoginField).toUTF8(); } bool validateField(Field field) @@ -157,13 +160,6 @@ class UserModel : public Wt::WFormModel if (user) error = Wt::WString::tr("Lms.Admin.User.user-already-exists"); } - else if (field == PasswordField) - { - if (Wt::asNumber(value(DemoField))) - setValidator(PasswordField, {}); - - validatePassword(error); - } else if (field == DemoField) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; diff --git a/src/lms/ui/common/PasswordValidator.cpp b/src/lms/ui/common/PasswordValidator.cpp index b923fe80..443d556f 100644 --- a/src/lms/ui/common/PasswordValidator.cpp +++ b/src/lms/ui/common/PasswordValidator.cpp @@ -48,10 +48,17 @@ namespace UserInterface const ::Auth::PasswordValidationContext context {_passwordValidationContextGetFunc()}; - if (Service<::Auth::IPasswordService>::get()->isPasswordSecureEnough(input.toUTF8(), context)) - return Wt::WValidator::Result {Wt::ValidationState::Valid}; + switch (Service<::Auth::IPasswordService>::get()->checkPasswordAcceptability(input.toUTF8(), context)) + { + case ::Auth::IPasswordService::PasswordAcceptabilityResult::OK: + return Wt::WValidator::Result {Wt::ValidationState::Valid}; + case ::Auth::IPasswordService::PasswordAcceptabilityResult::TooWeak: + return Wt::WValidator::Result {Wt::ValidationState::Invalid, Wt::WString::tr("Lms.password-too-weak")}; + case ::Auth::IPasswordService::PasswordAcceptabilityResult::MustMatchLoginName: + return Wt::WValidator::Result {Wt::ValidationState::Invalid, Wt::WString::tr("Lms.password-must-match-login")}; + } - return Wt::WValidator::Result {Wt::ValidationState::Invalid, Wt::WString::tr("Lms.password-too-weak")}; + throw LmsException {"internal error"}; } std::shared_ptr From f24ef27d2a5730d2834eb8aa3cb2bd28fe445b80 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 15 Sep 2021 12:56:01 +0200 Subject: [PATCH 09/25] Added non album tracks in the artist view. ref #165 --- approot/artist.xml | 11 +++++++ docroot/css/lms.css | 5 +++ src/libs/database/impl/Artist.cpp | 7 ++-- src/libs/database/include/database/Artist.hpp | 2 +- src/lms/ui/explore/ArtistView.cpp | 32 +++++++++++++++++-- src/lms/ui/explore/ArtistView.hpp | 3 ++ src/lms/ui/explore/Explore.cpp | 1 + src/test/database/Artist.cpp | 24 ++++++++++++++ 8 files changed, 79 insertions(+), 6 deletions(-) diff --git a/approot/artist.xml b/approot/artist.xml index 21f3e7a9..f1e839d8 100644 --- a/approot/artist.xml +++ b/approot/artist.xml @@ -8,9 +8,20 @@ ${clusters} ${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"} + ${}
${releases class="Lms-explore-artist-entries-container"}
+ ${
} + ${} +
+
+
+ ${tracks} +
+
+
+ ${
}
${}
diff --git a/docroot/css/lms.css b/docroot/css/lms.css index d8020abc..c2ff47ac 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -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; } diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 368de2ed..91925b58 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -465,15 +465,16 @@ Artist::getTracks(std::optional linkType) const } std::vector> -Artist::getTracksWithRelease(std::optional linkType) const +Artist::getNonReleaseTracks(std::optional linkType) const { assert(self()); assert(IdIsValid(self()->id())); assert(session()); - auto query {session()->query>("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") + auto query {session()->query>("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()) - .orderBy("t.year,r.name,t.disc_number,t.track_number")}; + .where("t.release_id is NULL") + .orderBy("t.name")}; if (linkType) query.where("t_a_l.type = ?").bind(*linkType); diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index 0b252d59..824243f8 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -104,7 +104,7 @@ class Artist : public Wt::Dbo::Dbo std::vector> getReleases(const std::set& clusterIds = {}) const; // if non empty, get the releases that match all these clusters std::size_t getReleaseCount() const; std::vector> getTracks(std::optional linkType = {}) const; - std::vector> getTracksWithRelease(std::optional linkType = {}) const; + std::vector> getNonReleaseTracks(std::optional linkType = {}) const; std::vector> getRandomTracks(std::optional count) const; // No artistLinkTypes means get them all diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 68b428d6..1a1be090 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -29,6 +29,7 @@ #include "database/Release.hpp" #include "database/ScanSettings.hpp" #include "database/Session.hpp" +#include "database/Track.hpp" #include "database/User.hpp" #include "recommendation/IEngine.hpp" #include "utils/Logger.hpp" @@ -40,6 +41,7 @@ #include "LmsApplication.hpp" #include "LmsApplicationException.hpp" #include "ReleaseListHelpers.hpp" +#include "TrackListHelpers.hpp" using namespace Database; @@ -107,6 +109,8 @@ Artist::refreshView() if (!artist) throw ArtistNotFoundException {}; + refreshReleases(artist); + refreshNonReleaseTracks(artist); refreshLinks(artist); refreshSimilarArtists(similarArtistIds); @@ -185,16 +189,40 @@ Artist::refreshView() popup->exec(moreBtn); }); } +} + +void +Artist::refreshReleases(const Wt::Dbo::ptr& artist) +{ + const auto releases {artist->getReleases(_filters->getClusterIds())}; + if (releases.empty()) + return; + + setCondition("if-has-release", true); Wt::WContainerWidget* releasesContainer = bindNew("releases"); - - auto releases = artist->getReleases(_filters->getClusterIds()); for (const auto& release : releases) { releasesContainer->addWidget(ReleaseListHelpers::createEntryForArtist(release, artist)); } } +void +Artist::refreshNonReleaseTracks(const Wt::Dbo::ptr& artist) +{ + const auto tracks {artist->getNonReleaseTracks()}; + if (tracks.empty()) + return; + + setCondition("if-has-non-release-track", true); + + Wt::WContainerWidget* tracksContainer = bindNew("tracks"); + for (const Track::pointer& track : tracks) + { + tracksContainer->addWidget(TrackListHelpers::createEntry(track, tracksAction)); + } +} + void Artist::refreshSimilarArtists(const std::unordered_set& similarArtistsId) { diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index 5b9fe034..3f17238b 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -44,9 +44,12 @@ class Artist : public Wt::WTemplate Artist(Filters* filters); PlayQueueActionSignal artistsAction; + PlayQueueActionSignal tracksAction; private: void refreshView(); + void refreshReleases(const Wt::Dbo::ptr& artist); + void refreshNonReleaseTracks(const Wt::Dbo::ptr& artist); void refreshSimilarArtists(const std::unordered_set& similarArtistsId); void refreshLinks(const Wt::Dbo::ptr& artist); diff --git a/src/lms/ui/explore/Explore.cpp b/src/lms/ui/explore/Explore.cpp index 06c32bd0..de7a1f6f 100644 --- a/src/lms/ui/explore/Explore.cpp +++ b/src/lms/ui/explore/Explore.cpp @@ -93,6 +93,7 @@ Explore::Explore(Filters* filters) auto artist = std::make_unique(_filters); artist->artistsAction.connect(this, &Explore::handleArtistsAction); + artist->tracksAction.connect(this, &Explore::handleTracksAction); contentsStack->addWidget(std::move(artist)); auto releases = std::make_unique(*_filters); diff --git a/src/test/database/Artist.cpp b/src/test/database/Artist.cpp index 4c16f5fc..d32d7c0e 100644 --- a/src/test/database/Artist.cpp +++ b/src/test/database/Artist.cpp @@ -312,3 +312,27 @@ TEST_F(DatabaseFixture, MultiArtistsSortMethod) } } +TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) +{ + ScopedArtist artist {session, "artist"}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedRelease release{session, "MyRelease"}; + { + auto transaction {session.createUniqueTransaction()}; + + TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist); + + track1.get().modify()->setRelease(release.get()); + } + + + { + auto transaction {session.createSharedTransaction()}; + + const auto tracks {artist->getNonReleaseTracks()}; + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front().id(), track2.getId()); + } +} From 598f01069eee4cff370238dc9f2eae1b79eca0ca Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 15 Sep 2021 14:03:21 +0200 Subject: [PATCH 10/25] Added non album tracks in the artist view. closes #165 --- approot/artist.xml | 6 +-- src/libs/database/impl/Artist.cpp | 35 ++++++++++-- src/libs/database/include/database/Artist.hpp | 3 +- src/lms/ui/explore/ArtistView.cpp | 54 +++++++++++++++---- src/lms/ui/explore/ArtistView.hpp | 41 +++++++------- src/test/database/Artist.cpp | 15 +++++- 6 files changed, 114 insertions(+), 40 deletions(-) diff --git a/approot/artist.xml b/approot/artist.xml index f1e839d8..79f7c3c9 100644 --- a/approot/artist.xml +++ b/approot/artist.xml @@ -15,11 +15,7 @@ ${} ${}
-
-
- ${tracks} -
-
+ ${tracks}
${
}
diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 91925b58..36c6dede 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -465,12 +465,39 @@ Artist::getTracks(std::optional linkType) const } std::vector> -Artist::getNonReleaseTracks(std::optional linkType) const +Artist::getNonReleaseTracks(std::optional linkType, std::optional range, bool& moreResults) const { assert(self()); assert(IdIsValid(self()->id())); assert(session()); + auto query {session()->query>("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("t.release_id is NULL") + .orderBy("t.name") + .limit(range ? static_cast(range->limit) + 1 : -1) + .offset(range ? static_cast(range->offset) : -1)}; + + if (linkType) + query.where("t_a_l.type = ?").bind(*linkType); + + Wt::Dbo::collection tracks {query.resultList()}; + + auto res {std::vector(tracks.begin(), tracks.end())}; + if (range && res.size() == static_cast(range->limit) + 1) + { + moreResults = true; + res.pop_back(); + } + else + moreResults = false; + + return res; +} + +bool +Artist::hasNonReleaseTracks(std::optional linkType) const +{ auto query {session()->query>("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("t.release_id is NULL") @@ -479,12 +506,10 @@ Artist::getNonReleaseTracks(std::optional linkType) const if (linkType) query.where("t_a_l.type = ?").bind(*linkType); - Wt::Dbo::collection> tracks {query.resultList()}; - - return std::vector>(tracks.begin(), tracks.end()); + Wt::Dbo::collection tracks {query.resultList()}; + return !tracks.empty(); } - std::vector> Artist::getRandomTracks(std::optional count) const { diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index 824243f8..a9ef7c81 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -104,7 +104,8 @@ class Artist : public Wt::Dbo::Dbo std::vector> getReleases(const std::set& clusterIds = {}) const; // if non empty, get the releases that match all these clusters std::size_t getReleaseCount() const; std::vector> getTracks(std::optional linkType = {}) const; - std::vector> getNonReleaseTracks(std::optional linkType = {}) const; + bool hasNonReleaseTracks(std::optional linkType = std::nullopt) const; + std::vector> getNonReleaseTracks(std::optional linkType, std::optional range, bool& moreResults) const; std::vector> getRandomTracks(std::optional count) const; // No artistLinkTypes means get them all diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 1a1be090..79941fc1 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -35,6 +35,7 @@ #include "utils/Logger.hpp" #include "utils/String.hpp" +#include "common/InfiniteScrollingContainer.hpp" #include "resource/DownloadResource.hpp" #include "ArtistListHelpers.hpp" #include "Filters.hpp" @@ -93,6 +94,8 @@ Artist::refreshView() return; clear(); + _artistId = {}; + _trackContainer = nullptr; const auto artistId {extractArtistIdFromInternalPath()}; if (!artistId) @@ -109,6 +112,8 @@ Artist::refreshView() if (!artist) throw ArtistNotFoundException {}; + _artistId = *artistId; + refreshReleases(artist); refreshNonReleaseTracks(artist); refreshLinks(artist); @@ -140,7 +145,7 @@ Artist::refreshView() playBtn->clicked().connect([=] { - artistsAction.emit(PlayQueueAction::Play, {*artistId}); + artistsAction.emit(PlayQueueAction::Play, {_artistId}); }); } @@ -154,12 +159,12 @@ Artist::refreshView() popup->addItem(Wt::WString::tr("Lms.Explore.play-shuffled")) ->triggered().connect(this, [=] { - artistsAction.emit(PlayQueueAction::PlayShuffled, {*artistId}); + artistsAction.emit(PlayQueueAction::PlayShuffled, {_artistId}); }); popup->addItem(Wt::WString::tr("Lms.Explore.play-last")) ->triggered().connect(this, [=] { - artistsAction.emit(PlayQueueAction::PlayLast, {*artistId}); + artistsAction.emit(PlayQueueAction::PlayLast, {_artistId}); }); bool isStarred {}; @@ -210,17 +215,17 @@ Artist::refreshReleases(const Wt::Dbo::ptr& artist) void Artist::refreshNonReleaseTracks(const Wt::Dbo::ptr& artist) { - const auto tracks {artist->getNonReleaseTracks()}; - if (tracks.empty()) + if (!artist->hasNonReleaseTracks()) return; setCondition("if-has-non-release-track", true); - - Wt::WContainerWidget* tracksContainer = bindNew("tracks"); - for (const Track::pointer& track : tracks) + _trackContainer = bindNew("tracks", Wt::WString::tr("Lms.Explore.Tracks.template.container")); + _trackContainer->onRequestElements.connect(this, [this] { - tracksContainer->addWidget(TrackListHelpers::createEntry(track, tracksAction)); - } + addSomeNonReleaseTracks(); + }); + + addSomeNonReleaseTracks(); } void @@ -257,5 +262,34 @@ Artist::refreshLinks(const Database::Artist::pointer& artist) } } +void +Artist::addSomeNonReleaseTracks() +{ + bool moreResults {}; + + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)}; + if (!artist) + return; + + const auto tracks {artist->getNonReleaseTracks(std::nullopt, Database::Range {static_cast(_trackContainer->getCount()), _tracksBatchSize}, moreResults)}; + + for (const auto& track : tracks) + { + if (_trackContainer->getCount() == _tracksMaxCount) + { + moreResults = false; + break; + } + + _trackContainer->add(TrackListHelpers::createEntry(track, tracksAction)); + } + } + + _trackContainer->setHasMore(moreResults); +} + } // namespace UserInterface diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index 3f17238b..2d5ce695 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -34,29 +34,34 @@ namespace Database class Release; } -namespace UserInterface { - -class Filters; - -class Artist : public Wt::WTemplate +namespace UserInterface { - public: - Artist(Filters* filters); - PlayQueueActionSignal artistsAction; - PlayQueueActionSignal tracksAction; + class Filters; + class InfiniteScrollingContainer; - private: - void refreshView(); - void refreshReleases(const Wt::Dbo::ptr& artist); - void refreshNonReleaseTracks(const Wt::Dbo::ptr& artist); - void refreshSimilarArtists(const std::unordered_set& similarArtistsId); - void refreshLinks(const Wt::Dbo::ptr& artist); + class Artist : public Wt::WTemplate + { + public: + Artist(Filters* filters); - std::unique_ptr createRelease(const Wt::Dbo::ptr& artist, const Wt::Dbo::ptr& release); + PlayQueueActionSignal artistsAction; + PlayQueueActionSignal tracksAction; - Filters* _filters {}; -}; + private: + void refreshView(); + void refreshReleases(const Wt::Dbo::ptr& artist); + void refreshNonReleaseTracks(const Wt::Dbo::ptr& artist); + void refreshSimilarArtists(const std::unordered_set& similarArtistsId); + void refreshLinks(const Wt::Dbo::ptr& artist); + void addSomeNonReleaseTracks(); + static constexpr std::size_t _tracksBatchSize {6}; + static constexpr std::size_t _tracksMaxCount {160}; + + Filters* _filters {}; + InfiniteScrollingContainer* _trackContainer {}; + Database::IdType _artistId {}; + }; } // namespace UserInterface diff --git a/src/test/database/Artist.cpp b/src/test/database/Artist.cpp index d32d7c0e..efb43d68 100644 --- a/src/test/database/Artist.cpp +++ b/src/test/database/Artist.cpp @@ -318,6 +318,16 @@ TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) ScopedTrack track1 {session, "MyTrack1"}; ScopedTrack track2 {session, "MyTrack2"}; ScopedRelease release{session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(artist->hasNonReleaseTracks(std::nullopt), false); + + bool moreResults; + const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )}; + EXPECT_EQ(tracks.size(), 0); + } + { auto transaction {session.createUniqueTransaction()}; @@ -331,7 +341,10 @@ TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) { auto transaction {session.createSharedTransaction()}; - const auto tracks {artist->getNonReleaseTracks()}; + bool moreResults; + const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )}; + EXPECT_EQ(artist->hasNonReleaseTracks(std::nullopt), true); + EXPECT_EQ(moreResults, false); ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front().id(), track2.getId()); } From 441aed622c70acec55257a9301fa307d436948b6 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 20 Sep 2021 23:53:38 +0200 Subject: [PATCH 11/25] Made database ID manipulations safer --- CMakeLists.txt | 3 + src/libs/auth/impl/AuthServiceBase.cpp | 6 +- src/libs/auth/impl/AuthServiceBase.hpp | 4 +- src/libs/auth/impl/AuthTokenService.cpp | 6 +- src/libs/auth/impl/AuthTokenService.hpp | 4 +- src/libs/auth/impl/PasswordServiceBase.cpp | 2 +- .../http-headers/HttpHeadersEnvService.cpp | 4 +- .../impl/internal/InternalPasswordService.cpp | 4 +- .../impl/internal/InternalPasswordService.hpp | 2 +- src/libs/auth/impl/pam/PAMPasswordService.cpp | 2 +- src/libs/auth/impl/pam/PAMPasswordService.hpp | 2 +- .../auth/include/auth/IAuthTokenService.hpp | 6 +- src/libs/auth/include/auth/IEnvService.hpp | 2 +- .../auth/include/auth/IPasswordService.hpp | 4 +- src/libs/cover/impl/CoverArtGrabber.cpp | 26 +- src/libs/cover/impl/CoverArtGrabber.hpp | 27 +- .../cover/include/cover/ICoverArtGrabber.hpp | 4 +- src/libs/database/impl/Artist.cpp | 166 ++++----- src/libs/database/impl/Cluster.cpp | 82 ++--- src/libs/database/impl/Release.cpp | 222 ++++++----- src/libs/database/impl/ScanSettings.cpp | 14 +- src/libs/database/impl/Track.cpp | 214 +++++------ src/libs/database/impl/TrackArtistLink.cpp | 14 +- src/libs/database/impl/TrackBookmark.cpp | 35 +- src/libs/database/impl/TrackFeatures.cpp | 8 +- src/libs/database/impl/TrackList.cpp | 226 ++++++------ src/libs/database/impl/Traits.hpp | 60 +++ src/libs/database/impl/User.cpp | 81 ++-- src/libs/database/include/database/Artist.hpp | 53 ++- .../database/include/database/Cluster.hpp | 36 +- .../database/include/database/Release.hpp | 43 +-- .../include/database/ScanSettings.hpp | 13 +- src/libs/database/include/database/Track.hpp | 61 ++- .../include/database/TrackArtistLink.hpp | 14 +- .../include/database/TrackBookmark.hpp | 20 +- .../include/database/TrackFeatures.hpp | 11 +- .../database/include/database/TrackList.hpp | 65 ++-- src/libs/database/include/database/Types.hpp | 104 +++++- src/libs/database/include/database/User.hpp | 44 +-- src/libs/recommendation/CMakeLists.txt | 4 +- src/libs/recommendation/impl/Engine.cpp | 31 +- src/libs/recommendation/impl/Engine.hpp | 12 +- ...rCreator.hpp => FeaturesEngineCreator.hpp} | 2 +- src/libs/recommendation/impl/IClassifier.hpp | 18 +- .../impl/clusters/ClustersClassifier.cpp | 37 +- .../impl/clusters/ClustersClassifier.hpp | 10 +- .../impl/features/FeaturesClassifier.hpp | 117 ------ ...turesClassifier.cpp => FeaturesEngine.cpp} | 263 +++++-------- .../impl/features/FeaturesEngine.hpp | 216 +++++++++++ ...ifierCache.cpp => FeaturesEngineCache.cpp} | 34 +- ...ifierCache.hpp => FeaturesEngineCache.hpp} | 16 +- .../include/recommendation/IEngine.hpp | 25 +- .../recommendation/IRecommendation.hpp} | 37 +- src/libs/scanner/impl/Scanner.cpp | 20 +- src/libs/scanner/impl/Scanner.hpp | 9 +- .../scanner/include/scanner/ScannerStats.hpp | 2 +- src/libs/scrobbling/impl/IScrobbler.hpp | 3 +- src/libs/scrobbling/impl/Scrobbling.cpp | 66 ++-- src/libs/scrobbling/impl/Scrobbling.hpp | 40 +- .../impl/internal/InternalScrobbler.cpp | 6 +- .../impl/internal/InternalScrobbler.hpp | 7 +- .../listenbrainz/ListenBrainzScrobbler.cpp | 2 +- .../listenbrainz/ListenBrainzScrobbler.hpp | 2 +- .../impl/listenbrainz/ListensSynchronizer.cpp | 14 +- .../impl/listenbrainz/ListensSynchronizer.hpp | 10 +- .../scrobbling/impl/listenbrainz/Utils.cpp | 2 +- .../scrobbling/impl/listenbrainz/Utils.hpp | 7 +- .../include/scrobbling/IScrobbling.hpp | 38 +- .../scrobbling/include/scrobbling/Listen.hpp | 4 +- src/libs/som/impl/Network.cpp | 3 +- src/libs/som/include/som/Network.hpp | 3 +- src/libs/subsonic/CMakeLists.txt | 1 - src/libs/subsonic/impl/ParameterParsing.hpp | 15 +- src/libs/subsonic/impl/RequestContext.hpp | 2 +- src/libs/subsonic/impl/Stream.cpp | 10 +- src/libs/subsonic/impl/SubsonicId.cpp | 175 ++++++--- src/libs/subsonic/impl/SubsonicId.hpp | 56 ++- src/libs/subsonic/impl/SubsonicResource.cpp | 347 +++++++----------- src/libs/utils/include/utils/Utils.hpp | 23 +- src/lms/ui/Auth.cpp | 10 +- src/lms/ui/Auth.hpp | 4 +- src/lms/ui/LmsApplication.cpp | 28 +- src/lms/ui/LmsApplication.hpp | 18 +- src/lms/ui/LmsApplicationManager.hpp | 2 +- src/lms/ui/MediaPlayer.cpp | 8 +- src/lms/ui/MediaPlayer.hpp | 18 +- src/lms/ui/PlayQueue.cpp | 27 +- src/lms/ui/PlayQueue.hpp | 14 +- src/lms/ui/PlayQueueAction.hpp | 5 +- src/lms/ui/SettingsView.cpp | 2 +- src/lms/ui/admin/InitWizardView.cpp | 2 +- src/lms/ui/admin/UserView.cpp | 10 +- src/lms/ui/admin/UsersView.cpp | 4 +- src/lms/ui/explore/ArtistCollector.cpp | 2 +- src/lms/ui/explore/ArtistCollector.hpp | 6 +- src/lms/ui/explore/ArtistListHelpers.cpp | 4 +- src/lms/ui/explore/ArtistListHelpers.hpp | 4 +- src/lms/ui/explore/ArtistView.cpp | 17 +- src/lms/ui/explore/ArtistView.hpp | 14 +- src/lms/ui/explore/Explore.cpp | 30 +- src/lms/ui/explore/Explore.hpp | 8 +- src/lms/ui/explore/Filters.cpp | 28 +- src/lms/ui/explore/Filters.hpp | 7 +- src/lms/ui/explore/ReleaseCollector.cpp | 6 +- src/lms/ui/explore/ReleaseCollector.hpp | 8 +- src/lms/ui/explore/ReleaseListHelpers.cpp | 4 +- src/lms/ui/explore/ReleaseListHelpers.hpp | 5 +- src/lms/ui/explore/ReleasePopup.cpp | 4 +- src/lms/ui/explore/ReleasePopup.hpp | 4 +- src/lms/ui/explore/ReleaseView.cpp | 23 +- src/lms/ui/explore/ReleaseView.hpp | 12 +- src/lms/ui/explore/ReleasesView.cpp | 2 +- src/lms/ui/explore/ReleasesView.hpp | 4 +- src/lms/ui/explore/SearchView.hpp | 2 +- src/lms/ui/explore/TrackCollector.cpp | 10 +- src/lms/ui/explore/TrackCollector.hpp | 8 +- src/lms/ui/explore/TrackListHelpers.cpp | 8 +- src/lms/ui/explore/TrackListHelpers.hpp | 3 +- src/lms/ui/explore/TrackPopup.cpp | 4 +- src/lms/ui/explore/TrackPopup.hpp | 4 +- src/lms/ui/explore/TracksView.cpp | 2 +- src/lms/ui/explore/TracksView.hpp | 4 +- src/lms/ui/resource/AudioFileResource.cpp | 8 +- src/lms/ui/resource/AudioFileResource.hpp | 2 +- .../ui/resource/AudioTranscodeResource.cpp | 10 +- .../ui/resource/AudioTranscodeResource.hpp | 6 +- src/lms/ui/resource/CoverResource.cpp | 12 +- src/lms/ui/resource/CoverResource.hpp | 4 +- src/lms/ui/resource/DownloadResource.cpp | 8 +- src/lms/ui/resource/DownloadResource.hpp | 12 +- src/test/database/Artist.cpp | 64 ++-- src/test/database/Cluster.cpp | 174 ++++----- src/test/database/Common.hpp | 8 +- src/test/database/DatabaseTest.cpp | 84 ++--- src/test/database/Track.cpp | 10 +- src/test/som/SomTest.cpp | 2 +- src/tools/cover/LmsCover.cpp | 6 +- .../recommendation/LmsRecommendation.cpp | 36 +- 138 files changed, 2164 insertions(+), 2054 deletions(-) create mode 100644 src/libs/database/impl/Traits.hpp rename src/libs/recommendation/impl/{FeaturesClassifierCreator.hpp => FeaturesEngineCreator.hpp} (93%) delete mode 100644 src/libs/recommendation/impl/features/FeaturesClassifier.hpp rename src/libs/recommendation/impl/features/{FeaturesClassifier.cpp => FeaturesEngine.cpp} (56%) create mode 100644 src/libs/recommendation/impl/features/FeaturesEngine.hpp rename src/libs/recommendation/impl/features/{FeaturesClassifierCache.cpp => FeaturesEngineCache.cpp} (85%) rename src/libs/recommendation/impl/features/{FeaturesClassifierCache.hpp => FeaturesEngineCache.hpp} (66%) rename src/libs/{subsonic/impl/ParameterParsing.cpp => recommendation/include/recommendation/IRecommendation.hpp} (64%) diff --git a/CMakeLists.txt b/CMakeLists.txt index b34a72cc..149ebd7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,9 @@ 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) diff --git a/src/libs/auth/impl/AuthServiceBase.cpp b/src/libs/auth/impl/AuthServiceBase.cpp index f31f737e..b4ab450a 100644 --- a/src/libs/auth/impl/AuthServiceBase.cpp +++ b/src/libs/auth/impl/AuthServiceBase.cpp @@ -25,7 +25,7 @@ namespace Auth { - Database::IdType + Database::UserId AuthServiceBase::getOrCreateUser(Database::Session& session, std::string_view loginName) { auto transaction {session.createUniqueTransaction()}; @@ -41,11 +41,11 @@ namespace Auth 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)}; diff --git a/src/libs/auth/impl/AuthServiceBase.hpp b/src/libs/auth/impl/AuthServiceBase.hpp index 8a7672e1..032d8f84 100644 --- a/src/libs/auth/impl/AuthServiceBase.hpp +++ b/src/libs/auth/impl/AuthServiceBase.hpp @@ -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); }; } diff --git a/src/libs/auth/impl/AuthTokenService.cpp b/src/libs/auth/impl/AuthTokenService.cpp index b22a5ac1..89ad8870 100644 --- a/src/libs/auth/impl/AuthTokenService.cpp +++ b/src/libs/auth/impl/AuthTokenService.cpp @@ -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()}; diff --git a/src/libs/auth/impl/AuthTokenService.hpp b/src/libs/auth/impl/AuthTokenService.hpp index 92dff681..088f82ee 100644 --- a/src/libs/auth/impl/AuthTokenService.hpp +++ b/src/libs/auth/impl/AuthTokenService.hpp @@ -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; diff --git a/src/libs/auth/impl/PasswordServiceBase.cpp b/src/libs/auth/impl/PasswordServiceBase.cpp index f1d53f6d..02a9c63d 100644 --- a/src/libs/auth/impl/PasswordServiceBase.cpp +++ b/src/libs/auth/impl/PasswordServiceBase.cpp @@ -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}; } diff --git a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp b/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp index 93f0b584..3db1fb4a 100644 --- a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp +++ b/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp @@ -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}; } diff --git a/src/libs/auth/impl/internal/InternalPasswordService.cpp b/src/libs/auth/impl/internal/InternalPasswordService.cpp index 82acd56b..a021dd3c 100644 --- a/src/libs/auth/impl/internal/InternalPasswordService.cpp +++ b/src/libs/auth/impl/internal/InternalPasswordService.cpp @@ -96,13 +96,13 @@ namespace Auth } 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!"}; diff --git a/src/libs/auth/impl/internal/InternalPasswordService.hpp b/src/libs/auth/impl/internal/InternalPasswordService.hpp index 6e599204..33cdb5cd 100644 --- a/src/libs/auth/impl/internal/InternalPasswordService.hpp +++ b/src/libs/auth/impl/internal/InternalPasswordService.hpp @@ -42,7 +42,7 @@ namespace Auth bool canSetPasswords() const override; PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override; - void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) 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; diff --git a/src/libs/auth/impl/pam/PAMPasswordService.cpp b/src/libs/auth/impl/pam/PAMPasswordService.cpp index 47b75932..1b25f537 100644 --- a/src/libs/auth/impl/pam/PAMPasswordService.cpp +++ b/src/libs/auth/impl/pam/PAMPasswordService.cpp @@ -193,7 +193,7 @@ namespace Auth } void - PAMPasswordService::setPassword(Database::Session&, Database::IdType, std::string_view) + PAMPasswordService::setPassword(Database::Session&, Database::UserId, std::string_view) { throw NotImplementedException {}; } diff --git a/src/libs/auth/impl/pam/PAMPasswordService.hpp b/src/libs/auth/impl/pam/PAMPasswordService.hpp index b6213e17..4b661b4b 100644 --- a/src/libs/auth/impl/pam/PAMPasswordService.hpp +++ b/src/libs/auth/impl/pam/PAMPasswordService.hpp @@ -38,7 +38,7 @@ namespace Auth bool canSetPasswords() 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; }; } diff --git a/src/libs/auth/include/auth/IAuthTokenService.hpp b/src/libs/auth/include/auth/IAuthTokenService.hpp index 44744198..e57ee6a9 100644 --- a/src/libs/auth/include/auth/IAuthTokenService.hpp +++ b/src/libs/auth/include/auth/IAuthTokenService.hpp @@ -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 createAuthTokenService(std::size_t maxThrottlerEntryCount); diff --git a/src/libs/auth/include/auth/IEnvService.hpp b/src/libs/auth/include/auth/IEnvService.hpp index 02a6d4e2..b001d602 100644 --- a/src/libs/auth/include/auth/IEnvService.hpp +++ b/src/libs/auth/include/auth/IEnvService.hpp @@ -57,7 +57,7 @@ namespace Auth }; State state {State::Denied}; - std::optional userId {}; + std::optional userId {}; }; virtual CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) = 0; diff --git a/src/libs/auth/include/auth/IPasswordService.hpp b/src/libs/auth/include/auth/IPasswordService.hpp index a5b9a0bc..69278fc5 100644 --- a/src/libs/auth/include/auth/IPasswordService.hpp +++ b/src/libs/auth/include/auth/IPasswordService.hpp @@ -53,7 +53,7 @@ namespace Auth Throttled, }; State state {State::Denied}; - std::optional userId {}; + std::optional userId {}; std::optional expiry {}; }; virtual CheckResult checkUserPassword(Database::Session& session, @@ -70,7 +70,7 @@ namespace Auth MustMatchLoginName, }; virtual PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const = 0; - virtual void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) = 0; + virtual void setPassword(Database::Session& session, Database::UserId userId, std::string_view newPassword) = 0; }; std::unique_ptr createPasswordService(std::string_view authPasswordBackend, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService); diff --git a/src/libs/cover/impl/CoverArtGrabber.cpp b/src/libs/cover/impl/CoverArtGrabber.cpp index 1227fcd6..226d28a1 100644 --- a/src/libs/cover/impl/CoverArtGrabber.cpp +++ b/src/libs/cover/impl/CoverArtGrabber.cpp @@ -45,11 +45,11 @@ namespace bool hasCover {}; bool isMultiDisc {}; std::filesystem::path trackPath; - std::optional releaseId; + std::optional releaseId; }; std::optional - getTrackInfo(Database::Session& dbSession, Database::IdType trackId) + getTrackInfo(Database::Session& dbSession, Database::TrackId trackId) { std::optional 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(jpegQuality, 1, 100)} + , _jpegQuality {Utils::clamp(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 -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 -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 cover {loadFromCache(cacheEntryDesc)}; if (cover) @@ -361,9 +357,9 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, Im } std::shared_ptr -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 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(); } } diff --git a/src/libs/cover/impl/CoverArtGrabber.hpp b/src/libs/cover/impl/CoverArtGrabber.hpp index abe02fbc..e5c33fb8 100644 --- a/src/libs/cover/impl/CoverArtGrabber.hpp +++ b/src/libs/cover/impl/CoverArtGrabber.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include "cover/ICoverArtGrabber.hpp" @@ -46,20 +47,12 @@ namespace CoverArt { struct CacheEntryDesc { - enum class Type - { - Track, - Release, - }; - - Type type; - Database::IdType id; + std::variant 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()(static_cast(e.type)); - h ^= std::hash()(e.id) << 1; + size_t h {}; + std::visit([&](auto id) + { + using IdType = std::decay_t; + h ^= std::hash()(id); + }, e.id); h ^= std::hash()(e.size) << 1; return h; } @@ -101,11 +98,11 @@ namespace CoverArt Grabber& operator=(Grabber&&) = delete; private: - std::shared_ptr getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width) override; - std::shared_ptr getFromRelease(Database::Session& dbSession, Database::IdType releaseId, ImageSize width) override; + std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width) override; + std::shared_ptr getFromRelease(Database::Session& dbSession, Database::ReleaseId releaseId, ImageSize width) override; void flushCache() override; - std::shared_ptr getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width, bool allowReleaseFallback); + std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback); std::unique_ptr getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const; std::unique_ptr getFromCoverFile(const std::filesystem::path& p, ImageSize width) const; diff --git a/src/libs/cover/include/cover/ICoverArtGrabber.hpp b/src/libs/cover/include/cover/ICoverArtGrabber.hpp index 8f9671da..544cee7a 100644 --- a/src/libs/cover/include/cover/ICoverArtGrabber.hpp +++ b/src/libs/cover/include/cover/ICoverArtGrabber.hpp @@ -37,8 +37,8 @@ namespace CoverArt public: virtual ~IGrabber() = default; - virtual std::shared_ptr getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width) = 0; - virtual std::shared_ptr getFromRelease(Database::Session& dbSession, Database::IdType releaseId, ImageSize width) = 0; + virtual std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width) = 0; + virtual std::shared_ptr getFromRelease(Database::Session& dbSession, Database::ReleaseId releaseId, ImageSize width) = 0; virtual void flushCache() = 0; }; diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 36c6dede..5c620561 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -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& MBID) _sortName {_name}, _MBID {MBID ? MBID->getAsString() : ""} { - } std::vector @@ -45,7 +45,7 @@ Artist::getByName(Session& session, const std::string& name) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().find() + Wt::Dbo::collection> res = session.getDboSession().find() .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().where("mbid = ?").bind(std::string {mbid.getAsString()}); + return session.getDboSession().find().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().where("id = ?").bind(id); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } Artist::pointer @@ -82,7 +82,7 @@ static Wt::Dbo::Query createQuery(Session& session, const std::string& queryStr, - const std::set& clusterIds, + const std::vector& clusterIds, const std::vector& keywords, std::optional 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 res = session.getDboSession().find(); + Wt::Dbo::collection> res = session.getDboSession().find(); return std::vector(res.begin(), res.end()); } @@ -167,7 +167,7 @@ Artist::getAll(Session& session, SortMethod sortMethod) break; } - Wt::Dbo::collection res = query; + Wt::Dbo::collection> res = query; return std::vector(res.begin(), res.end()); } @@ -177,7 +177,7 @@ Artist::getAll(Session& session, SortMethod sortMethod, std::optional ran { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT a FROM Artist a", {}, {}, std::nullopt)}; + auto query {createQuery>(session, "SELECT a FROM Artist a", {}, {}, std::nullopt)}; switch (sortMethod) { @@ -191,11 +191,11 @@ Artist::getAll(Session& session, SortMethod sortMethod, std::optional ran break; } - Wt::Dbo::collection collection = query + Wt::Dbo::collection> collection = query .limit(range ? static_cast(range->limit) + 1 : -1) .offset(range ? static_cast(range->offset) : -1); - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res (collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -207,27 +207,27 @@ Artist::getAll(Session& session, SortMethod sortMethod, std::optional ran return res; } -std::vector +std::vector Artist::getAllIds(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM artist"); - return std::vector(res.begin(), res.end()); + Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM artist"); + return std::vector(res.begin(), res.end()); } -std::vector -Artist::getAllIdsRandom(Session& session, const std::set& clusters, std::optional linkType, std::optional size) +std::vector +Artist::getAllIdsRandom(Session& session, const std::vector& clusters, std::optional linkType, std::optional size) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)}; + auto query {createQuery(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)}; - Wt::Dbo::collection res = query + Wt::Dbo::collection res = query .orderBy("RANDOM()") .limit(size ? static_cast(*size) : -1); - return std::vector(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } @@ -240,22 +240,22 @@ Artist::getAllOrphans(Session& session) return std::vector(res.begin(), res.end()); } -std::vector +std::vector Artist::getAllIdsWithClusters(Session& session, std::optional limit) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query + Wt::Dbo::collection res = session.getDboSession().query ("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(*limit) : -1); - return std::vector(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } std::vector -Artist::getByClusters(Session& session, const std::set& clusters, SortMethod sortMethod) +Artist::getByClusters(Session& session, const std::vector& clusters, SortMethod sortMethod) { assert(!clusters.empty()); @@ -266,7 +266,7 @@ Artist::getByClusters(Session& session, const std::set& clusters, SortMe std::vector Artist::getByFilter(Session& session, - const std::set& clusters, + const std::vector& clusters, const std::vector& keywords, std::optional linkType, SortMethod sortMethod, @@ -275,7 +275,7 @@ Artist::getByFilter(Session& session, { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT DISTINCT a from artist a", clusters, keywords, linkType)}; + auto query {createQuery>(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 collection = query + Wt::Dbo::collection> collection = query .limit(range ? static_cast(range->limit) + 1 : -1) .offset(range ? static_cast(range->offset) : -1); - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res (collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { @@ -308,23 +308,23 @@ Artist::getByFilter(Session& session, std::vector Artist::getLastWritten(Session& session, std::optional after, - const std::set& clusters, + const std::vector& clusters, std::optional linkType, std::optional range, bool& moreResults) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)}; + auto query {createQuery>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)}; if (after) query.where("t.file_last_write > ?").bind(*after); - Wt::Dbo::collection collection = query + Wt::Dbo::collection> collection = query .orderBy("t.file_last_write DESC") .limit(range ? static_cast(range->limit) + 1 : -1) .offset(range ? static_cast(range->offset) : -1); - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res (collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { @@ -340,14 +340,14 @@ Artist::getLastWritten(Session& session, std::vector Artist::getStarred(Session& session, User::pointer user, - const std::set& clusters, + const std::vector& clusters, std::optional linkType, SortMethod sortMethod, std::optional range, bool& moreResults) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)}; + auto query {createQuery>(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 collection = query + Wt::Dbo::collection> collection = query .groupBy("a.id") .limit(range ? static_cast(range->limit) + 1 : -1) .offset(range ? static_cast(range->offset) : -1); - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res (collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { @@ -389,11 +389,9 @@ Artist::getStarred(Session& session, return std::vector(res.begin(), res.end()); } -std::vector> -Artist::getReleases(const std::set& clusterIds) const +std::vector +Artist::getReleases(const std::vector& clusterIds) const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); WhereClause where; @@ -409,12 +407,12 @@ Artist::getReleases(const std::set& 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(); @@ -423,56 +421,48 @@ Artist::getReleases(const std::set& clusterIds) const oss << " ORDER BY t.year DESC, r.name COLLATE NOCASE"; - Wt::Dbo::Query query = session()->query( oss.str() ); + auto query {session()->query>(oss.str())}; for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - Wt::Dbo::collection> res = query; - - return std::vector>(res.begin(), res.end()); + auto res {query.resultList()}; + return std::vector(res.begin(), res.end()); } std::size_t Artist::getReleaseCount() const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); int res = session()->query("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> +std::vector Artist::getTracks(std::optional linkType) const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); auto query {session()->query>("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()) + .where("a.id = ?").bind(getId()) .orderBy("t.year DESC,t.release_id,t.disc_number,t.track_number")}; if (linkType) query.where("t_a_l.type = ?").bind(*linkType); - Wt::Dbo::collection> tracks {query.resultList()}; - - return std::vector>(tracks.begin(), tracks.end()); + auto tracks {query.resultList()}; + return std::vector(tracks.begin(), tracks.end()); } -std::vector> +std::vector Artist::getNonReleaseTracks(std::optional linkType, std::optional range, bool& moreResults) const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); auto query {session()->query>("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()) .where("t.release_id is NULL") .orderBy("t.name") .limit(range ? static_cast(range->limit) + 1 : -1) @@ -481,9 +471,8 @@ Artist::getNonReleaseTracks(std::optional linkType, std::op if (linkType) query.where("t_a_l.type = ?").bind(*linkType); - Wt::Dbo::collection tracks {query.resultList()}; - - auto res {std::vector(tracks.begin(), tracks.end())}; + Wt::Dbo::collection> tracks {query.resultList()}; + std::vector res(tracks.begin(), tracks.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -499,37 +488,32 @@ bool Artist::hasNonReleaseTracks(std::optional linkType) const { auto query {session()->query>("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()) .where("t.release_id is NULL") .orderBy("t.name")}; if (linkType) query.where("t_a_l.type = ?").bind(*linkType); - Wt::Dbo::collection tracks {query.resultList()}; - return !tracks.empty(); + return !query.resultList().empty(); } -std::vector> +std::vector Artist::getRandomTracks(std::optional count) const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); Wt::Dbo::collection> tracks {session()->query>("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(*count) : -1)}; - return std::vector>(tracks.begin(), tracks.end()); + return std::vector(tracks.begin(), tracks.end()); } -std::vector> +std::vector Artist::getSimilarArtists(EnumSet artistLinkTypes, std::optional range) const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); std::ostringstream oss; @@ -563,9 +547,9 @@ Artist::getSimilarArtists(EnumSet artistLinkTypes, std::opt oss << ")"; } - Wt::Dbo::Query query {session()->query(oss.str()) - .bind(self()->id()) - .bind(self()->id()) + Wt::Dbo::Query> query {session()->query>(oss.str()) + .bind(getId()) + .bind(getId()) .groupBy("a.id") .orderBy("COUNT(*) DESC, RANDOM()") .limit(range ? static_cast(range->limit) : -1) @@ -574,15 +558,13 @@ Artist::getSimilarArtists(EnumSet artistLinkTypes, std::opt for (TrackArtistLinkType type : artistLinkTypes) query.bind(type); - Wt::Dbo::collection res = query; + Wt::Dbo::collection> res {query.resultList()}; return std::vector(res.begin(), res.end()); } -std::vector>> +std::vector> Artist::getClusterGroups(std::vector clusterTypes, std::size_t size) const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); WhereClause where; @@ -590,34 +572,34 @@ Artist::getClusterGroups(std::vector 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 query = session()->query( oss.str() ); + Wt::Dbo::Query> query = session()->query>( oss.str() ); for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - Wt::Dbo::collection queryRes = query; + Wt::Dbo::collection> queryRes = query; - std::map> clusters; - for (auto cluster : queryRes) + std::map> 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> res; - for (auto cluster_list : clusters) - res.push_back(cluster_list.second); + for (const auto& [clusterTypeId, clusters] : clustersByType) + res.push_back(clusters); return res; } diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index 0a3143c8..184a03a1 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -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 type, std::string_view name) - : _name(std::string {name, 0, _maxNameLength}), - _clusterType {type} +Cluster::Cluster(ObjectPtr type, std::string_view name) + : _name {std::string {name, 0, _maxNameLength}}, + _clusterType {getDboPtr(type)} { } Cluster::pointer -Cluster::create(Session& session, Wt::Dbo::ptr type, std::string_view name) +Cluster::create(Session& session, ObjectPtr type, std::string_view name) { session.checkUniqueLocked(); @@ -54,8 +51,7 @@ Cluster::getAll(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res {session.getDboSession().find()}; - + Wt::Dbo::collection> res {session.getDboSession().find()}; return std::vector(res.begin(), res.end()); } @@ -63,67 +59,61 @@ std::vector Cluster::getAllOrphans(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res {session.getDboSession().query("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>("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(res.begin(), res.end()); } Cluster::pointer -Cluster::getById(Session& session, IdType id) +Cluster::getById(Session& session, ClusterId id) { session.checkSharedLocked(); - return session.getDboSession().find().where("id = ?").bind(id); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } void -Cluster::addTrack(Wt::Dbo::ptr track) +Cluster::addTrack(ObjectPtr track) { - _tracks.insert(track); + _tracks.insert(getDboPtr(track)); } -std::vector> +std::vector Cluster::getTracks(std::optional offset, std::optional limit) const { assert(session()); - assert(IdIsValid(self()->id())); - Wt::Dbo::collection res - {session()->query("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>("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(*offset) : -1) - .limit(limit ? static_cast(*limit) : -1)}; + .limit(limit ? static_cast(*limit) : -1) + .resultList()}; - return std::vector>(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } -std::set +std::vector Cluster::getTrackIds() const { assert(session()); - assert(IdIsValid(self()->id())); - Wt::Dbo::collection res = session()->query("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(res.begin(), res.end()); + Wt::Dbo::collection res = session()->query("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(res.begin(), res.end()); } std::size_t Cluster::getReleasesCount() const { assert(session()); - assert(IdIsValid(self()->id())); return session()->query("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 res = session.getDboSession().query>( + Wt::Dbo::collection> res = session.getDboSession().query>( "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 res = session.getDboSession().query>( + Wt::Dbo::collection> res = session.getDboSession().query>( "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().where("name = ?").bind(name); + return session.getDboSession().find().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().where("id= ?").bind(id); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } std::vector @@ -173,8 +163,7 @@ ClusterType::getAll(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().find(); - + auto res {session.getDboSession().find().resultList()}; return std::vector(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() .where("name = ?").bind(name) - .where("cluster_type_id = ?").bind(self()->id()); + .where("cluster_type_id = ?").bind(getId()).resultValue(); } std::vector ClusterType::getClusters() const { assert(self()); - assert(IdIsValid(self()->id())); assert(session()); - Wt::Dbo::collection res = session()->find() - .where("cluster_type_id = ?").bind(self()->id()) - .orderBy("name"); + auto res = session()->find() + .where("cluster_type_id = ?").bind(getId()) + .orderBy("name") + .resultList(); return std::vector(res.begin(), res.end()); } diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 5b0a2d1c..a782b06a 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -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 createQuery(Session& session, const std::string& queryStr, - const std::set& clusterIds, + const std::vector& clusterIds, const std::vector& 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& MBID) : _name {std::string(name, 0 , _maxNameLength)}, _MBID {MBID ? MBID->getAsString() : ""} { - } std::vector @@ -84,7 +84,11 @@ Release::getByName(Session& session, const std::string& name) { session.checkUniqueLocked(); - Wt::Dbo::collection res = session.getDboSession().find().where("name = ?").bind( std::string(name, 0, _maxNameLength) ); + auto res {session.getDboSession() + .find() + .where("name = ?").bind( std::string(name, 0, _maxNameLength) ) + .resultList()}; + return std::vector(res.begin(), res.end()); } @@ -93,15 +97,21 @@ Release::getByMBID(Session& session, const UUID& mbid) { session.checkSharedLocked(); - return session.getDboSession().find().where("mbid = ?").bind(std::string {mbid.getAsString()}); + return session.getDboSession() + .find() + .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().where("id = ?").bind(id); + return session.getDboSession() + .find() + .where("id = ?").bind(id) + .resultValue(); } Release::pointer @@ -120,8 +130,7 @@ Release::getCount(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection releases {session.getDboSession().find()}; - return releases.size(); + return session.getDboSession().find().resultList().size(); } std::vector @@ -129,21 +138,22 @@ Release::getAll(Session& session, std::optional range) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().find() + auto res {session.getDboSession().find() .offset(range ? static_cast(range->offset) : -1) .limit(range ? static_cast(range->limit) : -1) - .orderBy("name COLLATE NOCASE"); + .orderBy("name COLLATE NOCASE") + .resultList()}; return std::vector(res.begin(), res.end()); } -std::vector +std::vector Release::getAllIds(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM release"); - return std::vector(res.begin(), res.end()); + Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM release"); + return std::vector(res.begin(), res.end()); } std::vector @@ -151,44 +161,45 @@ Release::getAllOrderedByArtist(Session& session, std::optional offs { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query>( + auto res {session.getDboSession().query>( "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(*offset) : -1) .limit(size ? static_cast(*size) : -1) - .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE"); + .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") + .resultList()}; return std::vector(res.begin(), res.end()); } std::vector -Release::getAllRandom(Session& session, const std::set& clusterIds, std::optional size) +Release::getAllRandom(Session& session, const std::vector& clusterIds, std::optional size) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT DISTINCT r from release r", clusterIds,{})}; - - Wt::Dbo::collection res = query + auto query {createQuery>(session, "SELECT DISTINCT r from release r", clusterIds, {})}; + auto res {query .orderBy("RANDOM()") - .limit(size ? static_cast(*size) : -1); + .limit(size ? static_cast(*size) : -1) + .resultList()}; return std::vector(res.begin(), res.end()); } -std::vector -Release::getAllIdsRandom(Session& session, const std::set& clusterIds, std::optional size) +std::vector +Release::getAllIdsRandom(Session& session, const std::vector& clusterIds, std::optional size) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT DISTINCT r.id from release r", clusterIds,{})}; + auto query {createQuery(session, "SELECT DISTINCT r.id from release r", clusterIds, {})}; - Wt::Dbo::collection res = query + Wt::Dbo::collection res = query .orderBy("RANDOM()") .limit(size ? static_cast(*size) : -1); - return std::vector(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } @@ -197,31 +208,31 @@ Release::getAllOrphans(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query>("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>("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(res.begin(), res.end()); } std::vector Release::getLastWritten(Session& session, std::optional after, - const std::set& clusterIds, + const std::vector& clusterIds, std::optional range, bool& moreResults) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT r from release r", clusterIds, {})}; + auto query {createQuery>(session, "SELECT r from release r", clusterIds, {})}; if (after) query.where("t.file_last_write > ?").bind(after); - Wt::Dbo::collection collection = query + auto collection {query .orderBy("t.file_last_write DESC") .groupBy("r.id") .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1); + .limit(range ? static_cast(range->limit) + 1: -1) + .resultList()}; - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res(collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -236,13 +247,14 @@ Release::getLastWritten(Session& session, std::vector Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional range) { - Wt::Dbo::collection res = session.getDboSession().query + auto res {session.getDboSession().query> ("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") .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) : -1); + .limit(range ? static_cast(range->limit) : -1) + .resultList()}; return std::vector(res.begin(), res.end()); } @@ -250,30 +262,31 @@ Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional Release::getStarred(Session& session, User::pointer user, - const std::set& clusterIds, + const std::vector& clusterIds, std::optional range, bool& moreResults) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT r from release r", clusterIds, {})}; + auto query {createQuery>(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 collection = query + auto collection {query .groupBy("r.id") .orderBy("r.name COLLATE NOCASE") .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1); + .limit(range ? static_cast(range->limit) + 1: -1) + .resultList()}; - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res(collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -288,7 +301,7 @@ Release::getStarred(Session& session, } std::vector -Release::getByClusters(Session& session, const std::set& clusters) +Release::getByClusters(Session& session, const std::vector& clusters) { assert(!clusters.empty()); @@ -300,21 +313,21 @@ Release::getByClusters(Session& session, const std::set& clusters) std::vector Release::getByFilter(Session& session, - const std::set& clusterIds, + const std::vector& clusterIds, const std::vector& keywords, std::optional range, bool& moreResults) { session.checkSharedLocked(); - Wt::Dbo::collection collection = createQuery(session, "SELECT r from release r", clusterIds, keywords) + auto collection {createQuery>(session, "SELECT r from release r", clusterIds, keywords) .groupBy("r.id") .orderBy("r.name COLLATE NOCASE") .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - auto res {std::vector(collection.begin(), collection.end())}; + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; + std::vector res(collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -326,18 +339,18 @@ Release::getByFilter(Session& session, return res; } -std::vector +std::vector Release::getAllIdsWithClusters(Session& session, std::optional limit) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query + Wt::Dbo::collection res = session.getDboSession().query ("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(*limit) : -1); - return std::vector(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } @@ -345,11 +358,10 @@ std::optional Release::getTotalTrack(void) const { assert(session()); - assert(IdIsValid(self()->id())); int res = session()->query("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(res) : std::nullopt; } @@ -358,11 +370,10 @@ std::optional Release::getTotalDisc(void) const { assert(session()); - assert(IdIsValid(self()->id())); int res = session()->query("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(res) : std::nullopt; } @@ -372,13 +383,13 @@ Release::getReleaseYear(bool original) const { assert(session()); - const std::string field {original ? "original_year" : "year"}; + const char* field {original ? "original_year" : "year"}; Wt::Dbo::collection dates = session()->query( 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()); // various dates => no date if (dates.empty() || dates.size() > 1) @@ -388,8 +399,8 @@ Release::getReleaseYear(bool original) const if (date > 0) return date; - else - return std::nullopt; + + return std::nullopt; } std::optional @@ -401,7 +412,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 values(copyrights.begin(), copyrights.end()); @@ -421,7 +432,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 values(copyrights.begin(), copyrights.end()); @@ -432,32 +443,29 @@ Release::getCopyrightURL() const return values.front(); } -std::vector> +std::vector Release::getArtists(TrackArtistLinkType linkType) const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); - Wt::Dbo::collection> res = session()->query>( + auto res {session()->query>( "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>(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } std::vector Release::getSimilarReleases(std::optional offset, std::optional count) const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); - Wt::Dbo::Query query {session()->query( + auto res {session()->query>( "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 +473,14 @@ Release::getSimilarReleases(std::optional offset, std::optional ?" ) - .bind(self()->id()) - .bind(self()->id()) + .bind(getId()) + .bind(getId()) .groupBy("r.id") .orderBy("COUNT(*) DESC, RANDOM()") .limit(count ? static_cast(*count) : -1) - .offset(offset ? static_cast(*offset) : -1)}; + .offset(offset ? static_cast(*offset) : -1) + .resultList()}; - Wt::Dbo::collection res = query; return std::vector(res.begin(), res.end()); } @@ -483,11 +491,9 @@ Release::hasVariousArtists() const return getArtists().size() > 1; } -std::vector> -Release::getTracks(const std::set& clusterIds) const +std::vector +Release::getTracks(const std::vector& clusterIds) const { - assert(self()); - assert(self()->id() != Wt::Dbo::dbo_traits::invalidId() ); assert(session()); WhereClause where; @@ -502,12 +508,12 @@ Release::getTracks(const std::set& 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 +522,12 @@ Release::getTracks(const std::set& clusterIds) const oss << " ORDER BY t.disc_number,t.track_number"; - Wt::Dbo::Query query = session()->query( oss.str() ); - + auto query {session()->query>(oss.str())}; for (const std::string& bindArg : where.getBindArgs()) - { query.bind(bindArg); - } - Wt::Dbo::collection< Wt::Dbo::ptr > res = query; - - return std::vector< Wt::Dbo::ptr > (res.begin(), res.end()); + auto res {query.resultList()}; + return std::vector (res.begin(), res.end()); } std::size_t @@ -534,31 +536,28 @@ Release::getTracksCount() const return _tracks.size(); } -Wt::Dbo::ptr +Track::pointer Release::getFirstTrack() const { - assert(self()); - assert(self()->id() != Wt::Dbo::dbo_traits::invalidId()); assert(session()); - return session()->query("SELECT t from track t") + return session()->query>("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::invalidId()); assert(session()); using milli = std::chrono::duration; Wt::Dbo::Query query {session()->query("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 +565,17 @@ Release::getDuration() const Wt::WDateTime Release::getLastWritten() const { - assert(self()); - assert(self()->id() != Wt::Dbo::dbo_traits::invalidId()); assert(session()); Wt::Dbo::Query query {session()->query("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>> -Release::getClusterGroups(std::vector clusterTypes, std::size_t size) const +std::vector> +Release::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const { - assert(self()); - assert(self()->id() != Wt::Dbo::dbo_traits::invalidId() ); assert(session()); WhereClause where; @@ -589,33 +584,32 @@ Release::getClusterGroups(std::vector 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 query = session()->query( oss.str() ); - + auto query {session()->query>(oss.str())}; for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - Wt::Dbo::collection queryRes = query; + auto queryRes {query.resultList()}; - std::map> clusters; - for (auto cluster : queryRes) + std::map> clustersByType; + for (const Wt::Dbo::ptr& 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> res; - for (auto cluster_list : clusters) - res.push_back(cluster_list.second); + for (const auto& [clusterTypeId, clusters] : clustersByType) + res.push_back(clusters); return res; } diff --git a/src/libs/database/impl/ScanSettings.cpp b/src/libs/database/impl/ScanSettings.cpp index 3d9a1a86..9aafed53 100644 --- a/src/libs/database/impl/ScanSettings.cpp +++ b/src/libs/database/impl/ScanSettings.cpp @@ -60,14 +60,14 @@ ScanSettings::get(Session& session) { session.checkSharedLocked(); - return session.getDboSession().find(); + return session.getDboSession().find().resultValue(); } -std::unordered_set +std::vector ScanSettings::getAudioFileExtensions() const { - auto extensions = StringUtils::splitString(_audioFileExtensions, " "); - return std::unordered_set(std::cbegin(extensions), std::cend(extensions)); + const auto extensions {StringUtils::splitString(_audioFileExtensions, " ")}; + return std::vector(std::cbegin(extensions), std::cend(extensions)); } void @@ -111,19 +111,19 @@ ScanSettings::setClusterTypes(Session& session, const std::set& 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 : _clusterTypes) { if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(), [clusterType](const std::string& name) { return name == clusterType->getName(); })) diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 9e1ef2a0..91bdfd86 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -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 createQuery(Session& session, const std::string& queryStr, - const std::set& clusterIds, + const std::vector& clusterIds, const std::vector& 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 limit) { session.checkSharedLocked(); - Wt::Dbo::collection res {session.getDboSession().find() - .limit(limit ? static_cast(*limit) : -1)}; + auto res {session.getDboSession().find() + .limit(limit ? static_cast(*limit) : -1) + .resultList()}; return std::vector(std::cbegin(res), std::cend(res)); } std::vector -Track::getAllRandom(Session& session, const std::set& clusterIds, std::optional limit) +Track::getAllRandom(Session& session, const std::vector& clusterIds, std::optional limit) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT t from track t", clusterIds, {})}; - - Wt::Dbo::collection collection = query + auto query {createQuery>(session, "SELECT t from track t", clusterIds, {})}; + auto collection {query .orderBy("RANDOM()") - .limit(limit ? static_cast(*limit) + 1: -1); + .limit(limit ? static_cast(*limit) + 1: -1) + .resultList()}; return std::vector(collection.begin(), collection.end()); } -std::vector -Track::getAllIdsRandom(Session& session, const std::set& clusterIds, std::optional limit) +std::vector +Track::getAllIdsRandom(Session& session, const std::vector& clusterIds, std::optional limit) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT t.id from track t", clusterIds, {})}; + auto query {createQuery(session, "SELECT t.id from track t", clusterIds, {})}; - Wt::Dbo::collection collection = query + Wt::Dbo::collection collection = query .orderBy("RANDOM()") .limit(limit ? static_cast(*limit) + 1: -1); - return std::vector(collection.begin(), collection.end()); + return std::vector(collection.begin(), collection.end()); } -std::vector +std::vector Track::getAllIds(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM track"); - return std::vector(res.begin(), res.end()); + Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM track"); + return std::vector(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().where("file_path = ?").bind(p.string()); + return session.getDboSession().find().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() - .where("id = ?").bind(id); + .where("id = ?").bind(id) + .resultValue(); } std::vector @@ -157,8 +161,9 @@ Track::getByRecordingMBID(Session& session, const UUID& mbid) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().find() - .where("recording_mbid = ?").bind(std::string {mbid.getAsString()}); + auto res {session.getDboSession().find() + .where("recording_mbid = ?").bind(std::string {mbid.getAsString()}) + .resultList()}; return std::vector(res.begin(), res.end()); } @@ -174,17 +179,17 @@ Track::create(Session& session, const std::filesystem::path& p) return res; } -std::vector> +std::vector> Track::getAllPaths(Session& session, std::optional offset, std::optional size) { - using QueryResultType = std::tuple; + using QueryResultType = std::tuple; session.checkSharedLocked(); Wt::Dbo::collection queryRes = session.getDboSession().query("SELECT id,file_path FROM track") .limit(size ? static_cast(*size) + 1 : -1) .offset(offset ? static_cast(*offset) : -1); - std::vector> result; + std::vector> 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 res = session.getDboSession().query( "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>( "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(res.begin(), res.end()); } std::vector -Track::getLastWritten(Session& session, std::optional after, const std::set& clusterIds, std::optional range, bool& moreResults) +Track::getLastWritten(Session& session, std::optional after, const std::vector& clusterIds, std::optional range, bool& moreResults) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT t from track t", clusterIds, {})}; + auto query {createQuery>(session, "SELECT t from track t", clusterIds, {})}; if (after) query.where("t.file_last_write > ?").bind(after); - Wt::Dbo::collection collection = query + auto collection {query .orderBy("t.file_last_write DESC") .groupBy("t.id") .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1); + .limit(range ? static_cast(range->limit) + 1: -1) + .resultList()}; - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res(collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -237,63 +245,65 @@ Track::getAllWithRecordingMBIDAndMissingFeatures(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query + auto res {session.getDboSession().query> ("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(res.begin(), res.end()); } -std::vector +std::vector Track::getAllIdsWithFeatures(Session& session, std::optional limit) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query + Wt::Dbo::collection res = session.getDboSession().query ("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(*limit) : -1); - return std::vector(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } -std::vector +std::vector Track::getAllIdsWithClusters(Session& session, std::optional limit) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query + Wt::Dbo::collection res = session.getDboSession().query ("SELECT DISTINCT t.id FROM track t" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id") .limit(limit ? static_cast(*limit) : -1); - return std::vector(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } std::vector Track::getStarred(Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT t from track t", clusterIds, {})}; + auto query {createQuery>(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 collection = query + auto collection {query .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1); + .limit(range ? static_cast(range->limit) + 1: -1) + .resultList()}; - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res(collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -308,43 +318,41 @@ Track::getStarred(Session& session, std::vector Track::getClusters() const { - std::vector< Cluster::pointer > clusters; - std::copy(_clusters.begin(), _clusters.end(), std::back_inserter(clusters)); - return clusters; + return std::vector(_clusters.begin(), _clusters.end()); } -std::vector +std::vector Track::getClusterIds() const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); - Wt::Dbo::collection res = session()->query + auto res {session()->query ("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(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } bool Track::hasTrackFeatures() const { - return (_trackFeatures.lock() != Database::TrackFeatures::pointer()); + return (_trackFeatures.lock() != Wt::Dbo::ptr {}); } std::vector Track::getByFilter(Session& session, - const std::set& clusterIds, + const std::vector& clusterIds, const std::vector& keywords, std::optional range, bool& moreResults) { session.checkSharedLocked(); - Wt::Dbo::collection collection = createQuery(session, "SELECT t from track t", clusterIds, keywords) + auto collection {createQuery>(session, "SELECT t from track t", clusterIds, keywords) .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; std::vector res(collection.begin(), collection.end()); if (range && (res.size() == static_cast(range->limit) + 1)) @@ -362,17 +370,18 @@ std::vector Track::getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName) { session.checkSharedLocked(); - Wt::Dbo::collection collection = session.getDboSession().query("SELECT t from track t") + + auto res {session.getDboSession().query>("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(collection.begin(), collection.end()); + .where("r.name = ?").bind(releaseName) + .resultList()}; + return std::vector(res.begin(), res.end()); } std::vector Track::getSimilarTracks(Session& session, - const std::unordered_set& tracks, + const std::vector& tracks, std::optional offset, std::optional size) { @@ -387,7 +396,7 @@ Track::getSimilarTracks(Session& session, oss << "?"; } - Wt::Dbo::Query query {session.getDboSession().query( + auto query {session.getDboSession().query>( "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(*size) : -1) .offset(offset ? static_cast(*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 res = query; + auto res {query.resultList()}; return std::vector(res.begin(), res.end()); } std::vector -Track::getByClusters(Session& session, - const std::set& clusters) +Track::getByClusters(Session& session, const std::vector& clusters) { assert(!clusters.empty()); session.checkSharedLocked(); @@ -429,23 +437,23 @@ Track::clearArtistLinks() } void -Track::addArtistLink(const Wt::Dbo::ptr& artistLink) +Track::addArtistLink(const ObjectPtr& artistLink) { - _trackArtistLinks.insert(artistLink); + _trackArtistLinks.insert(getDboPtr(artistLink)); } void -Track::setClusters(const std::vector>& clusters) +Track::setClusters(const std::vector>& clusters) { _clusters.clear(); - for (const Wt::Dbo::ptr& cluster : clusters) - _clusters.insert(cluster); + for (const ObjectPtr& cluster : clusters) + _clusters.insert(getDboPtr(cluster)); } void -Track::setFeatures(const Wt::Dbo::ptr& features) +Track::setFeatures(const ObjectPtr& features) { - _trackFeatures = features; + _trackFeatures = getDboPtr(features); } std::optional @@ -496,11 +504,9 @@ Track::getCopyrightURL() const return _copyrightURL != "" ? std::make_optional(_copyrightURL) : std::nullopt; } -std::vector> +std::vector Track::getArtists(EnumSet linkTypes) const { - assert(self()); - assert(IdIsValid(self()->id())); assert(session()); std::ostringstream oss; @@ -525,22 +531,20 @@ Track::getArtists(EnumSet linkTypes) const oss << ")"; } - Wt::Dbo::Query query {session()->query(oss.str())}; - + auto query {session()->query>(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 res = query; + auto res {query.resultList()}; return std::vector(std::begin(res), std::end(res)); } -std::vector +std::vector Track::getArtistIds(EnumSet linkTypes) const { assert(self()); - assert(IdIsValid(self()->id())); assert(session()); std::ostringstream oss; @@ -565,33 +569,32 @@ Track::getArtistIds(EnumSet linkTypes) const oss << ")"; } - Wt::Dbo::Query query {session()->query(oss.str()) - .where("t.id = ?").bind(self()->id())}; + Wt::Dbo::Query query {session()->query(oss.str()) + .where("t.id = ?").bind(getId())}; for (TrackArtistLinkType type : linkTypes) query.bind(type); - Wt::Dbo::collection res = query; - return std::vector(std::begin(res), std::end(res)); + Wt::Dbo::collection res = query; + return std::vector(std::begin(res), std::end(res)); } -std::vector> +std::vector Track::getArtistLinks() const { - return std::vector>(_trackArtistLinks.begin(), _trackArtistLinks.end()); + return std::vector(_trackArtistLinks.begin(), _trackArtistLinks.end()); } -Wt::Dbo::ptr +ObjectPtr Track::getTrackFeatures() const { return _trackFeatures.lock(); } std::vector> -Track::getClusterGroups(std::vector clusterTypes, std::size_t size) const +Track::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const { assert(self()); - assert(IdIsValid(self()->id())); assert(session()); WhereClause where; @@ -600,28 +603,27 @@ Track::getClusterGroups(std::vector 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 query = session()->query( oss.str() ); - + auto query {session()->query>(oss.str())}; for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - Wt::Dbo::collection queryRes = query; + auto queryRes {query.resultList()}; - std::map> clusters; - for (auto cluster : queryRes) + std::map> clusters; + for (const Wt::Dbo::ptr& 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> res; diff --git a/src/libs/database/impl/TrackArtistLink.cpp b/src/libs/database/impl/TrackArtistLink.cpp index 45b62bbf..f8791dee 100644 --- a/src/libs/database/impl/TrackArtistLink.cpp +++ b/src/libs/database/impl/TrackArtistLink.cpp @@ -23,17 +23,19 @@ #include "database/Session.hpp" #include "database/Track.hpp" +#include "Traits.hpp" + namespace Database { -TrackArtistLink::TrackArtistLink(Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type) +TrackArtistLink::TrackArtistLink(ObjectPtr track, ObjectPtr 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, Wt::Dbo::ptr artist, TrackArtistLinkType type) +TrackArtistLink::create(Session& session, ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type) { session.checkUniqueLocked(); @@ -48,9 +50,9 @@ TrackArtistLink::getUsedTypes(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection collection = session.getDboSession().query("SELECT DISTINCT type from track_artist_link"); + auto res {session.getDboSession().query("SELECT DISTINCT type from track_artist_link").resultList()}; - return EnumSet(std::begin(collection), std::end(collection)); + return EnumSet(std::begin(res), std::end(res)); } } diff --git a/src/libs/database/impl/TrackBookmark.cpp b/src/libs/database/impl/TrackBookmark.cpp index 2a0c4bef..91f457bd 100644 --- a/src/libs/database/impl/TrackBookmark.cpp +++ b/src/libs/database/impl/TrackBookmark.cpp @@ -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, Wt::Dbo::ptr track) -: _user {user}, -_track {track} +TrackBookmark::TrackBookmark(ObjectPtr user, ObjectPtr track) +: _user {getDboPtr(user)}, +_track {getDboPtr(track)} { } - TrackBookmark::pointer -TrackBookmark::create(Session& session, Wt::Dbo::ptr user, Wt::Dbo::ptr track) +TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr track) { session.checkUniqueLocked(); @@ -48,42 +48,41 @@ TrackBookmark::getAll(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res {session.getDboSession().find()}; - + auto res {session.getDboSession().find().resultList()}; return std::vector(std::cbegin(res), std::cend(res)); } std::vector -TrackBookmark::getByUser(Session& session, Wt::Dbo::ptr user) +TrackBookmark::getByUser(Session& session, User::pointer user) { session.checkSharedLocked(); - Wt::Dbo::collection res - { - session.getDboSession().find() - .where("user_id = ?").bind(user.id()) - }; + auto res {session.getDboSession().find() + .where("user_id = ?").bind(user->getId()) + .resultList()}; return std::vector(std::cbegin(res), std::cend(res)); } TrackBookmark::pointer -TrackBookmark::getByUser(Session& session, Wt::Dbo::ptr user, Wt::Dbo::ptr track) +TrackBookmark::getByUser(Session& session, ObjectPtr user, ObjectPtr track) { session.checkSharedLocked(); return session.getDboSession().find() - .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() - .where("id = ?").bind(id); + .where("id = ?").bind(id) + .resultValue(); } diff --git a/src/libs/database/impl/TrackFeatures.cpp b/src/libs/database/impl/TrackFeatures.cpp index 6fb3c963..5770d0b1 100644 --- a/src/libs/database/impl/TrackFeatures.cpp +++ b/src/libs/database/impl/TrackFeatures.cpp @@ -28,14 +28,14 @@ namespace Database { -TrackFeatures::TrackFeatures(Wt::Dbo::ptr track, const std::string& jsonEncodedFeatures) -: _data(jsonEncodedFeatures), -_track(track) +TrackFeatures::TrackFeatures(ObjectPtr track, const std::string& jsonEncodedFeatures) +: _data {jsonEncodedFeatures}, +_track {getDboPtr(track)} { } TrackFeatures::pointer -TrackFeatures::create(Session& session, Wt::Dbo::ptr track, const std::string& jsonEncodedFeatures) +TrackFeatures::create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures) { session.checkUniqueLocked(); return session.getDboSession().add(std::make_unique(track, jsonEncodedFeatures)); diff --git a/src/libs/database/impl/TrackList.cpp b/src/libs/database/impl/TrackList.cpp index 73c1067a..a9863d52 100644 --- a/src/libs/database/impl/TrackList.cpp +++ b/src/libs/database/impl/TrackList.cpp @@ -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) +TrackList::TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr 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) +TrackList::create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr user) { session.checkUniqueLocked(); assert(user); - auto res = session.getDboSession().add( std::make_unique(name, type, isPublic, user) ); + TrackList::pointer res {session.getDboSession().add( std::make_unique(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) +TrackList::get(Session& session, std::string_view name, Type type, ObjectPtr 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() .where("name = ?").bind(name) .where("type = ?").bind(type) - .where("user_id = ?").bind(user.id()); + .where("user_id = ?").bind(user->getId()).resultValue(); } std::vector TrackList::getAll(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().find(); + + auto res = session.getDboSession().find().resultList(); + return std::vector(res.begin(), res.end()); +} + +std::vector +TrackList::getAll(Session& session, ObjectPtr user) +{ + session.checkSharedLocked(); + + auto res {session.getDboSession().find() + .where("user_id = ?").bind(user->getId()) + .orderBy("name COLLATE NOCASE") + .resultList()}; return std::vector(res.begin(), res.end()); } std::vector -TrackList::getAll(Session& session, Wt::Dbo::ptr user) +TrackList::getAll(Session& session, ObjectPtr user, Type type) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().find() - .where("user_id = ?").bind(user.id()) - .orderBy("name COLLATE NOCASE"); - - return std::vector(res.begin(), res.end()); -} - -std::vector -TrackList::getAll(Session& session, Wt::Dbo::ptr user, Type type) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().find() - .where("user_id = ?").bind(user.id()) + auto res {session.getDboSession().find() + .where("user_id = ?").bind(user->getId()) .where("type = ?").bind(type) - .orderBy("name COLLATE NOCASE"); + .orderBy("name COLLATE NOCASE") + .resultList()}; return std::vector(res.begin(), res.end()); } TrackList::pointer -TrackList::getById(Session& session, IdType id) +TrackList::getById(Session& session, TrackListId id) { session.checkSharedLocked(); - return session.getDboSession().find().where("id = ?").bind(id); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } bool @@ -120,10 +123,10 @@ TrackList::getCount() const return _entries.size(); } -Wt::Dbo::ptr +TrackListEntry::pointer TrackList::getEntry(std::size_t pos) const { - Wt::Dbo::ptr 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> +std::vector TrackList::getEntries(std::optional offset, std::optional size) const { assert(session()); - assert(IdIsValid(self()->id())); - Wt::Dbo::collection> entries = + auto entries { session()->find() - .where("tracklist_id = ?").bind(self().id()) + .where("tracklist_id = ?").bind(getId()) .orderBy("id") .limit(size ? static_cast(*size) : -1) - .offset(offset ? static_cast(*offset) : -1); + .offset(offset ? static_cast(*offset) : -1) + .resultList()}; - return std::vector>(entries.begin(), entries.end()); + return std::vector(entries.begin(), entries.end()); } -Wt::Dbo::ptr -TrackList::getEntryByTrackAndDateTime(Wt::Dbo::ptr track, const Wt::WDateTime& dateTime) const +TrackListEntry::pointer +TrackList::getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const { assert(session()); - assert(IdIsValid(self()->id())); return session()->find() - .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 -createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set& clusterIds, std::optional linkType) +Wt::Dbo::Query> +createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector& clusterIds, std::optional linkType) { - auto query {session.query(queryStr)}; + auto query {session.query>(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 -createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set& clusterIds) +Wt::Dbo::Query> +createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector& clusterIds) { - auto query {session.query(queryStr)}; + auto query {session.query>(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 -createTracksQuery(Wt::Dbo::Session& session, IdType tracklistId, const std::set& clusterIds) +Wt::Dbo::Query> +createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std::vector& clusterIds) { - auto query {session.query("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>("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 -TrackList::getArtistsReverse(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const +TrackList::getArtistsReverse(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const { assert(session()); - assert(IdIsValid(self()->id())); - Wt::Dbo::collection 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(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; auto res {std::vector(collection.begin(), collection.end())}; if (range && res.size() == static_cast(range->limit) + 1) @@ -291,18 +294,18 @@ TrackList::getArtistsReverse(const std::set& clusterIds, std::optional -TrackList::getReleasesReverse(const std::set& clusterIds, std::optional range, bool& moreResults) const +TrackList::getReleasesReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const { assert(session()); - assert(IdIsValid(self()->id())); - Wt::Dbo::collection 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(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res(collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -315,18 +318,18 @@ TrackList::getReleasesReverse(const std::set& clusterIds, std::optional< } std::vector -TrackList::getTracksReverse(const std::set& clusterIds, std::optional range, bool& moreResults) const +TrackList::getTracksReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const { assert(session()); - assert(IdIsValid(self()->id())); - Wt::Dbo::collection 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(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res(collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -338,29 +341,28 @@ TrackList::getTracksReverse(const std::set& clusterIds, std::optional> +std::vector TrackList::getClusters() const { assert(session()); - assert(IdIsValid(self()->id())); - Wt::Dbo::collection res = session()->query("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>("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>(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } bool -TrackList::hasTrack(IdType trackId) const +TrackList::hasTrack(TrackId trackId) const { assert(session()); - assert(IdIsValid(self()->id())); Wt::Dbo::collection res = session()->query("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 TrackList::getSimilarTracks(std::optional offset, std::optional size) const { assert(session()); - assert(IdIsValid(self()->id())); - Wt::Dbo::Query query {session()->query( + auto res {session()->query>( "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(*size) : -1) - .offset(offset ? static_cast(*offset) : -1)}; + .offset(offset ? static_cast(*offset) : -1) + .resultList()}; - Wt::Dbo::collection tracks = query; - return std::vector(tracks.begin(), tracks.end()); + return std::vector(res.begin(), res.end()); } -std::vector +std::vector TrackList::getTrackIds() const { assert(session()); - assert(IdIsValid(self()->id())); - Wt::Dbo::collection res = session()->query("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 res = session()->query("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(res.begin(), res.end()); + return std::vector(res.begin(), res.end()); } std::chrono::milliseconds TrackList::getDuration() const { assert(session()); - assert(IdIsValid(self()->id())); using milli = std::chrono::duration; Wt::Dbo::Query query {session()->query("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 -TrackList::getTopArtists(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const +TrackList::getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional 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 collection = query + auto collection {query .orderBy("COUNT(a.id) DESC") .groupBy("a.id") .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; - auto res {std::vector(collection.begin(), collection.end())}; + std::vector res(collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { @@ -439,26 +438,23 @@ TrackList::getTopArtists(const std::set& clusterIds, std::optional -TrackList::getTopReleases(const std::set& clusterIds, std::optional range, bool& moreResults) const +TrackList::getTopReleases(const std::vector& clusterIds, std::optional 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 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(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - auto res {std::vector(collection.begin(), collection.end())}; + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; + std::vector res(collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -471,21 +467,19 @@ TrackList::getTopReleases(const std::set& clusterIds, std::optional -TrackList::getTopTracks(const std::set& clusterIds, std::optional range, bool& moreResults) const +TrackList::getTopTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const { assert(session()); - assert(IdIsValid(self()->id())); - auto query {createTracksQuery(*session(), self()->id(), clusterIds)}; - - Wt::Dbo::collection collection = query + auto query {createTracksQuery(*session(), getId(), clusterIds)}; + auto collection {query .orderBy("COUNT(t.id) DESC") .groupBy("t.id") .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - auto res {std::vector(collection.begin(), collection.end())}; + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; + std::vector res(collection.begin(), collection.end()); if (range && res.size() == static_cast(range->limit) + 1) { moreResults = true; @@ -497,16 +491,16 @@ TrackList::getTopTracks(const std::set& clusterIds, std::optional return res; } -TrackListEntry::TrackListEntry(Wt::Dbo::ptr track, Wt::Dbo::ptr tracklist, const Wt::WDateTime& dateTime) +TrackListEntry::TrackListEntry(ObjectPtr track, ObjectPtr 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, Wt::Dbo::ptr tracklist, const Wt::WDateTime& dateTime) +TrackListEntry::create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) { session.checkUniqueLocked(); assert(track); @@ -519,11 +513,11 @@ TrackListEntry::create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::ptr } TrackListEntry::pointer -TrackListEntry::getById(Session& session, IdType id) +TrackListEntry::getById(Session& session, TrackListEntryId id) { session.checkSharedLocked(); - return session.getDboSession().find().where("id = ?").bind(id); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } } // namespace Database diff --git a/src/libs/database/impl/Traits.hpp b/src/libs/database/impl/Traits.hpp new file mode 100644 index 00000000..bdfa0045 --- /dev/null +++ b/src/libs/database/impl/Traits.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +#include "database/Types.hpp" + +namespace Wt::Dbo +{ + + template + struct sql_value_traits::value>::type> + { + static_assert(!std::is_same_v, "Cannot use IdType, use derived types"); + static const bool specialized = true; + + static std::string type(SqlConnection *conn, int size) + { + return sql_value_traits::type(conn, size); + } + + static void bind(const T& v, SqlStatement *statement, int column, int size) + { + sql_value_traits::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::read(value, statement, column, size)) + { + v = value; + return true; + } + + v = {}; + return false; + } + }; +} + diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp index 9a6deb73..44f8c2b1 100644 --- a/src/libs/database/impl/User.cpp +++ b/src/libs/database/impl/User.cpp @@ -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) +AuthToken::AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr 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) +AuthToken::create(Session& session, const std::string& value, const Wt::WDateTime& expiry, ObjectPtr user) { session.checkUniqueLocked(); - auto res {session.getDboSession().add(std::make_unique(value, expiry, user))}; - + AuthToken::pointer res {session.getDboSession().add(std::make_unique(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() - .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 res = session.getDboSession().find(); + auto res {session.getDboSession().find().resultList()}; return std::vector(res.begin(), res.end()); } -std::vector +std::vector User::getAllIds(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM user"); - return std::vector(res.begin(), res.end()); + auto res {session.getDboSession().query("SELECT id FROM user").resultList()}; + return std::vector(res.begin(), res.end()); } User::pointer @@ -98,8 +99,7 @@ User::getDemo(Session& session) { session.checkSharedLocked(); - pointer res = session.getDboSession().find().where("type = ?").bind(UserType::DEMO); - return res; + return session.getDboSession().find().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().where("id = ?").bind( id ); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } User::pointer User::getByLoginName(Session& session, std::string_view name) { return session.getDboSession().find() - .where("login_name = ?").bind(name); + .where("login_name = ?").bind(name) + .resultValue(); } void @@ -150,7 +151,7 @@ User::clearAuthTokens() _authTokens.clear(); } -Wt::Dbo::ptr +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) +User::starArtist(ObjectPtr 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) +User::unstarArtist(ObjectPtr 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) const +User::hasStarredArtist(ObjectPtr artist) const { - return _starredArtists.count(artist) != 0; + return _starredArtists.count(getDboPtr(artist)) != 0; } void -User::starRelease(Wt::Dbo::ptr release) +User::starRelease(ObjectPtr 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) +User::unstarRelease(ObjectPtr 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) const +User::hasStarredRelease(ObjectPtr release) const { - return _starredReleases.count(release) != 0; + return _starredReleases.count(getDboPtr(release)) != 0; } void -User::starTrack(Wt::Dbo::ptr track) +User::starTrack(ObjectPtr 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) +User::unstarTrack(ObjectPtr 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) const +User::hasStarredTrack(ObjectPtr track) const { - return _starredTracks.count(track) != 0; + return _starredTracks.count(getDboPtr(track)) != 0; } } // namespace Database diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index a9ef7c81..d5bab873 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -22,17 +22,15 @@ #include #include #include -#include #include #include #include +#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 +class Artist : public Object { public: - enum class SortMethod { None, @@ -55,43 +52,41 @@ class Artist : public Wt::Dbo::Dbo BySortName, }; - using pointer = Wt::Dbo::ptr; - - Artist() {} + Artist() = default; Artist(const std::string& name, const std::optional& 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 getByName(Session& session, const std::string& name); // exact match on name field static std::vector getByClusters(Session& session, - const std::set& clusters, // at least one track that belongs to these clusters + const std::vector& clusters, // at least one track that belongs to these clusters SortMethod sortMethod ); static std::vector getByFilter(Session& session, - const std::set& clusters, // if non empty, at least one artist that belongs to these clusters + const std::vector& clusters, // if non empty, at least one artist that belongs to these clusters const std::vector& keywords, // if non empty, name must match all of these keywords (name + sort name fields) std::optional linkType, // if set, only artists that have produced at least one track with this link type SortMethod sortMethod, std::optional range, bool& moreExpected); - static std::vector getAll(Session& session); - static std::vector getAll(Session& session, SortMethod sortMethod); - static std::vector getAll(Session& session, SortMethod sortMethod, std::optional range, bool& moreResults); - static std::vector getAllIds(Session& session); - static std::vector getAllIdsRandom(Session& session, const std::set& clusters, std::optional linkType, std::optional size = {}); - static std::vector getAllOrphans(Session& session); // No track related - static std::vector getLastWritten(Session& session, + static std::vector getAll(Session& session); + static std::vector getAll(Session& session, SortMethod sortMethod); + static std::vector getAll(Session& session, SortMethod sortMethod, std::optional range, bool& moreResults); + static std::vector getAllIds(Session& session); + static std::vector getAllIdsRandom(Session& session, const std::vector& clusters, std::optional linkType, std::optional size = {}); + static std::vector getAllOrphans(Session& session); // No track related + static std::vector getLastWritten(Session& session, std::optional after, - const std::set& clusters, + const std::vector& clusters, std::optional linkType, // if set, only artists that have produced at least one track with this link type std::optional, bool& moreResults); - static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); + static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); static std::vector getStarred(Session& session, - Wt::Dbo::ptr user, - const std::set& clusters, + ObjectPtr user, + const std::vector& clusters, std::optional linkType, // if set, only artists that have produced at least one track with this link type SortMethod sortMethod, std::optional, bool& moreResults); @@ -101,12 +96,12 @@ class Artist : public Wt::Dbo::Dbo const std::string& getSortName() const { return _sortName; } std::optional getMBID() const { return UUID::fromString(_MBID); } - std::vector> getReleases(const std::set& clusterIds = {}) const; // if non empty, get the releases that match all these clusters - std::size_t getReleaseCount() const; - std::vector> getTracks(std::optional linkType = {}) const; + std::vector> getReleases(const std::vector& clusterIds = {}) const; // if non empty, get the releases that match all these clusters + std::size_t getReleaseCount() const; + std::vector> getTracks(std::optional linkType = {}) const; bool hasNonReleaseTracks(std::optional linkType = std::nullopt) const; - std::vector> getNonReleaseTracks(std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getRandomTracks(std::optional count) const; + std::vector> getNonReleaseTracks(std::optional linkType, std::optional range, bool& moreResults) const; + std::vector> getRandomTracks(std::optional count) const; // No artistLinkTypes means get them all std::vector getSimilarArtists(EnumSet artistLinkTypes = {}, std::optional range = std::nullopt) const; @@ -114,14 +109,14 @@ class Artist : public Wt::Dbo::Dbo // 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>> getClusterGroups(std::vector> clusterTypes, std::size_t size) const; + std::vector>> getClusterGroups(std::vector> clusterTypes, std::size_t size) const; void setName(std::string_view name) { _name = name; } void setMBID(const std::optional& 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 = {}); + static pointer create(Session& session, const std::string& name, const std::optional& UUID = {}); template void persist(Action& a) diff --git a/src/libs/database/include/database/Cluster.hpp b/src/libs/database/include/database/Cluster.hpp index 71e0da7f..42a11e77 100644 --- a/src/libs/database/include/database/Cluster.hpp +++ b/src/libs/database/include/database/Cluster.hpp @@ -24,10 +24,9 @@ #include #include - #include -#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 +class Cluster : public Object { public: - using pointer = Wt::Dbo::ptr; - - Cluster(); - Cluster(Wt::Dbo::ptr type, std::string_view name); + Cluster() = default; + Cluster(ObjectPtr type, std::string_view name); // Find utility static std::vector getAll(Session& session); static std::vector 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 type, std::string_view name); + static pointer create(Session& session, ObjectPtr type, std::string_view name); // Accessors const std::string& getName() const { return _name; } - Wt::Dbo::ptr getType() const { return _clusterType; } + ObjectPtr getType() const { return _clusterType; } std::size_t getTracksCount() const { return _tracks.size(); } - std::vector> getTracks(std::optional offset = {}, std::optional limit = {}) const; - std::set getTrackIds() const; + std::vector> getTracks(std::optional offset = {}, std::optional limit = {}) const; + std::vector getTrackIds() const; std::size_t getReleasesCount() const; - void addTrack(Wt::Dbo::ptr track); + void addTrack(ObjectPtr track); template void persist(Action& a) @@ -72,7 +69,6 @@ class Cluster : public Wt::Dbo::Dbo } private: - static const std::size_t _maxNameLength = 128; std::string _name; @@ -82,19 +78,17 @@ class Cluster : public Wt::Dbo::Dbo }; -class ClusterType : public Wt::Dbo::Dbo +class ClusterType : public Object { public: + ClusterType() = default; + ClusterType(std::string_view name); - using pointer = Wt::Dbo::ptr; - - ClusterType() {} - ClusterType(std::string name); - + // Getters static std::vector getAllOrphans(Session& session); static std::vector 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 getAll(Session& session); static pointer create(Session& session, const std::string& name); diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index f3be2da3..d39b1469 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -20,13 +20,13 @@ #pragma once #include -#include +#include #include #include +#include "database/Types.hpp" #include "utils/UUID.hpp" -#include "Types.hpp" namespace Database { @@ -39,49 +39,46 @@ class Session; class Track; class User; -class Release : public Wt::Dbo::Dbo +class Release : public Object { public: - - using pointer = Wt::Dbo::ptr; - - Release() {} + Release() = default; Release(const std::string& name, const std::optional& MBID = {}); // Accessors static std::size_t getCount(Session& session); static pointer getByMBID(Session& session, const UUID& MBID); static std::vector getByName(Session& session, const std::string& name); - static pointer getById(Session& session, IdType id); + static pointer getById(Session& session, ReleaseId id); static std::vector getAllOrphans(Session& session); // no track related static std::vector getAll(Session& session, std::optional range = std::nullopt); - static std::vector getAllIds(Session& session); + static std::vector getAllIds(Session& session); static std::vector getAllOrderedByArtist(Session& session, std::optional offset = {}, std::optional size = {}); - static std::vector getAllRandom(Session& session, const std::set& clusters, std::optional size = {}); - static std::vector getAllIdsRandom(Session& session, const std::set& clusters, std::optional size = {}); - static std::vector getLastWritten(Session& session, std::optional after, const std::set& clusters, std::optional range, bool& moreResults); + static std::vector getAllRandom(Session& session, const std::vector& clusters, std::optional size = {}); + static std::vector getAllIdsRandom(Session& session, const std::vector& clusters, std::optional size = {}); + static std::vector getLastWritten(Session& session, std::optional after, const std::vector& clusters, std::optional range, bool& moreResults); static std::vector getByYear(Session& session, int yearFrom, int yearTo, std::optional range = std::nullopt); - static std::vector getStarred(Session& session, Wt::Dbo::ptr user, const std::set& clusters, std::optional range, bool& moreResults); + static std::vector getStarred(Session& session, ObjectPtr user, const std::vector& clusters, std::optional range, bool& moreResults); - static std::vector getByClusters(Session& session, const std::set& clusters); + static std::vector getByClusters(Session& session, const std::vector& clusters); static std::vector getByFilter(Session& session, - const std::set& clusters, // if non empty, at least one release that belongs to these clusters + const std::vector& clusters, // if non empty, at least one release that belongs to these clusters const std::vector& keywords, // if non empty, name must match all of these keywords std::optional range, bool& moreExpected); - static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); + static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); - std::vector> getTracks(const std::set& clusters = std::set()) const; - std::size_t getTracksCount() const; - Wt::Dbo::ptr getFirstTrack() const; + std::vector> getTracks(const std::vector& clusters = {}) const; + std::size_t getTracksCount() const; + ObjectPtr 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>> getClusterGroups(std::vector> clusterTypes, std::size_t size) const; + std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; // Create - static pointer create(Session& session, const std::string& name, const std::optional& MBID = {}); + static pointer create(Session& session, const std::string& name, const std::optional& MBID = {}); // Utility functions std::optional getReleaseYear(bool originalDate = false) const; // 0 if unknown or various @@ -97,8 +94,8 @@ class Release : public Wt::Dbo::Dbo Wt::WDateTime getLastWritten() const; // Get the artists of this release - std::vector > getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; - std::vector > getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); } + std::vector > getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; + std::vector > getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); } bool hasVariousArtists() const; std::vector getSimilarReleases(std::optional offset = {}, std::optional count = {}) const; diff --git a/src/libs/database/include/database/ScanSettings.hpp b/src/libs/database/include/database/ScanSettings.hpp index 3451137a..9cb8cb5a 100644 --- a/src/libs/database/include/database/ScanSettings.hpp +++ b/src/libs/database/include/database/ScanSettings.hpp @@ -19,23 +19,22 @@ #pragma once -#include +#include +#include #include #include -#include "utils/Path.hpp" +#include "database/Types.hpp" namespace Database { class ClusterType; class Session; -class ScanSettings : public Wt::Dbo::Dbo +class ScanSettings : public Object { public: - using pointer = Wt::Dbo::ptr; - // Do not modify values (just add) enum class UpdatePeriod { Never = 0, @@ -61,8 +60,8 @@ class ScanSettings : public Wt::Dbo::Dbo std::filesystem::path getMediaDirectory() const { return _mediaDirectory; } Wt::WTime getUpdateStartTime() const { return _startTime; } UpdatePeriod getUpdatePeriod() const { return _updatePeriod; } - std::vector> getClusterTypes() const; - std::unordered_set getAudioFileExtensions() const; + std::vector> getClusterTypes() const; + std::vector getAudioFileExtensions() const; RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; } // Setters diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 7fee6355..edfdbecc 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -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 +class Track : public Object { public: - - using pointer = Wt::Dbo::ptr; - - 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 getByRecordingMBID(Session& session, const UUID& MBID); static std::vector getSimilarTracks(Session& session, - const std::unordered_set& trackIds, + const std::vector& trackIds, std::optional offset = {}, std::optional size = {}); static std::vector getByClusters(Session& session, - const std::set& clusters); // tracks that belong to these clusters + const std::vector& clusters); // tracks that belong to these clusters static std::vector getByFilter(Session& session, - const std::set& clusters, // if non empty, tracks that belong to these clusters + const std::vector& clusters, // if non empty, tracks that belong to these clusters const std::vector& keywords, // if non empty, name must match all of these keywords std::optional range, bool& moreExpected); static std::vector getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName); static std::vector getAll(Session& session, std::optional limit = std::nullopt); - static std::vector getAllRandom(Session& session, const std::set& clusters, std::optional limit = std::nullopt); - static std::vector getAllIdsRandom(Session& session, const std::set& clusters, std::optional limit = std::nullopt); - static std::vector getAllIds(Session& session); - static std::vector> getAllPaths(Session& session, std::optional offset = std::nullopt, std::optional size = std::nullopt); + static std::vector getAllRandom(Session& session, const std::vector& clusters, std::optional limit = std::nullopt); + static std::vector getAllIdsRandom(Session& session, const std::vector& clusters, std::optional limit = std::nullopt); + static std::vector getAllIds(Session& session); + static std::vector> getAllPaths(Session& session, std::optional offset = std::nullopt, std::optional size = std::nullopt); static std::vector getMBIDDuplicates(Session& session); - static std::vector getLastWritten(Session& session, std::optional after, const std::set& clusters, std::optional range, bool& moreResults); + static std::vector getLastWritten(Session& session, std::optional after, const std::vector& clusters, std::optional range, bool& moreResults); static std::vector getAllWithRecordingMBIDAndMissingFeatures(Session& session); - static std::vector getAllIdsWithFeatures(Session& session, std::optional limit = {}); - static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); + static std::vector getAllIdsWithFeatures(Session& session, std::optional limit = {}); + static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); static std::vector getStarred(Session& session, - Wt::Dbo::ptr user, - const std::set& clusters, + ObjectPtr user, + const std::vector& clusters, std::optional range, bool& hasMore); // Create utility @@ -115,10 +112,10 @@ class Track : public Wt::Dbo::Dbo void setTrackReplayGain(float replayGain) { _trackReplayGain = replayGain; } void setReleaseReplayGain(float replayGain) { _releaseReplayGain = replayGain; } void clearArtistLinks(); - void addArtistLink(const Wt::Dbo::ptr& artistLink); - void setRelease(Wt::Dbo::ptr release) { _release = release; } - void setClusters(const std::vector>& clusters ); - void setFeatures(const Wt::Dbo::ptr& features); + void addArtistLink(const ObjectPtr& artistLink); + void setRelease(ObjectPtr release) { _release = getDboPtr(release); } + void setClusters(const std::vector>& clusters ); + void setFeatures(const ObjectPtr& features); std::size_t getScanVersion() const { return _scanVersion; } std::optional getTrackNumber() const; @@ -143,16 +140,16 @@ class Track : public Wt::Dbo::Dbo std::optional getReleaseReplayGain() const { return _releaseReplayGain; } // no artistLinkTypes means get all - std::vector> getArtists(EnumSet artistLinkTypes) const; - std::vector getArtistIds(EnumSet artistLinkTypes) const; - std::vector> getArtistLinks() const; - Wt::Dbo::ptr getRelease() const { return _release; } - std::vector> getClusters() const; - std::vector getClusterIds() const; - bool hasTrackFeatures() const; - Wt::Dbo::ptr getTrackFeatures() const; + std::vector> getArtists(EnumSet artistLinkTypes) const; + std::vector getArtistIds(EnumSet artistLinkTypes) const; + std::vector> getArtistLinks() const; + ObjectPtr getRelease() const { return _release; } + std::vector> getClusters() const; + std::vector getClusterIds() const; + bool hasTrackFeatures() const; + ObjectPtr getTrackFeatures() const; - std::vector>> getClusterGroups(std::vector> clusterTypes, std::size_t size) const; + std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; template void persist(Action& a) diff --git a/src/libs/database/include/database/TrackArtistLink.hpp b/src/libs/database/include/database/TrackArtistLink.hpp index 02a727e5..55d3b0c5 100644 --- a/src/libs/database/include/database/TrackArtistLink.hpp +++ b/src/libs/database/include/database/TrackArtistLink.hpp @@ -23,7 +23,7 @@ #include -#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 { public: - using pointer = Wt::Dbo::ptr; - TrackArtistLink() = default; - TrackArtistLink(Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type); + TrackArtistLink(ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type); - static pointer create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type); + static pointer create(Session& session, ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type); static EnumSet getUsedTypes(Session& session); - Wt::Dbo::ptr getTrack() const { return _track; } - Wt::Dbo::ptr getArtist() const { return _artist; } + ObjectPtr getTrack() const { return _track; } + ObjectPtr getArtist() const { return _artist; } TrackArtistLinkType getType() const { return _type; } template diff --git a/src/libs/database/include/database/TrackBookmark.hpp b/src/libs/database/include/database/TrackBookmark.hpp index d5ff0c0c..2c4af0db 100644 --- a/src/libs/database/include/database/TrackBookmark.hpp +++ b/src/libs/database/include/database/TrackBookmark.hpp @@ -23,7 +23,7 @@ #include -#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 +class TrackBookmark : public Object { public: - using pointer = Wt::Dbo::ptr; - TrackBookmark () = default; - TrackBookmark(Wt::Dbo::ptr user, Wt::Dbo::ptr track); + TrackBookmark(ObjectPtr user, ObjectPtr track); // utility - static pointer create(Session& session, Wt::Dbo::ptr user, Wt::Dbo::ptr track); + static pointer create(Session& session, ObjectPtr user, ObjectPtr track); // Find utility functions static std::vector getAll(Session& session); - static std::vector getByUser(Session& session, Wt::Dbo::ptr user); - static pointer getByUser(Session& session, Wt::Dbo::ptr user, Wt::Dbo::ptr track); - static pointer getById(Session& session, IdType id); + static std::vector getByUser(Session& session, ObjectPtr user); + static pointer getByUser(Session& session, ObjectPtr user, ObjectPtr 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 // Getters std::chrono::milliseconds getOffset() const { return _offset; } std::string_view getComment() const { return _comment; } - Wt::Dbo::ptr getTrack() const { return _track; } - Wt::Dbo::ptr getUser() const { return _user; } + ObjectPtr getTrack() const { return _track; } + ObjectPtr getUser() const { return _user; } template void persist(Action& a) diff --git a/src/libs/database/include/database/TrackFeatures.hpp b/src/libs/database/include/database/TrackFeatures.hpp index 86b58679..99c36d6d 100644 --- a/src/libs/database/include/database/TrackFeatures.hpp +++ b/src/libs/database/include/database/TrackFeatures.hpp @@ -26,7 +26,7 @@ #include -#include "Types.hpp" +#include "database/Types.hpp" namespace Database { @@ -37,17 +37,14 @@ using FeatureName = std::string; using FeatureValues = std::vector; using FeatureValuesMap = std::unordered_map; -class TrackFeatures : public Wt::Dbo::Dbo +class TrackFeatures : public Object { public: - - using pointer = Wt::Dbo::ptr; - TrackFeatures() = default; - TrackFeatures(Wt::Dbo::ptr track, const std::string& jsonEncodedFeatures); + TrackFeatures(ObjectPtr track, const std::string& jsonEncodedFeatures); // Create utility - static pointer create(Session& session, Wt::Dbo::ptr track, const std::string& jsonEncodedFeatures); + static pointer create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures); FeatureValues getFeatureValues(const FeatureName& feature) const; FeatureValuesMap getFeatureValuesMap(const std::unordered_set& featureNames) const; diff --git a/src/libs/database/include/database/TrackList.hpp b/src/libs/database/include/database/TrackList.hpp index d8505d23..03aaca0f 100644 --- a/src/libs/database/include/database/TrackList.hpp +++ b/src/libs/database/include/database/TrackList.hpp @@ -27,7 +27,7 @@ #include #include -#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 +class TrackList : public Object { public: - using pointer = Wt::Dbo::ptr; - enum class Type { Playlist, // user controlled playlists @@ -51,28 +49,28 @@ class TrackList : public Wt::Dbo::Dbo }; TrackList() = default; - TrackList(std::string_view name, Type type, bool isPublic, Wt::Dbo::ptr user); + TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr user); // Stats utility - std::vector> getTopArtists(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getTopReleases(const std::set& clusterIds, std::optional range, bool& moreResults) const; - std::vector> getTopTracks(const std::set& clusterIds, std::optional range, bool& moreResults) const; + std::vector> getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; + std::vector> getTopReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + std::vector> getTopTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const; // Search utility - static pointer get(Session& session, std::string_view name, Type type, Wt::Dbo::ptr user); - static pointer getById(Session& session, IdType tracklistId); + static pointer get(Session& session, std::string_view name, Type type, ObjectPtr user); + static pointer getById(Session& session, TrackListId tracklistId); static std::vector getAll(Session& session); - static std::vector getAll(Session& session, Wt::Dbo::ptr user); - static std::vector getAll(Session& session, Wt::Dbo::ptr user, Type type); + static std::vector getAll(Session& session, ObjectPtr user); + static std::vector getAll(Session& session, ObjectPtr user, Type type); // Create utility - static pointer create(Session& session, std::string_view name, Type type, bool isPublic, Wt::Dbo::ptr user); + static pointer create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr user); // Accessors std::string getName() const { return _name; } bool isPublic() const { return _isPublic; } Type getType() const { return _type; } - Wt::Dbo::ptr getUser() const { return _user; } + ObjectPtr getUser() const { return _user; } // Modifiers void setName(const std::string& name) { _name = name; } @@ -80,29 +78,29 @@ class TrackList : public Wt::Dbo::Dbo void clear() { _entries.clear(); } // Get tracks, ordered by position - bool isEmpty() const; - std::size_t getCount() const; - Wt::Dbo::ptr getEntry(std::size_t pos) const; - std::vector> getEntries(std::optional offset = {}, std::optional size = {}) const; - Wt::Dbo::ptr getEntryByTrackAndDateTime(Wt::Dbo::ptr track, const Wt::WDateTime& dateTime) const; + bool isEmpty() const; + std::size_t getCount() const; + ObjectPtr getEntry(std::size_t pos) const; + std::vector> getEntries(std::optional offset = {}, std::optional size = {}) const; + ObjectPtr getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const; // Get track bya - std::vector> getArtistsReverse(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getReleasesReverse(const std::set& clusterIds, std::optional range, bool& moreResults) const; - std::vector> getTracksReverse(const std::set& clusterIds, std::optional range, bool& moreResults) const; + std::vector> getArtistsReverse(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; + std::vector> getReleasesReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + std::vector> getTracksReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector getTrackIds() const; + std::vector getTrackIds() const; std::chrono::milliseconds getDuration() const; // Get clusters, order by occurence - std::vector> getClusters() const; + std::vector> getClusters() const; - bool hasTrack(IdType trackId) const; + bool hasTrack(TrackId trackId) const; // Ordered from most clusters in common - std::vector> getSimilarTracks(std::optional offset = {}, std::optional size = {}) const; + std::vector> getSimilarTracks(std::optional offset = {}, std::optional size = {}) const; template void persist(Action& a) @@ -122,27 +120,24 @@ class TrackList : public Wt::Dbo::Dbo bool _isPublic {false}; Wt::Dbo::ptr _user; - Wt::Dbo::collection< Wt::Dbo::ptr > _entries; + Wt::Dbo::collection> _entries; }; -class TrackListEntry : public Wt::Dbo::Dbo +class TrackListEntry : public Object { public: - - using pointer = Wt::Dbo::ptr; - TrackListEntry() = default; - TrackListEntry(Wt::Dbo::ptr track, Wt::Dbo::ptr tracklist, const Wt::WDateTime& dateTime); + TrackListEntry(ObjectPtr track, ObjectPtr 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, Wt::Dbo::ptr tracklist, const Wt::WDateTime& dateTime = Wt::WDateTime::currentDateTime()); + static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = Wt::WDateTime::currentDateTime()); // Accessors - Wt::Dbo::ptr getTrack() const { return _track; } + ObjectPtr getTrack() const { return _track; } const Wt::WDateTime& getDateTime() const { return _dateTime; } template diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 174bb65b..590518e3 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -20,16 +20,32 @@ #pragma once #include +#include +#include #include 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 { @@ -78,5 +94,83 @@ namespace Database ADMIN = 1, DEMO = 2, }; + + template + class ObjectPtr + { + public: + ObjectPtr() = default; + ObjectPtr(Wt::Dbo::ptr 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 friend class Object; + Wt::Dbo::ptr _obj; + }; + + template + class Object : public Wt::Dbo::Dbo + { + static_assert(std::is_base_of_v); + static_assert(!std::is_same_v); + + public: + using pointer = ObjectPtr; + using IdType = ObjectIdType; + + IdType getId() const { return Wt::Dbo::Dbo::self()->Wt::Dbo::Dbo::id(); } + + // catch some misuses + typename Wt::Dbo::dbo_traits::IdType id() const = delete; + + protected: + // Can get raw dbo ptr only from Objects + template + static + Wt::Dbo::ptr getDboPtr(ObjectPtr 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 \ + { \ + public: \ + size_t operator()(Database::name id) const \ + { \ + return std::hash()(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) + + diff --git a/src/libs/database/include/database/User.hpp b/src/libs/database/include/database/User.hpp index 2bd593fb..b94d41af 100644 --- a/src/libs/database/include/database/User.hpp +++ b/src/libs/database/include/database/User.hpp @@ -26,8 +26,8 @@ #include #include +#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 { public: - - using pointer = Wt::Dbo::ptr; - AuthToken() = default; - AuthToken(const std::string& value, const Wt::WDateTime& expiry, Wt::Dbo::ptr user); + AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr user); // Utility - static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, Wt::Dbo::ptr user); + static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, ObjectPtr 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 getUser() const { return _user; } + ObjectPtr getUser() const { return _user; } const std::string& getValue() const { return _value; } template @@ -75,11 +72,9 @@ class AuthToken Wt::Dbo::ptr _user; }; -class User : public Wt::Dbo::Dbo +class User : public Object { public: - using pointer = Wt::Dbo::ptr; - struct PasswordHash { std::string salt; @@ -120,17 +115,16 @@ class User : public Wt::Dbo::Dbo 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 getAll(Session& session); - static std::vector getAllIds(Session& session); + static std::vector getAllIds(Session& session); static pointer getDemo(Session& session); static std::size_t getCount(Session& session); @@ -171,20 +165,20 @@ class User : public Wt::Dbo::Dbo Scrobbler getScrobbler() const { return _scrobbler; } std::optional getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); } - Wt::Dbo::ptr getQueuedTrackList(Session& session) const; + ObjectPtr getQueuedTrackList(Session& session) const; - void starArtist(Wt::Dbo::ptr artist); - void unstarArtist(Wt::Dbo::ptr artist); - bool hasStarredArtist(Wt::Dbo::ptr artist) const; + void starArtist(ObjectPtr artist); + void unstarArtist(ObjectPtr artist); + bool hasStarredArtist(ObjectPtr artist) const; - void starRelease(Wt::Dbo::ptr release); - void unstarRelease(Wt::Dbo::ptr release); - bool hasStarredRelease(Wt::Dbo::ptr release) const; + void starRelease(ObjectPtr release); + void unstarRelease(ObjectPtr release); + bool hasStarredRelease(ObjectPtr release) const; // Stars - void starTrack(Wt::Dbo::ptr track); - void unstarTrack(Wt::Dbo::ptr track); - bool hasStarredTrack(Wt::Dbo::ptr track) const; + void starTrack(ObjectPtr track); + void unstarTrack(ObjectPtr track); + bool hasStarredTrack(ObjectPtr track) const; template void persist(Action& a) diff --git a/src/libs/recommendation/CMakeLists.txt b/src/libs/recommendation/CMakeLists.txt index 7bb9696f..890cf3f9 100644 --- a/src/libs/recommendation/CMakeLists.txt +++ b/src/libs/recommendation/CMakeLists.txt @@ -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 ) diff --git a/src/libs/recommendation/impl/Engine.cpp b/src/libs/recommendation/impl/Engine.cpp index 717b087c..741a9dad 100644 --- a/src/libs/recommendation/impl/Engine.cpp +++ b/src/libs/recommendation/impl/Engine.cpp @@ -23,7 +23,7 @@ #include #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 -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 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 -Engine::getSimilarTracks(Database::Session& dbSession, const std::unordered_set& trackIds, std::size_t maxCount) +Engine::TrackContainer +Engine::getSimilarTracks(Database::Session& dbSession, const std::vector& trackIds, std::size_t maxCount) { - std::unordered_set 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 -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 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 -Engine::getSimilarArtists(Database::Session& dbSession, - Database::IdType artistId, - EnumSet linkTypes, - std::size_t maxCount) +Engine::ArtistContainer +Engine::getSimilarArtists(Database::Session& dbSession, Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) { - std::unordered_set res; + ArtistContainer res; std::shared_lock lock {_classifiersMutex}; for (ClassifierType classifierType : _classifierPriorities) diff --git a/src/libs/recommendation/impl/Engine.hpp b/src/libs/recommendation/impl/Engine.hpp index 2e48e539..39338fbc 100644 --- a/src/libs/recommendation/impl/Engine.hpp +++ b/src/libs/recommendation/impl/Engine.hpp @@ -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& 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 getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) override; + ResultContainer getSimilarTracks(Database::Session& session, const std::vector& tracksId, std::size_t maxCount) override; + ResultContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) override; + ResultContainer getSimilarArtists(Database::Session& session, + Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) override; @@ -79,7 +80,6 @@ namespace Recommendation using ClassifierContainer = std::unordered_map>; ClassifierContainer _classifiers; std::vector _classifierPriorities; // ordered by priority - }; } // ns Recommendation diff --git a/src/libs/recommendation/impl/FeaturesClassifierCreator.hpp b/src/libs/recommendation/impl/FeaturesEngineCreator.hpp similarity index 93% rename from src/libs/recommendation/impl/FeaturesClassifierCreator.hpp rename to src/libs/recommendation/impl/FeaturesEngineCreator.hpp index 7f754768..5b948e70 100644 --- a/src/libs/recommendation/impl/FeaturesClassifierCreator.hpp +++ b/src/libs/recommendation/impl/FeaturesEngineCreator.hpp @@ -24,6 +24,6 @@ namespace Recommendation { - std::unique_ptr createFeaturesClassifier(); + std::unique_ptr createFeaturesEngine(); } diff --git a/src/libs/recommendation/impl/IClassifier.hpp b/src/libs/recommendation/impl/IClassifier.hpp index 4670a424..62483d9e 100644 --- a/src/libs/recommendation/impl/IClassifier.hpp +++ b/src/libs/recommendation/impl/IClassifier.hpp @@ -21,9 +21,10 @@ #include #include -#include +#include #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; + template + using ResultContainer = std::vector; - 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& 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 getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const = 0; + virtual ResultContainer getSimilarTracks(Database::Session& session, const std::vector& tracksId, std::size_t maxCount) const = 0; + virtual ResultContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const = 0; + virtual ResultContainer getSimilarArtists(Database::Session& session, + Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const = 0; }; diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp index 7a9c9ea9..44f39a76 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp @@ -33,23 +33,23 @@ std::unique_ptr createClustersClassifier() return std::make_unique(); } -std::unordered_set -ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::unordered_set& trackIds, std::size_t maxCount) const +IClassifier::ResultContainer +ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::vector& trackIds, std::size_t maxCount) const { + ResultContainer 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 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 -ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const +IClassifier::ResultContainer +ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const { - std::unordered_set res; + ResultContainer 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 -ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::IdType releaseId, std::size_t maxCount) const +IClassifier::ResultContainer +ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::ReleaseId releaseId, std::size_t maxCount) const { - std::unordered_set res; + ResultContainer 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 +IClassifier::ResultContainer ClusterClassifier::getSimilarArtists(Database::Session& dbSession, - Database::IdType artistId, + Database::ArtistId artistId, EnumSet artistLinkTypes, std::size_t maxCount) const { - std::unordered_set res; + ResultContainer 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; } diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp index dcb1a8dc..718d6872 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp @@ -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& 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 getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const override; + ResultContainer getSimilarTracks(Database::Session& session, const std::vector& tracksId, std::size_t maxCount) const override; + ResultContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const override; + ResultContainer getSimilarArtists(Database::Session& session, + Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const override; }; diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp deleted file mode 100644 index 177962ac..00000000 --- a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp +++ /dev/null @@ -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 . - */ - -#pragma once - -#include -#include -#include -#include - -#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>>(Database::IdType /*trackId*/, const std::unordered_set& /*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 getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; - std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override; - std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override; - std::unordered_set getSimilarArtists(Database::Session& session, - Database::IdType artistId, - EnumSet 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>; - using MatrixOfObjects = SOM::Matrix>; - - bool load(Database::Session& session, - SOM::Network network, - const ObjectPositions& tracksPosition); - - FeaturesClassifierCache toCache() const; - - static std::unordered_set getMatchingRefVectorsPosition(const std::unordered_set& ids, const ObjectPositions& objectPositions); - static std::unordered_set getObjectsIds(const std::unordered_set& positionSet, const MatrixOfObjects& objectsMap); - - std::unordered_set getSimilarObjects(const std::unordered_set& ids, - const SOM::Matrix>& objectsMap, - const ObjectPositions& objectPosition, - std::size_t maxCount) const; - - bool _loadCancelled {}; - std::unique_ptr _network; - double _networkRefVectorsDistanceMedian {}; - - ObjectPositions _artistPositions; - std::unordered_map _artistsMap; - - MatrixOfObjects _releasesMap; - ObjectPositions _releasePositions; - - MatrixOfObjects _tracksMap; - ObjectPositions _trackPositions; - - static inline FeaturesFetchFunc _featuresFetchFunc; -}; - -} // ns Recommendation diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp b/src/libs/recommendation/impl/features/FeaturesEngine.cpp similarity index 56% rename from src/libs/recommendation/impl/features/FeaturesClassifier.cpp rename to src/libs/recommendation/impl/features/FeaturesEngine.cpp index f0ccf38d..03fb0db6 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp +++ b/src/libs/recommendation/impl/features/FeaturesEngine.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "FeaturesClassifier.hpp" +#include "FeaturesEngine.hpp" #include @@ -35,13 +35,13 @@ namespace Recommendation { -std::unique_ptr createFeaturesClassifier() +std::unique_ptr createFeaturesEngine() { - return std::make_unique(); + return std::make_unique(); } const FeatureSettingsMap& -FeaturesClassifier::getDefaultTrainFeatureSettings() +FeaturesEngine::getDefaultTrainFeatureSettings() { static const FeatureSettingsMap defaultTrainFeatureSettings { @@ -57,16 +57,16 @@ FeaturesClassifier::getDefaultTrainFeatureSettings() static std::optional -getTrackFeatureValues(FeaturesClassifier::FeaturesFetchFunc func, Database::IdType trackId, const std::unordered_set& featureNames) +getTrackFeatureValues(FeaturesEngine::FeaturesFetchFunc func, Database::TrackId trackId, const std::unordered_set& featureNames) { return func(trackId, featureNames); } static std::optional -getTrackFeatureValuesFromDb(Database::Session& session, Database::IdType trackId, const std::unordered_set& featureNames) +getTrackFeatureValuesFromDb(Database::Session& session, Database::TrackId trackId, const std::unordered_set& featureNames) { - auto func = [&](Database::IdType trackId, const std::unordered_set& featureNames) + auto func = [&](Database::TrackId trackId, const std::unordered_set& featureNames) { std::optional 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 trackIds; + std::vector trackIds; { auto transaction {session.createSharedTransaction()}; @@ -151,13 +151,13 @@ FeaturesClassifier::loadFromTraining(Database::Session& session, const TrainSett } std::vector samples; - std::vector samplesTrackIds; + std::vector 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 -FeaturesClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount) const +IClassifier::ResultContainer +FeaturesEngine::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId trackListId, std::size_t maxCount) const { - const std::unordered_set trackIds {[&] + const std::vector trackIds {[&] { - std::unordered_set res; + std::vector res; auto transaction {session.createSharedTransaction()}; const Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)}; if (trackList) - { - const std::vector orderedTrackIds {trackList->getTrackIds()}; - res = std::unordered_set(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 -FeaturesClassifier::getSimilarTracks(Database::Session& session, const std::unordered_set& tracksIds, std::size_t maxCount) const +std::vector +FeaturesEngine::getSimilarTracks(Database::Session& session, const std::vector& 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 -FeaturesClassifier::getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const +std::vector +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({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 -FeaturesClassifier::getSimilarArtists(Database::Session& session, - Database::IdType artistId, +std::vector +FeaturesEngine::getSimilarArtists(Database::Session& session, + Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const { auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType) { - std::unordered_set similarArtistIds; + std::vector 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 similarArtistIds; + std::unordered_set 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 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 cache {FeaturesClassifierCache::read()}; + const std::optional 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& 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 -FeaturesClassifier::getMatchingRefVectorsPosition(const std::unordered_set& ids, const ObjectPositions& objectPositions) -{ - std::unordered_set 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 -FeaturesClassifier::getObjectsIds(const std::unordered_set& positionSet, const MatrixOfObjects& objectsMap) -{ - std::unordered_set res; - - for (const auto& position : positionSet) - { - for (auto id : objectsMap.get(position)) - res.insert(id); - } - - return res; -} - -std::unordered_set -FeaturesClassifier::getSimilarObjects(const std::unordered_set& ids, - const MatrixOfObjects& objectsMap, - const ObjectPositions& objectPosition, - std::size_t maxCount) const -{ - std::unordered_set res; - - std::unordered_set searchedRefVectorsPosition {getMatchingRefVectorsPosition(ids, objectPosition)}; - if (searchedRefVectorsPosition.empty()) - return res; - - while (1) - { - std::unordered_set 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 closestRefVectorPosition {_network->getClosestRefVectorPosition(searchedRefVectorsPosition, _networkRefVectorsDistanceMedian * 0.75)}; - if (!closestRefVectorPosition) - break; - - searchedRefVectorsPosition.insert(closestRefVectorPosition.value()); - } - - return res; -} - - - } // ns Recommendation diff --git a/src/libs/recommendation/impl/features/FeaturesEngine.hpp b/src/libs/recommendation/impl/features/FeaturesEngine.hpp new file mode 100644 index 00000000..9ed313a4 --- /dev/null +++ b/src/libs/recommendation/impl/features/FeaturesEngine.hpp @@ -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 . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#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>>(Database::TrackId, const std::unordered_set& /*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 getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const override; + ResultContainer getSimilarTracks(Database::Session& session, const std::vector& tracksId, std::size_t maxCount) const override; + ResultContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const override; + ResultContainer getSimilarArtists(Database::Session& session, + Database::ArtistId artistId, + EnumSet 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 + using ObjectPositions = std::unordered_map>; + + using ArtistPositions = ObjectPositions; + using ReleasePositions = ObjectPositions; + using TrackPositions = ObjectPositions; + + template + using ObjectMatrix = SOM::Matrix>; + using ArtistMatrix = ObjectMatrix; + using ReleaseMatrix = ObjectMatrix; + using TrackMatrix = ObjectMatrix; + + bool load(Database::Session& session, SOM::Network network, const TrackPositions& tracksPosition); + + FeaturesEngineCache toCache() const; + + template + static std::vector getMatchingRefVectorsPosition(const std::vector& ids, const ObjectPositions& objectPositions); + + template + static std::vector getObjectsIds(const std::vector& positions, const ObjectMatrix& objectsMatrix); + + template + std::vector getSimilarObjects(const std::vector& ids, + const ObjectMatrix& objectMatrix, + const ObjectPositions& objectPositions, + std::size_t maxCount) const; + + bool _loadCancelled {}; + std::unique_ptr _network; + double _networkRefVectorsDistanceMedian {}; + + ArtistPositions _artistPositions; + std::unordered_map _artistMatrix; + + ReleasePositions _releasePositions; + ReleaseMatrix _releaseMatrix; + + TrackPositions _trackPositions; + TrackMatrix _trackMatrix; + + static inline FeaturesFetchFunc _featuresFetchFunc; +}; + +template +std::vector +FeaturesEngine::getMatchingRefVectorsPosition(const std::vector& ids, const ObjectPositions& objectPositions) +{ + std::vector 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 +std::vector +FeaturesEngine::getObjectsIds(const std::vector& positions, const ObjectMatrix& objectMatrix) +{ + std::vector 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 +std::vector +FeaturesEngine::getSimilarObjects(const std::vector& ids, + const ObjectMatrix& objectMatrix, + const ObjectPositions& objectPositions, + std::size_t maxCount) const +{ + std::vector res; + + std::vector searchedRefVectorsPosition {getMatchingRefVectorsPosition(ids, objectPositions)}; + if (searchedRefVectorsPosition.empty()) + return res; + + while (1) + { + std::vector 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 closestRefVectorPosition {_network->getClosestRefVectorPosition(searchedRefVectorsPosition, _networkRefVectorsDistanceMedian * 0.75)}; + if (!closestRefVectorPosition) + break; + + Utils::push_back_if_not_present(searchedRefVectorsPosition, closestRefVectorPosition.value()); + } + + return res; +} + +} // ns Recommendation diff --git a/src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp b/src/libs/recommendation/impl/features/FeaturesEngineCache.cpp similarity index 85% rename from src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp rename to src/libs/recommendation/impl/features/FeaturesEngineCache.cpp index d184ac60..0f238627 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp +++ b/src/libs/recommendation/impl/features/FeaturesEngineCache.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "FeaturesClassifierCache.hpp" +#include "FeaturesEngineCache.hpp" #include #include @@ -90,7 +90,7 @@ networkToCacheFile(const SOM::Network& network, std::filesystem::path path) } std::optional -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::createObjectPositionsFromCacheFile(const std::filesystem::path& path) +std::optional +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("id"); + const Database::TrackId id {object.second.get("id")}; for (const auto& position : object.second.get_child("position")) { auto x = position.second.get("x"); auto y = position.second.get("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::read() +std::optional +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::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)} { diff --git a/src/libs/recommendation/impl/features/FeaturesClassifierCache.hpp b/src/libs/recommendation/impl/features/FeaturesEngineCache.hpp similarity index 66% rename from src/libs/recommendation/impl/features/FeaturesClassifierCache.hpp rename to src/libs/recommendation/impl/features/FeaturesEngineCache.hpp index 25ddaeff..08372345 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifierCache.hpp +++ b/src/libs/recommendation/impl/features/FeaturesEngineCache.hpp @@ -28,27 +28,27 @@ namespace Recommendation { -class FeaturesClassifierCache +class FeaturesEngineCache { public: static void invalidate(); - static std::optional read(); + static std::optional read(); void write() const; private: - using ObjectPositions = std::unordered_map>; + using TrackPositions = std::unordered_map>; - FeaturesClassifierCache(SOM::Network network, ObjectPositions trackPositions); + FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions); static std::optional createNetworkFromCacheFile(const std::filesystem::path& path); - static std::optional createObjectPositionsFromCacheFile(const std::filesystem::path& path); - static bool objectPositionToCacheFile(const ObjectPositions& objectsPosition, const std::filesystem::path& path); + static std::optional 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 diff --git a/src/libs/recommendation/include/recommendation/IEngine.hpp b/src/libs/recommendation/include/recommendation/IEngine.hpp index bd7ce05d..6a15187b 100644 --- a/src/libs/recommendation/include/recommendation/IEngine.hpp +++ b/src/libs/recommendation/include/recommendation/IEngine.hpp @@ -20,9 +20,7 @@ #pragma once #include -#include -#include - +#include #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; + template + using ResultContainer = std::vector; - virtual ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0; - virtual ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set& 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 linkTypes, - std::size_t maxCount) = 0; + using ArtistContainer = ResultContainer; + using ReleaseContainer = ResultContainer; + using TrackContainer = ResultContainer; + + virtual TrackContainer getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) = 0; + virtual TrackContainer getSimilarTracks(Database::Session& session, const std::vector& 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 linkTypes, std::size_t maxCount) = 0; + + protected: + virtual void requestCancelLoad() = 0; }; std::unique_ptr createEngine(Database::Db& db); diff --git a/src/libs/subsonic/impl/ParameterParsing.cpp b/src/libs/recommendation/include/recommendation/IRecommendation.hpp similarity index 64% rename from src/libs/subsonic/impl/ParameterParsing.cpp rename to src/libs/recommendation/include/recommendation/IRecommendation.hpp index bd8c8464..834a983e 100644 --- a/src/libs/subsonic/impl/ParameterParsing.cpp +++ b/src/libs/recommendation/include/recommendation/IRecommendation.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Emeric Poupon + * Copyright (C) 2021 Emeric Poupon * * This file is part of LMS. * @@ -17,27 +17,22 @@ * along with LMS. If not, see . */ -#include "ParameterParsing.hpp" +#pragma once -namespace StringUtils +#include "database/Types.hpp" +#include "utils/EnumSet.hpp" + +namespace Database { - template<> - std::optional - readAs(std::string_view str) - { - return API::Subsonic::IdFromString(str); - } - - template<> - std::optional - readAs(std::string_view str) - { - if (str == "true") - return true; - else if (str == "false") - return false; - - return {}; - } + class Db; + class Session; } +namespace Recommendation +{ + class IRecommendation + { + }; + +} // ns Recommendation + diff --git a/src/libs/scanner/impl/Scanner.cpp b/src/libs/scanner/impl/Scanner.cpp index f1b1fe3f..6a8444be 100644 --- a/src/libs/scanner/impl/Scanner.cpp +++ b/src/libs/scanner/impl/Scanner.cpp @@ -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 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 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; }()}; @@ -922,8 +922,8 @@ Scanner::removeMissingTracks(ScanStats& stats) stepStats.totalElems = trackCount; notifyInProgress(stepStats); - std::vector> trackPaths; - std::vector tracksToRemove; + std::vector> trackPaths; + std::vector 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}); } } diff --git a/src/libs/scanner/impl/Scanner.hpp b/src/libs/scanner/impl/Scanner.hpp index e8564c66..76aeb819 100644 --- a/src/libs/scanner/impl/Scanner.hpp +++ b/src/libs/scanner/impl/Scanner.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -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 _fileExtensions; - std::filesystem::path _mediaDirectory; + std::unordered_set _fileExtensions; + std::filesystem::path _mediaDirectory; Database::ScanSettings::RecommendationEngineType _recommendationEngineType; }; diff --git a/src/libs/scanner/include/scanner/ScannerStats.hpp b/src/libs/scanner/include/scanner/ScannerStats.hpp index b1996647..5528ce8b 100644 --- a/src/libs/scanner/include/scanner/ScannerStats.hpp +++ b/src/libs/scanner/include/scanner/ScannerStats.hpp @@ -53,7 +53,7 @@ namespace Scanner { struct ScanDuplicate { - Database::IdType trackId; + Database::TrackId trackId; DuplicateReason reason; }; diff --git a/src/libs/scrobbling/impl/IScrobbler.hpp b/src/libs/scrobbling/impl/IScrobbler.hpp index 01fae716..f442e773 100644 --- a/src/libs/scrobbling/impl/IScrobbler.hpp +++ b/src/libs/scrobbling/impl/IScrobbler.hpp @@ -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 getListensTrackList(Database::Session& session, Wt::Dbo::ptr user) = 0; + virtual Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user) = 0; }; std::unique_ptr createScrobbler(std::string_view backendName); diff --git a/src/libs/scrobbling/impl/Scrobbling.cpp b/src/libs/scrobbling/impl/Scrobbling.cpp index 65df227d..add4cf2e 100644 --- a/src/libs/scrobbling/impl/Scrobbling.cpp +++ b/src/libs/scrobbling/impl/Scrobbling.cpp @@ -64,7 +64,7 @@ namespace Scrobbling } std::optional - Scrobbling::getUserScrobbler(Database::IdType userId) + Scrobbling::getUserScrobbler(Database::UserId userId) { std::optional scrobbler; @@ -76,49 +76,49 @@ namespace Scrobbling return scrobbler; } - std::vector> + std::vector> Scrobbling::getRecentArtists(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) { - const Wt::Dbo::ptr history {getListensTrackList(session, user)}; + const Database::ObjectPtr history {getListensTrackList(session, user)}; - std::vector> res; + std::vector> res; if (history) res = history->getArtistsReverse(clusterIds, linkType, range, moreResults); return res; } - std::vector> + std::vector> Scrobbling::getRecentReleases(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) { - const Wt::Dbo::ptr history {getListensTrackList(session, user)}; + const Database::ObjectPtr history {getListensTrackList(session, user)}; - std::vector> res; + std::vector> res; if (history) res = history->getReleasesReverse(clusterIds, range, moreResults); return res; } - std::vector> + std::vector> Scrobbling::getRecentTracks(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) { - const Wt::Dbo::ptr history {getListensTrackList(session, user)}; + const Database::ObjectPtr history {getListensTrackList(session, user)}; - std::vector> res; + std::vector> res; if (history) res = history->getTracksReverse(clusterIds, range, moreResults); @@ -127,57 +127,57 @@ namespace Scrobbling // Top - std::vector> + std::vector> Scrobbling::getTopArtists(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) { - const Wt::Dbo::ptr history {getListensTrackList(session, user)}; + const Database::ObjectPtr history {getListensTrackList(session, user)}; - std::vector> res; + std::vector> res; if (history) res = history->getTopArtists(clusterIds, linkType, range, moreResults); return res; } - std::vector> + std::vector> Scrobbling::getTopReleases(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) { - const Wt::Dbo::ptr history {getListensTrackList(session, user)}; + const Database::ObjectPtr history {getListensTrackList(session, user)}; - std::vector> res; + std::vector> res; if (history) res = history->getTopReleases(clusterIds, range, moreResults); return res; } - std::vector> + std::vector> Scrobbling::getTopTracks(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) { - const Wt::Dbo::ptr history {getListensTrackList(session, user)}; + const Database::ObjectPtr history {getListensTrackList(session, user)}; - std::vector> res; + std::vector> res; if (history) res = history->getTopTracks(clusterIds, range, moreResults); return res; } - Wt::Dbo::ptr - Scrobbling::getListensTrackList(Database::Session& session, Wt::Dbo::ptr user) + Database::ObjectPtr + Scrobbling::getListensTrackList(Database::Session& session, Database::ObjectPtr user) { return _scrobblers[user->getScrobbler()]->getListensTrackList(session, user); } diff --git a/src/libs/scrobbling/impl/Scrobbling.hpp b/src/libs/scrobbling/impl/Scrobbling.hpp index 613335c8..6aa2ed2f 100644 --- a/src/libs/scrobbling/impl/Scrobbling.hpp +++ b/src/libs/scrobbling/impl/Scrobbling.hpp @@ -38,47 +38,47 @@ namespace Scrobbling void listenFinished(const Listen& listen, std::optional duration) override; void addTimedListen(const TimedListen& listen) override; - std::vector> getRecentArtists(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + std::vector> getRecentArtists(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) override; - std::vector> getRecentReleases(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + std::vector> getRecentReleases(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) override; - std::vector> getRecentTracks(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + std::vector> getRecentTracks(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) override; - std::vector> getTopArtists(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + std::vector> getTopArtists(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) override; - std::vector> getTopReleases(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + std::vector> getTopReleases(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) override; - std::vector> getTopTracks(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + std::vector> getTopTracks(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) override; - Wt::Dbo::ptr getListensTrackList(Database::Session& session, Wt::Dbo::ptr user); + Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user); - std::optional getUserScrobbler(Database::IdType userId); + std::optional getUserScrobbler(Database::UserId userId); Database::Db& _db; std::unordered_map> _scrobblers; diff --git a/src/libs/scrobbling/impl/internal/InternalScrobbler.cpp b/src/libs/scrobbling/impl/internal/InternalScrobbler.cpp index 019759f2..e5b23d51 100644 --- a/src/libs/scrobbling/impl/internal/InternalScrobbler.cpp +++ b/src/libs/scrobbling/impl/internal/InternalScrobbler.cpp @@ -61,7 +61,7 @@ namespace Scrobbling if (!user) return; - Wt::Dbo::ptr 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 - InternalScrobbler::getListensTrackList(Database::Session& session, Wt::Dbo::ptr user) + Database::TrackList::pointer + InternalScrobbler::getListensTrackList(Database::Session& session, Database::ObjectPtr user) { return Database::TrackList::get(session, historyTracklistName, Database::TrackList::Type::Internal, user); } diff --git a/src/libs/scrobbling/impl/internal/InternalScrobbler.hpp b/src/libs/scrobbling/impl/internal/InternalScrobbler.hpp index 4d3323ea..45c067da 100644 --- a/src/libs/scrobbling/impl/internal/InternalScrobbler.hpp +++ b/src/libs/scrobbling/impl/internal/InternalScrobbler.hpp @@ -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 getListensTrackList(Database::Session& session, Wt::Dbo::ptr user) override; + Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user) override; Database::Db& _db; }; diff --git a/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp b/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp index 0cdb8443..c17254da 100644 --- a/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp +++ b/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp @@ -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()}; diff --git a/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp b/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp index ea6e5d1d..30e85bad 100644 --- a/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp +++ b/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp @@ -50,7 +50,7 @@ namespace Scrobbling::ListenBrainz void listenStarted(const Listen& listen) override; void listenFinished(const Listen& listen, std::optional duration) override; void addTimedListen(const TimedListen& listen) override; - Wt::Dbo::ptr getListensTrackList(Database::Session& session, Wt::Dbo::ptr user) override; + Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user) override; // Submit listens void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint); diff --git a/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp index 1a59abcf..dd0b14a7 100644 --- a/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ b/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -195,7 +195,7 @@ namespace std::vector 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 userIds; + std::vector 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 - ListensSynchronizer::createValidateTokenRequestData(Database::IdType userId) + ListensSynchronizer::createValidateTokenRequestData(Database::UserId userId) { Database::Session& session {_db.getTLSSession()}; diff --git a/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp b/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp index 511e07e8..e2914dee 100644 --- a/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp +++ b/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp @@ -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 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 createValidateTokenRequestData(Database::IdType userId); + std::optional createValidateTokenRequestData(Database::UserId userId); std::optional 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 _userContexts; + std::unordered_map _userContexts; const std::size_t _maxSyncListenCount; const std::chrono::hours _syncListensPeriod; diff --git a/src/libs/scrobbling/impl/listenbrainz/Utils.cpp b/src/libs/scrobbling/impl/listenbrainz/Utils.cpp index 47cf901c..ab293954 100644 --- a/src/libs/scrobbling/impl/listenbrainz/Utils.cpp +++ b/src/libs/scrobbling/impl/listenbrainz/Utils.cpp @@ -30,7 +30,7 @@ static constexpr std::string_view historyTracklistName {"__scrobbler_listenbrain namespace Scrobbling::ListenBrainz::Utils { std::optional - getListenBrainzToken(Database::Session& session, Database::IdType userId) + getListenBrainzToken(Database::Session& session, Database::UserId userId) { auto transaction {session.createSharedTransaction()}; diff --git a/src/libs/scrobbling/impl/listenbrainz/Utils.hpp b/src/libs/scrobbling/impl/listenbrainz/Utils.hpp index 84074086..6b2d1271 100644 --- a/src/libs/scrobbling/impl/listenbrainz/Utils.hpp +++ b/src/libs/scrobbling/impl/listenbrainz/Utils.hpp @@ -21,6 +21,7 @@ #include #include "utils/UUID.hpp" + #include "database/Types.hpp" namespace Database @@ -32,7 +33,7 @@ namespace Database namespace Scrobbling::ListenBrainz::Utils { - std::optional getListenBrainzToken(Database::Session& session, Database::IdType userId); - Wt::Dbo::ptr getOrCreateListensTrackList(Database::Session& session, Wt::Dbo::ptr user); - Wt::Dbo::ptr getListensTrackList(Database::Session& session, Wt::Dbo::ptr user); + std::optional getListenBrainzToken(Database::Session& session, Database::UserId userId); + Database::ObjectPtr getOrCreateListensTrackList(Database::Session& session, Database::ObjectPtr user); + Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user); } diff --git a/src/libs/scrobbling/include/scrobbling/IScrobbling.hpp b/src/libs/scrobbling/include/scrobbling/IScrobbling.hpp index e1fc5ce9..6f4436c7 100644 --- a/src/libs/scrobbling/include/scrobbling/IScrobbling.hpp +++ b/src/libs/scrobbling/include/scrobbling/IScrobbling.hpp @@ -24,12 +24,12 @@ #include #include #include -#include #include #include #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> getRecentArtists(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + virtual std::vector> getRecentArtists(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) = 0; - virtual std::vector> getRecentReleases(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + virtual std::vector> getRecentReleases(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) = 0; - virtual std::vector> getRecentTracks(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + virtual std::vector> getRecentTracks(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) = 0; // Top - virtual std::vector> getTopArtists(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + virtual std::vector> getTopArtists(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) = 0; - virtual std::vector> getTopReleases(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + virtual std::vector> getTopReleases(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) = 0; - virtual std::vector> getTopTracks(Database::Session& session, - Wt::Dbo::ptr user, - const std::set& clusterIds, + virtual std::vector> getTopTracks(Database::Session& session, + Database::ObjectPtr user, + const std::vector& clusterIds, std::optional range, bool& moreResults) = 0; }; diff --git a/src/libs/scrobbling/include/scrobbling/Listen.hpp b/src/libs/scrobbling/include/scrobbling/Listen.hpp index b67a2ae9..ae80dbac 100644 --- a/src/libs/scrobbling/include/scrobbling/Listen.hpp +++ b/src/libs/scrobbling/include/scrobbling/Listen.hpp @@ -27,8 +27,8 @@ namespace Scrobbling { struct Listen { - Database::IdType userId {}; - Database::IdType trackId {}; + Database::UserId userId {}; + Database::TrackId trackId {}; }; struct TimedListen : public Listen diff --git a/src/libs/som/impl/Network.cpp b/src/libs/som/impl/Network.cpp index 535052a3..83f952d7 100644 --- a/src/libs/som/impl/Network.cpp +++ b/src/libs/som/impl/Network.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "utils/Logger.hpp" #include "utils/Random.hpp" @@ -197,7 +198,7 @@ Network::getClosestRefVectorPosition(const InputVector& data, InputVector::Dista } std::optional -Network::getClosestRefVectorPosition(const std::unordered_set& refVectorsPosition, InputVector::Distance maxDistance) const +Network::getClosestRefVectorPosition(const std::vector& refVectorsPosition, InputVector::Distance maxDistance) const { std::unordered_set neighboursPosition; for (const Position& refVectorPosition : refVectorsPosition) diff --git a/src/libs/som/include/som/Network.hpp b/src/libs/som/include/som/Network.hpp index f3ba2df1..be854237 100644 --- a/src/libs/som/include/som/Network.hpp +++ b/src/libs/som/include/som/Network.hpp @@ -20,7 +20,6 @@ #pragma once #include -#include #include #include #include @@ -70,7 +69,7 @@ class Network Position getClosestRefVectorPosition(const InputVector& data) const; std::optional getClosestRefVectorPosition(const InputVector& data, InputVector::Distance maxDistance) const; - std::optional getClosestRefVectorPosition(const std::unordered_set& refVectorsPosition, InputVector::Distance maxDistance) const; + std::optional getClosestRefVectorPosition(const std::vector& refVectorsPosition, InputVector::Distance maxDistance) const; InputVector::Distance getRefVectorsDistance(const Position& position1, const Position& position2) const; diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index e4aeeb0f..4c87d7cd 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -1,6 +1,5 @@ add_library(lmssubsonic SHARED - impl/ParameterParsing.cpp impl/Scan.cpp impl/Stream.cpp impl/SubsonicId.cpp diff --git a/src/libs/subsonic/impl/ParameterParsing.hpp b/src/libs/subsonic/impl/ParameterParsing.hpp index 0efae6c3..d805092f 100644 --- a/src/libs/subsonic/impl/ParameterParsing.hpp +++ b/src/libs/subsonic/impl/ParameterParsing.hpp @@ -20,8 +20,8 @@ #include +#include "database/Types.hpp" #include "utils/String.hpp" -#include "SubsonicId.hpp" #include "SubsonicResponse.hpp" namespace API::Subsonic @@ -82,18 +82,5 @@ namespace API::Subsonic return *res; } - - -} - -namespace StringUtils -{ - template<> - std::optional - readAs(std::string_view str); - - template<> - std::optional - readAs(std::string_view str); } diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index 6d599513..d2949a8b 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -36,7 +36,7 @@ namespace API::Subsonic { const Wt::Http::ParameterMap& parameters; Database::Session& dbSession; - Database::IdType userId; + Database::UserId userId; std::string clientName; }; } diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/Stream.cpp index 911b1514..d3d181ed 100644 --- a/src/libs/subsonic/impl/Stream.cpp +++ b/src/libs/subsonic/impl/Stream.cpp @@ -63,7 +63,7 @@ StreamParameters getStreamParameters(RequestContext& context) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; + const TrackId id {getMandatoryParameterAs(context.parameters, "id")}; // Optional params std::optional maxBitRate {getParameterAs(context.parameters, "maxBitRate")}; @@ -74,7 +74,7 @@ getStreamParameters(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; { - auto track {Track::getById(context.dbSession, id.value)}; + auto track {Track::getById(context.dbSession, id)}; if (!track) throw RequestedDataNotFoundError {}; @@ -94,7 +94,7 @@ getStreamParameters(RequestContext& context) // "If set to zero, no limit is imposed" if (maxBitRate && *maxBitRate != 0) - bitRate = clamp(*maxBitRate, std::size_t {48}, bitRate); + bitRate = Utils::clamp(*maxBitRate, std::size_t {48}, bitRate); Av::TranscodeParameters transcodeParameters; @@ -118,13 +118,13 @@ handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Ht if (!continuation) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; + Database::TrackId id {getMandatoryParameterAs(context.parameters, "id")}; std::filesystem::path trackPath; { auto transaction {context.dbSession.createSharedTransaction()}; - auto track {Track::getById(context.dbSession, id.value)}; + auto track {Track::getById(context.dbSession, id)}; if (!track) throw RequestedDataNotFoundError {}; diff --git a/src/libs/subsonic/impl/SubsonicId.cpp b/src/libs/subsonic/impl/SubsonicId.cpp index 368b7d94..edd052f1 100644 --- a/src/libs/subsonic/impl/SubsonicId.cpp +++ b/src/libs/subsonic/impl/SubsonicId.cpp @@ -26,64 +26,127 @@ namespace API::Subsonic { - -std::optional -IdFromString(const std::string_view id) -{ - if (id == "root") - return Id {Id::Type::Root}; - - std::vector values {StringUtils::splitString(id, "-")}; - if (values.size() != 2) - return std::nullopt; - - Id res; - - const std::string type {std::move(values[0])}; - if (type == "ar") - res.type = Id::Type::Artist; - else if (type == "al") - res.type = Id::Type::Release; - else if (type == "tr") - res.type = Id::Type::Track; - else if (type == "pl") - res.type = Id::Type::Playlist; - else - return std::nullopt; - - auto optId {StringUtils::readAs(values[1])}; - if (!optId) - return std::nullopt; - - res.value = *optId; - - return res; -} - -std::string -IdToString(const Id& id) -{ - std::string res; - - switch (id.type) + std::string + idToString(Database::ArtistId id) { - case Id::Type::Root: - return "root"; - case Id::Type::Artist: - res = "ar-"; - break; - case Id::Type::Release: - res = "al-"; - break; - case Id::Type::Track: - res = "tr-"; - break; - case Id::Type::Playlist: - res = "pl-"; - break; + return "ar-" + id.toString(); } - return res + std::to_string(id.value); + std::string + idToString(Database::ReleaseId id) + { + return "al-" + id.toString(); + } + + std::string + idToString(RootId) + { + return "root"; + } + + std::string + idToString(Database::TrackId id) + { + return "tr-" + id.toString(); + } + + std::string + idToString(Database::TrackListId id) + { + return "pl-" + id.toString(); + } +} // namespace API::Subsonic + +namespace StringUtils +{ + template<> + std::optional + readAs(std::string_view str) + { + std::vector values {StringUtils::splitString(str, "-")}; + if (values.size() != 2) + return std::nullopt; + + if (values[0] != "ar") + return std::nullopt; + + if (const auto value {StringUtils::readAs(values[1])}) + return Database::ArtistId {*value}; + + return std::nullopt; + } + + template<> + std::optional + readAs(std::string_view str) + { + std::vector values {StringUtils::splitString(str, "-")}; + if (values.size() != 2) + return std::nullopt; + + if (values[0] != "al") + return std::nullopt; + + if (const auto value {StringUtils::readAs(values[1])}) + return Database::ReleaseId {*value}; + + return std::nullopt; + } + + template<> + std::optional + readAs(std::string_view str) + { + if (str == "root") + return API::Subsonic::RootId {}; + + return std::nullopt; + } + + template<> + std::optional + readAs(std::string_view str) + { + std::vector values {StringUtils::splitString(str, "-")}; + if (values.size() != 2) + return std::nullopt; + + if (values[0] != "tr") + return std::nullopt; + + if (const auto value {StringUtils::readAs(values[1])}) + return Database::TrackId {*value}; + + return std::nullopt; + } + + template<> + std::optional + readAs(std::string_view str) + { + std::vector values {StringUtils::splitString(str, "-")}; + if (values.size() != 2) + return std::nullopt; + + if (values[0] != "pl") + return std::nullopt; + + if (const auto value {StringUtils::readAs(values[1])}) + return Database::TrackListId {*value}; + + return std::nullopt; + } + + template<> + std::optional + readAs(std::string_view str) + { + if (str == "true") + return true; + else if (str == "false") + return false; + + return {}; + } } -} // namespace API::Subsonic diff --git a/src/libs/subsonic/impl/SubsonicId.hpp b/src/libs/subsonic/impl/SubsonicId.hpp index 52760798..0ac2a16a 100644 --- a/src/libs/subsonic/impl/SubsonicId.hpp +++ b/src/libs/subsonic/impl/SubsonicId.hpp @@ -19,29 +19,45 @@ #pragma once -#include - #include "database/Types.hpp" +#include "utils/String.hpp" namespace API::Subsonic { + struct RootId {}; -struct Id -{ - enum class Type - { - Root, // Where all artists artistless albums reside - Track, - Release, - Artist, - Playlist, - }; - - Type type; - Database::IdType value {}; -}; - -std::optional IdFromString(std::string_view id); -std::string IdToString(const Id& id); - + std::string idToString(Database::ArtistId id); + std::string idToString(Database::ReleaseId id); + std::string idToString(Database::TrackId id); + std::string idToString(Database::TrackListId id); + std::string idToString(RootId); } // namespace API::Subsonic + +// Used to parse parameters +namespace StringUtils +{ + template<> + std::optional + readAs(std::string_view str); + + template<> + std::optional + readAs(std::string_view str); + + template<> + std::optional + readAs(std::string_view str); + + template<> + std::optional + readAs(std::string_view str); + + template<> + std::optional + readAs(std::string_view str); + + template<> + std::optional + readAs(std::string_view str); +} + diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 2c0a7e1a..a3f8b3f4 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -48,6 +48,7 @@ #include "RequestContext.hpp" #include "Scan.hpp" #include "Stream.hpp" +#include "SubsonicId.hpp" #include "SubsonicResponse.hpp" using namespace Database; @@ -321,7 +322,7 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: { Response::Node trackResponse; - trackResponse.setAttribute("id", IdToString({Id::Type::Track, track.id()})); + trackResponse.setAttribute("id", idToString(track->getId())); trackResponse.setAttribute("isDir", false); trackResponse.setAttribute("title", track->getName()); if (track->getTrackNumber()) @@ -348,7 +349,7 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: if (user->getSubsonicTranscodeEnable()) trackResponse.setAttribute("transcodedSuffix", formatToSuffix(user->getSubsonicTranscodeFormat())); - trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()})); + trackResponse.setAttribute("coverArt", idToString(track->getId())); const std::vector& artists {track->getArtists({TrackArtistLinkType::Artist})}; if (!artists.empty()) @@ -356,14 +357,14 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: trackResponse.setAttribute("artist", getArtistNames(artists)); if (artists.size() == 1) - trackResponse.setAttribute("artistId", IdToString({Id::Type::Artist, artists.front().id()})); + trackResponse.setAttribute("artistId", idToString(artists.front()->getId())); } if (track->getRelease()) { trackResponse.setAttribute("album", track->getRelease()->getName()); - trackResponse.setAttribute("albumId", IdToString({Id::Type::Release, track->getRelease().id()})); - trackResponse.setAttribute("parent", IdToString({Id::Type::Release, track->getRelease().id()})); + trackResponse.setAttribute("albumId", idToString(track->getRelease()->getId())); + trackResponse.setAttribute("parent", idToString(track->getRelease()->getId())); } trackResponse.setAttribute("duration", std::chrono::duration_cast(track->getDuration()).count()); @@ -420,8 +421,8 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const } albumNode.setAttribute("created", dateTimeToCreatedString(release->getLastWritten())); - albumNode.setAttribute("id", IdToString({Id::Type::Release, release.id()})); - albumNode.setAttribute("coverArt", IdToString({Id::Type::Release, release.id()})); + albumNode.setAttribute("id", idToString(release->getId())); + albumNode.setAttribute("coverArt", idToString(release->getId())); auto releaseYear {release->getReleaseYear()}; if (releaseYear) albumNode.setAttribute("year", *releaseYear); @@ -432,7 +433,7 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const if (artists.empty() && !id3) { - albumNode.setAttribute("parent", IdToString({Id::Type::Root})); + albumNode.setAttribute("parent", idToString(RootId {})); } else if (!artists.empty()) { @@ -440,15 +441,12 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const if (artists.size() == 1) { - if (id3) - albumNode.setAttribute("artistId", IdToString({Id::Type::Artist, artists.front().id()})); - else - albumNode.setAttribute("parent", IdToString({Id::Type::Artist, artists.front().id()})); + albumNode.setAttribute(id3 ? "artistId" : "parent", idToString(artists.front()->getId())); } else { if (!id3) - albumNode.setAttribute("parent", IdToString({Id::Type::Root})); + albumNode.setAttribute("parent", idToString(RootId {})); } } @@ -476,7 +474,7 @@ artistToResponseNode(const User::pointer& user, const Artist::pointer& artist, b { Response::Node artistNode; - artistNode.setAttribute("id", IdToString({Id::Type::Artist, artist.id()})); + artistNode.setAttribute("id", idToString(artist->getId())); artistNode.setAttribute("name", artist->getName()); if (id3) @@ -544,7 +542,7 @@ handleChangePassword(RequestContext& context) try { - Database::IdType userId; + Database::UserId userId; { auto transaction {context.dbSession.createSharedTransaction()}; @@ -554,7 +552,7 @@ handleChangePassword(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - userId = user.id(); + userId = user->getId(); } Service::get()->setPassword(context.dbSession, userId, password); @@ -580,15 +578,10 @@ Response handleCreatePlaylistRequest(RequestContext& context) { // Optional params - auto id {getParameterAs(context.parameters, "playlistId")}; - if (id && id->type != Id::Type::Playlist) - throw BadParameterGenericError {"playlistId"}; - + const auto id {getParameterAs(context.parameters, "playlistId")}; auto name {getParameterAs(context.parameters, "name")}; - std::vector trackIds {getMultiParametersAs(context.parameters, "songId")}; - if (!std::all_of(std::cbegin(trackIds), std::cend(trackIds ), [](const Id& id) { return id.type == Id::Type::Track; })) - throw BadParameterGenericError {"songId"}; + std::vector trackIds {getMultiParametersAs(context.parameters, "songId")}; if (!name && !id) throw RequiredParameterMissingError {"name or id"}; @@ -602,7 +595,7 @@ handleCreatePlaylistRequest(RequestContext& context) TrackList::pointer tracklist; if (id) { - tracklist = TrackList::getById(context.dbSession, id->value); + tracklist = TrackList::getById(context.dbSession, *id); if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) @@ -618,9 +611,9 @@ handleCreatePlaylistRequest(RequestContext& context) tracklist = TrackList::create(context.dbSession, *name, TrackList::Type::Playlist, false, user); } - for (const Id& trackId : trackIds) + for (const TrackId trackId : trackIds) { - Track::pointer track {Track::getById(context.dbSession, trackId.value)}; + Track::pointer track {Track::getById(context.dbSession, trackId)}; if (!track) continue; @@ -638,7 +631,7 @@ handleCreateUserRequest(RequestContext& context) std::string password {decodePasswordIfNeeded(getMandatoryParameterAs(context.parameters, "password"))}; // Just ignore all the other fields as we don't handle them - Database::IdType userId; + Database::UserId userId; { auto transaction {context.dbSession.createUniqueTransaction()}; @@ -647,7 +640,7 @@ handleCreateUserRequest(RequestContext& context) throw UserAlreadyExistsGenericError {}; user = User::create(context.dbSession, username); - userId = user.id(); + userId = user->getId(); } auto removeCreatedUser {[&]() @@ -685,9 +678,7 @@ static Response handleDeletePlaylistRequest(RequestContext& context) { - Id id {getMandatoryParameterAs(context.parameters, "id")}; - if (id.type != Id::Type::Playlist) - throw BadParameterGenericError {"id"}; + TrackListId id {getMandatoryParameterAs(context.parameters, "id")}; auto transaction {context.dbSession.createUniqueTransaction()}; @@ -695,7 +686,7 @@ handleDeletePlaylistRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)}; + TrackList::pointer tracklist {TrackList::getById(context.dbSession, id)}; if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) @@ -721,7 +712,7 @@ handleDeleteUserRequest(RequestContext& context) throw RequestedDataNotFoundError {}; // cannot delete ourself - if (user.id() == context.userId) + if (user->getId() == context.userId) throw UserNotAuthorizedError {}; user.remove(); @@ -809,7 +800,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) if (cluster) { bool more; - releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, range, more); + releases = Release::getByFilter(context.dbSession, {cluster->getId()}, {}, range, more); } } } @@ -876,14 +867,11 @@ Response handleGetAlbumRequest(RequestContext& context) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; - - if (id.type != Id::Type::Release) - throw BadParameterGenericError {"id"}; + ReleaseId id {getMandatoryParameterAs(context.parameters, "id")}; auto transaction {context.dbSession.createSharedTransaction()}; - Release::pointer release {Release::getById(context.dbSession, id.value)}; + Release::pointer release {Release::getById(context.dbSession, id)}; if (!release) throw RequestedDataNotFoundError {}; @@ -908,14 +896,11 @@ Response handleGetArtistRequest(RequestContext& context) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; - - if (id.type != Id::Type::Artist) - throw BadParameterGenericError {"id"}; + ArtistId id {getMandatoryParameterAs(context.parameters, "id")}; auto transaction {context.dbSession.createSharedTransaction()}; - Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; + Artist::pointer artist {Artist::getById(context.dbSession, id)}; if (!artist) throw RequestedDataNotFoundError {}; @@ -940,9 +925,7 @@ Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; - if (id.type != Id::Type::Artist) - throw BadParameterGenericError {"id"}; + ArtistId id {getMandatoryParameterAs(context.parameters, "id")}; // Optional params std::size_t count {getParameterAs(context.parameters, "count").value_or(20)}; @@ -953,7 +936,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) { auto transaction {context.dbSession.createSharedTransaction()}; - Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; + Artist::pointer artist {Artist::getById(context.dbSession, id)}; if (!artist) throw RequestedDataNotFoundError {}; @@ -963,7 +946,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) } auto similarArtistsId {Service::get()->getSimilarArtists(context.dbSession, - id.value, + id, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, count)}; @@ -974,7 +957,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) if (!user) throw UserNotAuthorizedError {}; - for ( const auto& similarArtistId : similarArtistsId ) + for ( const ArtistId similarArtistId : similarArtistsId ) { Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; if (similarArtist) @@ -1049,66 +1032,63 @@ Response handleGetMusicDirectoryRequest(RequestContext& context) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; + const auto artistId {getParameterAs(context.parameters, "id")}; + const auto releaseId {getParameterAs(context.parameters, "id")}; + const auto trackId {getParameterAs(context.parameters, "id")}; + const auto root {getParameterAs(context.parameters, "id")}; + + if (!root && !artistId && !releaseId && !trackId) + throw BadParameterGenericError {"id"}; Response response {Response::createOkResponse(context)}; Response::Node& directoryNode {response.createNode("directory")}; - directoryNode.setAttribute("id", IdToString(id)); - auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - switch (id.type) + if (root) { - case Id::Type::Root: - { - directoryNode.setAttribute("name", "Music"); + directoryNode.setAttribute("id", idToString(RootId {})); + directoryNode.setAttribute("name", "Music"); - bool moreResults{}; - auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName, std::nullopt, moreResults)}; - for (const Artist::pointer& artist : artists) - directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */)); - - break; - } - - case Id::Type::Artist: - { - auto artist {Artist::getById(context.dbSession, id.value)}; - if (!artist) - throw RequestedDataNotFoundError {}; - - directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName())); - - auto releases {artist->getReleases()}; - for (const Release::pointer& release : releases) - directoryNode.addArrayChild("child", releaseToResponseNode(release, context.dbSession, user, false /* no id3 */)); - - break; - } - - case Id::Type::Release: - { - auto release {Release::getById(context.dbSession, id.value)}; - if (!release) - throw RequestedDataNotFoundError {}; - - directoryNode.setAttribute("name", makeNameFilesystemCompatible(release->getName())); - - auto tracks {release->getTracks()}; - for (const Track::pointer& track : tracks) - directoryNode.addArrayChild("child", trackToResponseNode(track, context.dbSession, user)); - - break; - } - - default: - throw BadParameterGenericError {"id"}; + bool moreResults{}; + auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName, std::nullopt, moreResults)}; + for (const Artist::pointer& artist : artists) + directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */)); } + else if (artistId) + { + directoryNode.setAttribute("id", idToString(*artistId)); + + auto artist {Artist::getById(context.dbSession, *artistId)}; + if (!artist) + throw RequestedDataNotFoundError {}; + + directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName())); + + auto releases {artist->getReleases()}; + for (const Release::pointer& release : releases) + directoryNode.addArrayChild("child", releaseToResponseNode(release, context.dbSession, user, false /* no id3 */)); + } + else if (releaseId) + { + directoryNode.setAttribute("id", idToString(*releaseId)); + + auto release {Release::getById(context.dbSession, *releaseId)}; + if (!release) + throw RequestedDataNotFoundError {}; + + directoryNode.setAttribute("name", makeNameFilesystemCompatible(release->getName())); + + auto tracks {release->getTracks()}; + for (const Track::pointer& track : tracks) + directoryNode.addArrayChild("child", trackToResponseNode(track, context.dbSession, user)); + } + else + throw BadParameterGenericError {"id"}; return response; } @@ -1199,21 +1179,19 @@ Response handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) { // Mandatory params - const Id artistId {getMandatoryParameterAs(context.parameters, "id")}; - if (artistId.type != Id::Type::Artist) - throw BadParameterGenericError {"id"}; + const ArtistId artistId {getMandatoryParameterAs(context.parameters, "id")}; // Optional params std::size_t count {getParameterAs(context.parameters, "count").value_or(50)}; - auto similarArtistIds {Service::get()->getSimilarArtists(context.dbSession, - artistId.value, + const auto similarArtistIds {Service::get()->getSimilarArtists(context.dbSession, + artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, 5)}; auto transaction {context.dbSession.createSharedTransaction()}; - const Artist::pointer artist {Artist::getById(context.dbSession, artistId.value)}; + const Artist::pointer artist {Artist::getById(context.dbSession, artistId)}; if (!artist) throw RequestedDataNotFoundError {}; @@ -1223,7 +1201,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) // "Returns a random collection of songs from the given artist and similar artists" auto tracks {artist->getRandomTracks(count / 2)}; - for (const Database::IdType similarArtistId : similarArtistIds) + for (const ArtistId similarArtistId : similarArtistIds) { const Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; if (!similarArtist) @@ -1318,7 +1296,7 @@ tracklistToResponseNode(const TrackList::pointer& tracklist, Session&) { Response::Node playlistNode; - playlistNode.setAttribute("id", IdToString({Id::Type::Playlist, tracklist.id()})); + playlistNode.setAttribute("id", idToString(tracklist->getId())); playlistNode.setAttribute("name", tracklist->getName()); playlistNode.setAttribute("songCount", tracklist->getCount()); playlistNode.setAttribute("duration", std::chrono::duration_cast(tracklist->getDuration()).count()); @@ -1334,9 +1312,7 @@ Response handleGetPlaylistRequest(RequestContext& context) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; - if (id.type != Id::Type::Playlist) - throw BadParameterGenericError {"id"}; + TrackListId trackListId {getMandatoryParameterAs(context.parameters, "id")}; auto transaction {context.dbSession.createSharedTransaction()}; @@ -1344,7 +1320,7 @@ handleGetPlaylistRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)}; + TrackList::pointer tracklist {TrackList::getById(context.dbSession, trackListId)}; if (!tracklist) throw RequestedDataNotFoundError {}; @@ -1411,7 +1387,7 @@ handleGetSongsByGenreRequest(RequestContext& context) Response::Node& songsByGenreNode {response.createNode("songsByGenre")}; bool more; - auto tracks {Track::getByFilter(context.dbSession, {cluster.id()}, {}, Range {offset, size}, more)}; + auto tracks {Track::getByFilter(context.dbSession, {cluster->getId()}, {}, Range {offset, size}, more)}; for (const Track::pointer& track : tracks) songsByGenreNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); @@ -1504,9 +1480,9 @@ handleSearchRequestCommon(RequestContext& context, bool id3) struct StarParameters { - std::vector artistIds; - std::vector releaseIds; - std::vector trackIds; + std::vector artistIds; + std::vector releaseIds; + std::vector trackIds; }; static @@ -1515,34 +1491,10 @@ getStarParameters(const Wt::Http::ParameterMap& parameters) { StarParameters res; - std::vector ids {getMultiParametersAs(parameters, "id")}; - res.artistIds = getMultiParametersAs(parameters, "artistId"); - res.releaseIds = getMultiParametersAs(parameters, "albumId"); - - if (!std::all_of(std::cbegin(res.releaseIds ), std::cend(res.releaseIds ), [](const Id& id) { return id.type == Id::Type::Release; })) - throw BadParameterGenericError {"albumId"}; - - if (!std::all_of(std::cbegin(res.artistIds ), std::cend(res.artistIds ), [](const Id& id) { return id.type == Id::Type::Artist; })) - throw BadParameterGenericError {"artistId"}; - - // Redispatch the old "id" parameter in new lists - for (const Id& id : ids) - { - switch (id.type) - { - case Id::Type::Artist: - res.artistIds.emplace_back(id); - break; - case Id::Type::Release: - res.releaseIds.emplace_back(id); - break; - case Id::Type::Track: - res.trackIds.emplace_back(id); - break; - default: - throw BadParameterGenericError {"id"}; - } - } + // TODO handle parameters for legacy file browsing + res.trackIds = getMultiParametersAs(parameters, "id"); + res.artistIds = getMultiParametersAs(parameters, "artistId"); + res.releaseIds = getMultiParametersAs(parameters, "albumId"); return res; } @@ -1559,27 +1511,27 @@ handleStarRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - for (const Id& id : params.artistIds) + for (const ArtistId id : params.artistIds) { - Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; + Artist::pointer artist {Artist::getById(context.dbSession, id)}; if (!artist) continue; user.modify()->starArtist(artist); } - for (const Id& id : params.releaseIds) + for (const ReleaseId id : params.releaseIds) { - Release::pointer release {Release::getById(context.dbSession, id.value)}; + Release::pointer release {Release::getById(context.dbSession, id)}; if (!release) continue; user.modify()->starRelease(release); } - for (const Id& id : params.trackIds) + for (const TrackId id : params.trackIds) { - Track::pointer track {Track::getById(context.dbSession, id.value)}; + Track::pointer track {Track::getById(context.dbSession, id)}; if (!track) continue; @@ -1615,27 +1567,27 @@ handleUnstarRequest(RequestContext& context) if (!user) throw RequestedDataNotFoundError {}; - for (const Id& id : params.artistIds) + for (const ArtistId id : params.artistIds) { - Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; + Artist::pointer artist {Artist::getById(context.dbSession, id)}; if (!artist) continue; user.modify()->unstarArtist(artist); } - for (const Id& id : params.releaseIds) + for (const ReleaseId id : params.releaseIds) { - Release::pointer release {Release::getById(context.dbSession, id.value)}; + Release::pointer release {Release::getById(context.dbSession, id)}; if (!release) continue; user.modify()->unstarRelease(release); } - for (const Id& id : params.trackIds) + for (const TrackId id : params.trackIds) { - Track::pointer track {Track::getById(context.dbSession, id.value)}; + Track::pointer track {Track::getById(context.dbSession, id)}; if (!track) continue; @@ -1650,14 +1602,10 @@ static Response handleScrobble(RequestContext& context) { - const std::vector ids {getMandatoryMultiParametersAs(context.parameters, "id")}; + const std::vector ids {getMandatoryMultiParametersAs(context.parameters, "id")}; const std::vector times {getMultiParametersAs(context.parameters, "time")}; const bool submission{getParameterAs(context.parameters, "submission").value_or(true)}; - // only for tracks - if (!std::all_of(std::cbegin(ids), std::cend(ids), [](const Id& id) { return id.type == Id::Type::Track; })) - throw BadParameterGenericError {"id"}; - // playing now => no time to be provided if (!submission && !times.empty()) throw BadParameterGenericError {"time"}; @@ -1672,19 +1620,19 @@ handleScrobble(RequestContext& context) if (!submission) { - Service::get()->listenStarted({context.userId, ids.front().value}); + Service::get()->listenStarted({context.userId, ids.front()}); } else { if (times.empty()) { - Service::get()->listenFinished({context.userId, ids.front().value}); + Service::get()->listenFinished({context.userId, ids.front()}); } else { for (std::size_t i {}; i < ids.size(); ++i) { - const Database::IdType trackId {ids[i].value}; + const TrackId trackId {ids[i]}; const unsigned long time {times[i]}; Service::get()->addTimedListen({context.userId, trackId, Wt::WDateTime::fromTime_t(static_cast(time / 1000))}); } @@ -1701,7 +1649,7 @@ handleUpdateUserRequest(RequestContext& context) std::string username {getMandatoryParameterAs(context.parameters, "username")}; std::optional password {getParameterAs(context.parameters, "password")}; - Database::IdType userId; + UserId userId; { auto transaction {context.dbSession.createSharedTransaction()}; @@ -1709,7 +1657,7 @@ handleUpdateUserRequest(RequestContext& context) if (!user) throw RequestedDataNotFoundError {}; - userId = user.id(); + userId = user->getId(); } if (password) @@ -1742,18 +1690,13 @@ Response handleUpdatePlaylistRequest(RequestContext& context) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "playlistId")}; - if (id.type != Id::Type::Playlist) - throw BadParameterGenericError {"playlistId"}; + TrackListId id {getMandatoryParameterAs(context.parameters, "playlistId")}; // Optional parameters auto name {getParameterAs(context.parameters, "name")}; auto isPublic {getParameterAs(context.parameters, "public")}; - std::vector trackIdsToAdd {getMultiParametersAs(context.parameters, "songIdToAdd")}; - if (!std::all_of(std::cbegin(trackIdsToAdd), std::cend(trackIdsToAdd), [](const Id& id) { return id.type == Id::Type::Track; })) - throw BadParameterGenericError {"songIdToAdd"}; - + std::vector trackIdsToAdd {getMultiParametersAs(context.parameters, "songIdToAdd")}; std::vector trackPositionsToRemove {getMultiParametersAs(context.parameters, "songIndexToRemove")}; auto transaction {context.dbSession.createUniqueTransaction()}; @@ -1762,7 +1705,7 @@ handleUpdatePlaylistRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)}; + TrackList::pointer tracklist {TrackList::getById(context.dbSession, id)}; if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) @@ -1789,13 +1732,13 @@ handleUpdatePlaylistRequest(RequestContext& context) } // Add tracks - for (const Id& trackIdToAdd : trackIdsToAdd) + for (const TrackId trackIdToAdd : trackIdsToAdd) { - Track::pointer track {Track::getById(context.dbSession, trackIdToAdd.value)}; + Track::pointer track {Track::getById(context.dbSession, trackIdToAdd)}; if (!track) continue; - TrackListEntry::create(context.dbSession, track, tracklist ); + TrackListEntry::create(context.dbSession, track, tracklist); } return Response::createOkResponse(context); @@ -1832,10 +1775,7 @@ Response handleCreateBookmark(RequestContext& context) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; - if (id.type != Id::Type::Track) - throw BadParameterGenericError {"id"}; - + TrackId id {getMandatoryParameterAs(context.parameters, "id")}; unsigned long position {getMandatoryParameterAs(context.parameters, "position")}; const std::optional comment {getParameterAs(context.parameters, "comment")}; @@ -1845,7 +1785,7 @@ handleCreateBookmark(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - const Track::pointer track {Track::getById(context.dbSession, id.value)}; + const Track::pointer track {Track::getById(context.dbSession, id)}; if (!track) throw RequestedDataNotFoundError {}; @@ -1866,9 +1806,7 @@ Response handleDeleteBookmark(RequestContext& context) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; - if (id.type != Id::Type::Track) - throw BadParameterGenericError {"id"}; + TrackId id {getMandatoryParameterAs(context.parameters, "id")}; auto transaction {context.dbSession.createUniqueTransaction()}; @@ -1876,7 +1814,7 @@ handleDeleteBookmark(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - const Track::pointer track {Track::getById(context.dbSession, id.value)}; + const Track::pointer track {Track::getById(context.dbSession, id)}; if (!track) throw RequestedDataNotFoundError {}; @@ -1901,23 +1839,20 @@ void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; + const auto trackId {getParameterAs(context.parameters, "id")}; + const auto releaseId {getParameterAs(context.parameters, "id")}; + + if (!trackId && !releaseId) + throw BadParameterGenericError {"id"}; std::size_t size {getParameterAs(context.parameters, "size").value_or(256)}; - size = clamp(size, std::size_t {32}, std::size_t {1024}); + size = Utils::clamp(size, std::size_t {32}, std::size_t {1024}); std::shared_ptr cover; - switch (id.type) - { - case Id::Type::Track: - cover = Service::get()->getFromTrack(context.dbSession, id.value, size); - break; - case Id::Type::Release: - cover = Service::get()->getFromRelease(context.dbSession, id.value, size); - break; - default: - throw BadParameterGenericError {"id"}; - } + if (trackId) + cover = Service::get()->getFromTrack(context.dbSession, *trackId, size); + else if (releaseId) + cover = Service::get()->getFromRelease(context.dbSession, *releaseId, size); response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); response.setMimeType(std::string {cover->getMimeType()}); @@ -1928,7 +1863,7 @@ using CheckImplementedFunc = std::function; struct RequestEntryPointInfo { RequestHandlerFunc func; - EnumSet allowedUserTypes {Database::UserType::DEMO, Database::UserType::REGULAR, Database::UserType::ADMIN}; + EnumSet allowedUserTypes {UserType::DEMO, UserType::REGULAR, UserType::ADMIN}; CheckImplementedFunc checkFunc {}; }; @@ -2019,11 +1954,11 @@ static std::unordered_map requestEntryPoints // User management {"getUser", {handleGetUserRequest}}, - {"getUsers", {handleGetUsersRequest, {Database::UserType::ADMIN}}}, - {"createUser", {handleCreateUserRequest, {Database::UserType::ADMIN}, &checkSetPasswordImplemented}}, - {"updateUser", {handleUpdateUserRequest, {Database::UserType::ADMIN}}}, - {"deleteUser", {handleDeleteUserRequest, {Database::UserType::ADMIN}}}, - {"changePassword", {handleChangePassword, {Database::UserType::REGULAR, Database::UserType::ADMIN}, &checkSetPasswordImplemented}}, + {"getUsers", {handleGetUsersRequest, {UserType::ADMIN}}}, + {"createUser", {handleCreateUserRequest, {UserType::ADMIN}, &checkSetPasswordImplemented}}, + {"updateUser", {handleUpdateUserRequest, {UserType::ADMIN}}}, + {"deleteUser", {handleDeleteUserRequest, {UserType::ADMIN}}}, + {"changePassword", {handleChangePassword, {UserType::REGULAR, UserType::ADMIN}, &checkSetPasswordImplemented}}, // Bookmarks {"getBookmarks", {handleGetBookmarks}}, @@ -2033,8 +1968,8 @@ static std::unordered_map requestEntryPoints {"savePlayQueue", {handleNotImplemented}}, // Media library scanning - {"getScanStatus", {Scan::handleGetScanStatus, {Database::UserType::ADMIN}}}, - {"startScan", {Scan::handleStartScan, {Database::UserType::ADMIN}}}, + {"getScanStatus", {Scan::handleGetScanStatus, {UserType::ADMIN}}}, + {"startScan", {Scan::handleStartScan, {UserType::ADMIN}}}, }; using MediaRetrievalHandlerFunc = std::function; @@ -2047,7 +1982,7 @@ static std::unordered_map mediaRetrieval }; static -Database::IdType +Database::UserId authenticateUser(const Wt::Http::Request &request, const ClientInfo& clientInfo, Session& dbSession) { if (auto *authEnvService {Service<::Auth::IEnvService>::get()}) @@ -2108,7 +2043,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp Session& dbSession {_db.getTLSSession()}; - const Database::IdType userId {authenticateUser(request, clientInfo, dbSession)}; + const Database::UserId userId {authenticateUser(request, clientInfo, dbSession)}; RequestContext requestContext {parameters, dbSession, userId, clientInfo.name}; auto itEntryPoint {requestEntryPoints.find(requestPath)}; diff --git a/src/libs/utils/include/utils/Utils.hpp b/src/libs/utils/include/utils/Utils.hpp index 60cd80fc..ebf1df61 100644 --- a/src/libs/utils/include/utils/Utils.hpp +++ b/src/libs/utils/include/utils/Utils.hpp @@ -19,12 +19,25 @@ #pragma once +#include #include -template> -constexpr T clamp(T v, T lo, T hi, Compare comp = {}) +namespace Utils { - assert(!comp(hi, lo)); - return comp(v, lo) ? lo : comp(hi, v) ? hi : v; -} + template> + constexpr T clamp(T v, T lo, T hi, Compare comp = {}) + { + assert(!comp(hi, lo)); + return comp(v, lo) ? lo : comp(hi, v) ? hi : v; + } + + template + void + push_back_if_not_present(Container& container, const T& val) + { + if (std::find(std::cbegin(container), std::cend(container), val) == std::cend(container)) + container.push_back(val); + } + +} diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index 11a27a0b..5e1ec8ea 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -47,7 +47,7 @@ static const std::string authCookieName {"LmsAuth"}; static void -createAuthToken(Database::IdType userId, const Wt::WDateTime& expiry) +createAuthToken(Database::UserId userId, const Wt::WDateTime& expiry) { const std::string secret {Service<::Auth::IAuthTokenService>::get()->createAuthToken(LmsApp->getDbSession(), userId, expiry)}; @@ -60,7 +60,7 @@ createAuthToken(Database::IdType userId, const Wt::WDateTime& expiry) } -std::optional +std::optional processAuthToken(const Wt::WEnvironment& env) { const std::string* authCookie {env.getCookie(authCookieName)}; @@ -111,7 +111,7 @@ class AuthModel : public Wt::WFormModel Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginNameField).toUTF8())}; user.modify()->setLastLogin(Wt::WDateTime::currentDateTime()); - _userId = user.id(); + _userId = user->getId(); isDemo = user->isDemo(); } @@ -158,11 +158,11 @@ class AuthModel : public Wt::WFormModel return (validation(field).state() == Wt::ValidationState::Valid); } - std::optional getUserId() const { return _userId; } + std::optional getUserId() const { return _userId; } private: - std::optional _userId; + std::optional _userId; }; const AuthModel::Field AuthModel::LoginNameField {"login-name"}; diff --git a/src/lms/ui/Auth.hpp b/src/lms/ui/Auth.hpp index 421271dc..5fda238b 100644 --- a/src/lms/ui/Auth.hpp +++ b/src/lms/ui/Auth.hpp @@ -25,7 +25,7 @@ namespace UserInterface { -std::optional +std::optional processAuthToken(const Wt::WEnvironment& env); class Auth : public Wt::WTemplateFormView @@ -33,7 +33,7 @@ class Auth : public Wt::WTemplateFormView public: Auth(); - Wt::Signal userLoggedIn; + Wt::Signal userLoggedIn; }; } // namespace UserInterface diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 5a766550..a2d0b4e3 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -95,7 +95,7 @@ LmsApplication::getDbSession() return _db.getTLSSession(); } -Wt::Dbo::ptr +Database::User::pointer LmsApplication::getUser() { if (!_authenticatedUser) @@ -104,7 +104,7 @@ LmsApplication::getUser() return Database::User::getById(getDbSession(), _authenticatedUser->userId); } -Database::IdType +Database::UserId LmsApplication::getUserId() { return _authenticatedUser->userId; @@ -135,7 +135,7 @@ LmsApplication::getUserLoginName() LmsApplication::LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager, - std::optional userId) + std::optional userId) : Wt::WApplication {env} , _db {db} , _appManager {appManager} @@ -208,7 +208,7 @@ void LmsApplication::processPasswordAuth() { { - std::optional userId {processAuthToken(environment())}; + std::optional userId {processAuthToken(environment())}; if (userId) { LMS_LOG(UI, DEBUG) << "User authenticated using Auth token!"; @@ -236,7 +236,7 @@ LmsApplication::processPasswordAuth() else { Auth* auth {root()->addNew()}; - auth->userLoggedIn.connect(this, [this](Database::IdType userId) + auth->userLoggedIn.connect(this, [this](Database::UserId userId) { _authenticatedUser = {userId, true}; onUserLoggedIn(); @@ -272,7 +272,7 @@ LmsApplication::createArtistLink(Database::Artist::pointer artist) if (const auto mbid {artist->getMBID()}) return Wt::WLink {Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()}}; else - return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + std::to_string(artist.id())}; + return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString()}; } std::unique_ptr @@ -296,7 +296,7 @@ LmsApplication::createReleaseLink(Database::Release::pointer release) if (const auto mbid {release->getMBID()}) return Wt::WLink {Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()}}; else - return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + std::to_string(release.id())}; + return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + release->getId().toString()}; } std::unique_ptr @@ -320,7 +320,7 @@ LmsApplication::createCluster(Database::Cluster::pointer cluster, bool canDelete { auto getStyleClass = [](const Database::Cluster::pointer cluster) { - switch (cluster->getType().id() % 6) + switch (cluster->getType()->getId().getValue() % 6) { case 0: return "label-info"; case 1: return "label-warning"; @@ -529,7 +529,7 @@ LmsApplication::createHome() mainStack->addNew(); } - explore->tracksAction.connect([this] (PlayQueueAction action, const std::vector& trackIds) + explore->tracksAction.connect([this] (PlayQueueAction action, const std::vector& trackIds) { _playQueue->processTracks(action, trackIds); }); @@ -544,15 +544,15 @@ LmsApplication::createHome() _playQueue->playPrevious(); }); - _mediaPlayer->scrobbleListenNow.connect([this](Database::IdType trackId) + _mediaPlayer->scrobbleListenNow.connect([this](Database::TrackId trackId) { - LMS_LOG(UI, DEBUG) << "Received ScrobbleListenNow from player for trackId = " << trackId; + LMS_LOG(UI, DEBUG) << "Received ScrobbleListenNow from player for trackId = " << trackId.toString(); const Scrobbling::Listen listen {getUserId(), trackId}; Service::get()->listenStarted(listen); }); - _mediaPlayer->scrobbleListenFinished.connect([this](Database::IdType trackId, unsigned durationMs) + _mediaPlayer->scrobbleListenFinished.connect([this](Database::TrackId trackId, unsigned durationMs) { - LMS_LOG(UI, DEBUG) << "Received ScrobbleListenFinished from player for trackId = " << trackId << ", duration = " << (durationMs / 1000) << "s"; + LMS_LOG(UI, DEBUG) << "Received ScrobbleListenFinished from player for trackId = " << trackId.toString() << ", duration = " << (durationMs / 1000) << "s"; const std::chrono::milliseconds duration {durationMs}; const Scrobbling::Listen listen {getUserId(), trackId}; Service::get()->listenFinished(listen, std::chrono::duration_cast(duration)); @@ -563,7 +563,7 @@ LmsApplication::createHome() _playQueue->playNext(); }); - _playQueue->trackSelected.connect([this] (Database::IdType trackId, bool play, float replayGain) + _playQueue->trackSelected.connect([this] (Database::TrackId trackId, bool play, float replayGain) { _mediaPlayer->loadTrack(trackId, play, replayGain); }); diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 5a00c168..7f8f9446 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -51,7 +51,7 @@ class LmsApplication : public Wt::WApplication { public: - LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager, std::optional userId = std::nullopt); + LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager, std::optional userId = std::nullopt); ~LmsApplication(); static std::unique_ptr create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager); @@ -62,8 +62,8 @@ class LmsApplication : public Wt::WApplication std::shared_ptr getCoverResource() { return _coverResource; } Database::Session& getDbSession(); // always thread safe - Wt::Dbo::ptr getUser(); - Database::IdType getUserId(); + Database::ObjectPtr getUser(); + Database::UserId getUserId(); bool isUserAuthStrong() const; // user must be logged in prior this call Database::UserType getUserType(); // user must be logged in prior this call std::string getUserLoginName(); // user must be logged in prior this call @@ -84,11 +84,11 @@ class LmsApplication : public Wt::WApplication }; void notifyMsg(MsgType type, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {4000}); - static Wt::WLink createArtistLink(Wt::Dbo::ptr artist); - static std::unique_ptr createArtistAnchor(Wt::Dbo::ptr artist, bool addText = true); - static Wt::WLink createReleaseLink(Wt::Dbo::ptr release); - static std::unique_ptr createReleaseAnchor(Wt::Dbo::ptr release, bool addText = true); - static std::unique_ptr createCluster(Wt::Dbo::ptr cluster, bool canDelete = false); + static Wt::WLink createArtistLink(Database::ObjectPtr artist); + static std::unique_ptr createArtistAnchor(Database::ObjectPtr artist, bool addText = true); + static Wt::WLink createReleaseLink(Database::ObjectPtr release); + static std::unique_ptr createReleaseAnchor(Database::ObjectPtr release, bool addText = true); + static std::unique_ptr createCluster(Database::ObjectPtr cluster, bool canDelete = false); Wt::WPopupMenu* createPopupMenu(); MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; } @@ -119,7 +119,7 @@ class LmsApplication : public Wt::WApplication Scanner::Events _scannerEvents; struct UserAuthInfo { - Database::IdType userId; + Database::UserId userId; bool strongAuth {}; }; std::optional _authenticatedUser; diff --git a/src/lms/ui/LmsApplicationManager.hpp b/src/lms/ui/LmsApplicationManager.hpp index 2e3459cd..1d7761f7 100644 --- a/src/lms/ui/LmsApplicationManager.hpp +++ b/src/lms/ui/LmsApplicationManager.hpp @@ -43,6 +43,6 @@ namespace UserInterface void unregisterApplication(LmsApplication& application); std::mutex _mutex; - std::unordered_map> m_applications; + std::unordered_map> m_applications; }; } // UserInterface diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 5095c5a7..4ee65e06 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -154,7 +154,7 @@ replayGainPreAmpGainFromString(const std::string& str) if (!value) return std::nullopt; - return clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + return Utils::clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain); } static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings) @@ -233,9 +233,9 @@ MediaPlayer::MediaPlayer() } void -MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain) +MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain) { - LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId; + LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId.toString(); std::ostringstream oss; { @@ -252,7 +252,7 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain) oss << "var params = {" - << " trackId :\"" << trackId << "\"," + << " trackId :\"" << trackId.toString() << "\"," << " nativeResource: \"" << nativeResource << "\"," << " transcodeResource: \"" << transcodeResource << "\"," << " duration: " << std::chrono::duration_cast(track->getDuration()).count() << "," diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index a5be6bd5..d4f1700b 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -94,22 +94,22 @@ class MediaPlayer : public Wt::WTemplate MediaPlayer& operator=(const MediaPlayer&) = delete; MediaPlayer& operator=(MediaPlayer&&) = delete; - std::optional getTrackLoaded() const { return _trackIdLoaded; } + std::optional getTrackLoaded() const { return _trackIdLoaded; } - void loadTrack(Database::IdType trackId, bool play, float replayGain); + void loadTrack(Database::TrackId trackId, bool play, float replayGain); void stop(); std::optional getSettings() const { return _settings; } void setSettings(const Settings& settings); // Signals - Wt::JSignal<> playPrevious; - Wt::JSignal<> playNext; - Wt::Signal trackLoaded; - Wt::Signal<> settingsLoaded; + Wt::JSignal<> playPrevious; + Wt::JSignal<> playNext; + Wt::Signal trackLoaded; + Wt::Signal<> settingsLoaded; - Wt::JSignal scrobbleListenNow; - Wt::JSignal scrobbleListenFinished; + Wt::JSignal scrobbleListenNow; + Wt::JSignal scrobbleListenFinished; Wt::JSignal<> playbackEnded; @@ -117,7 +117,7 @@ class MediaPlayer : public Wt::WTemplate std::unique_ptr _audioFileResource; std::unique_ptr _audioTranscodeResource; - std::optional _trackIdLoaded; + std::optional _trackIdLoaded; std::optional _settings; Wt::JSignal _settingsLoaded; diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index be8c0c82..846069b0 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -23,6 +23,7 @@ #include #include "database/Cluster.hpp" +#include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackList.hpp" @@ -129,7 +130,7 @@ PlayQueue::PlayQueue() if (LmsApp->getUser()->isDemo()) { - LMS_LOG(UI, DEBUG) << "Removing tracklist id " << _tracklistId; + LMS_LOG(UI, DEBUG) << "Removing tracklist id " << _tracklistId.toString(); auto tracklist = Database::TrackList::getById(LmsApp->getDbSession(), _tracklistId); if (tracklist) tracklist.remove(); @@ -167,7 +168,7 @@ PlayQueue::PlayQueue() trackList = Database::TrackList::create(LmsApp->getDbSession(), currentPlayQueueName, Database::TrackList::Type::Internal, false, LmsApp->getUser()); } - _tracklistId = trackList.id(); + _tracklistId = trackList->getId(); } updateInfo(); @@ -226,7 +227,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play) { updateCurrentTrack(false); - Database::IdType trackId {}; + Database::TrackId trackId {}; bool addRadioTrack {}; std::optional replayGain {}; { @@ -253,7 +254,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play) _trackPos = pos; auto track = tracklist->getEntry(*_trackPos)->getTrack(); - trackId = track.id(); + trackId = track->getId(); replayGain = getReplayGain(pos, track); @@ -313,7 +314,7 @@ PlayQueue::updateCurrentTrack(bool selected) } std::size_t -PlayQueue::enqueueTracks(const std::vector& trackIds) +PlayQueue::enqueueTracks(const std::vector& trackIds) { std::size_t nbTracksQueued {}; @@ -323,7 +324,7 @@ PlayQueue::enqueueTracks(const std::vector& trackIds) auto tracklist {getTrackList()}; std::size_t nbTracksToEnqueue {tracklist->getCount() + trackIds.size() > _nbMaxEntries ? _nbMaxEntries - tracklist->getCount() : trackIds.size()}; - for (Database::IdType trackId : trackIds) + for (const Database::TrackId trackId : trackIds) { Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; if (!track) @@ -344,7 +345,7 @@ PlayQueue::enqueueTracks(const std::vector& trackIds) } void -PlayQueue::processTracks(PlayQueueAction action, const std::vector& trackIds) +PlayQueue::processTracks(PlayQueueAction action, const std::vector& trackIds) { std::size_t nbAddedTracks {}; @@ -367,7 +368,7 @@ PlayQueue::processTracks(PlayQueueAction action, const std::vector shuffledTrackIds {trackIds}; + std::vector shuffledTrackIds {trackIds}; Random::shuffleContainer(shuffledTrackIds); nbAddedTracks = enqueueTracks(shuffledTrackIds); } @@ -402,9 +403,9 @@ PlayQueue::addSome() void PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) { - const auto tracklistEntryId {tracklistEntry.id()}; + const Database::TrackListEntryId tracklistEntryId {tracklistEntry->getId()}; const auto track {tracklistEntry->getTrack()}; - const Database::IdType trackId {track->id()}; + const Database::TrackId trackId {track->getId()}; Wt::WTemplate* entry = _entriesContainer->addNew(Wt::WString::tr("Lms.PlayQueue.template.entry")); @@ -434,7 +435,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) { Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false)); auto cover = std::make_unique(); - cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release.id(), CoverResource::Size::Large)); + cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release->getId(), CoverResource::Size::Large)); cover->setStyleClass("Lms-cover"); cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); anchor->setImage(std::move(cover)); @@ -443,7 +444,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) else { auto cover = entry->bindNew("cover"); - cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(track.id(), CoverResource::Size::Large)); + cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(track->getId(), CoverResource::Size::Large)); cover->setStyleClass("Lms-cover"); cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); } @@ -520,7 +521,7 @@ PlayQueue::enqueueRadioTracks() { const auto similarTrackIds {Service::get()->getSimilarTracksFromTrackList(LmsApp->getDbSession(), _tracklistId, 3)}; - std::vector trackToAddIds(std::cbegin(similarTrackIds), std::cend(similarTrackIds)); + std::vector trackToAddIds(std::cbegin(similarTrackIds), std::cend(similarTrackIds)); Random::shuffleContainer(trackToAddIds); enqueueTracks(trackToAddIds); } diff --git a/src/lms/ui/PlayQueue.hpp b/src/lms/ui/PlayQueue.hpp index 77b10afa..1bbb996a 100644 --- a/src/lms/ui/PlayQueue.hpp +++ b/src/lms/ui/PlayQueue.hpp @@ -49,7 +49,7 @@ class PlayQueue : public Wt::WTemplate public: PlayQueue(); - void processTracks(PlayQueueAction action, const std::vector& trackIds); + void processTracks(PlayQueueAction action, const std::vector& trackIds); // play the next track in the queue void playNext(); @@ -58,19 +58,19 @@ class PlayQueue : public Wt::WTemplate void playPrevious(); // Signal emitted when a track is to be load(and optionally played) - Wt::Signal trackSelected; + Wt::Signal trackSelected; // Signal emitted when track is unselected (has to be stopped) Wt::Signal<> trackUnselected; private: - Wt::Dbo::ptr getTrackList() const; + Database::ObjectPtr getTrackList() const; bool isFull() const; void clearTracks(); - std::size_t enqueueTracks(const std::vector& trackIds); + std::size_t enqueueTracks(const std::vector& trackIds); void addSome(); - void addEntry(const Wt::Dbo::ptr& entry); + void addEntry(const Database::ObjectPtr& entry); void enqueueRadioTracks(); void updateInfo(); void updateCurrentTrack(bool selected); @@ -82,7 +82,7 @@ class PlayQueue : public Wt::WTemplate void addRadioTrackFromSimilarity(std::shared_ptr similarityFinder); void addRadioTrackFromClusters(); - std::optional getReplayGain(std::size_t pos, const Wt::Dbo::ptr& track) const; + std::optional getReplayGain(std::size_t pos, const Database::ObjectPtr& track) const; static inline constexpr std::size_t _nbMaxEntries {1000}; static inline constexpr std::size_t _batchSize {12}; @@ -90,7 +90,7 @@ class PlayQueue : public Wt::WTemplate bool _repeatAll {}; bool _radioMode {}; bool _mediaPlayerSettingsLoaded {}; - Database::IdType _tracklistId {}; + Database::TrackListId _tracklistId {}; InfiniteScrollingContainer* _entriesContainer {}; Wt::WText* _nbTracks {}; Wt::WText* _repeatBtn {}; diff --git a/src/lms/ui/PlayQueueAction.hpp b/src/lms/ui/PlayQueueAction.hpp index 29a91351..fffd8133 100644 --- a/src/lms/ui/PlayQueueAction.hpp +++ b/src/lms/ui/PlayQueueAction.hpp @@ -34,7 +34,8 @@ namespace UserInterface PlayShuffled, }; - using PlayQueueActionSignal = Wt::Signal&>; - + using PlayQueueActionArtistSignal = Wt::Signal&>; + using PlayQueueActionReleaseSignal = Wt::Signal&>; + using PlayQueueActionTrackSignal = Wt::Signal&>; } diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index fc446feb..1f25251e 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -197,7 +197,7 @@ class SettingsModel : public Wt::WFormModel if (_authPasswordService && !valueText(PasswordField).empty()) { - _authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8()); + _authPasswordService->setPassword(LmsApp->getDbSession(), user->getId(), valueText(PasswordField).toUTF8()); } } diff --git a/src/lms/ui/admin/InitWizardView.cpp b/src/lms/ui/admin/InitWizardView.cpp index c353dbf0..b187f472 100644 --- a/src/lms/ui/admin/InitWizardView.cpp +++ b/src/lms/ui/admin/InitWizardView.cpp @@ -70,7 +70,7 @@ class InitWizardModel : public Wt::WFormModel Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8())}; user.modify()->setType(Database::UserType::ADMIN); - Service<::Auth::IPasswordService>::get()->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8()); + Service<::Auth::IPasswordService>::get()->setPassword(LmsApp->getDbSession(), user->getId(), valueText(PasswordField).toUTF8()); } bool validateField(Field field) diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index e0467939..f27901b9 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -53,7 +53,7 @@ class UserModel : public Wt::WFormModel static inline const Field PasswordField {"password"}; static inline const Field DemoField {"demo"}; - UserModel(std::optional userId, ::Auth::IPasswordService* authPasswordService) + UserModel(std::optional userId, ::Auth::IPasswordService* authPasswordService) : _userId {userId} , _authPasswordService {authPasswordService} { @@ -87,7 +87,7 @@ class UserModel : public Wt::WFormModel throw UserNotFoundException {}; if (_authPasswordService && !valueText(PasswordField).empty()) - _authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8()); + _authPasswordService->setPassword(LmsApp->getDbSession(), user->getId(), valueText(PasswordField).toUTF8()); } else { @@ -103,7 +103,7 @@ class UserModel : public Wt::WFormModel user.modify()->setType(Database::UserType::DEMO); if (_authPasswordService) - _authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8()); + _authPasswordService->setPassword(LmsApp->getDbSession(), user->getId(), valueText(PasswordField).toUTF8()); } } @@ -176,7 +176,7 @@ class UserModel : public Wt::WFormModel return false; } - std::optional _userId; + std::optional _userId; ::Auth::IPasswordService* _authPasswordService {}; }; @@ -196,7 +196,7 @@ UserView::refreshView() if (!wApp->internalPathMatches("/admin/user")) return; - auto userId = StringUtils::readAs(wApp->internalPathNextPart("/admin/user/")); + const std::optional userId {StringUtils::readAs(wApp->internalPathNextPart("/admin/user/"))}; clear(); diff --git a/src/lms/ui/admin/UsersView.cpp b/src/lms/ui/admin/UsersView.cpp index a4fc63ca..50bc58e7 100644 --- a/src/lms/ui/admin/UsersView.cpp +++ b/src/lms/ui/admin/UsersView.cpp @@ -72,7 +72,7 @@ UsersView::refreshView() auto users = Database::User::getAll(LmsApp->getDbSession()); for (const auto& user : users) { - const Database::IdType userId {user.id()}; + const Database::UserId userId {user->getId()}; Wt::WTemplate* entry {_container->addNew(Wt::WString::tr("Lms.Admin.Users.template.entry"))}; @@ -93,7 +93,7 @@ UsersView::refreshView() Wt::WPushButton* editBtn = entry->bindNew("edit-btn", Wt::WString::tr("Lms.Admin.Users.edit")); editBtn->clicked().connect([=]() { - LmsApp->setInternalPath("/admin/user/" + std::to_string(userId), true); + LmsApp->setInternalPath("/admin/user/" + userId.toString(), true); }); Wt::WPushButton* delBtn = entry->bindNew("del-btn", Wt::WString::tr("Lms.Admin.Users.del")); diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp index 45ebf810..49e19731 100644 --- a/src/lms/ui/explore/ArtistCollector.cpp +++ b/src/lms/ui/explore/ArtistCollector.cpp @@ -31,7 +31,7 @@ namespace UserInterface { using namespace Database; - std::vector> + std::vector> ArtistCollector::get(std::optional range, bool& moreResults) { range = getActualRange(range); diff --git a/src/lms/ui/explore/ArtistCollector.hpp b/src/lms/ui/explore/ArtistCollector.hpp index b7bb04d7..05734af0 100644 --- a/src/lms/ui/explore/ArtistCollector.hpp +++ b/src/lms/ui/explore/ArtistCollector.hpp @@ -38,13 +38,13 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - std::vector> get(std::optional range, bool& moreResults); + std::vector> get(std::optional range, bool& moreResults); void reset() { _randomArtists.clear(); } void setArtistLinkType(std::optional linkType) { _linkType = linkType; } private: - std::vector> getRandomArtists(std::optional range, bool& moreResults); - std::vector _randomArtists; + std::vector> getRandomArtists(std::optional range, bool& moreResults); + std::vector _randomArtists; std::optional _linkType; }; } // ns UserInterface diff --git a/src/lms/ui/explore/ArtistListHelpers.cpp b/src/lms/ui/explore/ArtistListHelpers.cpp index a19906ae..85adb362 100644 --- a/src/lms/ui/explore/ArtistListHelpers.cpp +++ b/src/lms/ui/explore/ArtistListHelpers.cpp @@ -26,7 +26,7 @@ namespace UserInterface::ArtistListHelpers { std::unique_ptr - createEntry(const Wt::Dbo::ptr& artist) + createEntry(const Database::ObjectPtr& artist) { auto res {std::make_unique(Wt::WString::tr("Lms.Explore.Artists.template.entry"))}; res->bindWidget("name", LmsApplication::createArtistAnchor(artist)); @@ -35,7 +35,7 @@ namespace UserInterface::ArtistListHelpers } std::unique_ptr - createEntrySmall(const Wt::Dbo::ptr& artist) + createEntrySmall(const Database::ObjectPtr& artist) { auto res {std::make_unique(Wt::WString::tr("Lms.Explore.Artists.template.entry-small"))}; res->bindWidget("name", LmsApplication::createArtistAnchor(artist)); diff --git a/src/lms/ui/explore/ArtistListHelpers.hpp b/src/lms/ui/explore/ArtistListHelpers.hpp index 22dc67d8..02659bf7 100644 --- a/src/lms/ui/explore/ArtistListHelpers.hpp +++ b/src/lms/ui/explore/ArtistListHelpers.hpp @@ -32,7 +32,7 @@ namespace Database namespace UserInterface::ArtistListHelpers { - std::unique_ptr createEntry(const Wt::Dbo::ptr& artist); - std::unique_ptr createEntrySmall(const Wt::Dbo::ptr& artist); + std::unique_ptr createEntry(const Database::ObjectPtr& artist); + std::unique_ptr createEntrySmall(const Database::ObjectPtr& artist); } diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 79941fc1..0e803db5 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -26,6 +26,7 @@ #include #include "database/Artist.hpp" +#include "database/Cluster.hpp" #include "database/Release.hpp" #include "database/ScanSettings.hpp" #include "database/Session.hpp" @@ -68,7 +69,7 @@ Artist::Artist(Filters* filters) } static -std::optional +std::optional extractArtistIdFromInternalPath() { if (wApp->internalPathMatches("/artist/mbid/")) @@ -78,13 +79,13 @@ extractArtistIdFromInternalPath() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; if (const Database::Artist::pointer artist {Database::Artist::getByMBID(LmsApp->getDbSession(), *mbid)}) - return artist.id(); + return artist->getId(); } return std::nullopt; } - return StringUtils::readAs(wApp->internalPathNextPart("/artist/")); + return StringUtils::readAs(wApp->internalPathNextPart("/artist/")); } void @@ -129,7 +130,7 @@ Artist::refreshView() { for (auto cluster : clusters) { - auto clusterId = cluster.id(); + auto clusterId = cluster->getId(); auto entry = clusterContainers->addWidget(LmsApp->createCluster(cluster)); entry->clicked().connect([=] { @@ -197,7 +198,7 @@ Artist::refreshView() } void -Artist::refreshReleases(const Wt::Dbo::ptr& artist) +Artist::refreshReleases(const Database::ObjectPtr& artist) { const auto releases {artist->getReleases(_filters->getClusterIds())}; if (releases.empty()) @@ -213,7 +214,7 @@ Artist::refreshReleases(const Wt::Dbo::ptr& artist) } void -Artist::refreshNonReleaseTracks(const Wt::Dbo::ptr& artist) +Artist::refreshNonReleaseTracks(const Database::ObjectPtr& artist) { if (!artist->hasNonReleaseTracks()) return; @@ -229,7 +230,7 @@ Artist::refreshNonReleaseTracks(const Wt::Dbo::ptr& artist) } void -Artist::refreshSimilarArtists(const std::unordered_set& similarArtistsId) +Artist::refreshSimilarArtists(const std::vector& similarArtistsId) { if (similarArtistsId.empty()) return; @@ -237,7 +238,7 @@ Artist::refreshSimilarArtists(const std::unordered_set& simila setCondition("if-has-similar-artists", true); Wt::WContainerWidget* similarArtistsContainer {bindNew("similar-artists")}; - for (Database::IdType artistId : similarArtistsId) + for (const Database::ArtistId artistId : similarArtistsId) { const Database::Artist::pointer similarArtist{Database::Artist::getById(LmsApp->getDbSession(), artistId)}; if (!similarArtist) diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index 2d5ce695..18b3894c 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -45,15 +45,15 @@ namespace UserInterface public: Artist(Filters* filters); - PlayQueueActionSignal artistsAction; - PlayQueueActionSignal tracksAction; + PlayQueueActionArtistSignal artistsAction; + PlayQueueActionTrackSignal tracksAction; private: void refreshView(); - void refreshReleases(const Wt::Dbo::ptr& artist); - void refreshNonReleaseTracks(const Wt::Dbo::ptr& artist); - void refreshSimilarArtists(const std::unordered_set& similarArtistsId); - void refreshLinks(const Wt::Dbo::ptr& artist); + void refreshReleases(const Database::ObjectPtr& artist); + void refreshNonReleaseTracks(const Database::ObjectPtr& artist); + void refreshSimilarArtists(const std::vector& similarArtistsId); + void refreshLinks(const Database::ObjectPtr& artist); void addSomeNonReleaseTracks(); static constexpr std::size_t _tracksBatchSize {6}; @@ -61,7 +61,7 @@ namespace UserInterface Filters* _filters {}; InfiniteScrollingContainer* _trackContainer {}; - Database::IdType _artistId {}; + Database::ArtistId _artistId {}; }; } // namespace UserInterface diff --git a/src/lms/ui/explore/Explore.cpp b/src/lms/ui/explore/Explore.cpp index de7a1f6f..8c821fdb 100644 --- a/src/lms/ui/explore/Explore.cpp +++ b/src/lms/ui/explore/Explore.cpp @@ -129,16 +129,16 @@ Explore::search(const Wt::WString& searchText) } static -std::vector -getArtistsTracks(Database::Session& session, const std::vector& artistsId, const std::set&) +std::vector +getArtistsTracks(Database::Session& session, const std::vector& artistsId, const std::vector&) { - std::vector res; + std::vector res; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - for (Database::IdType artistId : artistsId) + for (const Database::ArtistId artistId : artistsId) { - Database::Artist::pointer artist {Database::Artist::getById(session, artistId)}; + const Database::Artist::pointer artist {Database::Artist::getById(session, artistId)}; if (!artist) continue; @@ -146,49 +146,49 @@ getArtistsTracks(Database::Session& session, const std::vector const std::vector tracks {artist->getTracks()}; res.reserve(res.size() + tracks.size()); - std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Database::Track::pointer& track) { return track.id(); }); + std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Database::Track::pointer& track) { return track->getId(); }); } return res; } static -std::vector -getReleasesTracks(Database::Session& session, const std::vector& releasesId, const std::set& clusters) +std::vector +getReleasesTracks(Database::Session& session, const std::vector& releasesId, const std::vector& clusters) { - std::vector res; + std::vector res; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - for (Database::IdType releaseId : releasesId) + for (const Database::ReleaseId releaseId : releasesId) { - Database::Release::pointer release {Database::Release::getById(session, releaseId)}; + const Database::Release::pointer release {Database::Release::getById(session, releaseId)}; if (!release) continue; const std::vector tracks {release->getTracks(clusters)}; res.reserve(res.size() + tracks.size()); - std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Database::Track::pointer& track) { return track.id(); }); + std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Database::Track::pointer& track) { return track->getId(); }); } return res; } void -Explore::handleArtistsAction(PlayQueueAction action, const std::vector& artistsId) +Explore::handleArtistsAction(PlayQueueAction action, const std::vector& artistsId) { tracksAction.emit(action, getArtistsTracks(LmsApp->getDbSession(), artistsId, _filters->getClusterIds())); } void -Explore::handleReleasesAction(PlayQueueAction action, const std::vector& releasesId) +Explore::handleReleasesAction(PlayQueueAction action, const std::vector& releasesId) { tracksAction.emit(action, getReleasesTracks(LmsApp->getDbSession(), releasesId, _filters->getClusterIds())); } void -Explore::handleTracksAction(PlayQueueAction action, const std::vector& tracksId) +Explore::handleTracksAction(PlayQueueAction action, const std::vector& tracksId) { tracksAction.emit(action, tracksId); } diff --git a/src/lms/ui/explore/Explore.hpp b/src/lms/ui/explore/Explore.hpp index 2db65540..66815002 100644 --- a/src/lms/ui/explore/Explore.hpp +++ b/src/lms/ui/explore/Explore.hpp @@ -36,13 +36,13 @@ class Explore : public Wt::WTemplate void search(const Wt::WString& searchText); - PlayQueueActionSignal tracksAction; + PlayQueueActionTrackSignal tracksAction; private: - void handleArtistsAction(PlayQueueAction action, const std::vector& artistsId); - void handleReleasesAction(PlayQueueAction action, const std::vector& releasesId); - void handleTracksAction(PlayQueueAction action, const std::vector& tracksId); + void handleArtistsAction(PlayQueueAction action, const std::vector& artistsId); + void handleReleasesAction(PlayQueueAction action, const std::vector& releasesId); + void handleTracksAction(PlayQueueAction action, const std::vector& tracksId); Filters* _filters {}; SearchView* _search {}; diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index 0796ab01..f8ef1c74 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -58,12 +58,10 @@ Filters::showDialog() if (!types.empty()) { - const auto values {types.front()->getClusters()}; - - for (const Database::Cluster::pointer& value : values) + for (const Database::Cluster::pointer& cluster : types.front()->getClusters()) { - if (_filterIds.find(value.id()) == _filterIds.end()) - valueCombo->addItem(Wt::WString::fromUTF8(value->getName())); + if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) + valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName())); } } } @@ -78,11 +76,10 @@ Filters::showDialog() auto clusterType = Database::ClusterType::getByName(LmsApp->getDbSession(), name); - const auto values = clusterType->getClusters(); - for (const Database::Cluster::pointer& value : values) + for (const Database::Cluster::pointer& cluster : clusterType->getClusters()) { - if (_filterIds.find(value.id()) == _filterIds.end()) - valueCombo->addItem(Wt::WString::fromUTF8(value->getName())); + if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) + valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName())); } }); @@ -101,7 +98,7 @@ Filters::showDialog() const std::string value {valueCombo->valueText().toUTF8()}; // TODO use a model to store the cluster.id() values - Database::IdType clusterId {}; + Database::ClusterId clusterId {}; { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; @@ -114,7 +111,7 @@ Filters::showDialog() if (!cluster) return; - clusterId = cluster.id(); + clusterId = cluster->getId(); } add(clusterId); @@ -124,7 +121,7 @@ Filters::showDialog() } void -Filters::add(Database::IdType clusterId) +Filters::add(Database::ClusterId clusterId) { Wt::WInteractWidget* filter {}; @@ -136,17 +133,18 @@ Filters::add(Database::IdType clusterId) if (!cluster) return; - auto res {_filterIds.insert(clusterId)}; - if (!res.second) + if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), clusterId) != std::cend(_clusterIds)) return; + _clusterIds.push_back(clusterId); + filter = _filters->addWidget(LmsApp->createCluster(cluster, true)); } filter->clicked().connect([=] { _filters->removeWidget(filter); - _filterIds.erase(clusterId); + _clusterIds.erase(std::remove_if(std::begin(_clusterIds), std::end(_clusterIds), [clusterId](Database::ClusterId id) { return id == clusterId; }), std::end(_clusterIds)); _sigUpdated.emit(); }); diff --git a/src/lms/ui/explore/Filters.hpp b/src/lms/ui/explore/Filters.hpp index 845f0305..079d52c2 100644 --- a/src/lms/ui/explore/Filters.hpp +++ b/src/lms/ui/explore/Filters.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include #include @@ -34,9 +35,9 @@ class Filters : public Wt::WTemplate public: Filters(); - void add(Database::IdType clusterId); + void add(Database::ClusterId clusterId); - std::set getClusterIds() const { return _filterIds; } + const std::vector& getClusterIds() const { return _clusterIds; } Wt::Signal<>& updated() { return _sigUpdated; } @@ -46,7 +47,7 @@ class Filters : public Wt::WTemplate Wt::WContainerWidget *_filters; Wt::Signal<> _sigUpdated; - std::set _filterIds; + std::vector _clusterIds; }; } // namespace UserInterface diff --git a/src/lms/ui/explore/ReleaseCollector.cpp b/src/lms/ui/explore/ReleaseCollector.cpp index 552f2e66..473278df 100644 --- a/src/lms/ui/explore/ReleaseCollector.cpp +++ b/src/lms/ui/explore/ReleaseCollector.cpp @@ -77,7 +77,7 @@ namespace UserInterface return releases; } - std::vector + std::vector ReleaseCollector::getAll() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; @@ -85,9 +85,9 @@ namespace UserInterface bool moreResults; const auto releases {get(std::nullopt, moreResults)}; - std::vector res; + std::vector res; res.reserve(releases.size()); - std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const Release::pointer& release) { return release.id(); }); + std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const Release::pointer& release) { return release->getId(); }); return res; } diff --git a/src/lms/ui/explore/ReleaseCollector.hpp b/src/lms/ui/explore/ReleaseCollector.hpp index 84b57fcb..3882cc80 100644 --- a/src/lms/ui/explore/ReleaseCollector.hpp +++ b/src/lms/ui/explore/ReleaseCollector.hpp @@ -35,13 +35,13 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - std::vector> get(std::optional range, bool& moreResults); - std::vector getAll(); + std::vector> get(std::optional range, bool& moreResults); + std::vector getAll(); void reset() { _randomReleases.clear(); } private: - std::vector> getRandomReleases(std::optional range, bool& moreResults); - std::vector _randomReleases; + std::vector> getRandomReleases(std::optional range, bool& moreResults); + std::vector _randomReleases; }; } // ns UserInterface diff --git a/src/lms/ui/explore/ReleaseListHelpers.cpp b/src/lms/ui/explore/ReleaseListHelpers.cpp index eb1984cc..71c5daa5 100644 --- a/src/lms/ui/explore/ReleaseListHelpers.cpp +++ b/src/lms/ui/explore/ReleaseListHelpers.cpp @@ -45,7 +45,7 @@ namespace UserInterface::ReleaseListHelpers Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false)); auto cover = std::make_unique(); - cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release.id(), CoverResource::Size::Large)); + cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release->getId(), CoverResource::Size::Large)); cover->setStyleClass("Lms-cover"); cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); anchor->setImage(std::move(cover)); @@ -100,7 +100,7 @@ namespace UserInterface::ReleaseListHelpers } std::unique_ptr - createEntryForArtist(const Wt::Dbo::ptr& release, const Wt::Dbo::ptr& artist) + createEntryForArtist(const Database::Release::pointer& release, const Database::Artist::pointer& artist) { return createEntry(release, artist, true); } diff --git a/src/lms/ui/explore/ReleaseListHelpers.hpp b/src/lms/ui/explore/ReleaseListHelpers.hpp index 31a936e6..3aad6698 100644 --- a/src/lms/ui/explore/ReleaseListHelpers.hpp +++ b/src/lms/ui/explore/ReleaseListHelpers.hpp @@ -22,6 +22,7 @@ #include #include +#include "database/Types.hpp" namespace Database { @@ -31,7 +32,7 @@ namespace Database namespace UserInterface::ReleaseListHelpers { - std::unique_ptr createEntry(const Wt::Dbo::ptr& release); - std::unique_ptr createEntryForArtist(const Wt::Dbo::ptr& release, const Wt::Dbo::ptr& artist); + std::unique_ptr createEntry(const Database::ObjectPtr& release); + std::unique_ptr createEntryForArtist(const Database::ObjectPtr& release, const Database::ObjectPtr& artist); } // namespace UserInterface diff --git a/src/lms/ui/explore/ReleasePopup.cpp b/src/lms/ui/explore/ReleasePopup.cpp index 79617838..7224683f 100644 --- a/src/lms/ui/explore/ReleasePopup.cpp +++ b/src/lms/ui/explore/ReleasePopup.cpp @@ -32,8 +32,8 @@ namespace UserInterface void displayReleasePopupMenu(Wt::WInteractWidget& target, - Database::IdType releaseId, - PlayQueueActionSignal& releasesAction) + Database::ReleaseId releaseId, + PlayQueueActionReleaseSignal& releasesAction) { Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; diff --git a/src/lms/ui/explore/ReleasePopup.hpp b/src/lms/ui/explore/ReleasePopup.hpp index 23e576b2..5ffb4f48 100644 --- a/src/lms/ui/explore/ReleasePopup.hpp +++ b/src/lms/ui/explore/ReleasePopup.hpp @@ -28,7 +28,7 @@ namespace UserInterface { void displayReleasePopupMenu(Wt::WInteractWidget& target, - Database::IdType releaseId, - PlayQueueActionSignal& releasesAction); + Database::ReleaseId releaseId, + PlayQueueActionReleaseSignal& releasesAction); } // namespace UserInterface diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index cc6d0f23..ceb82d90 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -25,6 +25,7 @@ #include #include +#include "database/Cluster.hpp" #include "database/Release.hpp" #include "database/ScanSettings.hpp" #include "database/Session.hpp" @@ -68,7 +69,7 @@ Release::Release(Filters* filters) } static -std::optional +std::optional extractReleaseIdFromInternalPath() { if (wApp->internalPathMatches("/release/mbid/")) @@ -78,13 +79,13 @@ extractReleaseIdFromInternalPath() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; if (const Database::Release::pointer release {Database::Release::getByMBID(LmsApp->getDbSession(), *mbid)}) - return release.id(); + return release->getId(); } return std::nullopt; } - return StringUtils::readAs(wApp->internalPathNextPart("/release/")); + return StringUtils::readAs(wApp->internalPathNextPart("/release/")); } @@ -131,7 +132,7 @@ Release::refreshView() refreshReleaseArtists(release); { - Wt::WImage* cover {bindNew("cover", Wt::WLink(LmsApp->getCoverResource()->getReleaseUrl(release.id(), CoverResource::Size::Large)))}; + Wt::WImage* cover {bindNew("cover", Wt::WLink(LmsApp->getCoverResource()->getReleaseUrl(release->getId(), CoverResource::Size::Large)))}; cover->setStyleClass("Lms-cover-large"); cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); } @@ -145,7 +146,7 @@ Release::refreshView() { for (const auto& cluster : clusters) { - auto clusterId {cluster.id()}; + const ClusterId clusterId {cluster->getId()}; auto entry {clusterContainers->addWidget(LmsApp->createCluster(cluster))}; entry->clicked().connect([=] { @@ -205,7 +206,7 @@ Release::refreshView() for (const auto& track : tracks) { - auto trackId {track.id()}; + auto trackId {track->getId()}; const auto discNumber {track->getDiscNumber()}; @@ -251,7 +252,7 @@ Release::refreshView() entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); - LmsApp->getMediaPlayer().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId) + LmsApp->getMediaPlayer().trackLoaded.connect(entry, [=] (Database::TrackId loadedTrackId) { entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : ""); }); @@ -269,7 +270,7 @@ Release::refreshView() void Release::refreshReleaseArtists(const Database::Release::pointer& release) { - std::vector> artists; + std::vector> artists; artists = release->getReleaseArtists(); if (artists.empty()) @@ -339,7 +340,7 @@ Release::refreshLinks(const Database::Release::pointer& release) } void -Release::refreshSimilarReleases(const std::unordered_set& similarReleasesId) +Release::refreshSimilarReleases(const std::vector& similarReleasesId) { if (similarReleasesId.empty()) return; @@ -347,9 +348,9 @@ Release::refreshSimilarReleases(const std::unordered_set& simi setCondition("if-has-similar-releases", true); auto* similarReleasesContainer {bindNew("similar-releases")}; - for (Database::IdType id : similarReleasesId) + for (const Database::ReleaseId id : similarReleasesId) { - Database::Release::pointer similarRelease{Database::Release::getById(LmsApp->getDbSession(), id)}; + const Database::Release::pointer similarRelease{Database::Release::getById(LmsApp->getDbSession(), id)}; if (!similarRelease) continue; diff --git a/src/lms/ui/explore/ReleaseView.hpp b/src/lms/ui/explore/ReleaseView.hpp index 037d6d5c..d3700eaa 100644 --- a/src/lms/ui/explore/ReleaseView.hpp +++ b/src/lms/ui/explore/ReleaseView.hpp @@ -39,15 +39,15 @@ namespace UserInterface public: Release(Filters* filters); - PlayQueueActionSignal releasesAction; - PlayQueueActionSignal tracksAction; + PlayQueueActionReleaseSignal releasesAction; + PlayQueueActionTrackSignal tracksAction; private: void refreshView(); - void refreshReleaseArtists(const Wt::Dbo::ptr& release); - void refreshCopyright(const Wt::Dbo::ptr& release); - void refreshLinks(const Wt::Dbo::ptr& release); - void refreshSimilarReleases(const std::unordered_set& similarReleasesId); + void refreshReleaseArtists(const Database::ObjectPtr& release); + void refreshCopyright(const Database::ObjectPtr& release); + void refreshLinks(const Database::ObjectPtr& release); + void refreshSimilarReleases(const std::vector& similarReleasesId); Filters* _filters {}; }; diff --git a/src/lms/ui/explore/ReleasesView.cpp b/src/lms/ui/explore/ReleasesView.cpp index d2040366..93c56428 100644 --- a/src/lms/ui/explore/ReleasesView.cpp +++ b/src/lms/ui/explore/ReleasesView.cpp @@ -128,7 +128,7 @@ Releases::addSome() _container->setHasMore(moreResults); } -std::vector +std::vector Releases::getAllReleases() { return _releaseCollector.getAll(); diff --git a/src/lms/ui/explore/ReleasesView.hpp b/src/lms/ui/explore/ReleasesView.hpp index 31f1d17e..f2a9b91b 100644 --- a/src/lms/ui/explore/ReleasesView.hpp +++ b/src/lms/ui/explore/ReleasesView.hpp @@ -38,7 +38,7 @@ namespace UserInterface public: Releases(Filters& filters); - PlayQueueActionSignal releasesAction; + PlayQueueActionReleaseSignal releasesAction; private: @@ -46,7 +46,7 @@ namespace UserInterface void refreshView(ReleaseCollector::Mode mode); void addSome(); - std::vector getAllReleases(); + std::vector getAllReleases(); static constexpr std::size_t _maxItemsPerLine {6}; static constexpr std::size_t _batchSize {_maxItemsPerLine}; diff --git a/src/lms/ui/explore/SearchView.hpp b/src/lms/ui/explore/SearchView.hpp index e7a802b4..f75bfd37 100644 --- a/src/lms/ui/explore/SearchView.hpp +++ b/src/lms/ui/explore/SearchView.hpp @@ -44,7 +44,7 @@ namespace UserInterface public: SearchView(Filters* filters); - PlayQueueActionSignal tracksAction; + PlayQueueActionTrackSignal tracksAction; void refreshView(const Wt::WString& searchText); diff --git a/src/lms/ui/explore/TrackCollector.cpp b/src/lms/ui/explore/TrackCollector.cpp index ffcebf4c..26495626 100644 --- a/src/lms/ui/explore/TrackCollector.cpp +++ b/src/lms/ui/explore/TrackCollector.cpp @@ -77,17 +77,17 @@ namespace UserInterface return releases; } - std::vector + std::vector TrackCollector::getAll() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; bool moreResults; - const auto releases {get(std::nullopt, moreResults)}; + const auto tracks {get(std::nullopt, moreResults)}; - std::vector res; - res.reserve(releases.size()); - std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const Track::pointer& release) { return release.id(); }); + std::vector res; + res.reserve(tracks.size()); + std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Track::pointer& track) { return track->getId(); }); return res; } diff --git a/src/lms/ui/explore/TrackCollector.hpp b/src/lms/ui/explore/TrackCollector.hpp index 11a7c7f7..79011adf 100644 --- a/src/lms/ui/explore/TrackCollector.hpp +++ b/src/lms/ui/explore/TrackCollector.hpp @@ -35,13 +35,13 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - std::vector> get(std::optional range, bool& moreResults); - std::vector getAll(); + std::vector> get(std::optional range, bool& moreResults); + std::vector getAll(); void reset() { _randomTracks.clear(); } private: - std::vector> getRandomTracks(std::optional range, bool& moreResults); - std::vector _randomTracks; + std::vector> getRandomTracks(std::optional range, bool& moreResults); + std::vector _randomTracks; }; } // ns UserInterface diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 0fc280fb..ba36cb9d 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -39,7 +39,7 @@ using namespace Database; namespace UserInterface::TrackListHelpers { std::unique_ptr - createEntry(const Wt::Dbo::ptr& track, PlayQueueActionSignal& tracksAction) + createEntry(const Database::ObjectPtr& track, PlayQueueActionTrackSignal& tracksAction) { auto entry {std::make_unique(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))}; auto* entryPtr {entry.get()}; @@ -49,7 +49,7 @@ namespace UserInterface::TrackListHelpers const auto artists {track->getArtists({TrackArtistLinkType::Artist})}; const Release::pointer release {track->getRelease()}; - const IdType trackId {track.id()}; + const TrackId trackId {track->getId()}; if (!artists.empty() || release) entry->setCondition("if-has-artists-or-release", true); @@ -73,7 +73,7 @@ namespace UserInterface::TrackListHelpers { Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))}; auto cover {std::make_unique()}; - cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release.id(), CoverResource::Size::Large)); + cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release->getId(), CoverResource::Size::Large)); cover->setStyleClass("Lms-cover"); cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); anchor->setImage(std::move(cover)); @@ -101,7 +101,7 @@ namespace UserInterface::TrackListHelpers displayTrackPopupMenu(*moreBtn, trackId, tracksAction); }); - LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::IdType loadedTrackId) + LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::TrackId loadedTrackId) { entryPtr->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : ""); }); diff --git a/src/lms/ui/explore/TrackListHelpers.hpp b/src/lms/ui/explore/TrackListHelpers.hpp index 32ac5b12..2e3ea306 100644 --- a/src/lms/ui/explore/TrackListHelpers.hpp +++ b/src/lms/ui/explore/TrackListHelpers.hpp @@ -22,6 +22,7 @@ #include #include +#include "database/Types.hpp" #include "PlayQueueAction.hpp" namespace Database @@ -31,6 +32,6 @@ namespace Database namespace UserInterface::TrackListHelpers { - std::unique_ptr createEntry(const Wt::Dbo::ptr& track, PlayQueueActionSignal& tracksAction); + std::unique_ptr createEntry(const Database::ObjectPtr& track, PlayQueueActionTrackSignal& tracksAction); } // namespace UserInterface diff --git a/src/lms/ui/explore/TrackPopup.cpp b/src/lms/ui/explore/TrackPopup.cpp index ff1bbd5f..8884347c 100644 --- a/src/lms/ui/explore/TrackPopup.cpp +++ b/src/lms/ui/explore/TrackPopup.cpp @@ -32,8 +32,8 @@ namespace UserInterface void displayTrackPopupMenu(Wt::WInteractWidget& target, - Database::IdType trackId, - PlayQueueActionSignal& tracksAction) + Database::TrackId trackId, + PlayQueueActionTrackSignal& tracksAction) { Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; diff --git a/src/lms/ui/explore/TrackPopup.hpp b/src/lms/ui/explore/TrackPopup.hpp index 66419e6b..1597d994 100644 --- a/src/lms/ui/explore/TrackPopup.hpp +++ b/src/lms/ui/explore/TrackPopup.hpp @@ -28,7 +28,7 @@ namespace UserInterface { void displayTrackPopupMenu(Wt::WInteractWidget& target, - Database::IdType releaseId, - PlayQueueActionSignal& releasesAction); + Database::TrackId trackId, + PlayQueueActionTrackSignal& tracksAction); } // namespace UserInterface diff --git a/src/lms/ui/explore/TracksView.cpp b/src/lms/ui/explore/TracksView.cpp index ba1b30d2..daec0fcd 100644 --- a/src/lms/ui/explore/TracksView.cpp +++ b/src/lms/ui/explore/TracksView.cpp @@ -132,7 +132,7 @@ Tracks::addSome() _container->setHasMore(moreResults); } -std::vector +std::vector Tracks::getAllTracks() { return _trackCollector.getAll(); diff --git a/src/lms/ui/explore/TracksView.hpp b/src/lms/ui/explore/TracksView.hpp index f81355ad..ac011e01 100644 --- a/src/lms/ui/explore/TracksView.hpp +++ b/src/lms/ui/explore/TracksView.hpp @@ -39,14 +39,14 @@ namespace UserInterface public: Tracks(Filters& filters); - PlayQueueActionSignal tracksAction; + PlayQueueActionTrackSignal tracksAction; private: void refreshView(); void refreshView(TrackCollector::Mode mode); void addSome(); - std::vector getAllTracks(); + std::vector getAllTracks(); static constexpr TrackCollector::Mode _defaultMode {TrackCollector::Mode::Random}; static constexpr std::size_t _batchSize {6}; diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp index 2a2b5ed4..def318df 100644 --- a/src/lms/ui/resource/AudioFileResource.cpp +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -40,14 +40,14 @@ AudioFileResource:: ~AudioFileResource() } std::string -AudioFileResource::getUrl(Database::IdType trackId) const +AudioFileResource::getUrl(Database::TrackId trackId) const { - return url()+ "&trackid=" + std::to_string(trackId); + return url()+ "&trackid=" + trackId.toString(); } static std::optional -getTrackPathFromTrackId(Database::IdType trackId) +getTrackPathFromTrackId(Database::TrackId trackId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; @@ -72,7 +72,7 @@ getTrackPathFromURLArgs(const Wt::Http::Request& request) return std::nullopt; } - const auto trackId {StringUtils::readAs(*trackIdParameter)}; + const std::optional trackId {StringUtils::readAs(*trackIdParameter)}; if (!trackId) { LOG(ERROR) << "Bad trackid URL parameter!"; diff --git a/src/lms/ui/resource/AudioFileResource.hpp b/src/lms/ui/resource/AudioFileResource.hpp index 0865376b..fda116eb 100644 --- a/src/lms/ui/resource/AudioFileResource.hpp +++ b/src/lms/ui/resource/AudioFileResource.hpp @@ -31,7 +31,7 @@ class AudioFileResource : public Wt::WResource public: ~AudioFileResource(); - std::string getUrl(Database::IdType trackId) const; + std::string getUrl(Database::TrackId trackId) const; private: diff --git a/src/lms/ui/resource/AudioTranscodeResource.cpp b/src/lms/ui/resource/AudioTranscodeResource.cpp index ea374c0c..66ecac06 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.cpp +++ b/src/lms/ui/resource/AudioTranscodeResource.cpp @@ -96,9 +96,9 @@ AudioTranscodeResource:: ~AudioTranscodeResource() } std::string -AudioTranscodeResource::getUrl(Database::IdType trackId) const +AudioTranscodeResource::getUrl(Database::TrackId trackId) const { - return url() + "&trackid=" + std::to_string(trackId); + return url() + "&trackid=" + trackId.toString(); } template @@ -132,9 +132,9 @@ readTranscodeParameters(const Wt::Http::Request& request) TranscodeParameters parameters; // mandatory parameters - auto trackId {readParameterAs(request, "trackid")}; - auto format {readParameterAs(request, "format")}; - auto bitrate {readParameterAs(request, "bitrate")}; + const std::optional trackId {readParameterAs(request, "trackid")}; + const auto format {readParameterAs(request, "format")}; + const auto bitrate {readParameterAs(request, "bitrate")}; if (!trackId || !format || !bitrate) return std::nullopt; diff --git a/src/lms/ui/resource/AudioTranscodeResource.hpp b/src/lms/ui/resource/AudioTranscodeResource.hpp index a23e6602..ef516e91 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.hpp +++ b/src/lms/ui/resource/AudioTranscodeResource.hpp @@ -37,13 +37,11 @@ class AudioTranscodeResource : public Wt::WResource ~AudioTranscodeResource(); // Url depends on the user since settings are used in parameters - std::string getUrl(Database::IdType trackId) const; + std::string getUrl(Database::TrackId trackId) const; - void handleRequest(const Wt::Http::Request& request, - Wt::Http::Response& response); + void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); private: - static constexpr std::size_t _chunkSize {262144}; }; diff --git a/src/lms/ui/resource/CoverResource.cpp b/src/lms/ui/resource/CoverResource.cpp index 8adf85d8..a2978c6b 100644 --- a/src/lms/ui/resource/CoverResource.cpp +++ b/src/lms/ui/resource/CoverResource.cpp @@ -50,15 +50,15 @@ CoverResource::~CoverResource() } std::string -CoverResource::getReleaseUrl(Database::IdType releaseId, Size size) const +CoverResource::getReleaseUrl(Database::ReleaseId releaseId, Size size) const { - return url() + "&releaseid=" + std::to_string(releaseId) + "&size=" + std::to_string(static_cast(size)); + return url() + "&releaseid=" + releaseId.toString() + "&size=" + std::to_string(static_cast(size)); } std::string -CoverResource::getTrackUrl(Database::IdType trackId, Size size) const +CoverResource::getTrackUrl(Database::TrackId trackId, Size size) const { - return url() + "&trackid=" + std::to_string(trackId) + "&size=" + std::to_string(static_cast(size)); + return url() + "&trackid=" + trackId.toString() + "&size=" + std::to_string(static_cast(size)); } void @@ -89,7 +89,7 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons { LOG(DEBUG) << "Requested cover for track " << *trackIdStr << ", size = " << *size; - const auto trackId {StringUtils::readAs(*trackIdStr)}; + const std::optional trackId {StringUtils::readAs(*trackIdStr)}; if (!trackId) { LOG(DEBUG) << "track not found"; @@ -102,7 +102,7 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons { LOG(DEBUG) << "Requested cover for release " << *releaseIdStr << ", size = " << *size; - const auto releaseId {StringUtils::readAs(*releaseIdStr)}; + const std::optional releaseId {StringUtils::readAs(*releaseIdStr)}; if (!releaseId) return; diff --git a/src/lms/ui/resource/CoverResource.hpp b/src/lms/ui/resource/CoverResource.hpp index ff58976c..1e8cb8ad 100644 --- a/src/lms/ui/resource/CoverResource.hpp +++ b/src/lms/ui/resource/CoverResource.hpp @@ -39,8 +39,8 @@ namespace UserInterface Large = 512, }; - std::string getReleaseUrl(Database::IdType releaseId, Size size) const; - std::string getTrackUrl(Database::IdType trackId, Size size) const; + std::string getReleaseUrl(Database::ReleaseId releaseId, Size size) const; + std::string getTrackUrl(Database::TrackId trackId, Size size) const; private: void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index ce3e7b2e..998096a5 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -97,7 +97,7 @@ getReleaseArtistPathName(Database::Release::pointer release) { std::string releaseArtistName; - std::vector> artists; + std::vector> artists; artists = release->getReleaseArtists(); if (artists.empty()) @@ -171,7 +171,7 @@ createZipper(const std::vector& tracks) return std::make_unique(files, Wt::WLocalDateTime::currentDateTime().toUTC()); } -DownloadArtistResource::DownloadArtistResource(Database::IdType artistId) +DownloadArtistResource::DownloadArtistResource(Database::ArtistId artistId) : _artistId {artistId} { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; @@ -196,7 +196,7 @@ DownloadArtistResource::createZipper() return UserInterface::createZipper(artist->getTracks()); } -DownloadReleaseResource::DownloadReleaseResource(Database::IdType releaseId) +DownloadReleaseResource::DownloadReleaseResource(Database::ReleaseId releaseId) : _releaseId {releaseId} { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; @@ -222,7 +222,7 @@ DownloadReleaseResource::createZipper() return UserInterface::createZipper(release->getTracks()); } -DownloadTrackResource::DownloadTrackResource(Database::IdType trackId) +DownloadTrackResource::DownloadTrackResource(Database::TrackId trackId) : _trackId {trackId} { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; diff --git a/src/lms/ui/resource/DownloadResource.hpp b/src/lms/ui/resource/DownloadResource.hpp index adc6ce76..581a55ec 100644 --- a/src/lms/ui/resource/DownloadResource.hpp +++ b/src/lms/ui/resource/DownloadResource.hpp @@ -43,31 +43,31 @@ class DownloadResource : public Wt::WResource class DownloadArtistResource : public DownloadResource { public: - DownloadArtistResource(Database::IdType artistId); + DownloadArtistResource(Database::ArtistId artistId); private: std::unique_ptr createZipper() override; - Database::IdType _artistId; + Database::ArtistId _artistId; }; class DownloadReleaseResource : public DownloadResource { public: - DownloadReleaseResource(Database::IdType releaseId); + DownloadReleaseResource(Database::ReleaseId releaseId); private: std::unique_ptr createZipper() override; - Database::IdType _releaseId; + Database::ReleaseId _releaseId; }; class DownloadTrackResource : public DownloadResource { public: - DownloadTrackResource(Database::IdType trackId); + DownloadTrackResource(Database::TrackId trackId); private: std::unique_ptr createZipper() override; - Database::IdType _trackId; + Database::TrackId _trackId; }; } // namespace UserInterface diff --git a/src/test/database/Artist.cpp b/src/test/database/Artist.cpp index efb43d68..7193b164 100644 --- a/src/test/database/Artist.cpp +++ b/src/test/database/Artist.cpp @@ -25,16 +25,24 @@ TEST_F(DatabaseFixture, SingleArtist) { ScopedArtist artist {session, "MyArtist"}; + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_TRUE(artist.get()); + EXPECT_FALSE(!artist.get()); + EXPECT_EQ(artist.get()->getId(), artist.getId()); + } + { auto transaction {session.createSharedTransaction()}; auto artists {Artist::getAll(session, Artist::SortMethod::ByName)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); artists = Artist::getAllOrphans(session); ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); } } @@ -59,14 +67,14 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtist) auto artists {track->getArtists({TrackArtistLinkType::Artist})}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(artist->getReleaseCount(), 0); ASSERT_EQ(track->getArtistLinks().size(), 1); auto artistLink {track->getArtistLinks().front()}; - EXPECT_EQ(artistLink->getTrack().id(), track.getId()); - EXPECT_EQ(artistLink->getArtist().id(), artist.getId()); + EXPECT_EQ(artistLink->getTrack()->getId(), track.getId()); + EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId()); ASSERT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 1); EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); @@ -78,7 +86,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtist) auto tracks {artist->getTracks()}; ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front().id(), track.getId()); + EXPECT_EQ(tracks.front()->getId(), track.getId()); EXPECT_TRUE(artist->getTracks(TrackArtistLinkType::ReleaseArtist).empty()); EXPECT_EQ(artist->getTracks(TrackArtistLinkType::Artist).size(), 1); @@ -117,11 +125,11 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles) auto artists {track->getArtists({TrackArtistLinkType::Artist})}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); artists = track->getArtists({TrackArtistLinkType::ReleaseArtist}); ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(track->getArtistLinks().size(), 3); @@ -156,8 +164,8 @@ TEST_F(DatabaseFixture,SingleTrackMultiArtists) auto artists {track->getArtists({TrackArtistLinkType::Artist})}; ASSERT_EQ(artists.size(), 2); - EXPECT_TRUE((artists[0].id() == artist1.getId() && artists[1].id() == artist2.getId()) - || (artists[0].id() == artist2.getId() && artists[1].id() == artist1.getId())); + EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId()) + || (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId())); EXPECT_EQ(track->getArtists({}).size(), 2); EXPECT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 2); @@ -198,11 +206,11 @@ TEST_F(DatabaseFixture, SingleArtistSearchByName) const auto artistsByAAA {Artist::Artist::getByFilter(session, {}, {"A"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; ASSERT_EQ(artistsByAAA.size(), 1); - EXPECT_EQ(artistsByAAA.front().id(), artist.getId()); + EXPECT_EQ(artistsByAAA.front()->getId(), artist.getId()); const auto artistsByZZZ {Artist::Artist::getByFilter(session, {}, {"Z"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; ASSERT_EQ(artistsByZZZ.size(), 1); - EXPECT_EQ(artistsByZZZ.front().id(), artist.getId()); + EXPECT_EQ(artistsByZZZ.front()->getId(), artist.getId()); EXPECT_TRUE(Artist::getByName(session, "NNN").empty()); } @@ -223,19 +231,19 @@ TEST_F(DatabaseFixture, MultipleArtistsSearchByNameEscaped) { const auto artists {Artist::getByName(session, "MyArtist%")}; ASSERT_TRUE(artists.size() == 1); - EXPECT_EQ(artists.front().id(), artist1.getId()); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); EXPECT_TRUE(Artist::getByName(session, "MyArtistFoo").empty()); } { const auto artists {Artist::getByName(session, "%MyArtist")}; ASSERT_TRUE(artists.size() == 1); - EXPECT_EQ(artists.front().id(), artist2.getId()); + EXPECT_EQ(artists.front()->getId(), artist2.getId()); EXPECT_TRUE(Artist::getByName(session, "FooMyArtist").empty()); } { const auto artists {Artist::getByName(session, "%_MyArtist")}; ASSERT_TRUE(artists.size() == 1); - ASSERT_EQ(artists.front().id(), artist3.getId()); + ASSERT_EQ(artists.front()->getId(), artist3.getId()); EXPECT_TRUE(Artist::getByName(session, "%CMyArtist").empty()); } } @@ -265,21 +273,21 @@ TEST_F(DatabaseFixture, MultipleArtistsSearchByNameEscaped) { const auto artists {Artist::getByFilter(session, {}, {"MyArtist%"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0].id(), artist1.getId()); - EXPECT_EQ(artists[1].id(), artist4.getId()); + EXPECT_EQ(artists[0]->getId(), artist1.getId()); + EXPECT_EQ(artists[1]->getId(), artist4.getId()); } { const auto artists {Artist::getByFilter(session, {}, {"%MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0].id(), artist2.getId()); - EXPECT_EQ(artists[1].id(), artist5.getId()); + EXPECT_EQ(artists[0]->getId(), artist2.getId()); + EXPECT_EQ(artists[1]->getId(), artist5.getId()); } { const auto artists {Artist::getByFilter(session, {}, {"_MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists[0].id(), artist3.getId()); + EXPECT_EQ(artists[0]->getId(), artist3.getId()); } } } @@ -303,12 +311,12 @@ TEST_F(DatabaseFixture, MultiArtistsSortMethod) auto allArtistsBySortName {Artist::getAll(session, Artist::SortMethod::BySortName)}; ASSERT_EQ(allArtistsByName.size(), 2); - EXPECT_EQ(allArtistsByName.front().id(), artistA.getId()); - EXPECT_EQ(allArtistsByName.back().id(), artistB.getId()); + EXPECT_EQ(allArtistsByName.front()->getId(), artistA.getId()); + EXPECT_EQ(allArtistsByName.back()->getId(), artistB.getId()); ASSERT_EQ(allArtistsBySortName.size(), 2); - EXPECT_EQ(allArtistsBySortName.front().id(), artistB.getId()); - EXPECT_EQ(allArtistsBySortName.back().id(), artistA.getId()); + EXPECT_EQ(allArtistsBySortName.front()->getId(), artistB.getId()); + EXPECT_EQ(allArtistsBySortName.back()->getId(), artistA.getId()); } } @@ -321,7 +329,7 @@ TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) { auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(artist->hasNonReleaseTracks(std::nullopt), false); + EXPECT_FALSE(artist->hasNonReleaseTracks(std::nullopt)); bool moreResults; const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )}; @@ -343,9 +351,9 @@ TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) bool moreResults; const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )}; - EXPECT_EQ(artist->hasNonReleaseTracks(std::nullopt), true); - EXPECT_EQ(moreResults, false); + EXPECT_TRUE(artist->hasNonReleaseTracks(std::nullopt)); + EXPECT_FALSE(moreResults); ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front().id(), track2.getId()); + EXPECT_EQ(tracks.front()->getId(), track2.getId()); } } diff --git a/src/test/database/Cluster.cpp b/src/test/database/Cluster.cpp index 9135b48c..aba28a41 100644 --- a/src/test/database/Cluster.cpp +++ b/src/test/database/Cluster.cpp @@ -36,20 +36,20 @@ TEST_F(DatabaseFixture, SingleCluster) auto clusters {Cluster::getAll(session)}; ASSERT_EQ(clusters.size(), 1); - EXPECT_EQ(clusters.front().id(), cluster.getId()); - EXPECT_EQ(clusters.front()->getType().id(), clusterType.getId()); + EXPECT_EQ(clusters.front()->getId(), cluster.getId()); + EXPECT_EQ(clusters.front()->getType()->getId(), clusterType.getId()); clusters = Cluster::getAllOrphans(session); ASSERT_EQ(clusters.size(), 1); - EXPECT_EQ(clusters.front().id(), cluster.getId()); + EXPECT_EQ(clusters.front()->getId(), cluster.getId()); auto clusterTypes {ClusterType::getAll(session)}; ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front().id(), clusterType.getId()); + EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); clusterTypes = ClusterType::getAllUsed(session); ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front().id(), clusterType.getId()); + EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); clusterTypes = ClusterType::getAllOrphans(session); EXPECT_TRUE(clusterTypes.empty()); @@ -61,7 +61,7 @@ TEST_F(DatabaseFixture, SingleCluster) auto clusterTypes {ClusterType::getAllOrphans(session)}; ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front().id(), clusterType.getId()); + EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); ASSERT_TRUE(ClusterType::getAllUsed(session).empty()); } @@ -77,7 +77,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster) EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); auto clusterTypes {ClusterType::getAllOrphans(session)}; ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front().id(), clusterType.getId()); + EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); } ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"}; @@ -113,7 +113,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster) auto transaction {session.createSharedTransaction()}; auto clusters {Cluster::getAllOrphans(session)}; ASSERT_EQ(clusters.size(), 1); - EXPECT_EQ(clusters.front().id(), cluster2.getId()); + EXPECT_EQ(clusters.front()->getId(), cluster2.getId()); EXPECT_TRUE(ClusterType::getAllOrphans(session).empty()); } @@ -123,7 +123,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster) auto tracks {Track::getByClusters(session, {cluster1.getId()})}; ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front().id(), track.getId()); + EXPECT_EQ(tracks.front()->getId(), track.getId()); tracks = Track::getByClusters(session, {cluster2.getId()}); EXPECT_TRUE(tracks.empty()); @@ -134,7 +134,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster) auto clusters {track->getClusters()}; ASSERT_EQ(clusters.size(), 1); - EXPECT_EQ(clusters.front().id(), cluster1.getId()); + EXPECT_EQ(clusters.front()->getId(), cluster1.getId()); auto clusterIds {track->getClusterIds()}; ASSERT_EQ(clusterIds.size(), 1); @@ -166,7 +166,7 @@ TEST_F(DatabaseFixture, MultipleTracksSingleCluster) for (auto trackCluster : cluster->getTracks()) { - auto it {std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackCluster.id() == track.getId(); })}; + auto it {std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackCluster->getId() == track.getId(); })}; EXPECT_TRUE(it != std::cend(tracks)); } } @@ -221,8 +221,8 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersTopRelease) bool hasMore; const auto releases{trackList->getTopReleases({}, std::nullopt, hasMore)}; ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), releaseB.getId()); - EXPECT_EQ(releases[1].id(), releaseA.getId()); + EXPECT_EQ(releases[0]->getId(), releaseB.getId()); + EXPECT_EQ(releases[1]->getId(), releaseA.getId()); } { @@ -231,11 +231,11 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersTopRelease) bool hasMore; auto releases{trackList->getTopReleases({cluster1.getId()}, std::nullopt, hasMore)}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0].id(), releaseA.getId()); + EXPECT_EQ(releases[0]->getId(), releaseA.getId()); releases = trackList->getTopReleases({cluster2.getId()}, std::nullopt, hasMore); ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0].id(), releaseB.getId()); + EXPECT_EQ(releases[0]->getId(), releaseB.getId()); releases = trackList->getTopReleases({cluster2.getId(), cluster1.getId()}, std::nullopt, hasMore); EXPECT_TRUE(releases.empty()); @@ -259,12 +259,12 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersTopRelease) bool hasMore; auto releases {trackList->getTopReleases({cluster2.getId(), cluster3.getId()}, std::nullopt, hasMore)}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0].id(), releaseC.getId()); + EXPECT_EQ(releases[0]->getId(), releaseC.getId()); releases = trackList->getTopReleases({cluster2.getId()}, std::nullopt, hasMore); ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), releaseC.getId()); - EXPECT_EQ(releases[1].id(), releaseB.getId()); + EXPECT_EQ(releases[0]->getId(), releaseC.getId()); + EXPECT_EQ(releases[1]->getId(), releaseB.getId()); } } @@ -306,7 +306,7 @@ TEST_F(DatabaseFixture,SingleTrackSingleReleaseSingleCluster) auto releases {Release::getByClusters(session, {cluster.getId()})}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release.getId()); + EXPECT_EQ(releases.front()->getId(), release.getId()); } { @@ -351,7 +351,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) auto artists {Artist::getByClusters(session, {cluster1.getId()}, Artist::SortMethod::ByName)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_TRUE(Artist::getByClusters(session, {cluster2.getId()}, Artist::SortMethod::ByName).empty()); EXPECT_TRUE(Artist::getByClusters(session, {cluster3.getId()}, Artist::SortMethod::ByName).empty()); @@ -364,15 +364,15 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) auto artists {Artist::getByClusters(session, {cluster1.getId()}, Artist::SortMethod::ByName)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); artists = Artist::getByClusters(session, {cluster2.getId()}, Artist::SortMethod::ByName); ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); artists = Artist::getByClusters(session, {cluster1.getId(), cluster2.getId()}, Artist::SortMethod::ByName); ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_TRUE(Artist::getByClusters(session, {cluster3.getId()}, Artist::SortMethod::ByName).empty()); } @@ -405,7 +405,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters) auto artists {Artist::getByClusters(session, {cluster.getId()}, Artist::SortMethod::ByName)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); } } @@ -442,12 +442,12 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters) { auto transaction {session.createSharedTransaction()}; - std::set clusterIds; - std::transform(std::cbegin(clusters), std::cend(clusters), std::inserter(clusterIds, std::begin(clusterIds)), [](const ScopedCluster& cluster) { return cluster.getId(); }); + std::vector clusterIds; + std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); }); auto artists {Artist::getByClusters(session, clusterIds, Artist::SortMethod::ByName)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); } } @@ -478,7 +478,7 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity) EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const ScopedTrack& track) { - return similarTrack.id() == track.getId(); + return similarTrack->getId() == track.getId(); }) != std::cend(tracks)); } } @@ -519,14 +519,14 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity) auto similarTracks {Track::getSimilarTracks(session, {tracks.back().getId()}, 0, 4)}; EXPECT_EQ(similarTracks.size(), 4); for (auto similarTrack : similarTracks) - EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const ScopedTrack& track) { return similarTrack.id() == track.getId(); }) != std::cend(tracks)); + EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const ScopedTrack& track) { return similarTrack->getId() == track.getId(); }) != std::cend(tracks)); } { auto similarTracks {Track::getSimilarTracks(session, {tracks.front().getId()})}; EXPECT_EQ(similarTracks.size(), tracks.size() - 1); for (auto similarTrack : similarTracks) - EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const ScopedTrack& track) { return similarTrack.id() == track.getId(); }) != std::cend(tracks)); + EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const ScopedTrack& track) { return similarTrack->getId() == track.getId(); }) != std::cend(tracks)); } } } @@ -573,15 +573,15 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster) auto artists {Artist::getByClusters(session, {cluster.getId()}, Artist::SortMethod::ByName)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); auto releases {artist->getReleases()}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release.getId()); + EXPECT_EQ(releases.front()->getId(), release.getId()); releases = artist->getReleases({cluster.getId()}); ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release.getId()); + EXPECT_EQ(releases.front()->getId(), release.getId()); } } @@ -608,11 +608,11 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters) auto releases {artist->getReleases()}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release.getId()); + EXPECT_EQ(releases.front()->getId(), release.getId()); releases = artist->getReleases({cluster1.getId(), cluster2.getId()}); ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release.getId()); + EXPECT_EQ(releases.front()->getId(), release.getId()); } } @@ -644,7 +644,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster) EXPECT_EQ(similarTracks.size(), 5); for (auto similarTrack : similarTracks) - EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 5), std::cend(tracks), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack.id(); })); + EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 5), std::cend(tracks), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack->getId(); })); } } @@ -685,7 +685,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters) ASSERT_EQ(similarTracks.size(), 5); for (auto similarTrack : similarTracks) - EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 5), std::next(std::cbegin(tracks), 10), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack.id(); })); + EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 5), std::next(std::cbegin(tracks), 10), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack->getId(); })); } { @@ -693,7 +693,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters) ASSERT_EQ(similarTracks.size(), 5); for (auto similarTrack : similarTracks) - EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 10), std::next(std::cbegin(tracks), 15), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack.id(); })); + EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 10), std::next(std::cbegin(tracks), 15), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack->getId(); })); } EXPECT_TRUE(trackList->getSimilarTracks(10, 10).empty()); @@ -752,15 +752,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist1.getId()); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size() , 1); - EXPECT_EQ(releases.front().id(), release1.getId()); + EXPECT_EQ(releases.front()->getId(), release1.getId()); const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front().id(), track1.getId()); + EXPECT_EQ(tracks.front()->getId(), track1.getId()); } { @@ -769,15 +769,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist1.getId()); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release1.getId()); + EXPECT_EQ(releases.front()->getId(), release1.getId()); const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front().id(), track1.getId()); + EXPECT_EQ(tracks.front()->getId(), track1.getId()); } { @@ -786,15 +786,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist1.getId()); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release1.getId()); + EXPECT_EQ(releases.front()->getId(), release1.getId()); const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front().id(), track1.getId()); + EXPECT_EQ(tracks.front()->getId(), track1.getId()); } { @@ -803,7 +803,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist1.getId()); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); } { @@ -812,7 +812,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist1.getId()); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); } { @@ -838,18 +838,18 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0].id(), artist2.getId()); - EXPECT_EQ(artists[1].id(), artist1.getId()); + EXPECT_EQ(artists[0]->getId(), artist2.getId()); + EXPECT_EQ(artists[1]->getId(), artist1.getId()); const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), release2.getId()); - EXPECT_EQ(releases[1].id(), release1.getId()); + EXPECT_EQ(releases[0]->getId(), release2.getId()); + EXPECT_EQ(releases[1]->getId(), release1.getId()); const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0].id(), track2.getId()); - EXPECT_EQ(tracks[1].id(),track1.getId()); + EXPECT_EQ(tracks[0]->getId(), track2.getId()); + EXPECT_EQ(tracks[1]->getId(),track1.getId()); } { @@ -858,18 +858,18 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0].id(), artist2.getId()); - EXPECT_EQ(artists[1].id(), artist1.getId()); + EXPECT_EQ(artists[0]->getId(), artist2.getId()); + EXPECT_EQ(artists[1]->getId(), artist1.getId()); const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), release2.getId()); - EXPECT_EQ(releases[1].id(), release1.getId()); + EXPECT_EQ(releases[0]->getId(), release2.getId()); + EXPECT_EQ(releases[1]->getId(), release1.getId()); const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0].id(), track2.getId()); - EXPECT_EQ(tracks[1].id(), track1.getId()); + EXPECT_EQ(tracks[0]->getId(), track2.getId()); + EXPECT_EQ(tracks[1]->getId(), track1.getId()); } { @@ -878,15 +878,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist1.getId()); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release1.getId()); + EXPECT_EQ(releases.front()->getId(), release1.getId()); const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front().id(), track1.getId()); + EXPECT_EQ(tracks.front()->getId(), track1.getId()); } { @@ -895,15 +895,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist2.getId()); + EXPECT_EQ(artists.front()->getId(), artist2.getId()); const auto releases {trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release2.getId()); + EXPECT_EQ(releases.front()->getId(), release2.getId()); const auto tracks {trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front().id(), track2.getId()); + EXPECT_EQ(tracks.front()->getId(), track2.getId()); } { @@ -918,18 +918,18 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0].id(), artist1.getId()); - EXPECT_EQ(artists[1].id(), artist2.getId()); + EXPECT_EQ(artists[0]->getId(), artist1.getId()); + EXPECT_EQ(artists[1]->getId(), artist2.getId()); const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), release1.getId()); - EXPECT_EQ(releases[1].id(), release2.getId()); + EXPECT_EQ(releases[0]->getId(), release1.getId()); + EXPECT_EQ(releases[1]->getId(), release2.getId()); const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0].id(), track1.getId()); - EXPECT_EQ(tracks[1].id(), track2.getId()); + EXPECT_EQ(tracks[0]->getId(), track1.getId()); + EXPECT_EQ(tracks[1]->getId(), track2.getId()); } { @@ -938,18 +938,18 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0].id(), artist1.getId()); - EXPECT_EQ(artists[1].id(), artist2.getId()); + EXPECT_EQ(artists[0]->getId(), artist1.getId()); + EXPECT_EQ(artists[1]->getId(), artist2.getId()); const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), release1.getId()); - EXPECT_EQ(releases[1].id(), release2.getId()); + EXPECT_EQ(releases[0]->getId(), release1.getId()); + EXPECT_EQ(releases[1]->getId(), release2.getId()); const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0].id(), track1.getId()); - EXPECT_EQ(tracks[1].id(), track2.getId()); + EXPECT_EQ(tracks[0]->getId(), track1.getId()); + EXPECT_EQ(tracks[1]->getId(), track2.getId()); } } @@ -1000,13 +1000,13 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters) { auto artists {artist1->getSimilarArtists()}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist2.getId()); + EXPECT_EQ(artists.front()->getId(), artist2.getId()); } { auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist})}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist2.getId()); + EXPECT_EQ(artists.front()->getId(), artist2.getId()); } { @@ -1017,7 +1017,7 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters) { auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist})}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist2.getId()); + EXPECT_EQ(artists.front()->getId(), artist2.getId()); } { @@ -1028,8 +1028,8 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters) { auto artists {artist2->getSimilarArtists()}; ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0].id(), artist1.getId()); - EXPECT_EQ(artists[1].id(), artist3.getId()); + EXPECT_EQ(artists[0]->getId(), artist1.getId()); + EXPECT_EQ(artists[1]->getId(), artist3.getId()); } } } @@ -1081,14 +1081,14 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleReleasesMultiClusters) { auto releases {release1->getSimilarReleases()}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release2.getId()); + EXPECT_EQ(releases.front()->getId(), release2.getId()); } { auto releases {release2->getSimilarReleases()}; ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), release1.getId()); - EXPECT_EQ(releases[1].id(), release3.getId()); + EXPECT_EQ(releases[0]->getId(), release1.getId()); + EXPECT_EQ(releases[1]->getId(), release3.getId()); } } } diff --git a/src/test/database/Common.hpp b/src/test/database/Common.hpp index 056ca415..8864e681 100644 --- a/src/test/database/Common.hpp +++ b/src/test/database/Common.hpp @@ -40,6 +40,8 @@ template class ScopedEntity { public: + using IdType = typename T::IdType; + template ScopedEntity(Database::Session& session, Args&& ...args) : _session {session} @@ -48,7 +50,7 @@ class ScopedEntity auto entity {T::create(_session, std::forward(args)...)}; EXPECT_TRUE(entity); - _id = entity.id(); + _id = entity->getId(); } ~ScopedEntity() @@ -84,11 +86,11 @@ class ScopedEntity return get(); } - Database::IdType getId() const { return _id; } + IdType getId() const { return _id; } private: Database::Session& _session; - Database::IdType _id {}; + IdType _id {}; }; using ScopedArtist = ScopedEntity; diff --git a/src/test/database/DatabaseTest.cpp b/src/test/database/DatabaseTest.cpp index c5d2ad4b..c62df827 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/test/database/DatabaseTest.cpp @@ -32,11 +32,11 @@ TEST_F(DatabaseFixture, SingleRelease) auto releases {Release::getAllOrphans(session)}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release.getId()); + EXPECT_EQ(releases.front()->getId(), release.getId()); releases = Release::getAll(session); ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release.getId()); + EXPECT_EQ(releases.front()->getId(), release.getId()); EXPECT_EQ(release->getDuration(), std::chrono::seconds {0}); } } @@ -61,21 +61,21 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease) EXPECT_EQ(release->getTracksCount(), 1); ASSERT_EQ(release->getTracks().size(), 1); - EXPECT_EQ(release->getTracks().front().id(), track.getId()); + EXPECT_EQ(release->getTracks().front()->getId(), track.getId()); } { auto transaction {session.createUniqueTransaction()}; ASSERT_TRUE(track->getRelease()); - EXPECT_EQ(track->getRelease().id(), release.getId()); + EXPECT_EQ(track->getRelease()->getId(), release.getId()); } { auto transaction {session.createUniqueTransaction()}; auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseName")}; ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front().id(), track.getId()); + EXPECT_EQ(tracks.front()->getId(), track.getId()); } { auto transaction {session.createUniqueTransaction()}; @@ -96,7 +96,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease) auto releases {Release::getAllOrphans(session)}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release.getId()); + EXPECT_EQ(releases.front()->getId(), release.getId()); } } @@ -140,29 +140,29 @@ TEST_F(DatabaseFixture, MulitpleReleaseSearchByName) { const auto releases {Release::getByFilter(session, {}, {"MyRelease"}, std::nullopt, more)}; EXPECT_EQ(releases.size(), 5); - EXPECT_TRUE(std::none_of(std::cbegin(releases), std::cend(releases), [&](const Release::pointer& release) { return release.id() == release6.getId(); })); + EXPECT_TRUE(std::none_of(std::cbegin(releases), std::cend(releases), [&](const Release::pointer& release) { return release->getId() == release6.getId(); })); } { const auto releases {Release::getByFilter(session, {}, {"MyRelease%"}, std::nullopt, more)}; - EXPECT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), release2.getId()); - EXPECT_EQ(releases[1].id(), release4.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0]->getId(), release2.getId()); + EXPECT_EQ(releases[1]->getId(), release4.getId()); } { const auto releases {Release::getByFilter(session, {}, {"%MyRelease"}, std::nullopt, more)}; - EXPECT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), release3.getId()); - EXPECT_EQ(releases[1].id(), release5.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0]->getId(), release3.getId()); + EXPECT_EQ(releases[1]->getId(), release5.getId()); } { const auto releases {Release::getByFilter(session, {}, {"Foo%MyRelease"}, std::nullopt, more)}; - EXPECT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0].id(), release5.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0]->getId(), release5.getId()); } { const auto releases {Release::getByFilter(session, {}, {"MyRelease%Foo"}, std::nullopt, more)}; - EXPECT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0].id(), release4.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0]->getId(), release4.getId()); } } } @@ -296,8 +296,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack) EXPECT_TRUE(release1->getFirstTrack()); EXPECT_TRUE(release2->getFirstTrack()); - EXPECT_EQ(release1->getFirstTrack().id(), track1A.getId()); - EXPECT_EQ(release2->getFirstTrack().id(), track2B.getId()); + EXPECT_EQ(release1->getFirstTrack()->getId(), track1A.getId()); + EXPECT_EQ(release2->getFirstTrack()->getId(), track2B.getId()); } } @@ -329,7 +329,7 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease) EXPECT_EQ(artist->getReleaseCount(), 1); ASSERT_EQ(artist->getReleases().size(), 1); - EXPECT_EQ(artist->getReleases().front().id(), release.getId()); + EXPECT_EQ(artist->getReleases().front()->getId(), release.getId()); EXPECT_EQ(release->getTracks().size(), nbTracks); } @@ -353,13 +353,13 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist) auto releases {artist->getReleases()}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release.getId()); + EXPECT_EQ(releases.front()->getId(), release.getId()); EXPECT_EQ(artist->getReleaseCount(), 1); auto artists {release->getArtists()}; ASSERT_EQ(artists.size(), 1); - ASSERT_EQ(artists.front().id(), artist.getId()); + ASSERT_EQ(artists.front()->getId(), artist.getId()); } } @@ -409,7 +409,7 @@ TEST_F(DatabaseFixture, SingleStarredArtist) bool hasMore {}; auto artists {Artist::getStarred(session, user.get(), {}, std::nullopt, Artist::SortMethod::BySortName, std::nullopt, hasMore)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist.getId()); + EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_FALSE(hasMore); } } @@ -441,7 +441,7 @@ TEST_F(DatabaseFixture, SingleStarredRelease) bool hasMore {}; auto releases {Release::getStarred(session, user.get(), {}, std::nullopt, hasMore)}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release.getId()); + EXPECT_EQ(releases.front()->getId(), release.getId()); EXPECT_FALSE(hasMore); } } @@ -471,7 +471,7 @@ TEST_F(DatabaseFixture, SingleStarredTrack) bool hasMore {}; auto tracks {Track::getStarred(session, user.get(), {}, std::nullopt, hasMore)}; ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front().id(), track.getId()); + EXPECT_EQ(tracks.front()->getId(), track.getId()); EXPECT_FALSE(hasMore); } } @@ -486,7 +486,7 @@ TEST_F(DatabaseFixture, SingleTrackList) auto trackLists {TrackList::getAll(session, user.get(), TrackList::Type::Playlist)}; ASSERT_EQ(trackLists.size(), 1); - EXPECT_EQ(trackLists.front().id(), trackList.getId()); + EXPECT_EQ(trackLists.front()->getId(), trackList.getId()); } } @@ -536,8 +536,8 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime) bool moreResults; const auto tracks {trackList.get()->getTracksReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 3); - EXPECT_EQ(tracks.front().id(), track3.getId()); - EXPECT_EQ(tracks.back().id(), track2.getId()); + EXPECT_EQ(tracks.front()->getId(), track3.getId()); + EXPECT_EQ(tracks.back()->getId(), track2.getId()); } } @@ -584,11 +584,11 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front().id(), artist1.getId()); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front().id(), release1.getId()); + EXPECT_EQ(releases.front()->getId(), release1.getId()); const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; EXPECT_EQ(tracks.size(), 1); @@ -606,18 +606,18 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0].id(), artist2.getId()); - EXPECT_EQ(artists[1].id(), artist1.getId()); + EXPECT_EQ(artists[0]->getId(), artist2.getId()); + EXPECT_EQ(artists[1]->getId(), artist1.getId()); const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), release2.getId()); - EXPECT_EQ(releases[1].id(), release1.getId()); + EXPECT_EQ(releases[0]->getId(), release2.getId()); + EXPECT_EQ(releases[1]->getId(), release1.getId()); const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0].id(), track2.getId()); - EXPECT_EQ(tracks[1].id(), track1.getId()); + EXPECT_EQ(tracks[0]->getId(), track2.getId()); + EXPECT_EQ(tracks[1]->getId(), track1.getId()); } { @@ -632,18 +632,18 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) bool moreResults {}; const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0].id(), artist1.getId()); - EXPECT_EQ(artists[1].id(), artist2.getId()); + EXPECT_EQ(artists[0]->getId(), artist1.getId()); + EXPECT_EQ(artists[1]->getId(), artist2.getId()); const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0].id(), release1.getId()); - EXPECT_EQ(releases[1].id(), release2.getId()); + EXPECT_EQ(releases[0]->getId(), release1.getId()); + EXPECT_EQ(releases[1]->getId(), release2.getId()); const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0].id(), track1.getId()); - EXPECT_EQ(tracks[1].id(), track2.getId()); + EXPECT_EQ(tracks[0]->getId(), track1.getId()); + EXPECT_EQ(tracks[1]->getId(), track2.getId()); } } diff --git a/src/test/database/Track.cpp b/src/test/database/Track.cpp index b5de2e56..cb1759a9 100644 --- a/src/test/database/Track.cpp +++ b/src/test/database/Track.cpp @@ -70,19 +70,19 @@ TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) { const auto tracks {Track::getByFilter(session, {}, {"MyTrack"}, std::nullopt, more)}; EXPECT_EQ(tracks.size(), 5); - EXPECT_TRUE(std::none_of(std::cbegin(tracks), std::cend(tracks), [&](const Track::pointer& track) { return track.id() == track6.getId(); })); + EXPECT_TRUE(std::none_of(std::cbegin(tracks), std::cend(tracks), [&](const Track::pointer& track) { return track->getId() == track6.getId(); })); } { const auto tracks {Track::getByFilter(session, {}, {"MyTrack%"}, std::nullopt, more)}; ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0].id(), track2.getId()); - EXPECT_EQ(tracks[1].id(), track3.getId()); + EXPECT_EQ(tracks[0]->getId(), track2.getId()); + EXPECT_EQ(tracks[1]->getId(), track3.getId()); } { const auto tracks {Track::getByFilter(session, {}, {"%MyTrack"}, std::nullopt, more)}; ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0].id(), track4.getId()); - EXPECT_EQ(tracks[1].id(), track5.getId()); + EXPECT_EQ(tracks[0]->getId(), track4.getId()); + EXPECT_EQ(tracks[1]->getId(), track5.getId()); } } } diff --git a/src/test/som/SomTest.cpp b/src/test/som/SomTest.cpp index c2ef65db..74186d85 100644 --- a/src/test/som/SomTest.cpp +++ b/src/test/som/SomTest.cpp @@ -17,8 +17,8 @@ * along with LMS. If not, see . */ +#include #include - #include "som/DataNormalizer.hpp" #include "som/Network.hpp" diff --git a/src/tools/cover/LmsCover.cpp b/src/tools/cover/LmsCover.cpp index 3a1eb30a..6a9e2767 100644 --- a/src/tools/cover/LmsCover.cpp +++ b/src/tools/cover/LmsCover.cpp @@ -38,15 +38,15 @@ static void dumpTrackCovers(Database::Session& session, CoverArt::ImageSize width) { - std::vector trackIds; + std::vector trackIds; { auto transaction {session.createSharedTransaction()}; trackIds = Database::Track::getAllIds(session); } - for (Database::IdType trackId : trackIds) + for (const Database::TrackId trackId : trackIds) { - std::cout << "Getting cover for track id " << trackId << std::endl; + std::cout << "Getting cover for track id " << trackId.toString() << std::endl; Service::get()->getFromTrack(session, trackId, width); } } diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index bea308e8..0a1f4a3c 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -39,16 +39,16 @@ static void dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) { - const std::vector trackIds {[&]() + const std::vector trackIds {[&]() { auto transaction {session.createSharedTransaction()}; return Database::Track::getAllIds(session); }()}; std::cout << "*** Tracks (" << trackIds.size() << ") ***" << std::endl; - for (Database::IdType trackId : trackIds) + for (Database::TrackId trackId : trackIds) { - auto trackToString = [&](Database::IdType trackId) + auto trackToString = [&](Database::TrackId trackId) { std::string res; auto transaction {session.createSharedTransaction()}; @@ -66,7 +66,7 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng }; std::cout << "Processing track '" << trackToString(trackId) << std::endl; - for (Database::IdType similarTrackId : engine.getSimilarTracks(session, {trackId}, maxSimilarityCount)) + for (Database::TrackId similarTrackId : engine.getSimilarTracks(session, {trackId}, maxSimilarityCount)) std::cout << "\t- Similar track '" << trackToString(similarTrackId) << std::endl; } } @@ -75,16 +75,16 @@ static void dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) { - const std::vector releaseIds = std::invoke([&]() - { + const std::vector releaseIds = std::invoke([&]() + { auto transaction {session.createSharedTransaction()}; return Database::Release::getAllIds(session); - }); + }); std::cout << "*** Releases ***" << std::endl; - for (Database::IdType releaseId : releaseIds) + for (Database::ReleaseId releaseId : releaseIds) { - auto releaseToString = [&](Database::IdType releaseId) + auto releaseToString = [&](Database::ReleaseId releaseId) { auto transaction {session.createSharedTransaction()}; @@ -93,7 +93,7 @@ dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& e }; std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl; - for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, releaseId, maxSimilarityCount)) + for (Database::ReleaseId similarReleaseId : engine.getSimilarReleases(session, releaseId, maxSimilarityCount)) std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl; } } @@ -102,16 +102,16 @@ static void dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) { - const std::vector artistIds = std::invoke([&]() - { - auto transaction {session.createSharedTransaction()}; - return Database::Artist::getAllIds(session); - }); + const std::vector artistIds = std::invoke([&]() + { + auto transaction {session.createSharedTransaction()}; + return Database::Artist::getAllIds(session); + }); std::cout << "*** Artists ***" << std::endl; - for (Database::IdType artistId : artistIds) + for (Database::ArtistId artistId : artistIds) { - auto artistToString = [&](Database::IdType artistId) + auto artistToString = [&](Database::ArtistId artistId) { auto transaction {session.createSharedTransaction()}; @@ -120,7 +120,7 @@ dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& en }; std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl; - for (Database::IdType similarArtistId : engine.getSimilarArtists(session, artistId, {Database::TrackArtistLinkType::Artist, Database::TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount)) + for (Database::ArtistId similarArtistId : engine.getSimilarArtists(session, artistId, {Database::TrackArtistLinkType::Artist, Database::TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount)) { std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl; } From 7c649c82ceb09332c60c87989c843bcb63ab1c2a Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 22 Sep 2021 12:54:54 +0200 Subject: [PATCH 12/25] Bumped from 3.13 to 3.14 for alpine images --- Dockerfile-build | 2 +- Dockerfile-release | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile-build b/Dockerfile-build index 9f5b0824..bd39c8a5 100644 --- a/Dockerfile-build +++ b/Dockerfile-build @@ -1,4 +1,4 @@ -FROM alpine:3.13 AS build +FROM alpine:3.14 AS build WORKDIR /tmp/workdir diff --git a/Dockerfile-release b/Dockerfile-release index a3ee5277..3d1b59bb 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -1,4 +1,4 @@ -FROM alpine:3.13 AS build +FROM alpine:3.14 AS build WORKDIR /tmp/workdir @@ -129,7 +129,7 @@ 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 " LABEL version=${LMS_VERSION} From 2ebc4280a01569e1b21e830b764a3b48e33db908 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 23 Sep 2021 13:18:43 +0200 Subject: [PATCH 13/25] Sort by release date, not just release year, fixes #128 --- src/libs/database/impl/Artist.cpp | 4 +- src/libs/database/impl/Release.cpp | 15 +- src/libs/database/impl/Session.cpp | 37 +- src/libs/database/impl/Track.cpp | 4 +- .../database/include/database/Release.hpp | 6 +- src/libs/database/include/database/Track.hpp | 44 +-- src/libs/metadata/CMakeLists.txt | 3 + src/libs/metadata/impl/AvFormatParser.cpp | 6 +- src/libs/metadata/impl/TagLibParser.cpp | 30 +- src/libs/metadata/impl/Utils.cpp | 60 +++ src/libs/metadata/impl/Utils.hpp | 27 ++ .../metadata/include/metadata/IParser.hpp | 21 +- src/libs/metadata/test/CMakeLists.txt | 18 + src/libs/metadata/test/Metadata.cpp | 27 ++ src/libs/metadata/test/Utils.cpp | 74 ++++ src/libs/scanner/impl/Scanner.cpp | 8 +- src/test/database/CMakeLists.txt | 1 + src/test/database/DatabaseTest.cpp | 278 -------------- src/test/database/Release.cpp | 354 ++++++++++++++++++ src/test/database/Track.cpp | 25 ++ src/tools/metadata/LmsMetadata.cpp | 8 +- 21 files changed, 701 insertions(+), 349 deletions(-) create mode 100644 src/libs/metadata/impl/Utils.cpp create mode 100644 src/libs/metadata/impl/Utils.hpp create mode 100644 src/libs/metadata/test/CMakeLists.txt create mode 100644 src/libs/metadata/test/Metadata.cpp create mode 100644 src/libs/metadata/test/Utils.cpp create mode 100644 src/test/database/Release.cpp diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 5c620561..a519f4c2 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -419,7 +419,7 @@ Artist::getReleases(const std::vector& clusterIds) const 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"; auto query {session()->query>(oss.str())}; @@ -447,7 +447,7 @@ Artist::getTracks(std::optional linkType) const auto query {session()->query>("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(getId()) - .orderBy("t.year DESC,t.release_id,t.disc_number,t.track_number")}; + .orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number")}; if (linkType) query.where("t_a_l.type = ?").bind(*linkType); diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index a782b06a..a42d7c91 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -249,9 +249,9 @@ Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional> ("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(range->offset) : -1) .limit(range ? static_cast(range->limit) : -1) .resultList()}; @@ -383,19 +383,20 @@ Release::getReleaseYear(bool original) const { assert(session()); - const char* field {original ? "original_year" : "year"}; + const char* field {original ? "original_date" : "date"}; - Wt::Dbo::collection dates = session()->query( + auto dates {session()->query( std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?") .groupBy(field) - .bind(getId()); + .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; diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 5141f6d6..56ec77d1 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -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)"); diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 91bdfd86..e8f3436b 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -483,13 +483,13 @@ Track::getTotalDisc() const std::optional Track::getYear() const { - return (_year > 0) ? std::make_optional(_year) : std::nullopt; + return (_date.isValid() ? std::make_optional(_date.year()) : std::nullopt); } std::optional Track::getOriginalYear() const { - return (_originalYear > 0) ? std::make_optional(_originalYear) : std::nullopt; + return (_originalDate.isValid() ? std::make_optional(_originalDate.year()) : std::nullopt); } std::optional diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index d39b1469..80c16eeb 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -81,9 +81,9 @@ class Release : public Object static pointer create(Session& session, const std::string& name, const std::optional& MBID = {}); // Utility functions - std::optional getReleaseYear(bool originalDate = false) const; // 0 if unknown or various - std::optional getCopyright() const; - std::optional getCopyrightURL() const; + std::optional getReleaseYear(bool originalDate = false) const; + std::optional getCopyright() const; + std::optional getCopyrightURL() const; // Accessors const std::string& getName() const { return _name; } diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index edfdbecc..1e193405 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -102,8 +102,8 @@ class Track : public Object 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& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } void setRecordingMBID(const std::optional& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } @@ -118,26 +118,26 @@ class Track : public Object void setFeatures(const ObjectPtr& features); std::size_t getScanVersion() const { return _scanVersion; } - std::optional getTrackNumber() const; - std::optional getTotalTrack() const; - std::optional getDiscNumber() const; - const std::string& getDiscSubtitle() const { return _discSubtitle; } - std::optional getTotalDisc() const; + std::optional getTrackNumber() const; + std::optional getTotalTrack() const; + std::optional getDiscNumber() const; + const std::string& getDiscSubtitle() const { return _discSubtitle; } + std::optional 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 getYear() const; std::optional getOriginalYear() const; Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; } Wt::WDateTime getAddedTime() const { return _fileAdded; } - bool hasCover() const { return _hasCover; } - std::optional getTrackMBID() const { return UUID::fromString(_trackMBID); } - std::optional getRecordingMBID() const { return UUID::fromString(_recordingMBID); } - std::optional getCopyright() const; - std::optional getCopyrightURL() const; - std::optional getTrackReplayGain() const { return _trackReplayGain; } - std::optional getReleaseReplayGain() const { return _releaseReplayGain; } + bool hasCover() const { return _hasCover; } + std::optional getTrackMBID() const { return UUID::fromString(_trackMBID); } + std::optional getRecordingMBID() const { return UUID::fromString(_recordingMBID); } + std::optional getCopyright() const; + std::optional getCopyrightURL() const; + std::optional getTrackReplayGain() const { return _trackReplayGain; } + std::optional getReleaseReplayGain() const { return _releaseReplayGain; } // no artistLinkTypes means get all std::vector> getArtists(EnumSet artistLinkTypes) const; @@ -160,10 +160,10 @@ class Track : public Object 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"); @@ -198,8 +198,8 @@ class Track : public Object std::string _artistName; std::string _releaseName; std::chrono::duration _duration {}; - int _year {}; - int _originalYear {}; + Wt::WDate _date; + Wt::WDate _originalDate; std::string _filePath; Wt::WDateTime _fileLastWrite; Wt::WDateTime _fileAdded; diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt index 40b56fc7..8ba7b93d 100644 --- a/src/libs/metadata/CMakeLists.txt +++ b/src/libs/metadata/CMakeLists.txt @@ -1,7 +1,10 @@ +add_subdirectory(test) + add_library(lmsmetadata SHARED impl/AvFormatParser.cpp impl/TagLibParser.cpp + impl/Utils.cpp ) target_include_directories(lmsmetadata INTERFACE diff --git a/src/libs/metadata/impl/AvFormatParser.cpp b/src/libs/metadata/impl/AvFormatParser.cpp index e35bdd99..9c588a2a 100644 --- a/src/libs/metadata/impl/AvFormatParser.cpp +++ b/src/libs/metadata/impl/AvFormatParser.cpp @@ -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(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(value); + track.originalDate = Utils::parseDate(value); } else if (tag == "ACOUSTID ID") { diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index e383da71..456747cd 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -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(value); - else if (tag == "ORIGINALDATE" && !track.originalYear) { - // Lower priority than ORIGINALYEAR - track.originalYear = StringUtils::readAs(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(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()) { diff --git a/src/libs/metadata/impl/Utils.cpp b/src/libs/metadata/impl/Utils.cpp new file mode 100644 index 00000000..090db19c --- /dev/null +++ b/src/libs/metadata/impl/Utils.cpp @@ -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 . + */ + +#include "Utils.hpp" +#include +#include +#include +#include + +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 {}; + } +} + diff --git a/src/libs/metadata/impl/Utils.hpp b/src/libs/metadata/impl/Utils.hpp new file mode 100644 index 00000000..30025627 --- /dev/null +++ b/src/libs/metadata/impl/Utils.hpp @@ -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 . + */ + +#include + +namespace MetaData::Utils +{ + Wt::WDate parseDate(const std::string& dateStr); +} + diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 61244066..18db7f1d 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -27,6 +27,7 @@ #include #include +#include #include "utils/UUID.hpp" namespace MetaData @@ -56,21 +57,21 @@ namespace MetaData struct Track { - std::vector artists; - std::vector albumArtists; - std::string title; - std::optional trackMBID; - std::optional recordingMBID; - std::optional album; - Clusters clusters; + std::vector artists; + std::vector albumArtists; + std::string title; + std::optional trackMBID; + std::optional recordingMBID; + std::optional album; + Clusters clusters; std::chrono::milliseconds duration; std::optional trackNumber; std::optional totalTrack; std::optional discNumber; std::optional totalDisc; - std::optional year; - std::optional originalYear; - bool hasCover {}; + Wt::WDate date; + Wt::WDate originalDate; + bool hasCover {}; std::vector audioStreams; std::optional acoustID; std::string copyright; diff --git a/src/libs/metadata/test/CMakeLists.txt b/src/libs/metadata/test/CMakeLists.txt new file mode 100644 index 00000000..acd4aec2 --- /dev/null +++ b/src/libs/metadata/test/CMakeLists.txt @@ -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) + diff --git a/src/libs/metadata/test/Metadata.cpp b/src/libs/metadata/test/Metadata.cpp new file mode 100644 index 00000000..88325e8b --- /dev/null +++ b/src/libs/metadata/test/Metadata.cpp @@ -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 . + */ + +#include + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + diff --git a/src/libs/metadata/test/Utils.cpp b/src/libs/metadata/test/Utils.cpp new file mode 100644 index 00000000..97ff5b17 --- /dev/null +++ b/src/libs/metadata/test/Utils.cpp @@ -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 . + */ + +#include + +#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 << "'"; + } +} diff --git a/src/libs/scanner/impl/Scanner.cpp b/src/libs/scanner/impl/Scanner.cpp index 6a8444be..bbc10fda 100644 --- a/src/libs/scanner/impl/Scanner.cpp +++ b/src/libs/scanner/impl/Scanner.cpp @@ -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); diff --git a/src/test/database/CMakeLists.txt b/src/test/database/CMakeLists.txt index 30c59057..0ad6756d 100644 --- a/src/test/database/CMakeLists.txt +++ b/src/test/database/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(test-database Artist.cpp Cluster.cpp DatabaseTest.cpp + Release.cpp Track.cpp ) diff --git a/src/test/database/DatabaseTest.cpp b/src/test/database/DatabaseTest.cpp index c62df827..1bca6150 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/test/database/DatabaseTest.cpp @@ -23,284 +23,6 @@ using namespace Database; -TEST_F(DatabaseFixture, SingleRelease) -{ - ScopedRelease release {session, "MyRelease"}; - - { - auto transaction {session.createSharedTransaction()}; - - auto releases {Release::getAllOrphans(session)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); - - releases = Release::getAll(session); - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); - EXPECT_EQ(release->getDuration(), std::chrono::seconds {0}); - } -} - -TEST_F(DatabaseFixture, SingleTrackSingleRelease) -{ - ScopedRelease release {session, "MyRelease"}; - - { - ScopedTrack track {session, "MyTrack"}; - { - auto transaction {session.createUniqueTransaction()}; - - track.get().modify()->setRelease(release.get()); - track.get().modify()->setName("MyTrackName"); - release.get().modify()->setName("MyReleaseName"); - } - - { - auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Release::getAllOrphans(session).empty()); - - EXPECT_EQ(release->getTracksCount(), 1); - ASSERT_EQ(release->getTracks().size(), 1); - EXPECT_EQ(release->getTracks().front()->getId(), track.getId()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - ASSERT_TRUE(track->getRelease()); - EXPECT_EQ(track->getRelease()->getId(), release.getId()); - } - - { - auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseName")}; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track.getId()); - } - { - auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseFoo")}; - EXPECT_EQ(tracks.size(), 0); - } - { - auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::getByNameAndReleaseName(session, "MyTrackFoo", "MyReleaseName")}; - EXPECT_EQ(tracks.size(), 0); - } - } - - { - auto transaction {session.createUniqueTransaction()}; - - EXPECT_TRUE(release->getTracks().empty()); - - auto releases {Release::getAllOrphans(session)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); - } -} - -TEST_F(DatabaseFixture, MulitpleReleaseSearchByName) -{ - ScopedRelease release1 {session, "MyRelease"}; - ScopedRelease release2 {session, "MyRelease%"}; - ScopedRelease release3 {session, "%MyRelease"}; - ScopedRelease release4 {session, "MyRelease%Foo"}; - ScopedRelease release5 {session, "Foo%MyRelease"}; - ScopedRelease release6 {session, "_yRelease"}; - - // filters does not work on orphans - ScopedTrack track1 {session, "MyTrack"}; - ScopedTrack track2 {session, "MyTrack"}; - ScopedTrack track3 {session, "MyTrack"}; - ScopedTrack track4 {session, "MyTrack"}; - ScopedTrack track5 {session, "MyTrack"}; - ScopedTrack track6 {session, "MyTrack"}; - - { - auto transaction {session.createUniqueTransaction()}; - - track1.get().modify()->setRelease(release1.get()); - track2.get().modify()->setRelease(release2.get()); - track3.get().modify()->setRelease(release3.get()); - track4.get().modify()->setRelease(release4.get()); - track5.get().modify()->setRelease(release5.get()); - track6.get().modify()->setRelease(release6.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool more; - { - const auto releases {Release::getByFilter(session, {}, {"Release"}, std::nullopt, more)}; - EXPECT_EQ(releases.size(), 6); - } - - { - const auto releases {Release::getByFilter(session, {}, {"MyRelease"}, std::nullopt, more)}; - EXPECT_EQ(releases.size(), 5); - EXPECT_TRUE(std::none_of(std::cbegin(releases), std::cend(releases), [&](const Release::pointer& release) { return release->getId() == release6.getId(); })); - } - { - const auto releases {Release::getByFilter(session, {}, {"MyRelease%"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release2.getId()); - EXPECT_EQ(releases[1]->getId(), release4.getId()); - } - { - const auto releases {Release::getByFilter(session, {}, {"%MyRelease"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release3.getId()); - EXPECT_EQ(releases[1]->getId(), release5.getId()); - } - { - const auto releases {Release::getByFilter(session, {}, {"Foo%MyRelease"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), release5.getId()); - } - { - const auto releases {Release::getByFilter(session, {}, {"MyRelease%Foo"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), release4.getId()); - } - } -} - -TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack) -{ - ScopedRelease release1 {session, "MyRelease"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_FALSE(release1->getTotalTrack()); - EXPECT_FALSE(release1->getTotalDisc()); - } - - ScopedTrack track1 {session, "MyTrack"}; - { - auto transaction {session.createUniqueTransaction()}; - - track1.get().modify()->setRelease(release1.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_FALSE(release1->getTotalTrack()); - EXPECT_FALSE(release1->getTotalDisc()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - track1.get().modify()->setTotalTrack(36); - track1.get().modify()->setTotalDisc(6); - } - - { - auto transaction {session.createSharedTransaction()}; - - ASSERT_TRUE(release1->getTotalTrack()); - EXPECT_EQ(*release1->getTotalTrack(), 36); - ASSERT_TRUE(release1->getTotalDisc()); - EXPECT_EQ(*release1->getTotalDisc(), 6); - } - - ScopedTrack track2 {session, "MyTrack2"}; - { - auto transaction {session.createUniqueTransaction()}; - - track2.get().modify()->setRelease(release1.get()); - track2.get().modify()->setTotalTrack(37); - track2.get().modify()->setTotalDisc(67); - } - - { - auto transaction {session.createSharedTransaction()}; - - ASSERT_TRUE(release1->getTotalTrack()); - EXPECT_EQ(*release1->getTotalTrack(), 37); - ASSERT_TRUE(release1->getTotalDisc()); - EXPECT_EQ(*release1->getTotalDisc(), 67); - } - - ScopedRelease release2 {session, "MyRelease2"}; - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_FALSE(release2->getTotalTrack()); - EXPECT_FALSE(release2->getTotalDisc()); - } - - ScopedTrack track3 {session, "MyTrack3"}; - { - auto transaction {session.createUniqueTransaction()}; - - track3.get().modify()->setRelease(release2.get()); - track3.get().modify()->setTotalTrack(7); - track3.get().modify()->setTotalDisc(5); - } - { - auto transaction {session.createSharedTransaction()}; - - ASSERT_TRUE(release1->getTotalTrack()); - EXPECT_EQ(*release1->getTotalTrack(), 37); - ASSERT_TRUE(release1->getTotalDisc()); - EXPECT_EQ(*release1->getTotalDisc(), 67); - ASSERT_TRUE(release2->getTotalTrack()); - EXPECT_EQ(*release2->getTotalTrack(), 7); - ASSERT_TRUE(release2->getTotalDisc()); - EXPECT_EQ(*release2->getTotalDisc(), 5); - } -} - -TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack) -{ - ScopedRelease release1 {session, "MyRelease1"}; - ScopedRelease release2 {session, "MyRelease2"}; - - ScopedTrack track1A {session, "MyTrack1A"}; - ScopedTrack track1B {session, "MyTrack1B"}; - ScopedTrack track2A {session, "MyTrack2A"}; - ScopedTrack track2B {session, "MyTrack2B"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_FALSE(release1->getFirstTrack()); - EXPECT_FALSE(release2->getFirstTrack()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - track1A.get().modify()->setRelease(release1.get()); - track1B.get().modify()->setRelease(release1.get()); - track2A.get().modify()->setRelease(release2.get()); - track2B.get().modify()->setRelease(release2.get()); - - track1A.get().modify()->setTrackNumber(1); - track1B.get().modify()->setTrackNumber(2); - - track2A.get().modify()->setDiscNumber(2); - track2A.get().modify()->setTrackNumber(1); - track2B.get().modify()->setTrackNumber(2); - track2B.get().modify()->setDiscNumber(1); - } - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_TRUE(release1->getFirstTrack()); - EXPECT_TRUE(release2->getFirstTrack()); - - EXPECT_EQ(release1->getFirstTrack()->getId(), track1A.getId()); - EXPECT_EQ(release2->getFirstTrack()->getId(), track2B.getId()); - } -} - TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease) { constexpr std::size_t nbTracks {10}; diff --git a/src/test/database/Release.cpp b/src/test/database/Release.cpp new file mode 100644 index 00000000..a65db032 --- /dev/null +++ b/src/test/database/Release.cpp @@ -0,0 +1,354 @@ +/* + * 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 . + */ + +#include "Common.hpp" + +using namespace Database; + +TEST_F(DatabaseFixture, SingleRelease) +{ + ScopedRelease release {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Release::getAllOrphans(session)}; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front()->getId(), release.getId()); + + releases = Release::getAll(session); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front()->getId(), release.getId()); + EXPECT_EQ(release->getDuration(), std::chrono::seconds {0}); + } +} + +TEST_F(DatabaseFixture, SingleTrackSingleRelease) +{ + ScopedRelease release {session, "MyRelease"}; + + { + ScopedTrack track {session, "MyTrack"}; + { + auto transaction {session.createUniqueTransaction()}; + + track.get().modify()->setRelease(release.get()); + track.get().modify()->setName("MyTrackName"); + release.get().modify()->setName("MyReleaseName"); + } + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_TRUE(Release::getAllOrphans(session).empty()); + + EXPECT_EQ(release->getTracksCount(), 1); + ASSERT_EQ(release->getTracks().size(), 1); + EXPECT_EQ(release->getTracks().front()->getId(), track.getId()); + } + + { + auto transaction {session.createUniqueTransaction()}; + + ASSERT_TRUE(track->getRelease()); + EXPECT_EQ(track->getRelease()->getId(), release.getId()); + } + + { + auto transaction {session.createUniqueTransaction()}; + auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseName")}; + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front()->getId(), track.getId()); + } + { + auto transaction {session.createUniqueTransaction()}; + auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseFoo")}; + EXPECT_EQ(tracks.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + auto tracks {Track::getByNameAndReleaseName(session, "MyTrackFoo", "MyReleaseName")}; + EXPECT_EQ(tracks.size(), 0); + } + } + + { + auto transaction {session.createUniqueTransaction()}; + + EXPECT_TRUE(release->getTracks().empty()); + + auto releases {Release::getAllOrphans(session)}; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front()->getId(), release.getId()); + } +} + +TEST_F(DatabaseFixture, MulitpleReleaseSearchByName) +{ + ScopedRelease release1 {session, "MyRelease"}; + ScopedRelease release2 {session, "MyRelease%"}; + ScopedRelease release3 {session, "%MyRelease"}; + ScopedRelease release4 {session, "MyRelease%Foo"}; + ScopedRelease release5 {session, "Foo%MyRelease"}; + ScopedRelease release6 {session, "_yRelease"}; + + // filters does not work on orphans + ScopedTrack track1 {session, "MyTrack"}; + ScopedTrack track2 {session, "MyTrack"}; + ScopedTrack track3 {session, "MyTrack"}; + ScopedTrack track4 {session, "MyTrack"}; + ScopedTrack track5 {session, "MyTrack"}; + ScopedTrack track6 {session, "MyTrack"}; + + { + auto transaction {session.createUniqueTransaction()}; + + track1.get().modify()->setRelease(release1.get()); + track2.get().modify()->setRelease(release2.get()); + track3.get().modify()->setRelease(release3.get()); + track4.get().modify()->setRelease(release4.get()); + track5.get().modify()->setRelease(release5.get()); + track6.get().modify()->setRelease(release6.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool more; + { + const auto releases {Release::getByFilter(session, {}, {"Release"}, std::nullopt, more)}; + EXPECT_EQ(releases.size(), 6); + } + + { + const auto releases {Release::getByFilter(session, {}, {"MyRelease"}, std::nullopt, more)}; + EXPECT_EQ(releases.size(), 5); + EXPECT_TRUE(std::none_of(std::cbegin(releases), std::cend(releases), [&](const Release::pointer& release) { return release->getId() == release6.getId(); })); + } + { + const auto releases {Release::getByFilter(session, {}, {"MyRelease%"}, std::nullopt, more)}; + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0]->getId(), release2.getId()); + EXPECT_EQ(releases[1]->getId(), release4.getId()); + } + { + const auto releases {Release::getByFilter(session, {}, {"%MyRelease"}, std::nullopt, more)}; + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0]->getId(), release3.getId()); + EXPECT_EQ(releases[1]->getId(), release5.getId()); + } + { + const auto releases {Release::getByFilter(session, {}, {"Foo%MyRelease"}, std::nullopt, more)}; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0]->getId(), release5.getId()); + } + { + const auto releases {Release::getByFilter(session, {}, {"MyRelease%Foo"}, std::nullopt, more)}; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0]->getId(), release4.getId()); + } + } +} + +TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack) +{ + ScopedRelease release1 {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_FALSE(release1->getTotalTrack()); + EXPECT_FALSE(release1->getTotalDisc()); + } + + ScopedTrack track1 {session, "MyTrack"}; + { + auto transaction {session.createUniqueTransaction()}; + + track1.get().modify()->setRelease(release1.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_FALSE(release1->getTotalTrack()); + EXPECT_FALSE(release1->getTotalDisc()); + } + + { + auto transaction {session.createUniqueTransaction()}; + + track1.get().modify()->setTotalTrack(36); + track1.get().modify()->setTotalDisc(6); + } + + { + auto transaction {session.createSharedTransaction()}; + + ASSERT_TRUE(release1->getTotalTrack()); + EXPECT_EQ(*release1->getTotalTrack(), 36); + ASSERT_TRUE(release1->getTotalDisc()); + EXPECT_EQ(*release1->getTotalDisc(), 6); + } + + ScopedTrack track2 {session, "MyTrack2"}; + { + auto transaction {session.createUniqueTransaction()}; + + track2.get().modify()->setRelease(release1.get()); + track2.get().modify()->setTotalTrack(37); + track2.get().modify()->setTotalDisc(67); + } + + { + auto transaction {session.createSharedTransaction()}; + + ASSERT_TRUE(release1->getTotalTrack()); + EXPECT_EQ(*release1->getTotalTrack(), 37); + ASSERT_TRUE(release1->getTotalDisc()); + EXPECT_EQ(*release1->getTotalDisc(), 67); + } + + ScopedRelease release2 {session, "MyRelease2"}; + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_FALSE(release2->getTotalTrack()); + EXPECT_FALSE(release2->getTotalDisc()); + } + + ScopedTrack track3 {session, "MyTrack3"}; + { + auto transaction {session.createUniqueTransaction()}; + + track3.get().modify()->setRelease(release2.get()); + track3.get().modify()->setTotalTrack(7); + track3.get().modify()->setTotalDisc(5); + } + { + auto transaction {session.createSharedTransaction()}; + + ASSERT_TRUE(release1->getTotalTrack()); + EXPECT_EQ(*release1->getTotalTrack(), 37); + ASSERT_TRUE(release1->getTotalDisc()); + EXPECT_EQ(*release1->getTotalDisc(), 67); + ASSERT_TRUE(release2->getTotalTrack()); + EXPECT_EQ(*release2->getTotalTrack(), 7); + ASSERT_TRUE(release2->getTotalDisc()); + EXPECT_EQ(*release2->getTotalDisc(), 5); + } +} + +TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack) +{ + ScopedRelease release1 {session, "MyRelease1"}; + ScopedRelease release2 {session, "MyRelease2"}; + + ScopedTrack track1A {session, "MyTrack1A"}; + ScopedTrack track1B {session, "MyTrack1B"}; + ScopedTrack track2A {session, "MyTrack2A"}; + ScopedTrack track2B {session, "MyTrack2B"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_FALSE(release1->getFirstTrack()); + EXPECT_FALSE(release2->getFirstTrack()); + } + + { + auto transaction {session.createUniqueTransaction()}; + + track1A.get().modify()->setRelease(release1.get()); + track1B.get().modify()->setRelease(release1.get()); + track2A.get().modify()->setRelease(release2.get()); + track2B.get().modify()->setRelease(release2.get()); + + track1A.get().modify()->setTrackNumber(1); + track1B.get().modify()->setTrackNumber(2); + + track2A.get().modify()->setDiscNumber(2); + track2A.get().modify()->setTrackNumber(1); + track2B.get().modify()->setTrackNumber(2); + track2B.get().modify()->setDiscNumber(1); + } + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_TRUE(release1->getFirstTrack()); + EXPECT_TRUE(release2->getFirstTrack()); + + EXPECT_EQ(release1->getFirstTrack()->getId(), track1A.getId()); + EXPECT_EQ(release2->getFirstTrack()->getId(), track2B.getId()); + } +} + +TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate) +{ + ScopedRelease release1 {session, "MyRelease1"}; + ScopedRelease release2 {session, "MyRelease2"}; + const Wt::WDate release1Date {Wt::WDate {1994, 2, 3}}; + const Wt::WDate release1OriginalDate {Wt::WDate {1993, 4, 5}}; + + ScopedTrack track1A {session, "MyTrack1A"}; + ScopedTrack track1B {session, "MyTrack1B"}; + ScopedTrack track2A {session, "MyTrack2A"}; + ScopedTrack track2B {session, "MyTrack2B"}; + + { + auto transaction {session.createSharedTransaction()}; + + const auto releases {Release::getByYear(session, 0, 3000)}; + EXPECT_EQ(releases.size(), 0); + } + + { + auto transaction {session.createUniqueTransaction()}; + + track1A.get().modify()->setRelease(release1.get()); + track1B.get().modify()->setRelease(release1.get()); + track2A.get().modify()->setRelease(release2.get()); + track2B.get().modify()->setRelease(release2.get()); + + + track1A.get().modify()->setDate(release1Date); + track1B.get().modify()->setDate(release1Date); + track1A.get().modify()->setOriginalDate(release1OriginalDate); + track1B.get().modify()->setOriginalDate(release1OriginalDate); + + EXPECT_EQ(release1.get()->getReleaseYear(), release1Date.year()); + EXPECT_EQ(release1.get()->getReleaseYear(true), release1OriginalDate.year()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Release::getByYear(session, 1950, 2000)}; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front()->getId(), release1.getId()); + + releases = Release::getByYear(session, 1994, 1994); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front()->getId(), release1.getId()); + + releases = Release::getByYear(session, 1993, 1993); + ASSERT_EQ(releases.size(), 0); + } +} + diff --git a/src/test/database/Track.cpp b/src/test/database/Track.cpp index cb1759a9..ac919bff 100644 --- a/src/test/database/Track.cpp +++ b/src/test/database/Track.cpp @@ -87,3 +87,28 @@ TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) } } +TEST_F(DatabaseFixture, SingleTrackDate) +{ + ScopedTrack track {session, "MyTrack"}; + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(track->getYear(), std::nullopt); + EXPECT_EQ(track->getOriginalYear(), std::nullopt); + } + + { + auto transaction {session.createUniqueTransaction()}; + track.get().modify()->setDate(Wt::WDate {1995, 5, 5}); + track.get().modify()->setOriginalDate(Wt::WDate {1994, 2, 2}); + } + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(track->getYear(), 1995); + EXPECT_EQ(track->getOriginalYear(), 1994); + } +} + + + diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 04d92a1f..2b25401d 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -134,11 +134,11 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file) if (track->totalDisc) std::cout << "TotalDisc: " << *track->totalDisc << std::endl; - if (track->year) - std::cout << "Year: " << *track->year << std::endl; + if (track->date.isValid()) + std::cout << "Date: " << track->date.toString("yyyy-MM-dd") << std::endl; - if (track->originalYear) - std::cout << "Original year: " << *track->originalYear << std::endl; + if (track->originalDate.isValid()) + std::cout << "Original date: " << track->originalDate.toString("yyyy-MM-dd") << std::endl; std::cout << "HasCover = " << std::boolalpha << track->hasCover << std::endl; From 5aa0e835cdf9c390fe8e8b1697619170913ce00d Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 23 Sep 2021 13:21:24 +0200 Subject: [PATCH 14/25] Fixed regression on ID parameter parsing --- src/libs/subsonic/impl/ParameterParsing.hpp | 6 ++---- src/libs/subsonic/impl/SubsonicResponse.hpp | 11 ----------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/libs/subsonic/impl/ParameterParsing.hpp b/src/libs/subsonic/impl/ParameterParsing.hpp index d805092f..bb8a91d3 100644 --- a/src/libs/subsonic/impl/ParameterParsing.hpp +++ b/src/libs/subsonic/impl/ParameterParsing.hpp @@ -40,10 +40,8 @@ namespace API::Subsonic for (const std::string& param : it->second) { auto value {StringUtils::readAs(param)}; - if (!value) - throw BadParameterFormatGenericError {paramName}; - - res.emplace_back(std::move(*value)); + if (value) + res.emplace_back(std::move(*value)); } return res; diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index a19c195d..dfc6b19b 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -178,17 +178,6 @@ class BadParameterGenericError : public GenericError const std::string _parameterName; }; -class BadParameterFormatGenericError : public GenericError -{ - public: - BadParameterFormatGenericError(const std::string& parameterName) : _parameterName {parameterName} {} - - private: - std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad format"; } - - const std::string _parameterName; -}; - class Response { public: From 828e2364a7b9275725d143fa587a6395568a9aaa Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 24 Sep 2021 12:37:54 +0200 Subject: [PATCH 15/25] Subsonic API: better compability with clients --- README.md | 6 +- conf/lms.conf | 4 + src/libs/subsonic/CMakeLists.txt | 1 + src/libs/subsonic/impl/ClientInfo.hpp | 34 ++ src/libs/subsonic/impl/ProtocolVersion.cpp | 56 ++++ src/libs/subsonic/impl/ProtocolVersion.hpp | 40 +++ src/libs/subsonic/impl/RequestContext.hpp | 5 +- src/libs/subsonic/impl/Scan.cpp | 4 +- src/libs/subsonic/impl/SubsonicResource.cpp | 295 +++++++++--------- src/libs/subsonic/impl/SubsonicResource.hpp | 55 ++++ src/libs/subsonic/impl/SubsonicResponse.cpp | 27 +- src/libs/subsonic/impl/SubsonicResponse.hpp | 11 +- .../include/subsonic/SubsonicResource.hpp | 18 +- src/libs/utils/impl/Config.cpp | 35 ++- src/libs/utils/impl/Config.hpp | 1 + src/libs/utils/include/utils/IConfig.hpp | 10 +- src/lms/main.cpp | 7 +- 17 files changed, 405 insertions(+), 204 deletions(-) create mode 100644 src/libs/subsonic/impl/ClientInfo.hpp create mode 100644 src/libs/subsonic/impl/ProtocolVersion.cpp create mode 100644 src/libs/subsonic/impl/ProtocolVersion.hpp create mode 100644 src/libs/subsonic/impl/SubsonicResource.hpp diff --git a/README.md b/README.md index a20d26d8..b765f0ac 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/conf/lms.conf b/conf/lms.conf index 550db81c..ab0ba1c9 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -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; diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 4c87d7cd..98093601 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(lmssubsonic SHARED + impl/ProtocolVersion.cpp impl/Scan.cpp impl/Stream.cpp impl/SubsonicId.cpp diff --git a/src/libs/subsonic/impl/ClientInfo.hpp b/src/libs/subsonic/impl/ClientInfo.hpp new file mode 100644 index 00000000..9c2dafd0 --- /dev/null +++ b/src/libs/subsonic/impl/ClientInfo.hpp @@ -0,0 +1,34 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include "ProtocolVersion.hpp" + +namespace API::Subsonic +{ + struct ClientInfo + { + std::string name; + std::string user; + std::string password; + ProtocolVersion version; + }; +} diff --git a/src/libs/subsonic/impl/ProtocolVersion.cpp b/src/libs/subsonic/impl/ProtocolVersion.cpp new file mode 100644 index 00000000..4a02a4e3 --- /dev/null +++ b/src/libs/subsonic/impl/ProtocolVersion.cpp @@ -0,0 +1,56 @@ +/* + * 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 . + */ + +#include "ProtocolVersion.hpp" + +namespace StringUtils +{ + template<> + std::optional + readAs(std::string_view str) + { + // Expects "X.Y.Z" + const auto numbers {StringUtils::splitString(str, ".")}; + if (numbers.size() < 2 || numbers.size() > 3) + return std::nullopt; + + API::Subsonic::ProtocolVersion version; + + auto number {StringUtils::readAs(numbers[0])}; + if (!number) + return std::nullopt; + version.major = *number; + + number = {StringUtils::readAs(numbers[1])}; + if (!number) + return std::nullopt; + version.minor = *number; + + if (numbers.size() == 3) + { + number = {StringUtils::readAs(numbers[2])}; + if (!number) + return std::nullopt; + version.patch = *number; + } + + return version; + } +} + diff --git a/src/libs/subsonic/impl/ProtocolVersion.hpp b/src/libs/subsonic/impl/ProtocolVersion.hpp new file mode 100644 index 00000000..fc046e22 --- /dev/null +++ b/src/libs/subsonic/impl/ProtocolVersion.hpp @@ -0,0 +1,40 @@ +/* + * 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 . + */ + +#pragma once + +#include "utils/String.hpp" + +namespace API::Subsonic +{ + struct ProtocolVersion + { + unsigned major {}; + unsigned minor {}; + unsigned patch {}; + }; + + static inline constexpr ProtocolVersion defaultServerProtocolVersion {1, 16, 0}; +} + +namespace StringUtils +{ + template<> std::optional readAs(std::string_view str); +} + diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index d2949a8b..9d73bcb4 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -24,6 +24,8 @@ #include #include "database/Types.hpp" +#include "ClientInfo.hpp" +#include "ProtocolVersion.hpp" namespace Database { @@ -37,7 +39,8 @@ namespace API::Subsonic const Wt::Http::ParameterMap& parameters; Database::Session& dbSession; Database::UserId userId; - std::string clientName; + ClientInfo clientInfo; + ProtocolVersion serverProtocolVersion; }; } diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index 9d57fb17..dee3c082 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -52,7 +52,7 @@ namespace API::Subsonic::Scan Response handleGetScanStatus(RequestContext& context) { - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; response.addNode("scanStatus", createStatusResponseNode()); return response; @@ -63,7 +63,7 @@ namespace API::Subsonic::Scan { Service::get()->requestImmediateScan(false); - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; response.addNode("scanStatus", createStatusResponseNode()); return response; diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index a3f8b3f4..dc9a904d 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -16,7 +16,8 @@ * You should have received a copy of the GNU General Public License * along with LMS. If not, see . */ -#include "subsonic/SubsonicResource.hpp" + +#include "SubsonicResource.hpp" #include #include @@ -39,12 +40,14 @@ #include "database/User.hpp" #include "recommendation/IEngine.hpp" #include "scrobbling/IScrobbling.hpp" +#include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Random.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" #include "utils/Utils.hpp" #include "ParameterParsing.hpp" +#include "ProtocolVersion.hpp" #include "RequestContext.hpp" #include "Scan.hpp" #include "Stream.hpp" @@ -58,56 +61,18 @@ static const std::string reportedStarredDate {"2000-01-01T00:00:00"}; static const std::string reportedDummyDate {"2000-01-01T00:00:00"}; static const unsigned long long reportedDummyDateULong {946684800000ULL}; // 2000-01-01T00:00:00 UTC -namespace API::Subsonic -{ - struct ClientVersion - { - unsigned major {}; - unsigned minor {}; - unsigned patch {}; - }; -} -namespace StringUtils -{ - template<> - std::optional - readAs(std::string_view str) - { - // Expects "X.Y.Z" - const auto numbers {StringUtils::splitString(str, ".")}; - if (numbers.size() < 2 || numbers.size() > 3) - return std::nullopt; - - API::Subsonic::ClientVersion version; - - auto number {StringUtils::readAs(numbers[0])}; - if (!number) - return std::nullopt; - version.major = *number; - - number = {StringUtils::readAs(numbers[1])}; - if (!number) - return std::nullopt; - version.minor = *number; - - if (numbers.size() == 3) - { - number = {StringUtils::readAs(numbers[2])}; - if (!number) - return std::nullopt; - version.patch = *number; - } - - return version; - } - -} namespace API::Subsonic { +std::unique_ptr +createSubsonicResource(Database::Db& db) +{ + return std::make_unique(db); +} + static void checkSetPasswordImplemented() @@ -140,38 +105,24 @@ decodePasswordIfNeeded(const std::string& password) return password; } -struct ClientInfo -{ - std::string name; - std::string user; - std::string password; - ClientVersion version; -}; - static -ClientInfo -getClientInfo(const Wt::Http::ParameterMap& parameters) +std::unordered_map +readConfigProtocolVersions() { - ClientInfo res; + std::unordered_map res; - // Mandatory parameters - res.name = getMandatoryParameterAs(parameters, "c"); - res.version = getMandatoryParameterAs(parameters, "v"); - if (res.version.major > API_VERSION_MAJOR) - throw ServerMustUpgradeError {}; - if (res.version.major < API_VERSION_MAJOR) - throw ClientMustUpgradeError {}; - if (res.version.minor > Response::getAPIMinorVersion(res.name)) - throw ServerMustUpgradeError {}; - - res.user = getMandatoryParameterAs(parameters, "u"); - res.password = decodePasswordIfNeeded(getMandatoryParameterAs(parameters, "p")); + Service::get()->visitStrings("api-subsonic-report-old-server-protocol", + [&](std::string_view client) + { + res.emplace(std::string {client}, ProtocolVersion {1, 12, 0}); + }, {"DSub"}); return res; } SubsonicResource::SubsonicResource(Db& db) -: _db {db} +: _serverProtocolVersionsByClient {readConfigProtocolVersions()} +, _db {db} { } @@ -530,7 +481,7 @@ static Response handlePingRequest(RequestContext& context) { - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -570,7 +521,7 @@ handleChangePassword(RequestContext& context) throw UserNotAuthorizedError {}; } - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -620,7 +571,7 @@ handleCreatePlaylistRequest(RequestContext& context) TrackListEntry::create(context.dbSession, track, tracklist ); } - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -671,7 +622,7 @@ handleCreateUserRequest(RequestContext& context) throw UserNotAuthorizedError {}; } - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -696,7 +647,7 @@ handleDeletePlaylistRequest(RequestContext& context) tracklist.remove(); - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -717,14 +668,14 @@ handleDeleteUserRequest(RequestContext& context) user.remove(); - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static Response handleGetLicenseRequest(RequestContext& context) { - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& licenseNode {response.createNode("license")}; licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43"); @@ -750,7 +701,7 @@ handleGetRandomSongsRequest(RequestContext& context) auto tracks {Track::getAllRandom(context.dbSession, {}, size)}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& randomSongsNode {response.createNode("randomSongs")}; for (const Track::pointer& track : tracks) @@ -839,7 +790,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) else throw NotImplementedGenericError {}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")}; for (const Release::pointer& release : releases) @@ -879,7 +830,7 @@ handleGetAlbumRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)}; auto tracks {release->getTracks()}; @@ -908,7 +859,7 @@ handleGetArtistRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)}; auto releases {artist->getReleases()}; @@ -930,7 +881,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) // Optional params std::size_t count {getParameterAs(context.parameters, "count").value_or(20)}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")}; { @@ -986,7 +937,7 @@ static Response handleGetArtistsRequest(RequestContext& context) { - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& artistsNode {response.createNode("artists")}; artistsNode.setAttribute("ignoredArticles", ""); @@ -1040,7 +991,7 @@ handleGetMusicDirectoryRequest(RequestContext& context) if (!root && !artistId && !releaseId && !trackId) throw BadParameterGenericError {"id"}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& directoryNode {response.createNode("directory")}; auto transaction {context.dbSession.createSharedTransaction()}; @@ -1097,7 +1048,7 @@ static Response handleGetMusicFoldersRequest(RequestContext& context) { - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& musicFoldersNode {response.createNode("musicFolders")}; Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")}; @@ -1111,7 +1062,7 @@ static Response handleGetGenresRequest(RequestContext& context) { - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& genresNode {response.createNode("genres")}; @@ -1133,7 +1084,7 @@ static Response handleGetIndexesRequest(RequestContext& context) { - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& artistsNode {response.createNode("indexes")}; artistsNode.setAttribute("ignoredArticles", ""); @@ -1216,7 +1167,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) Random::shuffleContainer(tracks); - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")}; for (const Track::pointer& track : tracks) similarSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); @@ -1248,7 +1199,7 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")}; { @@ -1324,7 +1275,7 @@ handleGetPlaylistRequest(RequestContext& context) if (!tracklist) throw RequestedDataNotFoundError {}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node playlistNode {tracklistToResponseNode(tracklist, context.dbSession)}; auto entries {tracklist->getEntries()}; @@ -1346,7 +1297,7 @@ handleGetPlaylistsRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& playlistsNode {response.createNode("playlists")}; auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)}; @@ -1383,7 +1334,7 @@ handleGetSongsByGenreRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& songsByGenreNode {response.createNode("songsByGenre")}; bool more; @@ -1408,7 +1359,7 @@ handleGetUserRequest(RequestContext& context) if (!user) throw RequestedDataNotFoundError {}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; response.addNode("user", userToResponseNode(user)); return response; @@ -1420,7 +1371,7 @@ handleGetUsersRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& usersNode {response.createNode("users")}; const auto users {User::getAll(context.dbSession)}; @@ -1453,7 +1404,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")}; bool more; @@ -1538,7 +1489,7 @@ handleStarRequest(RequestContext& context) user.modify()->starTrack(track); } - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -1595,7 +1546,7 @@ handleUnstarRequest(RequestContext& context) } - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -1639,7 +1590,7 @@ handleScrobble(RequestContext& context) } } - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -1682,7 +1633,7 @@ handleUpdateUserRequest(RequestContext& context) } } - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -1741,7 +1692,7 @@ handleUpdatePlaylistRequest(RequestContext& context) TrackListEntry::create(context.dbSession, track, tracklist); } - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -1756,7 +1707,7 @@ handleGetBookmarks(RequestContext& context) const auto bookmarks {TrackBookmark::getByUser(context.dbSession, user)}; - Response response {Response::createOkResponse(context)}; + Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& bookmarksNode {response.createNode("bookmarks")}; for (const TrackBookmark::pointer& bookmark : bookmarks) @@ -1798,7 +1749,7 @@ handleCreateBookmark(RequestContext& context) if (comment) bookmark.modify()->setComment(*comment); - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -1824,7 +1775,7 @@ handleDeleteBookmark(RequestContext& context) bookmark.remove(); - return Response::createOkResponse(context); + return Response::createOkResponse(context.serverProtocolVersion); } static @@ -1867,7 +1818,7 @@ struct RequestEntryPointInfo CheckImplementedFunc checkFunc {}; }; -static std::unordered_map requestEntryPoints +static const std::unordered_map requestEntryPoints { // System {"ping", {handlePingRequest}}, @@ -1981,38 +1932,6 @@ static std::unordered_map mediaRetrieval {"getCoverArt", handleGetCoverArt}, }; -static -Database::UserId -authenticateUser(const Wt::Http::Request &request, const ClientInfo& clientInfo, Session& dbSession) -{ - if (auto *authEnvService {Service<::Auth::IEnvService>::get()}) - { - const auto checkResult {authEnvService->processRequest(dbSession, request)}; - if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted) - throw UserNotAuthorizedError {}; - - return *checkResult.userId; - } - else if (auto *authPasswordService {Service<::Auth::IPasswordService>::get()}) - { - const auto checkResult {authPasswordService->checkUserPassword(dbSession, - boost::asio::ip::address::from_string(request.clientAddress()), - clientInfo.user, clientInfo.password)}; - - switch (checkResult.state) - { - case Auth::IPasswordService::CheckResult::State::Granted: - return *checkResult.userId; - break; - case Auth::IPasswordService::CheckResult::State::Denied: - throw WrongUsernameOrPasswordError {}; - case Auth::IPasswordService::CheckResult::State::Throttled: - throw LoginThrottledGenericError {}; - } - } - - throw InternalErrorGenericError {"No service avalaible to authenticate user"}; -} void SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) @@ -2027,24 +1946,16 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp if (StringUtils::stringEndsWith(requestPath, ".view")) requestPath.resize(requestPath.length() - 5); - const Wt::Http::ParameterMap& parameters {request.getParameterMap()}; - // Optional parameters - const ResponseFormat format {getParameterAs(parameters, "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml}; + const ResponseFormat format {getParameterAs(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml}; - std::string clientName; + ProtocolVersion protocolVersion {defaultServerProtocolVersion}; try { - // Mandatory parameters - const ClientInfo clientInfo {getClientInfo(parameters)}; - - clientName = clientInfo.name; - - Session& dbSession {_db.getTLSSession()}; - - const Database::UserId userId {authenticateUser(request, clientInfo, dbSession)}; - RequestContext requestContext {parameters, dbSession, userId, clientInfo.name}; + // We need to parse client a soon as possible to make sure to answer with the right protocol version + protocolVersion = getServerProtocolVersion(getMandatoryParameterAs(request.getParameterMap(), "c")); + RequestContext requestContext {buildRequestContext(request)}; auto itEntryPoint {requestEntryPoints.find(requestPath)}; if (itEntryPoint != requestEntryPoints.end()) @@ -2079,11 +1990,97 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << requestPath << "'" << ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]" << ", code = " << static_cast(e.getCode()) << ", msg = '" << e.getMessage() << "'"; - Response resp {Response::createFailedResponse(clientName, e)}; + Response resp {Response::createFailedResponse(protocolVersion, e)}; resp.write(response.out(), format); response.setMimeType(ResponseFormatToMimeType(format)); } } +ProtocolVersion +SubsonicResource::getServerProtocolVersion(const std::string& clientName) const +{ + auto it {_serverProtocolVersionsByClient.find(clientName)}; + if (it == std::cend(_serverProtocolVersionsByClient)) + return defaultServerProtocolVersion; + + return it->second; +} + +void +SubsonicResource::checkProtocolVersion(ProtocolVersion client, ProtocolVersion server) +{ + if (client.major > server.major) + throw ServerMustUpgradeError {}; + if (client.major < server.major) + throw ClientMustUpgradeError {}; + if (client.minor > server.minor) + throw ServerMustUpgradeError {}; + else if (client.minor == server.minor) + { + if (client.patch > server.patch) + throw ServerMustUpgradeError {}; + } +} + +ClientInfo +SubsonicResource::getClientInfo(const Wt::Http::ParameterMap& parameters) +{ + ClientInfo res; + + // Mandatory parameters + res.name = getMandatoryParameterAs(parameters, "c"); + res.version = getMandatoryParameterAs(parameters, "v"); + res.user = getMandatoryParameterAs(parameters, "u"); + res.password = decodePasswordIfNeeded(getMandatoryParameterAs(parameters, "p")); + + return res; +} + +RequestContext +SubsonicResource::buildRequestContext(const Wt::Http::Request& request) +{ + const Wt::Http::ParameterMap& parameters {request.getParameterMap()}; + + const ClientInfo clientInfo {getClientInfo(parameters)}; + + Session& dbSession {_db.getTLSSession()}; + + const Database::UserId userId {authenticateUser(request, clientInfo, dbSession)}; + + return {parameters, dbSession, userId, clientInfo, getServerProtocolVersion(clientInfo.name)}; +} + +Database::UserId +SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo, Session& dbSession) +{ + if (auto *authEnvService {Service<::Auth::IEnvService>::get()}) + { + const auto checkResult {authEnvService->processRequest(dbSession, request)}; + if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted) + throw UserNotAuthorizedError {}; + + return *checkResult.userId; + } + else if (auto *authPasswordService {Service<::Auth::IPasswordService>::get()}) + { + const auto checkResult {authPasswordService->checkUserPassword(dbSession, + boost::asio::ip::address::from_string(request.clientAddress()), + clientInfo.user, clientInfo.password)}; + + switch (checkResult.state) + { + case Auth::IPasswordService::CheckResult::State::Granted: + return *checkResult.userId; + break; + case Auth::IPasswordService::CheckResult::State::Denied: + throw WrongUsernameOrPasswordError {}; + case Auth::IPasswordService::CheckResult::State::Throttled: + throw LoginThrottledGenericError {}; + } + } + + throw InternalErrorGenericError {"No service avalaible to authenticate user"}; +} + } // namespace api::subsonic diff --git a/src/libs/subsonic/impl/SubsonicResource.hpp b/src/libs/subsonic/impl/SubsonicResource.hpp new file mode 100644 index 00000000..fd53029b --- /dev/null +++ b/src/libs/subsonic/impl/SubsonicResource.hpp @@ -0,0 +1,55 @@ +/* + * 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 . + */ +#pragma once + +#include +#include +#include + +#include "database/Types.hpp" +#include "ClientInfo.hpp" +#include "RequestContext.hpp" + +namespace Database +{ + class Db; +} + +namespace API::Subsonic +{ + + class SubsonicResource final : public Wt::WResource + { + public: + SubsonicResource(Database::Db& db); + + private: + void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override; + ProtocolVersion getServerProtocolVersion(const std::string& clientName) const; + + static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server); + ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters); + RequestContext buildRequestContext(const Wt::Http::Request& request); + Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo, Database::Session& dbSession); + + const std::unordered_map _serverProtocolVersionsByClient; + Database::Db& _db; + }; + +} // namespace diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index 47c47fcb..38f6b631 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -29,6 +29,7 @@ #include "utils/Exception.hpp" #include "utils/String.hpp" +#include "ProtocolVersion.hpp" namespace API::Subsonic { @@ -102,26 +103,32 @@ Response::Node::createArrayChild(const std::string& key) return _childrenArrays[key].back(); } +void +Response::Node::setVersionAttribute(ProtocolVersion protocolVersion) +{ + setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch)); +} + Response -Response::createOkResponse(const RequestContext& context) +Response::createOkResponse(ProtocolVersion protocolVersion) { Response response; Node& responseNode {response._root.createChild("subsonic-response")}; responseNode.setAttribute("status", "ok"); - responseNode.setAttribute("version", std::string {QUOTEME(API_VERSION_MAJOR) "."} + std::to_string(getAPIMinorVersion(context.clientName)) + ".0"); + responseNode.setVersionAttribute(protocolVersion); return response; } Response -Response::createFailedResponse(std::string_view clientName, const Error& error) +Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error) { Response response; Node& responseNode {response._root.createChild("subsonic-response")}; responseNode.setAttribute("status", "failed"); - responseNode.setAttribute("version", std::string {QUOTEME(API_VERSION_MAJOR) "."} + std::to_string(getAPIMinorVersion(clientName)) + ".0"); + responseNode.setVersionAttribute(protocolVersion); Node& errorNode {responseNode.createChild("error")}; errorNode.setAttribute("code", std::to_string(static_cast(error.getCode()))); @@ -214,18 +221,6 @@ Response::writeXML(std::ostream& os) boost::property_tree::write_xml(os, root); } -unsigned -Response::getAPIMinorVersion(std::string_view clientName) -{ - // Some clients do not rely on version to enable the clear text password auth scheme - if (clientName == "Audinaut") - return 16; - else if (clientName == "Sublime Music") - return 16; - else - return 12; -} - void Response::writeJSON(std::ostream& os) { diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index dfc6b19b..c0d49b1e 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -27,8 +27,6 @@ #include "RequestContext.hpp" -#define API_VERSION_MAJOR 1 - namespace API::Subsonic { @@ -205,6 +203,9 @@ class Response void addArrayChild(const std::string& key, Node node); private: + + void setVersionAttribute(ProtocolVersion version); + friend class Response; using Value = std::variant; std::map _attributes; @@ -213,8 +214,8 @@ class Response std::map> _childrenArrays; }; - static Response createOkResponse(const RequestContext& context); - static Response createFailedResponse(std::string_view clientName, const Error& error); + static Response createOkResponse(ProtocolVersion protocolVersion); + static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error); virtual ~Response() {} Response(const Response&) = delete; @@ -228,9 +229,7 @@ class Response void write(std::ostream& os, ResponseFormat format); - static unsigned getAPIMinorVersion(std::string_view clientName); private: - void writeJSON(std::ostream& os); void writeXML(std::ostream& os); diff --git a/src/libs/subsonic/include/subsonic/SubsonicResource.hpp b/src/libs/subsonic/include/subsonic/SubsonicResource.hpp index 6d3e14b9..2354f787 100644 --- a/src/libs/subsonic/include/subsonic/SubsonicResource.hpp +++ b/src/libs/subsonic/include/subsonic/SubsonicResource.hpp @@ -18,8 +18,9 @@ */ #pragma once +#include + #include -#include namespace Database { @@ -28,18 +29,5 @@ namespace Database namespace API::Subsonic { - -class SubsonicResource final : public Wt::WResource -{ - public: - SubsonicResource(Database::Db& db); - - static std::string getPath() { return "rest/"; } - private: - - void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override; - - Database::Db& _db; -}; - + std::unique_ptr createSubsonicResource(Database::Db& db); } // namespace diff --git a/src/libs/utils/impl/Config.cpp b/src/libs/utils/impl/Config.cpp index ba19cc0a..e53b90c5 100644 --- a/src/libs/utils/impl/Config.cpp +++ b/src/libs/utils/impl/Config.cpp @@ -50,7 +50,8 @@ Config::Config(const std::filesystem::path& p) std::string_view Config::getString(std::string_view setting, std::string_view def) { - try { + try + { return static_cast(_config.lookup(std::string {setting})); } catch (libconfig::ConfigException&) @@ -59,10 +60,30 @@ Config::getString(std::string_view setting, std::string_view def) } } +void +Config::visitStrings(std::string_view setting, std::function _func, std::initializer_list defs) +{ + try + { + const libconfig::Setting& values {_config.lookup(std::string {setting})}; + for (int i {}; i < values.getLength(); ++i) + _func(static_cast(values[i])); + } + catch (const libconfig::SettingNotFoundException&) + { + for (std::string_view def : defs) + _func(def); + } + catch (libconfig::ConfigException&) + { + } +} + std::filesystem::path Config::getPath(std::string_view setting, const std::filesystem::path& path) { - try { + try + { const char* res {_config.lookup(std::string {setting})}; return std::filesystem::path {std::string(res)}; } @@ -75,7 +96,8 @@ Config::getPath(std::string_view setting, const std::filesystem::path& path) unsigned long Config::getULong(std::string_view setting, unsigned long def) { - try { + try + { return static_cast(_config.lookup(std::string {setting})); } catch (libconfig::ConfigException&) @@ -87,7 +109,8 @@ Config::getULong(std::string_view setting, unsigned long def) long Config::getLong(std::string_view setting, long def) { - try { + try + { return _config.lookup(std::string {setting}); } catch (libconfig::ConfigException&) @@ -99,7 +122,8 @@ Config::getLong(std::string_view setting, long def) bool Config::getBool(std::string_view setting, bool def) { - try { + try + { return _config.lookup(std::string {setting}); } catch (libconfig::ConfigException&) @@ -108,4 +132,3 @@ Config::getBool(std::string_view setting, bool def) } } - diff --git a/src/libs/utils/impl/Config.hpp b/src/libs/utils/impl/Config.hpp index 7dce0423..cbbb2542 100644 --- a/src/libs/utils/impl/Config.hpp +++ b/src/libs/utils/impl/Config.hpp @@ -36,6 +36,7 @@ class Config final : public IConfig // Default values are returned in case of setting not found std::string_view getString(std::string_view setting, std::string_view def = "") override; + void visitStrings(std::string_view setting, std::function _func, std::initializer_list defs) override; std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) override; unsigned long getULong(std::string_view setting, unsigned long def = 0) override; long getLong(std::string_view setting, long def = 0) override; diff --git a/src/libs/utils/include/utils/IConfig.hpp b/src/libs/utils/include/utils/IConfig.hpp index 409d3702..975e5d6f 100644 --- a/src/libs/utils/include/utils/IConfig.hpp +++ b/src/libs/utils/include/utils/IConfig.hpp @@ -18,8 +18,9 @@ */ #pragma once -#include #include +#include +#include // Used to get config values from configuration files class IConfig @@ -30,10 +31,11 @@ class IConfig // Default values are returned in case of setting not found virtual std::string_view getString(std::string_view setting, std::string_view def = "") = 0; + virtual void visitStrings(std::string_view setting, std::function _func, std::initializer_list def = {}) = 0; virtual std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) = 0; - virtual unsigned long getULong(std::string_view setting, unsigned long def = 0) = 0; - virtual long getLong(std::string_view setting, long def = 0) = 0; - virtual bool getBool(std::string_view setting, bool def = false) = 0; + virtual unsigned long getULong(std::string_view setting, unsigned long def = 0) = 0; + virtual long getLong(std::string_view setting, long def = 0) = 0; + virtual bool getBool(std::string_view setting, bool def = false) = 0; }; diff --git a/src/lms/main.cpp b/src/lms/main.cpp index ae7b34e1..98c0cec6 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -267,11 +267,14 @@ int main(int argc, char* argv[]) Service scrobblingService {Scrobbling::createScrobbling(ioContext, database)}; - API::Subsonic::SubsonicResource subsonicResource {database}; + std::unique_ptr subsonicResource; // bind API resources if (config->getBool("api-subsonic", true)) - server.addResource(&subsonicResource, subsonicResource.getPath()); + { + subsonicResource = API::Subsonic::createSubsonicResource(database); + server.addResource(subsonicResource.get(), "rest/"); + } // bind UI entry point server.addEntryPoint(Wt::EntryPointType::Application, From 43fcb2a0d754b231320ae03e50929f164beb5aac Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 24 Sep 2021 20:24:00 +0200 Subject: [PATCH 16/25] Fixes clang build --- src/libs/av/impl/Transcoder.hpp | 1 - src/libs/database/include/database/Types.hpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/av/impl/Transcoder.hpp index 0ebb9179..54ebe5e5 100644 --- a/src/libs/av/impl/Transcoder.hpp +++ b/src/libs/av/impl/Transcoder.hpp @@ -64,7 +64,6 @@ namespace Av std::unique_ptr _childProcess; - bool _finished {}; std::string _outputMimeType; }; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 590518e3..c2fb4146 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -124,7 +124,7 @@ namespace Database using pointer = ObjectPtr; using IdType = ObjectIdType; - IdType getId() const { return Wt::Dbo::Dbo::self()->Wt::Dbo::Dbo::id(); } + IdType getId() const { return Wt::Dbo::Dbo::self()->Wt::Dbo::template Dbo::id(); } // catch some misuses typename Wt::Dbo::dbo_traits::IdType id() const = delete; From c36a0ca58d84c12100fc54362b897cbaa5263023 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 24 Sep 2021 20:25:47 +0200 Subject: [PATCH 17/25] Pushed timeout --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ebefc773..cb266ed7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,7 @@ on: jobs: Docker: name: Docker images + timeout-minutes: 720 environment: name: Docker runs-on: ubuntu-latest From d4d75236e08401c3e4558b878a0f5faefaee3a06 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 25 Sep 2021 10:30:46 +0200 Subject: [PATCH 18/25] Raised timeout --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb266ed7..30f1a5f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,6 @@ on: jobs: Docker: name: Docker images - timeout-minutes: 720 environment: name: Docker runs-on: ubuntu-latest @@ -51,6 +50,7 @@ jobs: - name: Build and push id: docker_build + timeout-minutes: 720 uses: docker/build-push-action@v2 with: context: ./ From 5acb5e4f739b01efcf21947c1f738a6a4d3162de Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Sep 2021 14:47:24 +0200 Subject: [PATCH 19/25] WIP --- CMakeLists.txt | 10 +++----- Dockerfile-build | 43 +++++++++++++++++++------------- Dockerfile-release | 3 --- cmake/modules/FindFFMPEGAV.cmake | 23 ----------------- src/libs/av/CMakeLists.txt | 3 +-- src/libs/metadata/CMakeLists.txt | 4 ++- src/libs/som/CMakeLists.txt | 3 ++- 7 files changed, 36 insertions(+), 53 deletions(-) delete mode 100644 cmake/modules/FindFFMPEGAV.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 149ebd7a..b14fb02e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,16 +14,15 @@ 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) @@ -70,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) diff --git a/Dockerfile-build b/Dockerfile-build index bd39c8a5..a589d12a 100644 --- a/Dockerfile-build +++ b/Dockerfile-build @@ -1,28 +1,39 @@ -FROM alpine:3.14 AS build + +FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx + +FROM --platform=$BUILDPLATFORM alpine:3.14 +ARG TARGETPLATFORM WORKDIR /tmp/workdir -ARG MAKEFLAGS="-j2" - ARG PREFIX="/tmp/install" -ARG BUILD_PACKAGES=" \ - ca-certificates \ - curl \ - coreutils \ - bzip2 \ - pkgconfig \ - g++ \ - ffmpeg-dev \ +ARG BUILD_PACKAGES="\ + clang \ make \ cmake \ + lld \ + curl \ + tree \ + 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" -RUN apk add --no-cache --update ${BUILD_PACKAGES} +# copy xx scripts to your build stage +COPY --from=xx / / + +RUN xx-apk add --no-scripts --no-cache ${LMS_BUILD_PACKAGES} # STB ARG STB_VERSION=b42009b3b9d4ca35bc703f5310eedc74f584be58 @@ -37,8 +48,6 @@ RUN \ COPY . /tmp/lms/ ARG LMS_BUILD_TYPE="Release" RUN \ - DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \ - CXXFLAGS="-I${PREFIX}/include" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} && \ - VERBOSE=1 make && \ - make test - + DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \ + PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} $(xx-clang --print-cmake-defines) -DCMAKE_PREFIX_PATH=/$(xx-info)/usr/lib/cmake -DBUILD_TESTING=OFF && \ + VERBOSE=1 make -j$(nproc) diff --git a/Dockerfile-release b/Dockerfile-release index 3d1b59bb..a30db9f2 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -2,8 +2,6 @@ FROM alpine:3.14 AS build WORKDIR /tmp/workdir -ARG MAKEFLAGS="-j2" - ARG PREFIX="/tmp/install" ARG BUILD_PACKAGES=" \ @@ -131,7 +129,6 @@ RUN \ ## Release Stage FROM alpine:3.14 AS release LABEL maintainer="Emeric Poupon " -LABEL version=${LMS_VERSION} ARG RUNTIME_PACKAGES=" \ libssl1.1 \ diff --git a/cmake/modules/FindFFMPEGAV.cmake b/cmake/modules/FindFFMPEGAV.cmake deleted file mode 100644 index 896c10b4..00000000 --- a/cmake/modules/FindFFMPEGAV.cmake +++ /dev/null @@ -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) - - diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt index c42826bf..4205e35c 100644 --- a/src/libs/av/CMakeLists.txt +++ b/src/libs/av/CMakeLists.txt @@ -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) diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt index 8ba7b93d..90b6fd92 100644 --- a/src/libs/metadata/CMakeLists.txt +++ b/src/libs/metadata/CMakeLists.txt @@ -1,5 +1,7 @@ -add_subdirectory(test) +if(BUILD_TESTING) + add_subdirectory(test) +endif() add_library(lmsmetadata SHARED impl/AvFormatParser.cpp diff --git a/src/libs/som/CMakeLists.txt b/src/libs/som/CMakeLists.txt index 2e19b9d2..4c967429 100644 --- a/src/libs/som/CMakeLists.txt +++ b/src/libs/som/CMakeLists.txt @@ -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) From c216e7c697dc88d25032920dce0d56ed79d381a9 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Sep 2021 14:48:29 +0200 Subject: [PATCH 20/25] WIP --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d23fd62..bc06035c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,9 +18,6 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v1 From ada50d80c8a96bb7586e6492f6c0cabcf0af4879 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Sep 2021 15:44:53 +0200 Subject: [PATCH 21/25] Fixed clang warning --- src/libs/subsonic/impl/SubsonicResource.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index dc9a904d..403e4ba4 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -1585,7 +1585,7 @@ handleScrobble(RequestContext& context) { const TrackId trackId {ids[i]}; const unsigned long time {times[i]}; - Service::get()->addTimedListen({context.userId, trackId, Wt::WDateTime::fromTime_t(static_cast(time / 1000))}); + Service::get()->addTimedListen({{context.userId, trackId}, Wt::WDateTime::fromTime_t(static_cast(time / 1000))}); } } } From 496e9161e996fb7bbd37c28e9086d7fc85201e40 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Sep 2021 15:53:53 +0200 Subject: [PATCH 22/25] Fixed clang warning --- src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp | 2 +- src/lms/ui/explore/SearchView.cpp | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp index dd0b14a7..8a1809cc 100644 --- a/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ b/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -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->getId(), listenedAt}); + result.matchedListens.emplace_back(Scrobbling::TimedListen {{userId, track->getId()}, listenedAt}); } } catch (const Wt::WException& error) diff --git a/src/lms/ui/explore/SearchView.cpp b/src/lms/ui/explore/SearchView.cpp index 83651022..90a950e8 100644 --- a/src/lms/ui/explore/SearchView.cpp +++ b/src/lms/ui/explore/SearchView.cpp @@ -38,8 +38,6 @@ #include "ReleaseListHelpers.hpp" #include "TrackListHelpers.hpp" -static constexpr std::size_t maxEntries {6}; - namespace UserInterface { @@ -56,7 +54,7 @@ namespace UserInterface stack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;"); _menu = bindNew("mode", stack); - auto addItem = [=](const Wt::WString& str, Mode mode, const Wt::WString& templateStr, std::function onRequestElementsFunc) + auto addItem = [=](const Wt::WString& str, [[maybe_unused]] Mode mode, const Wt::WString& templateStr, std::function onRequestElementsFunc) { assert(modeToIndex(mode) == _results.size()); From cc2cb735cc1bb353861c285e0daf3ab9ea9e7f45 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Sep 2021 18:24:29 +0200 Subject: [PATCH 23/25] Restored unit tests on build --- Dockerfile-build | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dockerfile-build b/Dockerfile-build index a589d12a..ffe3fc6c 100644 --- a/Dockerfile-build +++ b/Dockerfile-build @@ -49,5 +49,8 @@ COPY . /tmp/lms/ ARG LMS_BUILD_TYPE="Release" RUN \ DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \ - PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} $(xx-clang --print-cmake-defines) -DCMAKE_PREFIX_PATH=/$(xx-info)/usr/lib/cmake -DBUILD_TESTING=OFF && \ - VERBOSE=1 make -j$(nproc) + xx-info is-cross && export BUILD_TESTS=OFF || export BUILD_TESTS=ON && \ + PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" cmake /tmp/lms/ -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 From 885152dcbf4d808fc616e7a717b37b7748d96ca3 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Sep 2021 21:08:31 +0200 Subject: [PATCH 24/25] Restored STB detection --- Dockerfile-build | 14 ++++---------- cmake/modules/FindSTB.cmake | 1 - 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/Dockerfile-build b/Dockerfile-build index ffe3fc6c..0f5fbeba 100644 --- a/Dockerfile-build +++ b/Dockerfile-build @@ -1,12 +1,5 @@ - FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx - FROM --platform=$BUILDPLATFORM alpine:3.14 -ARG TARGETPLATFORM - -WORKDIR /tmp/workdir - -ARG PREFIX="/tmp/install" ARG BUILD_PACKAGES="\ clang \ @@ -14,7 +7,6 @@ ARG BUILD_PACKAGES="\ cmake \ lld \ curl \ - tree \ pkgconfig" RUN apk add --no-cache ${BUILD_PACKAGES} @@ -30,11 +22,13 @@ ARG LMS_BUILD_PACKAGES=" \ wt-dev \ gtest-dev" -# copy xx scripts to your build stage 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 \ @@ -50,7 +44,7 @@ ARG LMS_BUILD_TYPE="Release" RUN \ DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \ xx-info is-cross && export BUILD_TESTS=OFF || export BUILD_TESTS=ON && \ - PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} $(xx-clang --print-cmake-defines) -DCMAKE_PREFIX_PATH=/$(xx-info)/usr/lib/cmake -DBUILD_TESTING=${BUILD_TESTS} && \ + 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 diff --git a/cmake/modules/FindSTB.cmake b/cmake/modules/FindSTB.cmake index 0e071117..209bfdb2 100644 --- a/cmake/modules/FindSTB.cmake +++ b/cmake/modules/FindSTB.cmake @@ -4,7 +4,6 @@ include(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS( STB - FOUND_VAR STB_FOUND REQUIRED_VARS STB_INCLUDE_DIR ) From 0fc2100ef549b61d433303ef0cc47a7a49e8954f Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 28 Sep 2021 15:58:15 +0200 Subject: [PATCH 25/25] Removed bad release github action --- .github/workflows/release.yml | 67 ----------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 30f1a5f8..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Docker -on: - push: - tags: - - "*.*.*" -jobs: - Docker: - name: Docker images - environment: - name: Docker - runs-on: ubuntu-latest - steps: - - name: Check Out Repo - uses: actions/checkout@v2 - - - name: Prepare - id: prep - run: | - DOCKER_IMAGE=epoupon/lms - VERSION=edge - if [[ $GITHUB_REF == refs/tags/* ]]; then - VERSION=${GITHUB_REF#refs/tags/v} - fi - TAGS="${DOCKER_IMAGE}:${VERSION}" - if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then - TAGS="$TAGS,${DOCKER_IMAGE}:latest" - fi - echo ::set-output name=tags::${TAGS} - - - 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: Login to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - - - name: Build and push - id: docker_build - timeout-minutes: 720 - uses: docker/build-push-action@v2 - with: - context: ./ - file: ./Dockerfile-release - builder: ${{ steps.buildx.outputs.name }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.prep.outputs.tags }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache - platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} -