Merge branch 'develop' for release v3.22.0
This commit is contained in:
+185
@@ -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.</br>
|
||||
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).</br>
|
||||
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=<STB|GraphicksMagick++>`
|
||||
|
||||
```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
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
# LMS - Lightweight Music Server
|
||||
|
||||
[](https://travis-ci.org/epoupon/lms)  [](https://www.codefactor.io/repository/github/epoupon/lms/overview/master)
|
||||
 [](https://travis-ci.org/epoupon/lms) [](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.</br>
|
||||
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).</br>
|
||||
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=<STB|GraphicksMagick++>`
|
||||
|
||||
```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.
|
||||
|
||||
@@ -143,7 +143,12 @@
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.Artists.linktype-all">All artists</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">Track artists</message>
|
||||
<message id="Lms.Explore.Artists.linktype-composer">Composers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-lyricist">Lyricists</message>
|
||||
<message id="Lms.Explore.Artists.linktype-mixer">Mixers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-producer">Producers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">Album artists</message>
|
||||
<message id="Lms.Explore.Artists.linktype-remixer">Remixers</message>
|
||||
|
||||
<!--Explore:Release-->
|
||||
<message id="Lms.Explore.Release.similar-releases">Similar albums</message>
|
||||
@@ -194,6 +199,7 @@
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Track artists</message>
|
||||
<message id="Lms.Settings.subsonic-api">Subsonic API</message>
|
||||
<message id="Lms.Settings.transcode">Transcoding</message>
|
||||
<message id="Lms.Settings.transcode-bitrate">Transcode bitrate</message>
|
||||
|
||||
@@ -143,7 +143,12 @@
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.Artists.linktype-all">Tous les artistes</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">Artistes de piste</message>
|
||||
<message id="Lms.Explore.Artists.linktype-composer">Compositeurs</message>
|
||||
<message id="Lms.Explore.Artists.linktype-lyricist">Paroliers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-mixer">Mixers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-producer">Producteurs</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">Artistes d'album</message>
|
||||
<message id="Lms.Explore.Artists.linktype-remixer">Remixers</message>
|
||||
|
||||
<!--Explore:Release-->
|
||||
<message id="Lms.Explore.Release.similar-releases">Albums similaires</message>
|
||||
@@ -194,6 +199,7 @@
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Tous les artistes de piste</message>
|
||||
<message id="Lms.Settings.subsonic-api">API Subsonic</message>
|
||||
<message id="Lms.Settings.transcode">Transcodage</message>
|
||||
<message id="Lms.Settings.transcode-bitrate">Bitrate du transcodage</message>
|
||||
|
||||
@@ -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<Database::IdType> releaseId;
|
||||
};
|
||||
|
||||
std::optional<TrackInfo>
|
||||
getTrackInfo(Database::Session& dbSession, Database::IdType trackId)
|
||||
{
|
||||
std::optional<TrackInfo> 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<IEncodedImage>
|
||||
Grabber::getFromFile(const std::filesystem::path& p, ImageSize width) const
|
||||
Grabber::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> 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<IEncodedImage> image {getFromFile(_defaultCoverPath, width)};
|
||||
std::shared_ptr<IEncodedImage> 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<IEncodedImage>
|
||||
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<std::string, std::filesystem::path> coverPaths {getCoverPaths(p)};
|
||||
const std::multimap<std::string, std::filesystem::path> 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<IEncodedImage> 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<IEncodedImage>
|
||||
Grabber::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> 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<std::string, std::filesystem::path>
|
||||
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<IEncodedImage>
|
||||
Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width)
|
||||
{
|
||||
return getFromTrack(dbSession, trackId, width, true /* allow release fallback*/);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width, bool allowReleaseFallback)
|
||||
{
|
||||
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> 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<Database::IdType> trackId;
|
||||
struct ReleaseInfo
|
||||
{
|
||||
Database::IdType firstTrackId;
|
||||
std::filesystem::path releaseDirectory;
|
||||
};
|
||||
|
||||
auto getReleaseInfo {[&]
|
||||
{
|
||||
std::optional<ReleaseInfo> 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> 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)
|
||||
|
||||
@@ -105,14 +105,18 @@ namespace CoverArt
|
||||
std::shared_ptr<IEncodedImage> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, ImageSize width) override;
|
||||
void flushCache() override;
|
||||
|
||||
std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width, bool allowReleaseFallback);
|
||||
std::unique_ptr<IEncodedImage> getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const;
|
||||
std::unique_ptr<IEncodedImage> getFromFile(const std::filesystem::path& p, ImageSize width) const;
|
||||
std::unique_ptr<IEncodedImage> getFromCoverFile(const std::filesystem::path& p, ImageSize width) const;
|
||||
|
||||
std::unique_ptr<IEncodedImage> getFromTrack(const std::filesystem::path& path, ImageSize width) const;
|
||||
std::multimap<std::string, std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const;
|
||||
std::unique_ptr<IEncodedImage> getFromDirectory(const std::filesystem::path& path, std::string_view preferredFileName, ImageSize width) const;
|
||||
std::unique_ptr<IEncodedImage> getFromDirectory(const std::filesystem::path& directory, ImageSize width) const;
|
||||
std::unique_ptr<IEncodedImage> getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const;
|
||||
std::shared_ptr<IEncodedImage> getDefault(ImageSize width);
|
||||
|
||||
bool checkCoverFile(const std::filesystem::path& directoryPath) const;
|
||||
|
||||
std::shared_mutex _cacheMutex;
|
||||
std::unordered_map<CacheEntryDesc, std::shared_ptr<IEncodedImage>> _cache;
|
||||
std::unordered_map<ImageSize, std::shared_ptr<IEncodedImage>> _defaultCoverCache;
|
||||
|
||||
@@ -81,7 +81,7 @@ createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<TrackArtistLink::Type> linkType)
|
||||
std::optional<TrackArtistLinkType> linkType)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -214,7 +214,7 @@ Artist::getAllIds(Session& session)
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Artist::getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<std::size_t> size)
|
||||
Artist::getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -265,7 +265,7 @@ std::vector<Artist::pointer>
|
||||
Artist::getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
@@ -306,7 +306,7 @@ std::vector<Artist::pointer>
|
||||
Artist::getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::set<IdType>& clusters,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
@@ -338,7 +338,7 @@ std::vector<Artist::pointer>
|
||||
Artist::getStarred(Session& session,
|
||||
User::pointer user,
|
||||
const std::set<IdType>& clusters,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
@@ -443,7 +443,7 @@ Artist::getReleaseCount() const
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>>
|
||||
Artist::getTracks(std::optional<TrackArtistLink::Type> linkType) const
|
||||
Artist::getTracks(std::optional<TrackArtistLinkType> linkType) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
@@ -462,7 +462,7 @@ Artist::getTracks(std::optional<TrackArtistLink::Type> linkType) const
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>>
|
||||
Artist::getTracksWithRelease(std::optional<TrackArtistLink::Type> linkType) const
|
||||
Artist::getTracksWithRelease(std::optional<TrackArtistLinkType> linkType) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
@@ -497,13 +497,14 @@ Artist::getRandomTracks(std::optional<std::size_t> count) const
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>>
|
||||
Artist::getSimilarArtists(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
|
||||
Artist::getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::Query<pointer> query {session()->query<pointer>(
|
||||
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<std::size_t> offset, std::optional<std::
|
||||
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
||||
" WHERE a.id = ?)"
|
||||
" AND a.id <> ?"
|
||||
)
|
||||
" 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<pointer> query {session()->query<pointer>(oss.str())
|
||||
.bind(self()->id())
|
||||
.bind(self()->id())
|
||||
.groupBy("a.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(count ? static_cast<int>(*count) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)};
|
||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)};
|
||||
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
query.bind(type);
|
||||
|
||||
Wt::Dbo::collection<pointer> res = query;
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
|
||||
@@ -132,11 +132,25 @@ ClusterType::getAllOrphans(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>("select c_t from cluster_type c_t LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id WHERE c.id IS NULL");
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
|
||||
"SELECT c_t from cluster_type c_t"
|
||||
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
|
||||
.where("c.id IS NULL");
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<ClusterType::pointer>
|
||||
ClusterType::getAllUsed(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
|
||||
"SELECT DISTINCT c_t from cluster_type c_t")
|
||||
.join("cluster c ON c_t.id = c.cluster_type_id");
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
ClusterType::pointer
|
||||
ClusterType::getByName(Session& session, const std::string& name)
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
|
||||
#include "database/Release.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#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::pointer>
|
||||
Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<std::size_t> offset, std::optional<std::size_t> limit)
|
||||
Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<Range> range)
|
||||
{
|
||||
Wt::Dbo::collection<Release::pointer> res = session.getDboSession().query<Release::pointer>
|
||||
("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<int>(*offset) : -1)
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) : -1);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
@@ -431,7 +432,7 @@ Release::getCopyrightURL() const
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>>
|
||||
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<Track>
|
||||
Release::getFirstTrack() const
|
||||
{
|
||||
assert(self());
|
||||
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId());
|
||||
assert(session());
|
||||
|
||||
return session()->query<Track::pointer>("SELECT t from track t")
|
||||
.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
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<Wt::Dbo::ptr<Artist>>
|
||||
Track::getArtists(TrackArtistLink::Type type) const
|
||||
Track::getArtists(EnumSet<TrackArtistLinkType> linkTypes) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> artists {session()->query<Artist::pointer>("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<Wt::Dbo::ptr<Artist>>(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<Artist::pointer> query {session()->query<Artist::pointer>(oss.str())};
|
||||
|
||||
for (TrackArtistLinkType type : linkTypes)
|
||||
query.bind(type);
|
||||
|
||||
query.where("t.id = ?").bind(self()->id());
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> res = query;
|
||||
return std::vector<Artist::pointer>(std::begin(res), std::end(res));
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Track::getArtistIds(TrackArtistLink::Type type) const
|
||||
Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
|
||||
{
|
||||
assert(self());
|
||||
assert(IdIsValid(self()->id()));
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<IdType> artists {session()->query<IdType>("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<IdType>(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<IdType> query {session()->query<IdType>(oss.str())
|
||||
.where("t.id = ?").bind(self()->id())};
|
||||
|
||||
for (TrackArtistLinkType type : linkTypes)
|
||||
query.bind(type);
|
||||
|
||||
Wt::Dbo::collection<IdType> res = query;
|
||||
return std::vector<IdType>(std::begin(res), std::end(res));
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<TrackArtistLink>>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
TrackArtistLink::TrackArtistLink(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, Type type)
|
||||
TrackArtistLink::TrackArtistLink(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> 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> track, Wt::Dbo::ptr<Artist> artist,Type type)
|
||||
TrackArtistLink::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, TrackArtistLinkType type)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
@@ -43,5 +43,15 @@ TrackArtistLink::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::pt
|
||||
return res;
|
||||
}
|
||||
|
||||
EnumSet<TrackArtistLinkType>
|
||||
TrackArtistLink::getUsedTypes(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackArtistLinkType> collection = session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link");
|
||||
|
||||
return EnumSet<TrackArtistLinkType>(std::begin(collection), std::end(collection));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ TrackList::getEntriesReverse(std::optional<std::size_t> offset, std::optional<st
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Artist::pointer>
|
||||
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType)
|
||||
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
||||
{
|
||||
auto query {session.query<Artist::pointer>(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<Artist::pointer>
|
||||
TrackList::getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
@@ -417,7 +417,7 @@ TrackList::getDuration() const
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
TrackList::getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
@@ -21,14 +21,15 @@
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#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<Artist>
|
||||
@@ -68,7 +70,7 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters, // if non empty, at least one artist that belongs to these clusters
|
||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
|
||||
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
@@ -77,19 +79,19 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod);
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<std::size_t> size = {});
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // No track related
|
||||
static std::vector<pointer> getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::set<IdType>& clusters,
|
||||
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
std::optional<Range>,
|
||||
bool& moreResults);
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getStarred(Session& session,
|
||||
Wt::Dbo::ptr<User> user,
|
||||
const std::set<IdType>& clusters,
|
||||
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range>, bool& moreResults);
|
||||
|
||||
@@ -100,10 +102,12 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Release>> getReleases(const std::set<IdType>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
|
||||
std::size_t getReleaseCount() const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracks(std::optional<TrackArtistLink::Type> linkType = {}) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracksWithRelease(std::optional<TrackArtistLink::Type> linkType = {}) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracksWithRelease(std::optional<TrackArtistLinkType> linkType = {}) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getRandomTracks(std::optional<std::size_t> count) const;
|
||||
std::vector<pointer> getSimilarArtists(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||
|
||||
// No artistLinkTypes means get them all
|
||||
std::vector<pointer> getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
|
||||
|
||||
// Get the cluster of the tracks made by this artist
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||
|
||||
@@ -92,6 +92,7 @@ class ClusterType : public Wt::Dbo::Dbo<ClusterType>
|
||||
ClusterType(std::string name);
|
||||
|
||||
static std::vector<pointer> getAllOrphans(Session& session);
|
||||
static std::vector<pointer> getAllUsed(Session& session);
|
||||
static pointer getByName(Session& session, const std::string& name);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
|
||||
@@ -20,11 +20,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <set>
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#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<Release>
|
||||
static std::vector<pointer> getAllRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<Range> range = std::nullopt);
|
||||
static std::vector<pointer> getStarred(Session& session, Wt::Dbo::ptr<User> user, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
|
||||
static std::vector<pointer> getByClusters(Session& session, const std::set<IdType>& clusters);
|
||||
@@ -71,6 +73,7 @@ class Release : public Wt::Dbo::Dbo<Release>
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracks(const std::set<IdType>& clusters = std::set<IdType>()) const;
|
||||
std::size_t getTracksCount() const;
|
||||
Wt::Dbo::ptr<Track> getFirstTrack() const;
|
||||
|
||||
// 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<Release>
|
||||
Wt::WDateTime getLastWritten() const;
|
||||
|
||||
// Get the artists of this release
|
||||
std::vector<Wt::Dbo::ptr<Artist> > getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
|
||||
std::vector<Wt::Dbo::ptr<Artist> > getReleaseArtists() const { return getArtists(TrackArtistLink::Type::ReleaseArtist); }
|
||||
std::vector<Wt::Dbo::ptr<Artist> > getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||
std::vector<Wt::Dbo::ptr<Artist> > getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
||||
bool hasVariousArtists() const;
|
||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||
|
||||
|
||||
@@ -26,12 +26,13 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#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<Track>
|
||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
|
||||
std::vector<IdType> getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
|
||||
// no artistLinkTypes means get all
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const;
|
||||
std::vector<IdType> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const;
|
||||
std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() const;
|
||||
Wt::Dbo::ptr<Release> getRelease() const { return _release; }
|
||||
std::vector<Wt::Dbo::ptr<Cluster>> getClusters() const;
|
||||
|
||||
@@ -19,64 +19,53 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#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<TrackArtistLink>;
|
||||
class Artist;
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
TrackArtistLink() = default;
|
||||
TrackArtistLink(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, Type type);
|
||||
class TrackArtistLink
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<TrackArtistLink>;
|
||||
|
||||
static pointer create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist,Type type);
|
||||
TrackArtistLink() = default;
|
||||
TrackArtistLink(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, TrackArtistLinkType type);
|
||||
|
||||
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
||||
Wt::Dbo::ptr<Artist> getArtist() const { return _artist; }
|
||||
Type getType() const { return _type; }
|
||||
static pointer create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, TrackArtistLinkType type);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _type, "name");
|
||||
static EnumSet<TrackArtistLinkType> getUsedTypes(Session& session);
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
||||
Wt::Dbo::ptr<Artist> getArtist() const { return _artist; }
|
||||
TrackArtistLinkType getType() const { return _type; }
|
||||
|
||||
private:
|
||||
template<class Action>
|
||||
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> _track;
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
};
|
||||
private:
|
||||
TrackArtistLinkType _type;
|
||||
std::string _name;
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "TrackArtistLink.hpp"
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
@@ -53,7 +52,7 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
||||
TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
|
||||
|
||||
// Stats utility
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
|
||||
@@ -85,7 +84,7 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
||||
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntriesReverse(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -132,8 +132,9 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
// Do not change enum values!
|
||||
enum class SubsonicArtistListMode
|
||||
{
|
||||
AllArtists = 0,
|
||||
AllArtists = 0,
|
||||
ReleaseArtists = 1,
|
||||
TrackArtists = 2,
|
||||
};
|
||||
|
||||
static inline const std::size_t MinNameLength {3};
|
||||
|
||||
@@ -42,13 +42,13 @@ namespace MetaData
|
||||
|
||||
template<typename T>
|
||||
std::vector<T>
|
||||
getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::set<std::string>& keys)
|
||||
getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::vector<std::string_view>& keys)
|
||||
{
|
||||
std::vector<T> 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<Artist>
|
||||
getArtists(const TagLib::PropertyMap& properties)
|
||||
getArtists(const TagLib::PropertyMap& properties,
|
||||
const std::vector<std::string_view>& artistTagNames,
|
||||
const std::vector<std::string_view>& artistSortTagNames,
|
||||
const std::vector<std::string_view>& artistMBIDTagNames
|
||||
)
|
||||
{
|
||||
std::vector<std::string> artistNames {getPropertyValuesAs<std::string>(properties, "ARTISTS")};
|
||||
if (artistNames.empty())
|
||||
artistNames = getPropertyValuesAs<std::string>(properties, "ARTIST");
|
||||
|
||||
const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(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<std::string> artistSortNames {getPropertyValuesAs<std::string>(properties, "ARTISTSORT")};
|
||||
const std::vector<std::string> artistSortNames {getPropertyValuesFirstMatchAs<std::string>(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<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"})};
|
||||
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(properties, artistMBIDTagNames)};
|
||||
|
||||
if (artistNames.size() == artistsMBID.size())
|
||||
{
|
||||
@@ -128,41 +129,6 @@ getArtists(const TagLib::PropertyMap& properties)
|
||||
return artists;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getAlbumArtists(const TagLib::PropertyMap& properties)
|
||||
{
|
||||
std::vector<std::string> artistNames {getPropertyValuesAs<std::string>(properties, "ALBUMARTIST")};
|
||||
if (artistNames.empty())
|
||||
return {};
|
||||
|
||||
std::vector<Artist> 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<std::string> artistSortNames {getPropertyValuesAs<std::string>(properties, "ALBUMARTISTSORT")};
|
||||
if (artistSortNames.size() == artists.size())
|
||||
{
|
||||
for (std::size_t i {}; i < artistSortNames.size(); ++i)
|
||||
artists[i].sortName = artistSortNames[i];
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(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<Album>
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,12 @@ namespace MetaData
|
||||
std::optional<float> trackReplayGain;
|
||||
std::optional<float> albumReplayGain;
|
||||
std::string discSubtitle;
|
||||
std::vector<Artist> conductorArtists;
|
||||
std::vector<Artist> composerArtists;
|
||||
std::vector<Artist> lyricistArtists;
|
||||
std::vector<Artist> mixerArtists;
|
||||
std::vector<Artist> producerArtists;
|
||||
std::vector<Artist> remixerArtists;
|
||||
};
|
||||
|
||||
class IParser
|
||||
|
||||
@@ -132,7 +132,10 @@ Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releas
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount)
|
||||
Engine::getSimilarArtists(Database::Session& dbSession,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount)
|
||||
{
|
||||
std::unordered_set<Database::IdType> 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() << "'";
|
||||
|
||||
@@ -56,10 +56,13 @@ namespace Recommendation
|
||||
void load(bool forceReload, const ProgressCallback& progressCallback) override;
|
||||
void cancelLoad() override;
|
||||
|
||||
std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override;
|
||||
std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) override;
|
||||
std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override;
|
||||
std::unordered_set<Database::IdType> 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<Database::IdType>& tracksId, std::size_t maxCount) override;
|
||||
ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override;
|
||||
ResultContainer getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) override;
|
||||
|
||||
void setClassifierPriorities(const std::vector<ClassifierType>& classifierTypes);
|
||||
void clearClassifiers();
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <unordered_set>
|
||||
|
||||
#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<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0;
|
||||
virtual std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const = 0;
|
||||
virtual std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0;
|
||||
virtual std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const = 0;
|
||||
using ResultContainer = std::unordered_set<Database::IdType>;
|
||||
|
||||
virtual ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
|
||||
};
|
||||
|
||||
} // ns Recommendation
|
||||
|
||||
@@ -83,7 +83,10 @@ ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::Id
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount) const
|
||||
ClusterClassifier::getSimilarArtists(Database::Session& dbSession,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> artistLinkTypes,
|
||||
std::size_t maxCount) const
|
||||
{
|
||||
std::unordered_set<Database::IdType> 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(); });
|
||||
|
||||
|
||||
@@ -40,10 +40,13 @@ namespace Recommendation
|
||||
bool load(Database::Session&, bool, const ProgressCallback&) override { return true; }
|
||||
void requestCancelLoad() override {}
|
||||
|
||||
std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override;
|
||||
std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const override;
|
||||
std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override;
|
||||
std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, 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<Database::IdType>& tracksId, std::size_t maxCount) const override;
|
||||
ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override;
|
||||
ResultContainer getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) const override;
|
||||
};
|
||||
|
||||
} // namespace Recommendation
|
||||
|
||||
@@ -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<Database::IdType>
|
||||
FeaturesClassifier::getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const
|
||||
FeaturesClassifier::getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) const
|
||||
{
|
||||
auto similarArtistIds {getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount)};
|
||||
auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType)
|
||||
{
|
||||
std::unordered_set<Database::IdType> 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<Database::IdType> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,10 @@ class FeaturesClassifier : public IClassifier
|
||||
std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override;
|
||||
std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const override;
|
||||
std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override;
|
||||
std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override;
|
||||
std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) const override;
|
||||
|
||||
bool loadFromCache(Database::Session& session, const FeaturesClassifierCache& cache);
|
||||
|
||||
@@ -99,8 +102,8 @@ class FeaturesClassifier : public IClassifier
|
||||
std::unique_ptr<SOM::Network> _network;
|
||||
double _networkRefVectorsDistanceMedian {};
|
||||
|
||||
MatrixOfObjects _artistsMap;
|
||||
ObjectPositions _artistPositions;
|
||||
ObjectPositions _artistPositions;
|
||||
std::unordered_map<Database::TrackArtistLinkType, MatrixOfObjects> _artistsMap;
|
||||
|
||||
MatrixOfObjects _releasesMap;
|
||||
ObjectPositions _releasePositions;
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <unordered_set>
|
||||
|
||||
#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<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0;
|
||||
virtual std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) = 0;
|
||||
virtual std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0;
|
||||
virtual std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) = 0;
|
||||
using ResultContainer = std::unordered_set<Database::IdType>;
|
||||
|
||||
virtual ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0;
|
||||
virtual ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) = 0;
|
||||
virtual ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0;
|
||||
virtual ResultContainer getSimilarArtists(Database::Session& session,
|
||||
Database::IdType artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IEngine> createEngine(Database::Db& db);
|
||||
|
||||
@@ -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<Cluster::pointer> clusters {getOrCreateClusters(_dbSession, trackInfo->clusters)};
|
||||
|
||||
// ***** Artists
|
||||
std::vector<Artist::pointer> artists {getOrCreateArtists(_dbSession, trackInfo->artists)};
|
||||
|
||||
// ***** Release artists
|
||||
std::vector<Artist::pointer> 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});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#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
|
||||
|
||||
@@ -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<Artist::pointer>& 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<std::string>(context.parameters, "type")};
|
||||
const std::string type {getMandatoryParameterAs<std::string>(context.parameters, "type")};
|
||||
|
||||
// Optional params
|
||||
std::size_t size {getParameterAs<std::size_t>(context.parameters, "size").value_or(10)};
|
||||
std::size_t offset {getParameterAs<std::size_t>(context.parameters, "offset").value_or(0)};
|
||||
const std::size_t size {getParameterAs<std::size_t>(context.parameters, "size").value_or(10)};
|
||||
const std::size_t offset {getParameterAs<std::size_t>(context.parameters, "offset").value_or(0)};
|
||||
|
||||
const Range range {offset, size};
|
||||
|
||||
std::vector<Release::pointer> 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<int>(context.parameters, "fromYear")};
|
||||
int toYear {getMandatoryParameterAs<int>(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<int>(context.parameters, "fromYear")};
|
||||
int toYear {getMandatoryParameterAs<int>(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<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession, id.value, count)};
|
||||
auto similarArtistsId {Service<Recommendation::IEngine>::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<TrackArtistLink::Type> linkType;
|
||||
std::optional<TrackArtistLinkType> 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<TrackArtistLink::Type> linkType;
|
||||
std::optional<TrackArtistLinkType> 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<Id>(context.parameters, "id")};
|
||||
if (id.type != Id::Type::Artist)
|
||||
const Id artistId {getMandatoryParameterAs<Id>(context.parameters, "id")};
|
||||
if (artistId.type != Id::Type::Artist)
|
||||
throw BadParameterGenericError {"id"};
|
||||
|
||||
// Optional params
|
||||
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)};
|
||||
|
||||
auto similarArtistsId {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession, id.value, 5)};
|
||||
auto similarArtistIds {Service<Recommendation::IEngine>::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<Id> ids {getMandatoryMultiParametersAs<Id>(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<std::string, RequestEntryPointInfo> 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}},
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <type_traits>
|
||||
|
||||
template <typename T, typename underlying_type = std::uint32_t>
|
||||
class EnumSet
|
||||
{
|
||||
static_assert(std::is_enum<T>::value);
|
||||
static_assert(std::is_same<underlying_type, std::uint64_t>::value || std::is_same<underlying_type, std::uint32_t>::value);
|
||||
|
||||
using index_type = std::uint_fast8_t;
|
||||
|
||||
public:
|
||||
EnumSet() = default;
|
||||
constexpr EnumSet(std::initializer_list<T> values)
|
||||
{
|
||||
for (T value : values)
|
||||
insert(value);
|
||||
}
|
||||
|
||||
template <typename It>
|
||||
constexpr EnumSet(It begin, It end)
|
||||
{
|
||||
for (It it {begin}; it != end; ++it)
|
||||
insert(*it);
|
||||
}
|
||||
|
||||
constexpr void insert(T value)
|
||||
{
|
||||
assert(static_cast<size_t>(value) < sizeof(_bitfield) * 8);
|
||||
_bitfield |= (underlying_type{ 1 } << static_cast<underlying_type>(value));
|
||||
}
|
||||
|
||||
constexpr void erase(T value)
|
||||
{
|
||||
assert(static_cast<size_t>(value) < sizeof(_bitfield) * 8);
|
||||
_bitfield &= ~(underlying_type{ 1 } << static_cast<underlying_type>(value));
|
||||
}
|
||||
|
||||
constexpr bool empty() const
|
||||
{
|
||||
return _bitfield == 0;
|
||||
}
|
||||
|
||||
constexpr bool contains(T value) const
|
||||
{
|
||||
assert(static_cast<size_t>(value) < sizeof(_bitfield) * 8);
|
||||
return _bitfield & (underlying_type{ 1 } << static_cast<underlying_type>(value));
|
||||
}
|
||||
|
||||
class iterator
|
||||
{
|
||||
public:
|
||||
using value_type = T;
|
||||
|
||||
constexpr value_type operator*() const
|
||||
{
|
||||
return static_cast<value_type>(_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<index_type>::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{};
|
||||
};
|
||||
|
||||
|
||||
+2
-1
@@ -77,7 +77,8 @@ generateWtConfig(std::string execPath)
|
||||
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
|
||||
|
||||
{
|
||||
const unsigned long httpServerThreadCount {configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(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<unsigned long>(2, std::thread::hardware_concurrency())};
|
||||
args.push_back("--threads=" + std::to_string(httpServerThreadCount));
|
||||
}
|
||||
|
||||
|
||||
@@ -541,7 +541,7 @@ LmsApplication::createHome()
|
||||
{
|
||||
const std::string sessionId {LmsApp->sessionId()};
|
||||
|
||||
Service<Scanner::IMediaScanner>::get()->scanStarted().connect(this, [=] ()
|
||||
Service<Scanner::IMediaScanner>::get()->scanStarted().connect(this, [=]
|
||||
{
|
||||
Wt::WServer::instance()->post(sessionId, [=]
|
||||
{
|
||||
@@ -550,9 +550,9 @@ LmsApplication::createHome()
|
||||
});
|
||||
});
|
||||
|
||||
Service<Scanner::IMediaScanner>::get()->scanComplete().connect(this, [=] ()
|
||||
Service<Scanner::IMediaScanner>::get()->scanComplete().connect(this, [=]
|
||||
{
|
||||
Wt::WServer::instance()->post(sessionId, [=]
|
||||
Wt::WServer::instance()->post(sessionId, [this]
|
||||
{
|
||||
_events.dbScanned.emit();
|
||||
triggerUpdate();
|
||||
|
||||
@@ -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 = {"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -333,6 +333,7 @@ class SettingsModel : public Wt::WFormModel
|
||||
_subsonicArtistListModeModel = std::make_shared<ValueStringModel<User::SubsonicArtistListMode>>();
|
||||
_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();
|
||||
});
|
||||
|
||||
@@ -167,10 +167,10 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
|
||||
DatabaseSettingsView::DatabaseSettingsView()
|
||||
{
|
||||
wApp->internalPathChanged().connect(std::bind([=]
|
||||
wApp->internalPathChanged().connect(this, [this]
|
||||
{
|
||||
refreshView();
|
||||
}));
|
||||
});
|
||||
|
||||
refreshView();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#include <Wt/WResource.h>
|
||||
#include <Wt/WSplitButton.h>
|
||||
|
||||
#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"));
|
||||
});
|
||||
|
||||
@@ -242,7 +242,7 @@ class UserModel : public Wt::WFormModel
|
||||
|
||||
UserView::UserView()
|
||||
{
|
||||
wApp->internalPathChanged().connect([this]()
|
||||
wApp->internalPathChanged().connect(this, [this]()
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ UsersView::UsersView()
|
||||
LmsApp->setInternalPath("/admin/user", true);
|
||||
});
|
||||
|
||||
wApp->internalPathChanged().connect([this]()
|
||||
wApp->internalPathChanged().connect(this, [this]()
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
|
||||
@@ -73,6 +73,12 @@ class ValueStringModel : public Wt::WStringListModel
|
||||
setData(rowCount() - 1, 0, str, Wt::ItemDataRole::Display);
|
||||
}
|
||||
|
||||
void
|
||||
clear()
|
||||
{
|
||||
removeRows(0, rowCount());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -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<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)};
|
||||
const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(),
|
||||
*artistId,
|
||||
{TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist},
|
||||
5)};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
|
||||
@@ -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<std::optional<TrackArtistLink::Type>>;
|
||||
using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLinkType>>;
|
||||
|
||||
Artists::Artists(Filters* filters)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")},
|
||||
@@ -68,14 +69,14 @@ Artists::Artists(Filters* filters)
|
||||
}
|
||||
|
||||
_linkType = bindNew<Wt::WComboBox>("link-type");
|
||||
{
|
||||
auto linkTypeModel {std::make_shared<ArtistLinkModel>()};
|
||||
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<ArtistLinkModel>());
|
||||
_linkType->changed().connect([this] { refreshView(); });
|
||||
refreshArtistLinkTypes();
|
||||
|
||||
LmsApp->getEvents().dbScanned.connect(this, [this]
|
||||
{
|
||||
refreshArtistLinkTypes();
|
||||
});
|
||||
|
||||
_container = bindNew<Wt::WContainerWidget>("artists");
|
||||
hideLoadingIndicator();
|
||||
@@ -100,6 +101,37 @@ Artists::refreshView(Mode mode)
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void
|
||||
Artists::refreshArtistLinkTypes()
|
||||
{
|
||||
std::shared_ptr<ArtistLinkModel> linkTypeModel {std::static_pointer_cast<ArtistLinkModel>(_linkType->model())};
|
||||
|
||||
EnumSet<Database::TrackArtistLinkType> 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> range, bool& moreResults)
|
||||
{
|
||||
std::vector<Artist::pointer> artists;
|
||||
|
||||
const std::optional<TrackArtistLink::Type> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
|
||||
const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkModel*>(_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> range, bool& moreResults)
|
||||
{
|
||||
std::vector<Artist::pointer> artists;
|
||||
|
||||
const std::optional<TrackArtistLink::Type> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
|
||||
const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
|
||||
|
||||
const std::optional<std::size_t> modeLimit{maxItemsPerMode[_mode]};
|
||||
if (modeLimit)
|
||||
|
||||
@@ -56,6 +56,7 @@ class Artists : public Wt::WTemplate
|
||||
|
||||
void refreshView();
|
||||
void refreshView(Mode mode);
|
||||
void refreshArtistLinkTypes();
|
||||
void displayLoadingIndicator();
|
||||
void hideLoadingIndicator();
|
||||
void addSome();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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()));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace UserInterface::TrackListHelpers
|
||||
Wt::WText* name {entry->bindNew<Wt::WText>("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()};
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <Wt/WResource.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
@@ -17,39 +17,34 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef COVER_RESOURCE_HPP_
|
||||
#define COVER_RESOURCE_HPP_
|
||||
|
||||
#include <mutex>
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WResource.h>
|
||||
|
||||
#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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<Database::IdType> 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<Database::IdType> 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<Database::IdType> 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<unsigned>()->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<unsigned>()};
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user