diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 00000000..0fd64d34 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,185 @@ +- [Installation](#installation) + * [Docker](#docker) + * [Debian Buster packages](#debian-buster-packages) + * [From source](#from-source) + + [Build dependencies](#build-dependencies) + + [Build](#build) + + [Installation](#installation-1) + + [Upgrade](#upgrade) +- [Deployment](#deployment) + * [Configuration](#configuration) + * [Deploy on non root path](#deploy-on-non-root-path) + * [Reverse proxy settings](#reverse-proxy-settings) +- [Run](#run) + +# Installation + +## Docker +_Docker_ images are available, please see detailed instructions on https://hub.docker.com/r/epoupon/lms. + +## Debian Buster packages +_Buster_ packages are provided for _amd64_ and _armhf_ architectures. + +As root, trust the following debian package provider and add it in your list of repositories: +```sh +wget -O - https://debian.poupon.io/apt/debian/epoupon.gpg.key | apt-key add - +echo "deb https://debian.poupon.io/apt/debian buster main" > /etc/apt/sources.list.d/epoupon.list +``` + +To install or upgrade _LMS_: +```sh +apt update +apt install lms +``` + +The _lms_ service is started just after the package installation, run by a dedicated _lms_ system user.
+Please refer to [Deployment](#deployment) for further configuration options. + +## From source +__Note__: this installation process and the default values of the configuration files have been written for _Debian Buster_. Therefore, you may have to adapt commands and/or paths in order to fit to your distribution. + +### Build dependencies +__Notes__: +* a C++17 compiler is needed +* ffmpeg version 4 minimum is required +```sh +apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev libpam0g-dev +``` +__Notes__: +* libpam0g-dev is optional (only for using PAM authentication) +* libstb-dev can be replaced by libgraphicsmagick++1-dev (the latter will likely use more RAM) + +You also need _Wt4_, which is not packaged yet on _Debian_. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).
+No optional requirement is needed, except openSSL if you plan not to deploy behind a reverse proxy (which is not recommended). + +### Build + +Get the latest stable release and build it: +```sh +git clone https://github.com/epoupon/lms.git lms +cd lms +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +``` +__Notes__: +* you can customize the installation directory using `-DCMAKE_INSTALL_PREFIX=path` (defaults to `/usr/local`). +* you can customize the image library using `-DIMAGE_LIBRARY=` + +```sh +make +``` +__Note__: you can use `make -jN` to speed up compilation time (N is the number of compilation workers to spawn). + +### Installation + +__Note__: the commands of this section require root privileges. + +```sh +make install +``` + +Create a dedicated system user: +```sh +useradd --system --group lms +``` + +Copy the configuration files: +```sh +cp /usr/share/lms/lms.conf /etc/lms.conf +cp /usr/share/lms/lms.service /lib/systemd/system/lms.service +``` + +Create the working directory and give it access to the _lms_ user: +```sh +mkdir /var/lms +chown lms:lms /var/lms +``` + +To make _LMS_ run automatically during startup: +```sh +systemctl enable lms +``` + +### Upgrade + +To upgrade _LMS_ from sources, you need to update the master branch and rebuild/install it: +```sh +cd build +git pull +make +``` + +Then using root privileges: +```sh +make install +systemctl restart lms +``` + +# Deployment + +__Note__: don't forget to give the _lms_ user read access to the music directory you want to scan. + +## Configuration +_LMS_ uses a configuration file, installed by default in `/etc/lms.conf`. It is recommended to edit this file and change relevant settings (listen address, listen port, working directory, Subsonic API activation, deployment path, ...). + +All other settings are set using the web interface (user management, scan settings, transcode settings, ...). + +If a setting is not present in the configuration file, a hardcoded default value is used (the same as in the [default.conf](conf/lms.conf) file) + +## Deploy on non root path +If you want to deploy on non root path (e.g. https://mydomain.com/newroot/), you have to set the `deploy-path` option accordingly in `lms.conf`. + +As static resources are __not__ related to the `deploy-path` option, you have to perform the following steps if you want them to be on a non root path too: +* Create a new intermediary `newroot` directory in `/usr/share/lms/docroot` and move everything in it. +* Symlink `/usr/share/lms/docroot/newroot/resources` to `/usr/share/Wt/resources`. +* Edit `lms.conf` and set: +``` +wt-resources = "" # do not comment the whole line +docroot = "/usr/share/lms/docroot/;/newroot/resources,/newroot/css,/newroot/images,/newroot/js,/newroot/favicon.ico";` +deploy-path = "/newroot/"; # ending slash is important +``` + +If you use nginx as a reverse proxy, you can simply replace `location /` with `location /newroot/` to achieve the same result. + +## Reverse proxy settings +_LMS_ is shipped with an embedded web server, but it is recommended to deploy behind a reverse proxy. You have to set the _behind-reverse-proxy_ option to _true_ in the `lms.conf` configuration file. + +Here is an example to make _LMS_ properly work on _myserver.org_ using _nginx_: +``` +server { + listen 80; + + server_name myserver.org; + + access_log /var/log/nginx/myserver.access.log; + + proxy_request_buffering off; + proxy_buffering off; + proxy_buffer_size 4k; + + location / { + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://localhost:5082/; + proxy_read_timeout 120; + } +} +``` + +# Run +```sh +systemctl start lms +``` + +Log traces can be accessed using journactl: +```sh +journalctl -u lms.service +``` + +To connect to _LMS_, just open your favorite browser and go to http://localhost:5082 + diff --git a/README.md b/README.md index da137975..32ffdae2 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ # LMS - Lightweight Music Server -[![Build Status](https://travis-ci.org/epoupon/lms.svg?branch=master)](https://travis-ci.org/epoupon/lms) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/epoupon/lms) [![CodeFactor](https://www.codefactor.io/repository/github/epoupon/lms/badge/master)](https://www.codefactor.io/repository/github/epoupon/lms/overview/master) +![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) _LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface! -A [demo](http://lms.demo.poupon.io) instance is available. Note the administration panel is not available. +A [demo instance](http://lms.demo.poupon.io) is available. Note the administration panel is not available. ## Main features -* Low memory requirements: the demo instance runs on a Raspberry Pi3B+ +* Low memory requirements: the demo instance runs on a _Raspberry Pi Zero W_ * Recommendation engine * Audio transcode for maximum interoperability and low bandwith requirements -* Multi-value tags: artists, genres, ... -* Custom tags support: _mood_, _genre_, _albummood_, _albumgrouping_, ... +* Multi-value tags: artists, genres, composers, lyricists, moods, ... * Compilation support * [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names * Disc subtitles support @@ -31,6 +30,7 @@ _LMS_ provides several ways to help you find the music you like: * Radio mode, based on what is in the current playqueue * Searches in album, artist and track names (including sort names) * Starred Albums/Artists/Tracks +* Custom tags support to help you filter your music: _mood_, _albummood_, _albumgenre_, _albumgrouping_, ... * Random/Starred/Most played/Recently played/Recently added for Artist/Albums/Tracks, allowing you to search for things like: * Recently added _Electronic_ artists * Random _Metal_ and _Aggressive_ albums @@ -63,180 +63,8 @@ __Note__: since _LMS_ stores hashed and salted passwords, it cannot handle the _ ## Installation -### Docker -_Docker_ images are available, please see detailed instructions on https://hub.docker.com/r/epoupon/lms. +See [INSTALL.md](INSTALL.md) file. -### Debian Buster packages -_Buster_ packages are provided for _amd64_ and _armhf_ architectures. +## Contributing -As root, trust the following debian package provider and add it in your list of repositories: -```sh -wget -O - https://debian.poupon.io/apt/debian/epoupon.gpg.key | apt-key add - -echo "deb https://debian.poupon.io/apt/debian buster main" > /etc/apt/sources.list.d/epoupon.list -``` - -To install or upgrade _LMS_: -```sh -apt update -apt install lms -``` - -The _lms_ service is started just after the package installation, run by a dedicated _lms_ system user.
-Please refer to [Deployment](#deployment) for further configuration options. - -### From source -__Note__: this installation process and the default values of the configuration files have been written for _Debian Buster_. Therefore, you may have to adapt commands and/or paths in order to fit to your distribution. - -#### Build dependencies -__Notes__: -* a C++17 compiler is needed -* ffmpeg version 4 minimum is required -```sh -apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev libpam0g-dev -``` -__Notes__: -* libpam0g-dev is optional (only for using PAM authentication) -* libstb-dev can be replaced by libgraphicsmagick++1-dev (the latter will likely use more RAM) - -You also need _Wt4_, which is not packaged yet on _Debian_. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).
-No optional requirement is needed, except openSSL if you plan not to deploy behind a reverse proxy (which is not recommended). - -#### Build - -Get the latest stable release and build it: -```sh -git clone https://github.com/epoupon/lms.git lms -cd lms -mkdir build -cd build -cmake .. -DCMAKE_BUILD_TYPE=Release -``` -__Notes__: -* you can customize the installation directory using `-DCMAKE_INSTALL_PREFIX=path` (defaults to `/usr/local`). -* you can customize the image library using `-DIMAGE_LIBRARY=` - -```sh -make -``` -__Note__: you can use `make -jN` to speed up compilation time (N is the number of compilation workers to spawn). - -#### Installation - -__Note__: the commands of this section require root privileges. - -```sh -make install -``` - -Create a dedicated system user: -```sh -useradd --system --group lms -``` - -Copy the configuration files: -```sh -cp /usr/share/lms/lms.conf /etc/lms.conf -cp /usr/share/lms/lms.service /lib/systemd/system/lms.service -``` - -Create the working directory and give it access to the _lms_ user: -```sh -mkdir /var/lms -chown lms:lms /var/lms -``` - -To make _LMS_ run automatically during startup: -```sh -systemctl enable lms -``` - -#### Upgrade - -To upgrade _LMS_ from sources, you need to update the master branch and rebuild/install it: -```sh -cd build -git pull -make -``` - -Then using root privileges: -```sh -make install -systemctl restart lms -``` - -## Deployment - -__Note__: don't forget to give the _lms_ user read access to the music directory you want to scan. - -### Configuration -_LMS_ uses a configuration file, installed by default in `/etc/lms.conf`. It is recommended to edit this file and change relevant settings (listen address, listen port, working directory, Subsonic API activation, deployment path, ...). - -All other settings are set using the web interface (user management, scan settings, transcode settings, ...). - -If a setting is not present in the configuration file, a hardcoded default value is used (the same as in the [default.conf](https://github.com/epoupon/lms/blob/master/conf/lms.conf) file) - -### Deploy on non root path -If you want to deploy on non root path (e.g. https://mydomain.com/newroot/), you have to set the `deploy-path` option accordingly in `lms.conf`. - -As static resources are __not__ related to the `deploy-path` option, you have to perform the following steps if you want them to be on a non root path too: -* Create a new intermediary `newroot` directory in `/usr/share/lms/docroot` and move everything in it. -* Symlink `/usr/share/lms/docroot/newroot/resources` to `/usr/share/Wt/resources`. -* Edit `lms.conf` and set: -``` -wt-resources = "" # do not comment the whole line -docroot = "/usr/share/lms/docroot/;/newroot/resources,/newroot/css,/newroot/images,/newroot/js,/newroot/favicon.ico";` -deploy-path = "/newroot/"; # ending slash is important -``` - -If you use nginx as a reverse proxy, you can simply replace `location /` with `location /newroot/` to achieve the same result. - -### Reverse proxy settings -_LMS_ is shipped with an embedded web server, but it is recommended to deploy behind a reverse proxy. You have to set the _behind-reverse-proxy_ option to _true_ in the `lms.conf` configuration file. - -Here is an example to make _LMS_ properly work on _myserver.org_ using _nginx_: -``` -server { - listen 80; - - server_name myserver.org; - - access_log /var/log/nginx/myserver.access.log; - - proxy_request_buffering off; - proxy_buffering off; - proxy_buffer_size 4k; - - location / { - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_pass http://localhost:5082/; - proxy_read_timeout 120; - } -} -``` - -## Run -```sh -systemctl start lms -``` - -Log traces can be accessed using journactl: -```sh -journalctl -u lms.service -``` - -To connect to _LMS_, just open your favorite browser and go to http://localhost:5082 - -## Credits -* Bootstrap Notify: https://github.com/mouse0270/bootstrap-notify -* Bootstrap3 (https://getbootstrap.com/) -* Bootswatch (https://bootswatch.com/) -* Ffmpeg project (https://ffmpeg.org/) -* GraphicsMagick++ (http://www.graphicsmagick.org/) -* MetaBrainz (https://metabrainz.org/) -* Wt (http://www.webtoolkit.eu/) +Contributions are welcome! Please submit your pull requests against the [develop](../../tree/develop) branch. diff --git a/approot/messages.xml b/approot/messages.xml index 2bbb4907..82885ada 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -143,7 +143,12 @@ All artists Track artists +Composers +Lyricists +Mixers +Producers Album artists +Remixers Similar albums @@ -194,6 +199,7 @@ Artist list mode All artists Album artists +Track artists Subsonic API Transcoding Transcode bitrate diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 0b66924a..8634953f 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -143,7 +143,12 @@ Tous les artistes Artistes de piste +Compositeurs +Paroliers +Mixers +Producteurs Artistes d'album +Remixers Albums similaires @@ -194,6 +199,7 @@ Mode de listage des artistes Tous les artistes Tous les artistes d'album +Tous les artistes de piste API Subsonic Transcodage Bitrate du transcodage diff --git a/src/libs/cover/impl/CoverArtGrabber.cpp b/src/libs/cover/impl/CoverArtGrabber.cpp index 52cd9729..8644ef4b 100644 --- a/src/libs/cover/impl/CoverArtGrabber.cpp +++ b/src/libs/cover/impl/CoverArtGrabber.cpp @@ -38,6 +38,44 @@ using RawImage = CoverArt::GraphicsMagick::RawImage; #include "utils/Utils.hpp" #include "Exception.hpp" +namespace +{ + struct TrackInfo + { + bool hasCover {}; + bool isMultiDisc {}; + std::filesystem::path trackPath; + std::optional releaseId; + }; + + std::optional + getTrackInfo(Database::Session& dbSession, Database::IdType trackId) + { + std::optional res; + + auto transaction {dbSession.createSharedTransaction()}; + + const Database::Track::pointer track {Database::Track::getById(dbSession, trackId)}; + if (!track) + return res; + + res = TrackInfo {}; + + res->hasCover = track->hasCover(); + res->trackPath = track->getPath(); + + if (const Database::Release::pointer& release {track->getRelease()}) + { + res->releaseId = release.id(); + if (release->getTotalDisc() > 1) + res->isMultiDisc = true; + } + + return res; + } +} + + namespace CoverArt { static @@ -112,7 +150,7 @@ Grabber::getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const } std::unique_ptr -Grabber::getFromFile(const std::filesystem::path& p, ImageSize width) const +Grabber::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const { std::unique_ptr image; @@ -146,7 +184,7 @@ Grabber::getDefault(ImageSize width) if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache)) return it->second; - std::shared_ptr image {getFromFile(_defaultCoverPath, width)}; + std::shared_ptr image {getFromCoverFile(_defaultCoverPath, width)}; _defaultCoverCache[width] = image; LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size(); @@ -155,9 +193,9 @@ Grabber::getDefault(ImageSize width) } std::unique_ptr -Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view preferredFileName, ImageSize width) const +Grabber::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const { - const std::multimap coverPaths {getCoverPaths(p)}; + const std::multimap coverPaths {getCoverPaths(directory)}; auto tryLoadImageFromFilename = [&](std::string_view fileName) { @@ -166,7 +204,7 @@ Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view prefe auto range {coverPaths.equal_range(std::string {fileName})}; for (auto it {range.first}; it != range.second; ++it) { - image = getFromFile(it->second, width); + image = getFromCoverFile(it->second, width); if (image) break; } @@ -175,13 +213,6 @@ Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view prefe std::unique_ptr image; - if (!preferredFileName.empty()) - { - image = tryLoadImageFromFilename(preferredFileName); - if (image) - return image; - } - for (std::string_view filename : _preferredFileNames) { image = tryLoadImageFromFilename(filename); @@ -192,7 +223,7 @@ Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view prefe // Just pick one for (const auto& [filename, coverPath] : coverPaths) { - image = getFromFile(coverPath, width); + image = getFromCoverFile(coverPath, width); if (image) return image; } @@ -200,6 +231,50 @@ Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view prefe return image; } +std::unique_ptr +Grabber::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const +{ + std::unique_ptr res; + + std::filesystem::path coverPath {filePath}; + for (const std::filesystem::path& extension : _fileExtensions) + { + coverPath.replace_extension(extension); + + if (!checkCoverFile(coverPath)) + continue; + + res = getFromCoverFile(coverPath, width); + if (res) + break; + } + + return res; +} + +bool +Grabber::checkCoverFile(const std::filesystem::path& filePath) const +{ + std::error_code ec; + + if (!isFileSupported(filePath, _fileExtensions)) + return false; + + if (!std::filesystem::exists(filePath, ec)) + return false; + + if (!std::filesystem::is_regular_file(filePath, ec)) + return false; + + if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec) + { + LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize; + return false; + } + + return true; +} + std::multimap Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const { @@ -210,22 +285,12 @@ Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const std::filesystem::directory_iterator itEnd; while (!ec && itPath != itEnd) { - const std::filesystem::path path {*itPath}; + const std::filesystem::path& path {*itPath}; + + if (checkCoverFile(path)) + res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path); + itPath.increment(ec); - - if (!std::filesystem::is_regular_file(path)) - continue; - - if (!isFileSupported(path, _fileExtensions)) - continue; - - if (std::filesystem::file_size(path) > _maxFileSize) - { - LMS_LOG(COVER, INFO) << "Cover file '" << path.string() << " is too big (" << std::filesystem::file_size(path) << "), limit is " << _maxFileSize; - continue; - } - - res.emplace(std::filesystem::path{path}.filename().replace_extension("").string(), path); } return res; @@ -238,8 +303,7 @@ Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const try { - Av::MediaFile input {p}; - + const Av::MediaFile input {p}; image = getFromAvMediaFile(input, width); } catch (Av::AvException& e) @@ -252,6 +316,15 @@ Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const std::shared_ptr Grabber::getFromTrack(Database::Session& dbSession, Database::IdType 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) { using namespace Database; @@ -261,37 +334,24 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, Im if (cover) return cover; - bool hasCover {}; - bool isMultiDisc {}; - std::filesystem::path trackPath; - + if (const std::optional trackInfo {getTrackInfo(dbSession, trackId)}) { - auto transaction {dbSession.createSharedTransaction()}; + if (trackInfo->hasCover) + cover = getFromTrack(trackInfo->trackPath, width); - const Track::pointer track {Track::getById(dbSession, trackId)}; - if (track) + if (!cover) + cover = getFromSameNamedFile(trackInfo->trackPath, width); + + if (!cover && trackInfo->releaseId && allowReleaseFallback) + cover = getFromRelease(dbSession, *trackInfo->releaseId, width); + + if (!cover && trackInfo->isMultiDisc) { - hasCover = track->hasCover(); - trackPath = track->getPath(); - - auto release {track->getRelease()}; - if (release && release->getTotalDisc() > 1) - isMultiDisc = true; + if (trackInfo->trackPath.parent_path().has_parent_path()) + cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width); } } - if (hasCover) - cover = getFromTrack(trackPath, width); - - if (!cover) - cover = getFromDirectory(trackPath.parent_path(), trackPath.filename().replace_extension("").string(), width); - - if (!cover && isMultiDisc) - { - if (trackPath.parent_path().has_parent_path()) - cover = getFromDirectory(trackPath.parent_path().parent_path(), {}, width); - } - if (!cover) cover = getDefault(width); @@ -310,22 +370,39 @@ Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, if (cover) return cover; - std::optional trackId; + struct ReleaseInfo { + Database::IdType firstTrackId; + std::filesystem::path releaseDirectory; + }; + + auto getReleaseInfo {[&] + { + std::optional res; + auto transaction {session.createSharedTransaction()}; - const auto release {Database::Release::getById(session, releaseId)}; - if (release) + if (const Database::Release::pointer release {Database::Release::getById(session, releaseId)}) { - const auto tracks {release->getTracks()}; - if (!tracks.empty()) - trackId = tracks.front().id(); + if (const auto firstTrack {release->getFirstTrack()}) + { + res = ReleaseInfo {}; + res->firstTrackId = firstTrack.id(); + res->releaseDirectory = firstTrack->getPath().parent_path(); + } } + + return res; + }}; + + if (const std::optional releaseInfo {getReleaseInfo()}) + { + cover = getFromDirectory(releaseInfo->releaseDirectory, width); + if (!cover) + cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */); } - if (trackId) - cover = getFromTrack(session, *trackId, width); - else + if (!cover) cover = getDefault(width); if (cover) diff --git a/src/libs/cover/impl/CoverArtGrabber.hpp b/src/libs/cover/impl/CoverArtGrabber.hpp index 87fbbeb9..3ba39a41 100644 --- a/src/libs/cover/impl/CoverArtGrabber.hpp +++ b/src/libs/cover/impl/CoverArtGrabber.hpp @@ -105,14 +105,18 @@ namespace CoverArt std::shared_ptr getFromRelease(Database::Session& dbSession, Database::IdType releaseId, ImageSize width) override; void flushCache() override; + std::shared_ptr getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width, bool allowReleaseFallback); std::unique_ptr getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const; - std::unique_ptr getFromFile(const std::filesystem::path& p, ImageSize width) const; + std::unique_ptr getFromCoverFile(const std::filesystem::path& p, ImageSize width) const; std::unique_ptr getFromTrack(const std::filesystem::path& path, ImageSize width) const; std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; - std::unique_ptr getFromDirectory(const std::filesystem::path& path, std::string_view preferredFileName, ImageSize width) const; + std::unique_ptr getFromDirectory(const std::filesystem::path& directory, ImageSize width) const; + std::unique_ptr getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const; std::shared_ptr getDefault(ImageSize width); + bool checkCoverFile(const std::filesystem::path& directoryPath) const; + std::shared_mutex _cacheMutex; std::unordered_map> _cache; std::unordered_map> _defaultCoverCache; diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 2f41aa80..5ade2f65 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -81,7 +81,7 @@ createQuery(Session& session, const std::string& queryStr, const std::set& clusterIds, const std::vector& keywords, - std::optional linkType) + std::optional linkType) { session.checkSharedLocked(); @@ -214,7 +214,7 @@ Artist::getAllIds(Session& session) } std::vector -Artist::getAllIdsRandom(Session& session, const std::set& clusters, std::optional linkType, std::optional size) +Artist::getAllIdsRandom(Session& session, const std::set& clusters, std::optional linkType, std::optional size) { session.checkSharedLocked(); @@ -265,7 +265,7 @@ std::vector Artist::getByFilter(Session& session, const std::set& clusters, const std::vector& keywords, - std::optional linkType, + std::optional linkType, SortMethod sortMethod, std::optional range, bool& moreResults) @@ -306,7 +306,7 @@ std::vector Artist::getLastWritten(Session& session, std::optional after, const std::set& clusters, - std::optional linkType, + std::optional linkType, std::optional range, bool& moreResults) { session.checkSharedLocked(); @@ -338,7 +338,7 @@ std::vector Artist::getStarred(Session& session, User::pointer user, const std::set& clusters, - std::optional linkType, + std::optional linkType, SortMethod sortMethod, std::optional range, bool& moreResults) { @@ -443,7 +443,7 @@ Artist::getReleaseCount() const } std::vector> -Artist::getTracks(std::optional linkType) const +Artist::getTracks(std::optional linkType) const { assert(self()); assert(IdIsValid(self()->id())); @@ -462,7 +462,7 @@ Artist::getTracks(std::optional linkType) const } std::vector> -Artist::getTracksWithRelease(std::optional linkType) const +Artist::getTracksWithRelease(std::optional linkType) const { assert(self()); assert(IdIsValid(self()->id())); @@ -497,13 +497,14 @@ Artist::getRandomTracks(std::optional count) const } std::vector> -Artist::getSimilarArtists(std::optional offset, std::optional count) const +Artist::getSimilarArtists(EnumSet artistLinkTypes, std::optional range) const { assert(self()); assert(IdIsValid(self()->id())); assert(session()); - Wt::Dbo::Query query {session()->query( + std::ostringstream oss; + oss << "SELECT 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" @@ -515,14 +516,34 @@ Artist::getSimilarArtists(std::optional offset, std::optional ?" - ) + " AND a.id <> ?"; + + if (!artistLinkTypes.empty()) + { + oss << " AND t_a_l.type IN ("; + + bool first {true}; + for (TrackArtistLinkType type : artistLinkTypes) + { + (void) type; + if (!first) + oss << ", "; + oss << "?"; + first = false; + } + oss << ")"; + } + + Wt::Dbo::Query query {session()->query(oss.str()) .bind(self()->id()) .bind(self()->id()) .groupBy("a.id") .orderBy("COUNT(*) DESC, RANDOM()") - .limit(count ? static_cast(*count) : -1) - .offset(offset ? static_cast(*offset) : -1)}; + .limit(range ? static_cast(range->limit) : -1) + .offset(range ? static_cast(range->offset) : -1)}; + + for (TrackArtistLinkType type : artistLinkTypes) + query.bind(type); Wt::Dbo::collection res = query; return std::vector(res.begin(), res.end()); diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index 26dd4b21..0a3143c8 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -132,11 +132,25 @@ ClusterType::getAllOrphans(Session& session) { session.checkSharedLocked(); - 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"); + 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"); return std::vector(res.begin(), res.end()); } +std::vector +ClusterType::getAllUsed(Session& session) +{ + session.checkSharedLocked(); + + 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"); + + return std::vector(res.begin(), res.end()); +} ClusterType::pointer ClusterType::getByName(Session& session, const std::string& name) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 64a04b31..473e7e08 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -19,13 +19,14 @@ #include "database/Release.hpp" -#include "utils/Logger.hpp" +#include #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/User.hpp" +#include "utils/Logger.hpp" #include "SqlQuery.hpp" namespace Database @@ -232,15 +233,15 @@ Release::getLastWritten(Session& session, } std::vector -Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional offset, std::optional limit) +Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional range) { Wt::Dbo::collection 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(offset ? static_cast(*offset) : -1) - .limit(limit ? static_cast(*limit) : -1); + .offset(range ? static_cast(range->offset) : -1) + .limit(range ? static_cast(range->limit) : -1); return std::vector(res.begin(), res.end()); } @@ -431,7 +432,7 @@ Release::getCopyrightURL() const } std::vector> -Release::getArtists(TrackArtistLink::Type linkType) const +Release::getArtists(TrackArtistLinkType linkType) const { assert(self()); assert(IdIsValid(self()->id())); @@ -532,6 +533,20 @@ Release::getTracksCount() const return _tracks.size(); } +Wt::Dbo::ptr +Release::getFirstTrack() const +{ + assert(self()); + assert(self()->id() != Wt::Dbo::dbo_traits::invalidId()); + assert(session()); + + return session()->query("SELECT t from track t") + .join("release r ON t.release_id = r.id") + .where("r.id = ?").bind(self()->id()) + .orderBy("t.disc_number,t.track_number") + .limit(1); +} + std::chrono::milliseconds Release::getDuration() const { diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 2782be74..2cbf47ac 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -40,7 +40,7 @@ namespace Database { -#define LMS_DATABASE_VERSION 26 +#define LMS_DATABASE_VERSION 27 using Version = std::size_t; @@ -278,6 +278,12 @@ 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 == 26) + { + // Composer, mixer, etc. support + // 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"; diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index aaf4b53a..13ec9cf6 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -24,6 +24,7 @@ #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Release.hpp" +#include "database/TrackArtistLink.hpp" #include "database/TrackFeatures.hpp" #include "database/Session.hpp" #include "utils/Logger.hpp" @@ -480,31 +481,82 @@ Track::getCopyrightURL() const } std::vector> -Track::getArtists(TrackArtistLink::Type type) const +Track::getArtists(EnumSet linkTypes) const { assert(self()); assert(IdIsValid(self()->id())); assert(session()); - Wt::Dbo::collection> artists {session()->query("SELECT a from artist a INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id INNER JOIN track t ON t.id = t_a_l.track_id") - .where("t.id = ?").bind(self()->id()) - .where("t_a_l.type = ?").bind(type)}; + std::ostringstream oss; + oss << + "SELECT a from artist a" + " INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id" + " INNER JOIN track t ON t.id = t_a_l.track_id"; - return std::vector>(artists.begin(), artists.end()); + if (!linkTypes.empty()) + { + oss << " AND t_a_l.type IN ("; + + bool first {true}; + for (TrackArtistLinkType type : linkTypes) + { + (void) type; + if (!first) + oss << ", "; + oss << "?"; + first = false; + } + oss << ")"; + } + + Wt::Dbo::Query query {session()->query(oss.str())}; + + for (TrackArtistLinkType type : linkTypes) + query.bind(type); + + query.where("t.id = ?").bind(self()->id()); + + Wt::Dbo::collection res = query; + return std::vector(std::begin(res), std::end(res)); } std::vector -Track::getArtistIds(TrackArtistLink::Type type) const +Track::getArtistIds(EnumSet linkTypes) const { assert(self()); assert(IdIsValid(self()->id())); assert(session()); - Wt::Dbo::collection artists {session()->query("SELECT a.id from artist a INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id INNER JOIN track t ON t.id = t_a_l.track_id") - .where("t.id = ?").bind(self()->id()) - .where("t_a_l.type = ?").bind(type)}; + std::ostringstream oss; + oss << + "SELECT a.id from artist a" + " INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id" + " INNER JOIN track t ON t.id = t_a_l.track_id"; - return std::vector(artists.begin(), artists.end()); + if (!linkTypes.empty()) + { + oss << " AND t_a_l.type IN ("; + + bool first {true}; + for (TrackArtistLinkType type : linkTypes) + { + (void) type; + if (!first) + oss << ", "; + oss << "?"; + first = false; + } + oss << ")"; + } + + Wt::Dbo::Query query {session()->query(oss.str()) + .where("t.id = ?").bind(self()->id())}; + + for (TrackArtistLinkType type : linkTypes) + query.bind(type); + + Wt::Dbo::collection res = query; + return std::vector(std::begin(res), std::end(res)); } std::vector> diff --git a/src/libs/database/impl/TrackArtistLink.cpp b/src/libs/database/impl/TrackArtistLink.cpp index 57526eab..45b62bbf 100644 --- a/src/libs/database/impl/TrackArtistLink.cpp +++ b/src/libs/database/impl/TrackArtistLink.cpp @@ -25,7 +25,7 @@ namespace Database { -TrackArtistLink::TrackArtistLink(Wt::Dbo::ptr track, Wt::Dbo::ptr artist, Type type) +TrackArtistLink::TrackArtistLink(Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type) : _type {type}, _track {track}, _artist {artist} @@ -33,7 +33,7 @@ _artist {artist} } TrackArtistLink::pointer -TrackArtistLink::create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::ptr artist,Type type) +TrackArtistLink::create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type) { session.checkUniqueLocked(); @@ -43,5 +43,15 @@ TrackArtistLink::create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::pt return res; } +EnumSet +TrackArtistLink::getUsedTypes(Session& session) +{ + session.checkSharedLocked(); + + Wt::Dbo::collection collection = session.getDboSession().query("SELECT DISTINCT type from track_artist_link"); + + return EnumSet(std::begin(collection), std::end(collection)); +} + } diff --git a/src/libs/database/impl/TrackList.cpp b/src/libs/database/impl/TrackList.cpp index 981dd9cd..39cff1a8 100644 --- a/src/libs/database/impl/TrackList.cpp +++ b/src/libs/database/impl/TrackList.cpp @@ -165,7 +165,7 @@ TrackList::getEntriesReverse(std::optional offset, std::optional -createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set& clusterIds, std::optional linkType) +createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set& clusterIds, std::optional linkType) { auto query {session.query(queryStr)}; query.join("track t ON t.id = t_a_l.track_id"); @@ -270,7 +270,7 @@ 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::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const { assert(session()); assert(IdIsValid(self()->id())); @@ -417,7 +417,7 @@ TrackList::getDuration() const } std::vector -TrackList::getTopArtists(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const +TrackList::getTopArtists(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const { assert(session()); assert(IdIsValid(self()->id())); diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index 472ce5b4..2afe5a6c 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -21,14 +21,15 @@ #include #include +#include #include #include #include +#include "utils/EnumSet.hpp" #include "utils/UUID.hpp" -#include "TrackArtistLink.hpp" #include "Types.hpp" namespace Database @@ -39,6 +40,7 @@ class ClusterType; class Release; class Session; class Track; +class TrackArtistLink; class User; class Artist : public Wt::Dbo::Dbo @@ -68,7 +70,7 @@ class Artist : public Wt::Dbo::Dbo 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& 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 + 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); @@ -77,19 +79,19 @@ class Artist : public Wt::Dbo::Dbo 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 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, std::optional after, const std::set& clusters, - std::optional linkType, // if set, only artists that have produced at least one track with this link type + 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 getStarred(Session& session, Wt::Dbo::ptr user, const std::set& clusters, - std::optional linkType, // if set, only artists that have produced at least one track with this link type + std::optional linkType, // if set, only artists that have produced at least one track with this link type SortMethod sortMethod, std::optional, bool& moreResults); @@ -100,10 +102,12 @@ 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> getTracks(std::optional linkType = {}) const; + std::vector> getTracksWithRelease(std::optional linkType = {}) const; std::vector> getRandomTracks(std::optional count) const; - std::vector getSimilarArtists(std::optional offset = {}, std::optional count = {}) const; + + // No artistLinkTypes means get them all + std::vector getSimilarArtists(EnumSet artistLinkTypes = {}, std::optional range = std::nullopt) const; // Get the cluster of the tracks made by this artist // Each clusters are grouped by cluster type, sorted by the number of occurence diff --git a/src/libs/database/include/database/Cluster.hpp b/src/libs/database/include/database/Cluster.hpp index fb0e4518..71e0da7f 100644 --- a/src/libs/database/include/database/Cluster.hpp +++ b/src/libs/database/include/database/Cluster.hpp @@ -92,6 +92,7 @@ class ClusterType : public Wt::Dbo::Dbo ClusterType(std::string name); 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 std::vector getAll(Session& session); diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index bfa10b18..8149fff1 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -20,11 +20,12 @@ #pragma once #include +#include -#include +#include +#include #include "utils/UUID.hpp" -#include "TrackArtistLink.hpp" #include "Types.hpp" namespace Database @@ -34,6 +35,7 @@ class Artist; class Cluster; class ClusterType; class Release; +class Session; class Track; class User; @@ -58,7 +60,7 @@ class Release : public Wt::Dbo::Dbo 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 getByYear(Session& session, int yearFrom, int yearTo, std::optional offset = {}, std::optional size = {}); + 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 getByClusters(Session& session, const std::set& clusters); @@ -71,6 +73,7 @@ class Release : public Wt::Dbo::Dbo std::vector> getTracks(const std::set& clusters = std::set()) const; std::size_t getTracksCount() const; + Wt::Dbo::ptr 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) @@ -94,8 +97,8 @@ class Release : public Wt::Dbo::Dbo Wt::WDateTime getLastWritten() const; // Get the artists of this release - std::vector > getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; - std::vector > getReleaseArtists() const { return getArtists(TrackArtistLink::Type::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/Track.hpp b/src/libs/database/include/database/Track.hpp index 4bbb4b5e..22e97643 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -26,12 +26,13 @@ #include #include -#include #include +#include +#include +#include "utils/EnumSet.hpp" #include "utils/UUID.hpp" -#include "TrackArtistLink.hpp" #include "Types.hpp" namespace Database { @@ -40,6 +41,8 @@ class Artist; class Cluster; class ClusterType; class Release; +class Session; +class TrackArtistLink; class TrackFeatures; class TrackListEntry; class TrackStats; @@ -134,8 +137,9 @@ class Track : public Wt::Dbo::Dbo std::optional getTrackReplayGain() const { return _trackReplayGain; } std::optional getReleaseReplayGain() const { return _releaseReplayGain; } - std::vector> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; - std::vector getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; + // 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; diff --git a/src/libs/database/include/database/TrackArtistLink.hpp b/src/libs/database/include/database/TrackArtistLink.hpp index 7601d0c9..02a727e5 100644 --- a/src/libs/database/include/database/TrackArtistLink.hpp +++ b/src/libs/database/include/database/TrackArtistLink.hpp @@ -19,64 +19,53 @@ #pragma once +#include + #include #include "Types.hpp" +#include "utils/EnumSet.hpp" -namespace Database { - -class Artist; -class Session; -class Track; - -class TrackArtistLink +namespace Database { - public: - enum class Type - { - Artist, // regular artist - Arranger, - Composer, - Conductor, - Lyricist, - Mixer, - Performer, - Producer, - ReleaseArtist, - Remixer, - Writer, - }; - using pointer = Wt::Dbo::ptr; + class Artist; + class Session; + class Track; - TrackArtistLink() = default; - TrackArtistLink(Wt::Dbo::ptr track, Wt::Dbo::ptr artist, Type type); + class TrackArtistLink + { + public: + using pointer = Wt::Dbo::ptr; - static pointer create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::ptr artist,Type type); + TrackArtistLink() = default; + TrackArtistLink(Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type); - Wt::Dbo::ptr getTrack() const { return _track; } - Wt::Dbo::ptr getArtist() const { return _artist; } - Type getType() const { return _type; } + static pointer create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type); - template - void persist(Action& a) - { - Wt::Dbo::field(a, _type, "type"); - Wt::Dbo::field(a, _type, "name"); + static EnumSet getUsedTypes(Session& session); - Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); - } + Wt::Dbo::ptr getTrack() const { return _track; } + Wt::Dbo::ptr getArtist() const { return _artist; } + TrackArtistLinkType getType() const { return _type; } - private: + template + void persist(Action& a) + { + Wt::Dbo::field(a, _type, "type"); + Wt::Dbo::field(a, _type, "name"); - Type _type; - std::string _name; + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); + } - Wt::Dbo::ptr _track; - Wt::Dbo::ptr _artist; -}; + private: + TrackArtistLinkType _type; + std::string _name; + + Wt::Dbo::ptr _track; + Wt::Dbo::ptr _artist; + }; } - diff --git a/src/libs/database/include/database/TrackList.hpp b/src/libs/database/include/database/TrackList.hpp index f05f830b..b890391e 100644 --- a/src/libs/database/include/database/TrackList.hpp +++ b/src/libs/database/include/database/TrackList.hpp @@ -25,7 +25,6 @@ #include -#include "TrackArtistLink.hpp" #include "Types.hpp" namespace Database { @@ -53,7 +52,7 @@ class TrackList : public Wt::Dbo::Dbo TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr user); // Stats utility - std::vector> getTopArtists(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; + 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; @@ -85,7 +84,7 @@ class TrackList : public Wt::Dbo::Dbo std::vector> getEntries(std::optional offset = {}, std::optional size = {}) const; std::vector> getEntriesReverse(std::optional offset = {}, std::optional size = {}) const; - std::vector> getArtistsReverse(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; + 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; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 058b5052..15fdb0b3 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -35,5 +35,21 @@ namespace Database std::size_t offset {}; std::size_t limit {}; }; + + enum class TrackArtistLinkType + { + Artist, // regular artist + Arranger, + Composer, + Conductor, + Lyricist, + Mixer, + Performer, + Producer, + ReleaseArtist, + Remixer, + Writer, + }; + } diff --git a/src/libs/database/include/database/User.hpp b/src/libs/database/include/database/User.hpp index 73f0749b..576328d6 100644 --- a/src/libs/database/include/database/User.hpp +++ b/src/libs/database/include/database/User.hpp @@ -132,8 +132,9 @@ class User : public Wt::Dbo::Dbo // Do not change enum values! enum class SubsonicArtistListMode { - AllArtists = 0, + AllArtists = 0, ReleaseArtists = 1, + TrackArtists = 2, }; static inline const std::size_t MinNameLength {3}; diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index e4cbb4e2..8b7d70c3 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -42,13 +42,13 @@ namespace MetaData template std::vector -getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::set& keys) +getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::vector& keys) { std::vector res; - for (const std::string& key : keys) + for (std::string_view key : keys) { - const TagLib::StringList& values {properties[key]}; + const TagLib::StringList& values {properties[std::string {key}]}; if (values.isEmpty()) continue; @@ -91,12 +91,13 @@ splitAndTrimString(const std::string& str, const std::string& delimiters) static std::vector -getArtists(const TagLib::PropertyMap& properties) +getArtists(const TagLib::PropertyMap& properties, + const std::vector& artistTagNames, + const std::vector& artistSortTagNames, + const std::vector& artistMBIDTagNames + ) { - std::vector artistNames {getPropertyValuesAs(properties, "ARTISTS")}; - if (artistNames.empty()) - artistNames = getPropertyValuesAs(properties, "ARTIST"); - + const std::vector artistNames {getPropertyValuesFirstMatchAs(properties, artistTagNames)}; if (artistNames.empty()) return {}; @@ -106,7 +107,7 @@ getArtists(const TagLib::PropertyMap& properties) [&](const std::string& name) { return Artist {name}; }); { - const std::vector artistSortNames {getPropertyValuesAs(properties, "ARTISTSORT")}; + const std::vector artistSortNames {getPropertyValuesFirstMatchAs(properties, artistSortTagNames)}; if (artistSortNames.size() == artists.size()) { for (std::size_t i {}; i < artistSortNames.size(); ++i) @@ -115,7 +116,7 @@ getArtists(const TagLib::PropertyMap& properties) } { - const std::vector artistsMBID {getPropertyValuesFirstMatchAs(properties, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"})}; + const std::vector artistsMBID {getPropertyValuesFirstMatchAs(properties, artistMBIDTagNames)}; if (artistNames.size() == artistsMBID.size()) { @@ -128,41 +129,6 @@ getArtists(const TagLib::PropertyMap& properties) return artists; } -static -std::vector -getAlbumArtists(const TagLib::PropertyMap& properties) -{ - std::vector artistNames {getPropertyValuesAs(properties, "ALBUMARTIST")}; - if (artistNames.empty()) - return {}; - - std::vector artists; - artists.reserve(artistNames.size()); - std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists), - [&](const std::string& name) { return Artist {name}; }); - - { - const std::vector artistSortNames {getPropertyValuesAs(properties, "ALBUMARTISTSORT")}; - if (artistSortNames.size() == artists.size()) - { - for (std::size_t i {}; i < artistSortNames.size(); ++i) - artists[i].sortName = artistSortNames[i]; - } - } - - { - const std::vector artistsMBID {getPropertyValuesFirstMatchAs(properties, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"})}; - - if (artistsMBID.size() == artists.size()) - { - for (std::size_t i {}; i < artistsMBID.size(); ++i) - artists[i].musicBrainzArtistID = artistsMBID[i]; - } - } - - return artists; -} - static std::optional getAlbum(const TagLib::PropertyMap& properties) @@ -430,9 +396,15 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) processTag(track, tag, values, debug); } - track.artists = getArtists(properties); - track.albumArtists = getAlbumArtists(properties); track.album = getAlbum(properties); + track.artists = getArtists(properties, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"}); + track.albumArtists = getArtists(properties, {"ALBUMARTIST"}, {"ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"}); + track.conductorArtists = getArtists(properties, {"CONDUCTOR"}, {""}, {}); + track.composerArtists = getArtists(properties, {"COMPOSER"}, {"COMPOSERSORT"}, {}); + track.lyricistArtists = getArtists(properties, {"LYRICIST"}, {"LYRICISTSORT"}, {}); + track.mixerArtists = getArtists(properties, {"MIXER"}, {""}, {}); + track.producerArtists = getArtists(properties, {"PRODUCER"}, {""}, {}); + track.remixerArtists = getArtists(properties, {"REMIXER", "ModifiedBy"}, {""}, {}); return track; } diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 624af218..06a521bf 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -78,6 +78,12 @@ namespace MetaData std::optional trackReplayGain; std::optional albumReplayGain; std::string discSubtitle; + std::vector conductorArtists; + std::vector composerArtists; + std::vector lyricistArtists; + std::vector mixerArtists; + std::vector producerArtists; + std::vector remixerArtists; }; class IParser diff --git a/src/libs/recommendation/impl/Engine.cpp b/src/libs/recommendation/impl/Engine.cpp index e3986ce5..717b087c 100644 --- a/src/libs/recommendation/impl/Engine.cpp +++ b/src/libs/recommendation/impl/Engine.cpp @@ -132,7 +132,10 @@ Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releas } std::unordered_set -Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount) +Engine::getSimilarArtists(Database::Session& dbSession, + Database::IdType artistId, + EnumSet linkTypes, + std::size_t maxCount) { std::unordered_set res; @@ -144,7 +147,7 @@ Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistI continue; const IClassifier& classifier {*itClassifier->second}; - res = classifier.getSimilarArtists(dbSession, artistId, maxCount); + res = classifier.getSimilarArtists(dbSession, artistId, linkTypes, maxCount); if (!res.empty()) { LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'"; diff --git a/src/libs/recommendation/impl/Engine.hpp b/src/libs/recommendation/impl/Engine.hpp index dc516863..2e48e539 100644 --- a/src/libs/recommendation/impl/Engine.hpp +++ b/src/libs/recommendation/impl/Engine.hpp @@ -56,10 +56,13 @@ namespace Recommendation void load(bool forceReload, const ProgressCallback& progressCallback) override; void cancelLoad() override; - std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override; - std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) override; - std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override; - std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) 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, + EnumSet linkTypes, + std::size_t maxCount) override; void setClassifierPriorities(const std::vector& classifierTypes); void clearClassifiers(); diff --git a/src/libs/recommendation/impl/IClassifier.hpp b/src/libs/recommendation/impl/IClassifier.hpp index 4c53df48..4670a424 100644 --- a/src/libs/recommendation/impl/IClassifier.hpp +++ b/src/libs/recommendation/impl/IClassifier.hpp @@ -24,6 +24,7 @@ #include #include "database/Types.hpp" +#include "utils/EnumSet.hpp" namespace Database { @@ -49,10 +50,14 @@ namespace Recommendation virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0; virtual void requestCancelLoad() = 0; - virtual std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0; - virtual std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const = 0; - virtual std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0; - virtual std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const = 0; + using ResultContainer = std::unordered_set; + + 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, + EnumSet linkTypes, std::size_t maxCount) const = 0; }; } // ns Recommendation diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp index c03f517e..7a9c9ea9 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp @@ -83,7 +83,10 @@ ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::Id } std::unordered_set -ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount) const +ClusterClassifier::getSimilarArtists(Database::Session& dbSession, + Database::IdType artistId, + EnumSet artistLinkTypes, + std::size_t maxCount) const { std::unordered_set res; @@ -93,7 +96,7 @@ ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdT if (!artist) return res; - const auto artists {artist->getSimilarArtists(0, maxCount)}; + 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(); }); diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp index 7f96a828..dcb1a8dc 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp @@ -40,10 +40,13 @@ namespace Recommendation bool load(Database::Session&, bool, const ProgressCallback&) override { return true; } 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, std::size_t maxCount) const 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, + EnumSet linkTypes, + std::size_t maxCount) const override; }; } // namespace Recommendation diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp index a98011de..b28f2160 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp @@ -25,6 +25,7 @@ #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/TrackArtistLink.hpp" #include "database/TrackFeatures.hpp" #include "database/TrackList.hpp" #include "som/DataNormalizer.hpp" @@ -308,9 +309,34 @@ FeaturesClassifier::getSimilarReleases(Database::Session& session, Database::IdT } std::unordered_set -FeaturesClassifier::getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const +FeaturesClassifier::getSimilarArtists(Database::Session& session, + Database::IdType artistId, + EnumSet linkTypes, + std::size_t maxCount) const { - auto similarArtistIds {getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount)}; + auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType) + { + std::unordered_set similarArtistIds; + + const auto itArtists {_artistsMap.find(linkType)}; + if (itArtists == std::cend(_artistsMap)) + { + return similarArtistIds; + } + + similarArtistIds = getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount); + + return similarArtistIds; + }}; + + std::unordered_set similarArtistIds; + + for (Database::TrackArtistLinkType linkType : linkTypes) + { + const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)}; + similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType)); + } + if (!similarArtistIds.empty()) { // Report only existing ids @@ -319,13 +345,16 @@ FeaturesClassifier::getSimilarArtists(Database::Session& session, Database::IdTy for (auto it {std::begin(similarArtistIds)}; it != std::end(similarArtistIds);) { const Database::IdType similarArtistId {*it}; - if (!Database::Release::getById(session, similarArtistId)) + if (!Database::Artist::getById(session, similarArtistId)) it = similarArtistIds.erase(it); else it++; } } + while (similarArtistIds.size() > maxCount) + similarArtistIds.erase(Random::pickRandom(similarArtistIds)); + return similarArtistIds; } @@ -339,6 +368,7 @@ bool FeaturesClassifier::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) { if (forceReload) + { FeaturesClassifierCache::invalidate(); } @@ -377,7 +407,6 @@ FeaturesClassifier::load(Database::Session& session, const SOM::Coordinate width {network.getWidth()}; const SOM::Coordinate height {network.getHeight()}; - _artistsMap = MatrixOfObjects {width, height}; _releasesMap = MatrixOfObjects {width, height}; _tracksMap = MatrixOfObjects {width, height}; @@ -407,10 +436,18 @@ FeaturesClassifier::load(Database::Session& session, _releasePositions[track->getRelease().id()].insert(position); _releasesMap[position].insert(track->getRelease().id()); } - for (const auto& artist : track->getArtists()) + for (const auto& artistLink : track->getArtistLinks()) { - _artistPositions[artist.id()].insert(position); - _artistsMap[position].insert(artist.id()); + _artistPositions[artistLink->getArtist().id()].insert(position); + auto itArtists {_artistsMap.find(artistLink->getType())}; + if (itArtists == std::cend(_artistsMap)) + { + auto [it, inserted] = _artistsMap.try_emplace(artistLink->getType(), MatrixOfObjects {}); + assert(inserted); + itArtists = it; + itArtists->second = MatrixOfObjects {width, height}; + } + itArtists->second[position].insert(artistLink->getArtist().id()); } } } diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp index 2d5b3992..177962ac 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp @@ -65,7 +65,10 @@ class FeaturesClassifier : public IClassifier 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, 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); @@ -99,8 +102,8 @@ class FeaturesClassifier : public IClassifier std::unique_ptr _network; double _networkRefVectorsDistanceMedian {}; - MatrixOfObjects _artistsMap; - ObjectPositions _artistPositions; + ObjectPositions _artistPositions; + std::unordered_map _artistsMap; MatrixOfObjects _releasesMap; ObjectPositions _releasePositions; diff --git a/src/libs/recommendation/include/recommendation/IEngine.hpp b/src/libs/recommendation/include/recommendation/IEngine.hpp index f1b4ce2b..bd7ce05d 100644 --- a/src/libs/recommendation/include/recommendation/IEngine.hpp +++ b/src/libs/recommendation/include/recommendation/IEngine.hpp @@ -20,9 +20,11 @@ #pragma once #include +#include #include #include "database/Types.hpp" +#include "utils/EnumSet.hpp" namespace Database { @@ -46,10 +48,15 @@ namespace Recommendation virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; virtual void cancelLoad() = 0; - virtual std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0; - virtual std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) = 0; - virtual std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0; - virtual std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) = 0; + using ResultContainer = std::unordered_set; + + 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; }; std::unique_ptr createEngine(Database::Db& db); diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index d82b63e7..3bc6984e 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -29,6 +29,7 @@ #include "database/Release.hpp" #include "database/ScanSettings.hpp" #include "database/Track.hpp" +#include "database/TrackArtistLink.hpp" #include "database/TrackFeatures.hpp" #include "metadata/TagLibParser.hpp" #include "recommendation/IEngine.hpp" @@ -750,20 +751,6 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S title = file.filename().string(); } - // ***** Clusters - std::vector clusters {getOrCreateClusters(_dbSession, trackInfo->clusters)}; - - // ***** Artists - std::vector artists {getOrCreateArtists(_dbSession, trackInfo->artists)}; - - // ***** Release artists - std::vector releaseArtists {getOrCreateArtists(_dbSession, trackInfo->albumArtists)}; - - // ***** Release - Release::pointer release; - if (trackInfo->album) - release = getOrCreateRelease(_dbSession, *trackInfo->album); - // If file already exist, update data // Otherwise, create it if (!track) @@ -784,15 +771,34 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S assert(track); track.modify()->clearArtistLinks(); - for (const auto& artist : artists) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLink::Type::Artist)); + for (const Artist::pointer& artist : getOrCreateArtists(_dbSession, trackInfo->artists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLinkType::Artist)); - for (const auto& releaseArtist : releaseArtists) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLink::Type::ReleaseArtist)); + for (const Artist::pointer& releaseArtist : getOrCreateArtists(_dbSession, trackInfo->albumArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLinkType::ReleaseArtist)); + + for (const Artist::pointer& conductor : getOrCreateArtists(_dbSession, trackInfo->conductorArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, conductor, Database::TrackArtistLinkType::Conductor)); + + for (const Artist::pointer& composer : getOrCreateArtists(_dbSession, trackInfo->composerArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, composer, Database::TrackArtistLinkType::Composer)); + + for (const Artist::pointer& lyricist : getOrCreateArtists(_dbSession, trackInfo->lyricistArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, lyricist, Database::TrackArtistLinkType::Lyricist)); + + for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, mixer, Database::TrackArtistLinkType::Mixer)); + + for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, producer, Database::TrackArtistLinkType::Producer)); + + for (const Artist::pointer& remixer : getOrCreateArtists(_dbSession, trackInfo->remixerArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, remixer, Database::TrackArtistLinkType::Remixer)); track.modify()->setScanVersion(_scanVersion); - track.modify()->setRelease(release); - track.modify()->setClusters(clusters); + if (trackInfo->album) + track.modify()->setRelease(getOrCreateRelease(_dbSession, *trackInfo->album)); + track.modify()->setClusters(getOrCreateClusters(_dbSession, trackInfo->clusters)); track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setName(title); track.modify()->setDuration(trackInfo->duration); @@ -1012,7 +1018,7 @@ MediaScanner::checkDuplicatedAudioFiles(ScanStats& stats) if (track->getMBID()) { LMS_LOG(DBUPDATER, INFO) << "Found duplicated MBID [" << track->getMBID()->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName(); - stats.duplicates.emplace_back(ScanDuplicate {track->getPath(), DuplicateReason::SameMBID}); + stats.duplicates.emplace_back(ScanDuplicate {track.id(), DuplicateReason::SameMBID}); } } diff --git a/src/libs/scanner/include/scanner/MediaScannerStats.hpp b/src/libs/scanner/include/scanner/MediaScannerStats.hpp index 145ea5c3..b1996647 100644 --- a/src/libs/scanner/include/scanner/MediaScannerStats.hpp +++ b/src/libs/scanner/include/scanner/MediaScannerStats.hpp @@ -24,6 +24,8 @@ #include #include +#include "database/Types.hpp" + namespace Scanner { enum class ScanErrorType @@ -51,9 +53,8 @@ namespace Scanner { struct ScanDuplicate { - std::filesystem::path file; + Database::IdType trackId; DuplicateReason reason; - }; enum class ScanProgressStep : unsigned diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 322a5817..65f7427e 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -316,7 +316,8 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()})); - auto artists {track->getArtists()}; + const std::vector& artists {track->getArtists({TrackArtistLinkType::Artist})}; + LMS_LOG(API_SUBSONIC, DEBUG) << "Artists count = " << artists.size(); if (!artists.empty()) { trackResponse.setAttribute("artist", getArtistNames(artists)); @@ -479,7 +480,7 @@ userToResponseNode(const User::pointer& user) Response::Node userNode; userNode.setAttribute("username", user->getLoginName()); - userNode.setAttribute("scrobblingEnabled", false); + userNode.setAttribute("scrobblingEnabled", true); userNode.setAttribute("adminRole", user->isAdmin()); userNode.setAttribute("settingsRole", true); userNode.setAttribute("downloadRole", true); @@ -704,11 +705,13 @@ Response handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) { // Mandatory params - std::string type {getMandatoryParameterAs(context.parameters, "type")}; + const std::string type {getMandatoryParameterAs(context.parameters, "type")}; // Optional params - std::size_t size {getParameterAs(context.parameters, "size").value_or(10)}; - std::size_t offset {getParameterAs(context.parameters, "offset").value_or(0)}; + const std::size_t size {getParameterAs(context.parameters, "size").value_or(10)}; + const std::size_t offset {getParameterAs(context.parameters, "offset").value_or(0)}; + + const Range range {offset, size}; std::vector releases; @@ -718,36 +721,14 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) if (!user) throw UserNotAuthorizedError {}; - if (type == "random") + if (type == "alphabeticalByName") { - // Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums - releases = Release::getAllRandom(context.dbSession, {}, size); - } - else if (type == "newest") - { - bool moreResults {}; - releases = Release::getLastWritten(context.dbSession, std::nullopt, {}, Range {offset, size}, moreResults); - } - else if (type == "alphabeticalByName") - { - releases = Release::getAll(context.dbSession, Range {offset, size}); + releases = Release::getAll(context.dbSession, range); } else if (type == "alphabeticalByArtist") { releases = Release::getAllOrderedByArtist(context.dbSession, offset, size); } - else if (type == "byYear") - { - int fromYear {getMandatoryParameterAs(context.parameters, "fromYear")}; - int toYear {getMandatoryParameterAs(context.parameters, "toYear")}; - - releases = Release::getByYear(context.dbSession, fromYear, toYear, offset, size); - } - else if (type == "starred") - { - bool moreResults {}; - releases = Release::getStarred(context.dbSession, user, {}, Range {offset, size}, moreResults); - } else if (type == "byGenre") { // Mandatory param @@ -760,10 +741,42 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) if (cluster) { bool more; - releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, Range {offset, size}, more); + releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, range, more); } } } + else if (type == "byYear") + { + int fromYear {getMandatoryParameterAs(context.parameters, "fromYear")}; + int toYear {getMandatoryParameterAs(context.parameters, "toYear")}; + + releases = Release::getByYear(context.dbSession, fromYear, toYear, range); + } + else if (type == "frequent") + { + bool moreResults {}; + releases = user->getPlayedTrackList(context.dbSession)->getTopReleases({}, range, moreResults); + } + else if (type == "newest") + { + bool moreResults {}; + releases = Release::getLastWritten(context.dbSession, std::nullopt, {}, range, moreResults); + } + else if (type == "random") + { + // Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums + releases = Release::getAllRandom(context.dbSession, {}, size); + } + else if (type == "recent") + { + bool moreResults {}; + releases = user->getPlayedTrackList(context.dbSession)->getReleasesReverse({}, range, moreResults); + } + else if (type == "starred") + { + bool moreResults {}; + releases = Release::getStarred(context.dbSession, user, {}, range, moreResults); + } else throw NotImplementedGenericError {}; @@ -881,7 +894,10 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString()); } - auto similarArtistsId {Service::get()->getSimilarArtists(context.dbSession, id.value, count)}; + auto similarArtistsId {Service::get()->getSimilarArtists(context.dbSession, + id.value, + {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, + count)}; { auto transaction {context.dbSession.createSharedTransaction()}; @@ -931,13 +947,16 @@ handleGetArtistsRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - std::optional linkType; + std::optional linkType; switch (user->getSubsonicArtistListMode()) { case User::SubsonicArtistListMode::AllArtists: break; case User::SubsonicArtistListMode::ReleaseArtists: - linkType = TrackArtistLink::Type::ReleaseArtist; + linkType = TrackArtistLinkType::ReleaseArtist; + break; + case User::SubsonicArtistListMode::TrackArtists: + linkType = TrackArtistLinkType::Artist; break; } @@ -1075,13 +1094,16 @@ handleGetIndexesRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - std::optional linkType; + std::optional linkType; switch (user->getSubsonicArtistListMode()) { case User::SubsonicArtistListMode::AllArtists: break; case User::SubsonicArtistListMode::ReleaseArtists: - linkType = TrackArtistLink::Type::ReleaseArtist; + linkType = TrackArtistLinkType::ReleaseArtist; + break; + case User::SubsonicArtistListMode::TrackArtists: + linkType = TrackArtistLinkType::Artist; break; } @@ -1103,36 +1125,39 @@ Response handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; - if (id.type != Id::Type::Artist) + const Id artistId {getMandatoryParameterAs(context.parameters, "id")}; + if (artistId.type != Id::Type::Artist) throw BadParameterGenericError {"id"}; // Optional params std::size_t count {getParameterAs(context.parameters, "count").value_or(50)}; - auto similarArtistsId {Service::get()->getSimilarArtists(context.dbSession, id.value, 5)}; + auto similarArtistIds {Service::get()->getSimilarArtists(context.dbSession, + artistId.value, + {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, + 5)}; auto transaction {context.dbSession.createSharedTransaction()}; - Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; + const Artist::pointer artist {Artist::getById(context.dbSession, artistId.value)}; if (!artist) throw RequestedDataNotFoundError {}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + const User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw UserNotAuthorizedError {}; // "Returns a random collection of songs from the given artist and similar artists" auto tracks {artist->getRandomTracks(count / 2)}; - for ( const auto& similarArtistId : similarArtistsId ) + for (const Database::IdType similarArtistId : similarArtistIds) { - Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; + const Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; if (!similarArtist) continue; auto similarArtistTracks {similarArtist->getRandomTracks((count / 2) / 5)}; - tracks.insert(tracks.end(), + tracks.insert(std::end(tracks), std::make_move_iterator(std::begin(similarArtistTracks)), std::make_move_iterator(std::end(similarArtistTracks))); } @@ -1547,6 +1572,34 @@ handleUnstarRequest(RequestContext& context) return Response::createOkResponse(context); } +static +Response +handleScrobble(RequestContext& context) +{ + const std::vector ids {getMandatoryMultiParametersAs(context.parameters, "id")}; + // TODO handle time in some way (need underlying refacto) + + if (!std::all_of(std::cbegin(ids), std::cend(ids), [](const Id& id) { return id.type == Id::Type::Track; })) + throw BadParameterGenericError {"id"}; + + auto transaction {context.dbSession.createUniqueTransaction()}; + + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + if (!user) + throw RequestedDataNotFoundError {}; + + for (Id id : ids) + { + Track::pointer track {Track::getById(context.dbSession, id.value)}; + if (!track) + continue; + + TrackListEntry::create(context.dbSession, track, user->getPlayedTrackList(context.dbSession)); + } + + return Response::createOkResponse(context); +} + static Response handleUpdateUserRequest(RequestContext& context) @@ -1824,10 +1877,10 @@ static std::unordered_map requestEntryPoints {"getAvatar", {handleNotImplemented, false}}, // Media annotation - {"star", {handleStarRequest, false}}, - {"unstar", {handleUnstarRequest, false}}, + {"star", {handleStarRequest, false}}, + {"unstar", {handleUnstarRequest, false}}, {"setRating", {handleNotImplemented, false}}, - {"scrobble", {handleNotImplemented, false}}, + {"scrobble", {handleScrobble, false}}, // Sharing {"getShares", {handleNotImplemented, false}}, diff --git a/src/libs/utils/impl/FileResourceHandler.cpp b/src/libs/utils/impl/FileResourceHandler.cpp index f031adfc..f3eb1291 100644 --- a/src/libs/utils/impl/FileResourceHandler.cpp +++ b/src/libs/utils/impl/FileResourceHandler.cpp @@ -60,7 +60,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http:: const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())}; ifs.seekg(0, std::ios::beg); - LMS_LOG(UTILS, DEBUG) << "fileSize = " << fileSize; + LMS_LOG(UTILS, DEBUG) << "File '" << _path.string() << "', fileSize = " << fileSize; const Wt::Http::Request::ByteRangeSpecifier ranges {request.getRanges(fileSize)}; if (!ranges.isSatisfiable()) diff --git a/src/libs/utils/include/utils/EnumSet.hpp b/src/libs/utils/include/utils/EnumSet.hpp new file mode 100644 index 00000000..38d42c67 --- /dev/null +++ b/src/libs/utils/include/utils/EnumSet.hpp @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +template +class EnumSet +{ + static_assert(std::is_enum::value); + static_assert(std::is_same::value || std::is_same::value); + + using index_type = std::uint_fast8_t; + + public: + EnumSet() = default; + constexpr EnumSet(std::initializer_list values) + { + for (T value : values) + insert(value); + } + + template + constexpr EnumSet(It begin, It end) + { + for (It it {begin}; it != end; ++it) + insert(*it); + } + + constexpr void insert(T value) + { + assert(static_cast(value) < sizeof(_bitfield) * 8); + _bitfield |= (underlying_type{ 1 } << static_cast(value)); + } + + constexpr void erase(T value) + { + assert(static_cast(value) < sizeof(_bitfield) * 8); + _bitfield &= ~(underlying_type{ 1 } << static_cast(value)); + } + + constexpr bool empty() const + { + return _bitfield == 0; + } + + constexpr bool contains(T value) const + { + assert(static_cast(value) < sizeof(_bitfield) * 8); + return _bitfield & (underlying_type{ 1 } << static_cast(value)); + } + + class iterator + { + public: + using value_type = T; + + constexpr value_type operator*() const + { + return static_cast(_index); + } + + constexpr bool operator==(const iterator& _other) const + { + return &_container == &_other._container && _index == _other._index; + } + + constexpr bool operator!=(const iterator& _other) const + { + return !(*this == _other); + } + + constexpr iterator& operator++() + { + _index = _container.getFirstBitSetIndex(_index + 1); + return *this; + } + + private: + friend class EnumSet; + + constexpr iterator(const EnumSet& _container, index_type _index) + : _container {_container} + , _index {_index} + { + } + + const EnumSet& _container; + index_type _index; + }; + + constexpr iterator begin() const + { + return iterator {*this, getFirstBitSetIndex()}; + } + + constexpr iterator end() const + { + return iterator {*this, npos}; + } + + private: + static_assert(std::numeric_limits::max() >= sizeof(underlying_type) * 8); + enum : index_type { npos = sizeof(underlying_type) * 8 }; + + constexpr index_type getFirstBitSetIndex(index_type start = {}) const + { + assert(start < npos); + + // return npos if no bit found + index_type res {countTrailingZero(_bitfield >> start)}; + if (res == npos) + return res; + + return res + start; + } + + static constexpr index_type countTrailingZero(underlying_type bitField) + { + index_type res {}; + + while (res < (sizeof(underlying_type) * 8) && (bitField & 1) == 0) + { + ++res; + bitField >>= 1; + } + + if (res == sizeof(underlying_type) * 8) + res = npos; + + return res; + } + + underlying_type _bitfield{}; +}; + + diff --git a/src/lms/main.cpp b/src/lms/main.cpp index c75b9cc1..202b71d7 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -77,7 +77,8 @@ generateWtConfig(std::string execPath) args.push_back("--accesslog=" + wtAccessLogFilePath.string()); { - const unsigned long httpServerThreadCount {configHttpServerThreadCount ? configHttpServerThreadCount : std::max(1, std::thread::hardware_concurrency())}; + // Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg) + const unsigned long httpServerThreadCount {configHttpServerThreadCount ? configHttpServerThreadCount : std::max(2, std::thread::hardware_concurrency())}; args.push_back("--threads=" + std::to_string(httpServerThreadCount)); } diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index b836ebab..a38c3da1 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -541,7 +541,7 @@ LmsApplication::createHome() { const std::string sessionId {LmsApp->sessionId()}; - Service::get()->scanStarted().connect(this, [=] () + Service::get()->scanStarted().connect(this, [=] { Wt::WServer::instance()->post(sessionId, [=] { @@ -550,9 +550,9 @@ LmsApplication::createHome() }); }); - Service::get()->scanComplete().connect(this, [=] () + Service::get()->scanComplete().connect(this, [=] { - Wt::WServer::instance()->post(sessionId, [=] + Wt::WServer::instance()->post(sessionId, [this] { _events.dbScanned.emit(); triggerUpdate(); diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index ca96f30f..abebac64 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -30,6 +30,7 @@ #include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackList.hpp" +#include "database/Types.hpp" #include "database/User.hpp" #include "resource/ImageResource.hpp" @@ -242,7 +243,7 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain) const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)}; const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)}; - const auto artists {track->getArtists()}; + const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; oss << "var params = {" diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index ff0e5db1..7b8e89cb 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -419,7 +419,7 @@ PlayQueue::addSome() entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); - const auto artists {track->getArtists()}; + const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; const auto release {track->getRelease()}; if (!artists.empty() || release) diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 133ae59e..33f50856 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -333,6 +333,7 @@ class SettingsModel : public Wt::WFormModel _subsonicArtistListModeModel = std::make_shared>(); _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists); _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), User::SubsonicArtistListMode::ReleaseArtists); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), User::SubsonicArtistListMode::TrackArtists); } bool _withOldPassword {}; @@ -346,12 +347,12 @@ class SettingsModel : public Wt::WFormModel SettingsView::SettingsView() { - wApp->internalPathChanged().connect([=] + wApp->internalPathChanged().connect(this, [this] { refreshView(); }); - LmsApp->getMediaPlayer().settingsLoaded.connect([=]() + LmsApp->getMediaPlayer().settingsLoaded.connect([this] { refreshView(); }); diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index fa0fbbc8..af11f6ea 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -167,10 +167,10 @@ class DatabaseSettingsModel : public Wt::WFormModel DatabaseSettingsView::DatabaseSettingsView() { - wApp->internalPathChanged().connect(std::bind([=] + wApp->internalPathChanged().connect(this, [this] { refreshView(); - })); + }); refreshView(); } diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 2e394e61..87f9ae9b 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -28,6 +28,8 @@ #include #include +#include "database/Session.hpp" +#include "database/Track.hpp" #include "utils/Service.hpp" #include "LmsApplication.hpp" @@ -82,8 +84,22 @@ class ReportResource : public Wt::WResource response.out() << Wt::WString::tr("Lms.Admin.ScannerController.duplicates-header").arg(_stats.duplicates.size()).toUTF8() << std::endl; - for (const auto& duplicate : _stats.duplicates) - response.out() << duplicate.file.string() << " - " << duplicateReasonToWString(duplicate.reason).toUTF8() << std::endl; + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + for (const auto& duplicate : _stats.duplicates) + { + const auto& track {Database::Track::getById(LmsApp->getDbSession(), duplicate.trackId)}; + if (!track) + continue; + + response.out() << track->getPath().string(); + if (auto mbid {track->getMBID()}) + response.out() << " (MBID " << mbid->getAsString() << ")"; + + response.out() << " - " << duplicateReasonToWString(duplicate.reason).toUTF8() << '\n'; + } + } } private: @@ -123,7 +139,7 @@ ScannerController::ScannerController() auto onDbEvent = [&]() { refreshContents(); }; - LmsApp->getEvents().dbScanStarted.connect(this, []() + LmsApp->getEvents().dbScanStarted.connect(this, [] { LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched")); }); diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 859519d7..33cfb6fb 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -242,7 +242,7 @@ class UserModel : public Wt::WFormModel UserView::UserView() { - wApp->internalPathChanged().connect([this]() + wApp->internalPathChanged().connect(this, [this]() { refreshView(); }); diff --git a/src/lms/ui/admin/UsersView.cpp b/src/lms/ui/admin/UsersView.cpp index af01413e..853023c8 100644 --- a/src/lms/ui/admin/UsersView.cpp +++ b/src/lms/ui/admin/UsersView.cpp @@ -44,7 +44,7 @@ UsersView::UsersView() LmsApp->setInternalPath("/admin/user", true); }); - wApp->internalPathChanged().connect([this]() + wApp->internalPathChanged().connect(this, [this]() { refreshView(); }); diff --git a/src/lms/ui/common/ValueStringModel.hpp b/src/lms/ui/common/ValueStringModel.hpp index 333694f4..5c4dce95 100644 --- a/src/lms/ui/common/ValueStringModel.hpp +++ b/src/lms/ui/common/ValueStringModel.hpp @@ -73,6 +73,12 @@ class ValueStringModel : public Wt::WStringListModel setData(rowCount() - 1, 0, str, Wt::ItemDataRole::Display); } + void + clear() + { + removeRows(0, rowCount()); + } + }; } // namespace UserInterface diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 5b16c86f..70f4242d 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -50,12 +50,12 @@ Artist::Artist(Filters* filters) { addFunction("tr", &Wt::WTemplate::Functions::tr); - LmsApp->internalPathChanged().connect([=] + LmsApp->internalPathChanged().connect(this, [this] { refreshView(); }); - filters->updated().connect([=] + filters->updated().connect([this] { refreshView(); }); @@ -75,7 +75,10 @@ Artist::refreshView() if (!artistId) throw ArtistNotFoundException {*artistId}; - const auto similarArtistIds {Service::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)}; + const auto similarArtistIds {Service::get()->getSimilarArtists(LmsApp->getDbSession(), + *artistId, + {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, + 5)}; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; diff --git a/src/lms/ui/explore/ArtistsView.cpp b/src/lms/ui/explore/ArtistsView.cpp index 28752654..bdf7430e 100644 --- a/src/lms/ui/explore/ArtistsView.cpp +++ b/src/lms/ui/explore/ArtistsView.cpp @@ -27,6 +27,7 @@ #include "database/Artist.hpp" #include "database/Session.hpp" #include "database/User.hpp" +#include "database/TrackArtistLink.hpp" #include "database/TrackList.hpp" #include "utils/Logger.hpp" @@ -39,7 +40,7 @@ using namespace Database; namespace UserInterface { -using ArtistLinkModel = ValueStringModel>; +using ArtistLinkModel = ValueStringModel>; Artists::Artists(Filters* filters) : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}, @@ -68,14 +69,14 @@ Artists::Artists(Filters* filters) } _linkType = bindNew("link-type"); - { - auto linkTypeModel {std::make_shared()}; - linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), {}); - linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-artist"), TrackArtistLink::Type::Artist); - linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-releaseartist"), TrackArtistLink::Type::ReleaseArtist); - _linkType->setModel(linkTypeModel); - } + _linkType->setModel(std::make_shared()); _linkType->changed().connect([this] { refreshView(); }); + refreshArtistLinkTypes(); + + LmsApp->getEvents().dbScanned.connect(this, [this] + { + refreshArtistLinkTypes(); + }); _container = bindNew("artists"); hideLoadingIndicator(); @@ -100,6 +101,37 @@ Artists::refreshView(Mode mode) refreshView(); } +void +Artists::refreshArtistLinkTypes() +{ + std::shared_ptr linkTypeModel {std::static_pointer_cast(_linkType->model())}; + + EnumSet usedLinkTypes; + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + usedLinkTypes = Database::TrackArtistLink::getUsedTypes(LmsApp->getDbSession()); + } + + auto addTypeIfUsed {[&](Database::TrackArtistLinkType linkType, std::string_view stringKey) + { + if (!usedLinkTypes.contains(linkType)) + return; + + linkTypeModel->add(Wt::WString::tr(std::string {stringKey}), linkType); + }}; + + linkTypeModel->clear(); + + linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), {}); + addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist"); + addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist"); + addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); + addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist"); + addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer"); + addTypeIfUsed(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer"); + addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer"); +} + void Artists::displayLoadingIndicator() { @@ -125,7 +157,7 @@ Artists::getRandomArtists(std::optional range, bool& moreResults) { std::vector artists; - const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())}; + const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())}; if (_randomArtists.empty()) _randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), linkType, maxItemsPerMode[Mode::Random]); @@ -152,7 +184,7 @@ Artists::getArtists(std::optional range, bool& moreResults) { std::vector artists; - const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())}; + const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())}; const std::optional modeLimit{maxItemsPerMode[_mode]}; if (modeLimit) diff --git a/src/lms/ui/explore/ArtistsView.hpp b/src/lms/ui/explore/ArtistsView.hpp index f1614392..1c1810fe 100644 --- a/src/lms/ui/explore/ArtistsView.hpp +++ b/src/lms/ui/explore/ArtistsView.hpp @@ -56,6 +56,7 @@ class Artists : public Wt::WTemplate void refreshView(); void refreshView(Mode mode); + void refreshArtistLinkTypes(); void displayLoadingIndicator(); void hideLoadingIndicator(); void addSome(); diff --git a/src/lms/ui/explore/Explore.cpp b/src/lms/ui/explore/Explore.cpp index c7444bbd..27288c05 100644 --- a/src/lms/ui/explore/Explore.cpp +++ b/src/lms/ui/explore/Explore.cpp @@ -113,7 +113,7 @@ Explore::Explore(Filters* filters) tracks->tracksAction.connect(this, &Explore::handleTracksAction); contentsStack->addWidget(std::move(tracks)); - wApp->internalPathChanged().connect([=] + wApp->internalPathChanged().connect(this, [=] { handleContentsPathChange(contentsStack); }); diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index e893ba40..06c34da9 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -52,7 +52,7 @@ Filters::showDialog() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const auto types {Database::ClusterType::getAll(LmsApp->getDbSession())}; + const auto types {Database::ClusterType::getAllUsed(LmsApp->getDbSession())}; for (const Database::ClusterType::pointer& type : types) typeCombo->addItem(Wt::WString::fromUTF8(type->getName())); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 59ed4dc3..d2dcacac 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -54,12 +54,12 @@ Release::Release(Filters* filters) { addFunction("tr", &Wt::WTemplate::Functions::tr); - wApp->internalPathChanged().connect([=] + wApp->internalPathChanged().connect(this, [this] { refreshView(); }); - filters->updated().connect([=] + filters->updated().connect([this] { refreshView(); }); @@ -214,7 +214,7 @@ Release::refreshView() entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); - auto artists {track->getArtists()}; + const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; if (variousArtists && !artists.empty()) { entry->setCondition("if-has-artists", true); diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 9a8930e7..7d241b3c 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -47,7 +47,7 @@ namespace UserInterface::TrackListHelpers Wt::WText* name {entry->bindNew("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain)}; name->setToolTip(Wt::WString::fromUTF8(track->getName())); - const auto artists {track->getArtists()}; + const auto artists {track->getArtists({TrackArtistLinkType::Artist})}; const Release::pointer release {track->getRelease()}; const IdType trackId {track.id()}; diff --git a/src/lms/ui/resource/AudioFileResource.hpp b/src/lms/ui/resource/AudioFileResource.hpp index 9e52a077..0865376b 100644 --- a/src/lms/ui/resource/AudioFileResource.hpp +++ b/src/lms/ui/resource/AudioFileResource.hpp @@ -19,7 +19,6 @@ #pragma once -#include #include #include "database/Types.hpp" diff --git a/src/lms/ui/resource/ImageResource.hpp b/src/lms/ui/resource/ImageResource.hpp index ea6adbf1..76c574aa 100644 --- a/src/lms/ui/resource/ImageResource.hpp +++ b/src/lms/ui/resource/ImageResource.hpp @@ -17,39 +17,34 @@ * along with LMS. If not, see . */ -#ifndef COVER_RESOURCE_HPP_ -#define COVER_RESOURCE_HPP_ - -#include +#pragma once #include - #include "database/Types.hpp" -namespace UserInterface { - - -class ImageResource : public Wt::WResource +namespace UserInterface { - public: - static const std::size_t maxSize {512}; - ~ImageResource(); + class ImageResource : public Wt::WResource + { + public: + static const std::size_t maxSize {512}; - enum class Size : std::size_t - { - Small = 128, - Large = 512, - }; + ~ImageResource(); - std::string getReleaseUrl(Database::IdType releaseId, Size size) const; - std::string getTrackUrl(Database::IdType trackId, Size size) const; + enum class Size : std::size_t + { + Small = 128, + Large = 512, + }; - private: - void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; + std::string getReleaseUrl(Database::IdType releaseId, Size size) const; + std::string getTrackUrl(Database::IdType trackId, Size size) const; -}; + private: + void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; + + }; } // namespace UserInterface -#endif diff --git a/src/test/database/DatabaseTest.cpp b/src/test/database/DatabaseTest.cpp index 2a54fbd7..6569f1a4 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/test/database/DatabaseTest.cpp @@ -28,6 +28,7 @@ #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/TrackArtistLink.hpp" #include "database/TrackBookmark.hpp" #include "database/TrackList.hpp" #include "database/User.hpp" @@ -227,7 +228,6 @@ testSingleCluster(Session& session) { auto transaction {session.createUniqueTransaction()}; - auto clusters {Cluster::getAll(session)}; CHECK(clusters.size() == 1); CHECK(clusters.front().id() == cluster.getId()); @@ -241,6 +241,10 @@ testSingleCluster(Session& session) CHECK(clusterTypes.size() == 1); CHECK(clusterTypes.front().id() == clusterType.getId()); + clusterTypes = ClusterType::getAllUsed(session); + CHECK(clusterTypes.size() == 1); + CHECK(clusterTypes.front().id() == clusterType.getId()); + clusterTypes = ClusterType::getAllOrphans(session); CHECK(clusterTypes.empty()); } @@ -252,6 +256,8 @@ testSingleCluster(Session& session) auto clusterTypes {ClusterType::getAllOrphans(session)}; CHECK(clusterTypes.size() == 1); CHECK(clusterTypes.front().id() == clusterType.getId()); + + CHECK(ClusterType::getAllUsed(session).empty()); } } @@ -265,7 +271,7 @@ testSingleTrackSingleArtist(Session& session) { auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); } { @@ -276,7 +282,7 @@ testSingleTrackSingleArtist(Session& session) { auto transaction {session.createSharedTransaction()}; - auto artists {track->getArtists()}; + auto artists {track->getArtists({TrackArtistLinkType::Artist})}; CHECK(artists.size() == 1); CHECK(artists.front().id() == artist.getId()); @@ -287,8 +293,9 @@ testSingleTrackSingleArtist(Session& session) CHECK(artistLink->getTrack().id() == track.getId()); CHECK(artistLink->getArtist().id() == artist.getId()); - CHECK(track->getArtists(TrackArtistLink::Type::Artist).size() == 1); - CHECK(track->getArtists(TrackArtistLink::Type::ReleaseArtist).empty()); + CHECK(track->getArtists({TrackArtistLinkType::Artist}).size() == 1); + CHECK(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); + CHECK(track->getArtists({}).size() == 1); } { @@ -298,8 +305,8 @@ testSingleTrackSingleArtist(Session& session) CHECK(tracks.size() == 1); CHECK(tracks.front().id() == track.getId()); - CHECK(artist->getTracks(TrackArtistLink::Type::ReleaseArtist).empty()); - CHECK(artist->getTracks(TrackArtistLink::Type::Artist).size() == 1); + CHECK(artist->getTracks(TrackArtistLinkType::ReleaseArtist).empty()); + CHECK(artist->getTracks(TrackArtistLinkType::Artist).size() == 1); } } @@ -312,9 +319,9 @@ testSingleTrackSingleArtistMultiRoles(Session& session) { auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::ReleaseArtist); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Writer); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer); } { @@ -326,29 +333,29 @@ testSingleTrackSingleArtistMultiRoles(Session& session) auto transaction {session.createSharedTransaction()}; bool hasMore{}; CHECK(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); - CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Artist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); - CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::ReleaseArtist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); - CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Writer, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); - CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty()); + CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Artist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); + CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::ReleaseArtist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); + CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Writer, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); + CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty()); } { auto transaction {session.createSharedTransaction()}; - auto artists {track->getArtists(TrackArtistLink::Type::Artist)}; + auto artists {track->getArtists({TrackArtistLinkType::Artist})}; CHECK(artists.size() == 1); CHECK(artists.front().id() == artist.getId()); - artists = track->getArtists(TrackArtistLink::Type::ReleaseArtist); + artists = track->getArtists({TrackArtistLinkType::ReleaseArtist}); CHECK(artists.size() == 1); CHECK(artists.front().id() == artist.getId()); CHECK(track->getArtistLinks().size() == 3); CHECK(artist->getTracks().size() == 1); - CHECK(artist->getTracks(TrackArtistLink::Type::ReleaseArtist).size() == 1); - CHECK(artist->getTracks(TrackArtistLink::Type::Artist).size() == 1); - CHECK(artist->getTracks(TrackArtistLink::Type::Writer).size() == 1); + CHECK(artist->getTracks({TrackArtistLinkType::ReleaseArtist}).size() == 1); + CHECK(artist->getTracks({TrackArtistLinkType::Artist}).size() == 1); + CHECK(artist->getTracks({TrackArtistLinkType::Writer}).size() == 1); } } @@ -364,8 +371,8 @@ testSingleTrackMultiArtists(Session& session) { auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLink::Type::Artist); - TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLink::Type::Artist); + TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist); } { @@ -376,13 +383,14 @@ testSingleTrackMultiArtists(Session& session) { auto transaction {session.createSharedTransaction()}; - auto artists {track->getArtists()}; + auto artists {track->getArtists({TrackArtistLinkType::Artist})}; CHECK(artists.size() == 2); CHECK((artists[0].id() == artist1.getId() && artists[1].id() == artist2.getId()) || (artists[0].id() == artist2.getId() && artists[1].id() == artist1.getId())); - CHECK(track->getArtists(TrackArtistLink::Type::Artist).size() == 2); - CHECK(track->getArtists(TrackArtistLink::Type::ReleaseArtist).empty()); + CHECK(track->getArtists({}).size() == 2); + CHECK(track->getArtists({TrackArtistLinkType::Artist}).size() == 2); + CHECK(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); CHECK(Artist::getAll(session, Artist::SortMethod::ByName).size() == 2); CHECK(Artist::getAllIds(session).size() == 2); } @@ -393,10 +401,10 @@ testSingleTrackMultiArtists(Session& session) CHECK(artist1->getTracks().front() == track.get()); CHECK(artist2->getTracks().front() == track.get()); - CHECK(artist1->getTracks(TrackArtistLink::Type::ReleaseArtist).empty()); - CHECK(artist1->getTracks(TrackArtistLink::Type::Artist).size() == 1); - CHECK(artist2->getTracks(TrackArtistLink::Type::ReleaseArtist).empty()); - CHECK(artist2->getTracks(TrackArtistLink::Type::Artist).size() == 1); + CHECK(artist1->getTracks(TrackArtistLinkType::ReleaseArtist).empty()); + CHECK(artist1->getTracks(TrackArtistLinkType::Artist).size() == 1); + CHECK(artist2->getTracks(TrackArtistLinkType::ReleaseArtist).empty()); + CHECK(artist2->getTracks(TrackArtistLinkType::Artist).size() == 1); } } @@ -410,7 +418,7 @@ testSingleArtistSearchByName(Session& session) { auto transaction {session.createUniqueTransaction()}; artist.get().modify()->setSortName("ZZZ"); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); } { @@ -586,6 +594,53 @@ testMultiTracksSingleReleaseTotalDiscTrack(Session& session) } } +static +void +testMultiTracksSingleReleaseFirstTrack(Session& session) +{ + 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()}; + + CHECK(!release1->getFirstTrack()); + CHECK(!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()}; + + CHECK(release1->getFirstTrack()); + CHECK(release2->getFirstTrack()); + + CHECK(release1->getFirstTrack().id() == track1A.getId()); + CHECK(release2->getFirstTrack().id() == track2B.getId()); + } +} + static void testSingleTrackSingleCluster(Session& session) @@ -942,7 +997,7 @@ testSingleTrackSingleArtistMultiClusters(Session& session) { auto transaction {session.createUniqueTransaction()}; - auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist)}; + auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)}; cluster1.get().modify()->addTrack(track.get()); } @@ -1004,8 +1059,8 @@ testSingleTrackSingleArtistMultiRolesMultiClusters(Session& session) { auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::ReleaseArtist); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); cluster.get().modify()->addTrack(track.get()); } @@ -1045,7 +1100,7 @@ testMultiTracksSingleArtistMultiClusters(Session& session) tracks.emplace_back(session, "MyTrackFile" + std::to_string(i)); auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLink::Type::Artist); + TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); for (auto& cluster : clusters) cluster.get().modify()->addTrack(tracks.back().get()); @@ -1084,7 +1139,7 @@ testMultiTracksSingleArtistSingleRelease(Session& session) auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLink::Type::Artist); + TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); tracks.back().get().modify()->setRelease(release.get()); } @@ -1117,7 +1172,7 @@ testSingleTrackSingleReleaseSingleArtist(Session& session) { auto transaction {session.createUniqueTransaction()}; - auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist)}; + auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)}; track.get().modify()->setRelease(release.get()); } @@ -1154,7 +1209,7 @@ testSingleTrackSingleReleaseSingleArtistSingleCluster(Session& session) { auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); track.get().modify()->setRelease(release.get()); cluster.get().modify()->addTrack(track.get()); } @@ -1206,7 +1261,7 @@ testSingleTrackSingleReleaseSingleArtistMultiClusters(Session& session) { auto transaction {session.createUniqueTransaction()}; - auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist)}; + auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)}; track.get().modify()->setRelease(release.get()); cluster1.get().modify()->addTrack(track.get()); cluster2.get().modify()->addTrack(track.get()); @@ -1254,7 +1309,7 @@ testSingleStarredArtist(Session& session) { auto transaction {session.createUniqueTransaction()}; - auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist)}; + auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)}; user.get().modify()->starArtist(artist.get()); } @@ -1479,8 +1534,8 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session) track1.get().modify()->setRelease(release1.get()); track2.get().modify()->setRelease(release2.get()); - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLink::Type::Artist); - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLink::Type::Artist); + TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); cluster1.get().modify()->addTrack(track1.get()); cluster2.get().modify()->addTrack(track2.get()); @@ -1558,7 +1613,7 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster1.getId()}, TrackArtistLink::Type::Artist, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsReverse({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; CHECK(artists.size() == 1); CHECK(artists.front().id() == artist1.getId()); } @@ -1567,7 +1622,7 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, TrackArtistLink::Type::Artist, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsReverse({}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; CHECK(artists.size() == 1); CHECK(artists.front().id() == artist1.getId()); } @@ -1580,7 +1635,7 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session) CHECK(trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults).empty()); CHECK(trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults).empty()); - CHECK(trackList->getArtistsReverse({}, TrackArtistLink::Type::ReleaseArtist, std::nullopt, moreResults).empty()); + CHECK(trackList->getArtistsReverse({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty()); } { @@ -1738,10 +1793,10 @@ testMultipleTracksMultipleArtistsMultiClusters(Session& session) auto transaction {session.createUniqueTransaction()}; if (i < 5) - TrackArtistLink::create(session, tracks.back().get(), artist1.get(), TrackArtistLink::Type::Artist); + TrackArtistLink::create(session, tracks.back().get(), artist1.get(), TrackArtistLinkType::Artist); else { - TrackArtistLink::create(session, tracks.back().get(), artist2.get(), TrackArtistLink::Type::Artist); + TrackArtistLink::create(session, tracks.back().get(), artist2.get(), TrackArtistLinkType::Artist); cluster2.get().modify()->addTrack(tracks.back().get()); } @@ -1751,7 +1806,7 @@ testMultipleTracksMultipleArtistsMultiClusters(Session& session) tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size())); { auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLink::Type::Artist); + TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist); cluster2.get().modify()->addTrack(tracks.back().get()); } @@ -1764,6 +1819,28 @@ testMultipleTracksMultipleArtistsMultiClusters(Session& session) CHECK(artists.front().id() == artist2.getId()); } + { + auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist})}; + CHECK(artists.size() == 1); + CHECK(artists.front().id() == artist2.getId()); + } + + { + auto artists {artist1->getSimilarArtists({TrackArtistLinkType::ReleaseArtist})}; + CHECK(artists.empty() == 1); + } + + { + auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist})}; + CHECK(artists.size() == 1); + CHECK(artists.front().id() == artist2.getId()); + } + + { + auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Composer})}; + CHECK(artists.empty()); + } + { auto artists {artist2->getSimilarArtists()}; CHECK(artists.size() == 2); @@ -1930,6 +2007,7 @@ int main() RUN_TEST(testSingleTrackSingleRelease); RUN_TEST(testMultiTracksSingleReleaseTotalDiscTrack); + RUN_TEST(testMultiTracksSingleReleaseFirstTrack); RUN_TEST(testSingleTrackSingleCluster); RUN_TEST(testMultipleTracksSingleCluster); diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 963d44b0..f7103ee1 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -76,9 +76,27 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file) for (const Artist& artist : track->artists) std::cout << "Artist: " << artist << std::endl; - for (const Artist& artist: track->albumArtists) + for (const Artist& artist : track->albumArtists) std::cout << "Album artist: " << artist << std::endl; + for (const Artist& artist : track->conductorArtists) + std::cout << "Conductor: " << artist << std::endl; + + for (const Artist& artist : track->composerArtists) + std::cout << "Composer: " << artist << std::endl; + + for (const Artist& artist : track->lyricistArtists) + std::cout << "Lyricist: " << artist << std::endl; + + for (const Artist& artist : track->mixerArtists) + std::cout << "Mixer: " << artist << std::endl; + + for (const Artist& artist : track->producerArtists) + std::cout << "Producer: " << artist << std::endl; + + for (const Artist& artist : track->remixerArtists) + std::cout << "Remixer: " << artist << std::endl; + if (track->album) std::cout << "Album: " << *track->album << std::endl; diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index 5c722c7f..bea308e8 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -35,10 +35,9 @@ #include "utils/StreamLogger.hpp" #include "recommendation/IEngine.hpp" - static void -dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& engine) +dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) { const std::vector trackIds {[&]() { @@ -58,7 +57,7 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng res += track->getName(); if (track->getRelease()) res += " [" + track->getRelease()->getName() + "]"; - for (auto artist : track->getArtists()) + for (auto artist : track->getArtists({Database::TrackArtistLinkType::Artist})) res += " - " + artist->getName(); for (auto cluster : track->getClusters()) res += " {" + cluster->getType()->getName() + "-"+ cluster->getName() + "}"; @@ -67,14 +66,14 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng }; std::cout << "Processing track '" << trackToString(trackId) << std::endl; - for (Database::IdType similarTrackId : engine.getSimilarTracks(session, {trackId}, 3)) + for (Database::IdType similarTrackId : engine.getSimilarTracks(session, {trackId}, maxSimilarityCount)) std::cout << "\t- Similar track '" << trackToString(similarTrackId) << std::endl; } } static void -dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& engine) +dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) { const std::vector releaseIds = std::invoke([&]() { @@ -94,14 +93,14 @@ dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& e }; std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl; - for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, releaseId, 3)) + for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, releaseId, maxSimilarityCount)) std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl; } } static void -dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& engine) +dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) { const std::vector artistIds = std::invoke([&]() { @@ -121,8 +120,10 @@ dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& en }; std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl; - for (Database::IdType similarArtistId : engine.getSimilarArtists(session, artistId, 3)) + for (Database::IdType similarArtistId : engine.getSimilarArtists(session, artistId, {Database::TrackArtistLinkType::Artist, Database::TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount)) + { std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl; + } } } @@ -143,6 +144,7 @@ int main(int argc, char *argv[]) ("artists,a", "Display recommendation for artists") ("releases,r", "Display recommendation for releases") ("tracks,t", "Display recommendation for tracks") + ("max,m", po::value()->default_value(3), "Max similarity result count") ; po::variables_map vm; @@ -165,16 +167,19 @@ int main(int argc, char *argv[]) std::cout << "Loading recommendation engine..." << std::endl; engine->load(false); + + unsigned maxSimilarityCount {vm["max"].as()}; + std::cout << "Recommendation engine loaded!" << std::endl; if (vm.count("tracks")) - dumpTracksRecommendation(db, *engine); + dumpTracksRecommendation(db, *engine, maxSimilarityCount); if (vm.count("releases")) - dumpReleasesRecommendation(db, *engine); + dumpReleasesRecommendation(db, *engine, maxSimilarityCount); if (vm.count("artists")) - dumpArtistsRecommendation(db, *engine); + dumpArtistsRecommendation(db, *engine, maxSimilarityCount); } catch( std::exception& e) {