Merge branch 'develop' for release v3.22.0

This commit is contained in:
emeric
2020-11-13 20:11:13 +01:00
58 changed files with 1263 additions and 609 deletions
+185
View File
@@ -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
+8 -180
View File
@@ -1,17 +1,16 @@
# LMS - Lightweight Music Server # LMS - Lightweight Music Server
[![Build Status](https://travis-ci.org/epoupon/lms.svg?branch=master)](https://travis-ci.org/epoupon/lms) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/epoupon/lms) [![CodeFactor](https://www.codefactor.io/repository/github/epoupon/lms/badge/master)](https://www.codefactor.io/repository/github/epoupon/lms/overview/master) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/epoupon/lms) [![Build Status](https://travis-ci.org/epoupon/lms.svg?branch=master)](https://travis-ci.org/epoupon/lms) [![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/epoupon/lms.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/epoupon/lms/context:cpp)
_LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface! _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 ## 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 * Recommendation engine
* Audio transcode for maximum interoperability and low bandwith requirements * Audio transcode for maximum interoperability and low bandwith requirements
* Multi-value tags: artists, genres, ... * Multi-value tags: artists, genres, composers, lyricists, moods, ...
* Custom tags support: _mood_, _genre_, _albummood_, _albumgrouping_, ...
* Compilation support * Compilation support
* [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names * [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names
* Disc subtitles support * 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 * Radio mode, based on what is in the current playqueue
* Searches in album, artist and track names (including sort names) * Searches in album, artist and track names (including sort names)
* Starred Albums/Artists/Tracks * 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: * Random/Starred/Most played/Recently played/Recently added for Artist/Albums/Tracks, allowing you to search for things like:
* Recently added _Electronic_ artists * Recently added _Electronic_ artists
* Random _Metal_ and _Aggressive_ albums * Random _Metal_ and _Aggressive_ albums
@@ -63,180 +63,8 @@ __Note__: since _LMS_ stores hashed and salted passwords, it cannot handle the _
## Installation ## Installation
### Docker See [INSTALL.md](INSTALL.md) file.
_Docker_ images are available, please see detailed instructions on https://hub.docker.com/r/epoupon/lms.
### Debian Buster packages ## Contributing
_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: Contributions are welcome! Please submit your pull requests against the [develop](../../tree/develop) branch.
```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/)
+6
View File
@@ -143,7 +143,12 @@
<!--Explore:Artists--> <!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">All artists</message> <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-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-releaseartist">Album artists</message>
<message id="Lms.Explore.Artists.linktype-remixer">Remixers</message>
<!--Explore:Release--> <!--Explore:Release-->
<message id="Lms.Explore.Release.similar-releases">Similar albums</message> <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">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.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.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.subsonic-api">Subsonic API</message>
<message id="Lms.Settings.transcode">Transcoding</message> <message id="Lms.Settings.transcode">Transcoding</message>
<message id="Lms.Settings.transcode-bitrate">Transcode bitrate</message> <message id="Lms.Settings.transcode-bitrate">Transcode bitrate</message>
+6
View File
@@ -143,7 +143,12 @@
<!--Explore:Artists--> <!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">Tous les artistes</message> <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-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-releaseartist">Artistes d'album</message>
<message id="Lms.Explore.Artists.linktype-remixer">Remixers</message>
<!--Explore:Release--> <!--Explore:Release-->
<message id="Lms.Explore.Release.similar-releases">Albums similaires</message> <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">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.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.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.subsonic-api">API Subsonic</message>
<message id="Lms.Settings.transcode">Transcodage</message> <message id="Lms.Settings.transcode">Transcodage</message>
<message id="Lms.Settings.transcode-bitrate">Bitrate du transcodage</message> <message id="Lms.Settings.transcode-bitrate">Bitrate du transcodage</message>
+141 -64
View File
@@ -38,6 +38,44 @@ using RawImage = CoverArt::GraphicsMagick::RawImage;
#include "utils/Utils.hpp" #include "utils/Utils.hpp"
#include "Exception.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 { namespace CoverArt {
static static
@@ -112,7 +150,7 @@ Grabber::getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const
} }
std::unique_ptr<IEncodedImage> 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; std::unique_ptr<IEncodedImage> image;
@@ -146,7 +184,7 @@ Grabber::getDefault(ImageSize width)
if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache)) if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache))
return it->second; return it->second;
std::shared_ptr<IEncodedImage> image {getFromFile(_defaultCoverPath, width)}; std::shared_ptr<IEncodedImage> image {getFromCoverFile(_defaultCoverPath, width)};
_defaultCoverCache[width] = image; _defaultCoverCache[width] = image;
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size(); LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
@@ -155,9 +193,9 @@ Grabber::getDefault(ImageSize width)
} }
std::unique_ptr<IEncodedImage> 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) 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})}; auto range {coverPaths.equal_range(std::string {fileName})};
for (auto it {range.first}; it != range.second; ++it) for (auto it {range.first}; it != range.second; ++it)
{ {
image = getFromFile(it->second, width); image = getFromCoverFile(it->second, width);
if (image) if (image)
break; break;
} }
@@ -175,13 +213,6 @@ Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view prefe
std::unique_ptr<IEncodedImage> image; std::unique_ptr<IEncodedImage> image;
if (!preferredFileName.empty())
{
image = tryLoadImageFromFilename(preferredFileName);
if (image)
return image;
}
for (std::string_view filename : _preferredFileNames) for (std::string_view filename : _preferredFileNames)
{ {
image = tryLoadImageFromFilename(filename); image = tryLoadImageFromFilename(filename);
@@ -192,7 +223,7 @@ Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view prefe
// Just pick one // Just pick one
for (const auto& [filename, coverPath] : coverPaths) for (const auto& [filename, coverPath] : coverPaths)
{ {
image = getFromFile(coverPath, width); image = getFromCoverFile(coverPath, width);
if (image) if (image)
return image; return image;
} }
@@ -200,6 +231,50 @@ Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view prefe
return image; 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> std::multimap<std::string, std::filesystem::path>
Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const 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; std::filesystem::directory_iterator itEnd;
while (!ec && itPath != 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); 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; return res;
@@ -238,8 +303,7 @@ Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const
try try
{ {
Av::MediaFile input {p}; const Av::MediaFile input {p};
image = getFromAvMediaFile(input, width); image = getFromAvMediaFile(input, width);
} }
catch (Av::AvException& e) catch (Av::AvException& e)
@@ -252,6 +316,15 @@ Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const
std::shared_ptr<IEncodedImage> std::shared_ptr<IEncodedImage>
Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width) 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; using namespace Database;
@@ -261,37 +334,24 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, Im
if (cover) if (cover)
return cover; return cover;
bool hasCover {}; if (const std::optional<TrackInfo> trackInfo {getTrackInfo(dbSession, trackId)})
bool isMultiDisc {};
std::filesystem::path trackPath;
{ {
auto transaction {dbSession.createSharedTransaction()}; if (trackInfo->hasCover)
cover = getFromTrack(trackInfo->trackPath, width);
const Track::pointer track {Track::getById(dbSession, trackId)}; if (!cover)
if (track) cover = getFromSameNamedFile(trackInfo->trackPath, width);
if (!cover && trackInfo->releaseId && allowReleaseFallback)
cover = getFromRelease(dbSession, *trackInfo->releaseId, width);
if (!cover && trackInfo->isMultiDisc)
{ {
hasCover = track->hasCover(); if (trackInfo->trackPath.parent_path().has_parent_path())
trackPath = track->getPath(); cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width);
auto release {track->getRelease()};
if (release && release->getTotalDisc() > 1)
isMultiDisc = true;
} }
} }
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) if (!cover)
cover = getDefault(width); cover = getDefault(width);
@@ -310,22 +370,39 @@ Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId,
if (cover) if (cover)
return 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()}; auto transaction {session.createSharedTransaction()};
const auto release {Database::Release::getById(session, releaseId)}; if (const Database::Release::pointer release {Database::Release::getById(session, releaseId)})
if (release)
{ {
const auto tracks {release->getTracks()}; if (const auto firstTrack {release->getFirstTrack()})
if (!tracks.empty()) {
trackId = tracks.front().id(); 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) if (!cover)
cover = getFromTrack(session, *trackId, width);
else
cover = getDefault(width); cover = getDefault(width);
if (cover) if (cover)
+6 -2
View File
@@ -105,14 +105,18 @@ namespace CoverArt
std::shared_ptr<IEncodedImage> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, ImageSize width) override; std::shared_ptr<IEncodedImage> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, ImageSize width) override;
void flushCache() 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> 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::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::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); std::shared_ptr<IEncodedImage> getDefault(ImageSize width);
bool checkCoverFile(const std::filesystem::path& directoryPath) const;
std::shared_mutex _cacheMutex; std::shared_mutex _cacheMutex;
std::unordered_map<CacheEntryDesc, std::shared_ptr<IEncodedImage>> _cache; std::unordered_map<CacheEntryDesc, std::shared_ptr<IEncodedImage>> _cache;
std::unordered_map<ImageSize, std::shared_ptr<IEncodedImage>> _defaultCoverCache; std::unordered_map<ImageSize, std::shared_ptr<IEncodedImage>> _defaultCoverCache;
+34 -13
View File
@@ -81,7 +81,7 @@ createQuery(Session& session,
const std::string& queryStr, const std::string& queryStr,
const std::set<IdType>& clusterIds, const std::set<IdType>& clusterIds,
const std::vector<std::string>& keywords, const std::vector<std::string>& keywords,
std::optional<TrackArtistLink::Type> linkType) std::optional<TrackArtistLinkType> linkType)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -214,7 +214,7 @@ Artist::getAllIds(Session& session)
} }
std::vector<IdType> 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(); session.checkSharedLocked();
@@ -265,7 +265,7 @@ std::vector<Artist::pointer>
Artist::getByFilter(Session& session, Artist::getByFilter(Session& session,
const std::set<IdType>& clusters, const std::set<IdType>& clusters,
const std::vector<std::string>& keywords, const std::vector<std::string>& keywords,
std::optional<TrackArtistLink::Type> linkType, std::optional<TrackArtistLinkType> linkType,
SortMethod sortMethod, SortMethod sortMethod,
std::optional<Range> range, std::optional<Range> range,
bool& moreResults) bool& moreResults)
@@ -306,7 +306,7 @@ std::vector<Artist::pointer>
Artist::getLastWritten(Session& session, Artist::getLastWritten(Session& session,
std::optional<Wt::WDateTime> after, std::optional<Wt::WDateTime> after,
const std::set<IdType>& clusters, const std::set<IdType>& clusters,
std::optional<TrackArtistLink::Type> linkType, std::optional<TrackArtistLinkType> linkType,
std::optional<Range> range, bool& moreResults) std::optional<Range> range, bool& moreResults)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -338,7 +338,7 @@ std::vector<Artist::pointer>
Artist::getStarred(Session& session, Artist::getStarred(Session& session,
User::pointer user, User::pointer user,
const std::set<IdType>& clusters, const std::set<IdType>& clusters,
std::optional<TrackArtistLink::Type> linkType, std::optional<TrackArtistLinkType> linkType,
SortMethod sortMethod, SortMethod sortMethod,
std::optional<Range> range, bool& moreResults) std::optional<Range> range, bool& moreResults)
{ {
@@ -443,7 +443,7 @@ Artist::getReleaseCount() const
} }
std::vector<Wt::Dbo::ptr<Track>> std::vector<Wt::Dbo::ptr<Track>>
Artist::getTracks(std::optional<TrackArtistLink::Type> linkType) const Artist::getTracks(std::optional<TrackArtistLinkType> linkType) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
@@ -462,7 +462,7 @@ Artist::getTracks(std::optional<TrackArtistLink::Type> linkType) const
} }
std::vector<Wt::Dbo::ptr<Track>> std::vector<Wt::Dbo::ptr<Track>>
Artist::getTracksWithRelease(std::optional<TrackArtistLink::Type> linkType) const Artist::getTracksWithRelease(std::optional<TrackArtistLinkType> linkType) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
@@ -497,13 +497,14 @@ Artist::getRandomTracks(std::optional<std::size_t> count) const
} }
std::vector<Wt::Dbo::ptr<Artist>> 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(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
assert(session()); assert(session());
Wt::Dbo::Query<pointer> query {session()->query<pointer>( std::ostringstream oss;
oss <<
"SELECT a FROM artist a" "SELECT a FROM artist a"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN track t ON t.id = t_a_l.track_id" " INNER JOIN 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 artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" WHERE a.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())
.bind(self()->id()) .bind(self()->id())
.groupBy("a.id") .groupBy("a.id")
.orderBy("COUNT(*) DESC, RANDOM()") .orderBy("COUNT(*) DESC, RANDOM()")
.limit(count ? static_cast<int>(*count) : -1) .limit(range ? static_cast<int>(range->limit) : -1)
.offset(offset ? static_cast<int>(*offset) : -1)}; .offset(range ? static_cast<int>(range->offset) : -1)};
for (TrackArtistLinkType type : artistLinkTypes)
query.bind(type);
Wt::Dbo::collection<pointer> res = query; Wt::Dbo::collection<pointer> res = query;
return std::vector<pointer>(res.begin(), res.end()); return std::vector<pointer>(res.begin(), res.end());
+15 -1
View File
@@ -132,11 +132,25 @@ ClusterType::getAllOrphans(Session& session)
{ {
session.checkSharedLocked(); 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()); 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::pointer
ClusterType::getByName(Session& session, const std::string& name) ClusterType::getByName(Session& session, const std::string& name)
+20 -5
View File
@@ -19,13 +19,14 @@
#include "database/Release.hpp" #include "database/Release.hpp"
#include "utils/Logger.hpp" #include <Wt/Dbo/WtSqlTraits.h>
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "utils/Logger.hpp"
#include "SqlQuery.hpp" #include "SqlQuery.hpp"
namespace Database namespace Database
@@ -232,15 +233,15 @@ Release::getLastWritten(Session& session,
} }
std::vector<Release::pointer> 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> 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") ("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(yearFrom)
.where("t.year <= ?").bind(yearTo) .where("t.year <= ?").bind(yearTo)
.orderBy("t.year, r.name COLLATE NOCASE") .orderBy("t.year, r.name COLLATE NOCASE")
.offset(offset ? static_cast<int>(*offset) : -1) .offset(range ? static_cast<int>(range->offset) : -1)
.limit(limit ? static_cast<int>(*limit) : -1); .limit(range ? static_cast<int>(range->limit) : -1);
return std::vector<pointer>(res.begin(), res.end()); return std::vector<pointer>(res.begin(), res.end());
} }
@@ -431,7 +432,7 @@ Release::getCopyrightURL() const
} }
std::vector<Wt::Dbo::ptr<Artist>> std::vector<Wt::Dbo::ptr<Artist>>
Release::getArtists(TrackArtistLink::Type linkType) const Release::getArtists(TrackArtistLinkType linkType) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
@@ -532,6 +533,20 @@ Release::getTracksCount() const
return _tracks.size(); 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 std::chrono::milliseconds
Release::getDuration() const Release::getDuration() const
{ {
+7 -1
View File
@@ -40,7 +40,7 @@
namespace Database { namespace Database {
#define LMS_DATABASE_VERSION 26 #define LMS_DATABASE_VERSION 27
using Version = std::size_t; 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 // Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion(); 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 else
{ {
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
+62 -10
View File
@@ -24,6 +24,7 @@
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackFeatures.hpp" #include "database/TrackFeatures.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -480,31 +481,82 @@ Track::getCopyrightURL() const
} }
std::vector<Wt::Dbo::ptr<Artist>> std::vector<Wt::Dbo::ptr<Artist>>
Track::getArtists(TrackArtistLink::Type type) const Track::getArtists(EnumSet<TrackArtistLinkType> linkTypes) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
assert(session()); 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") std::ostringstream oss;
.where("t.id = ?").bind(self()->id()) oss <<
.where("t_a_l.type = ?").bind(type)}; "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> std::vector<IdType>
Track::getArtistIds(TrackArtistLink::Type type) const Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
assert(session()); 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") std::ostringstream oss;
.where("t.id = ?").bind(self()->id()) oss <<
.where("t_a_l.type = ?").bind(type)}; "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>> std::vector<Wt::Dbo::ptr<TrackArtistLink>>
+12 -2
View File
@@ -25,7 +25,7 @@
namespace Database { 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}, : _type {type},
_track {track}, _track {track},
_artist {artist} _artist {artist}
@@ -33,7 +33,7 @@ _artist {artist}
} }
TrackArtistLink::pointer 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(); session.checkUniqueLocked();
@@ -43,5 +43,15 @@ TrackArtistLink::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::pt
return res; 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));
}
} }
+3 -3
View File
@@ -165,7 +165,7 @@ TrackList::getEntriesReverse(std::optional<std::size_t> offset, std::optional<st
static static
Wt::Dbo::Query<Artist::pointer> 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)}; auto query {session.query<Artist::pointer>(queryStr)};
query.join("track t ON t.id = t_a_l.track_id"); 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> 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(session());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
@@ -417,7 +417,7 @@ TrackList::getDuration() const
} }
std::vector<Artist::pointer> 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(session());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
+12 -8
View File
@@ -21,14 +21,15 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <unordered_set>
#include <vector> #include <vector>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include "utils/EnumSet.hpp"
#include "utils/UUID.hpp" #include "utils/UUID.hpp"
#include "TrackArtistLink.hpp"
#include "Types.hpp" #include "Types.hpp"
namespace Database namespace Database
@@ -39,6 +40,7 @@ class ClusterType;
class Release; class Release;
class Session; class Session;
class Track; class Track;
class TrackArtistLink;
class User; class User;
class Artist : public Wt::Dbo::Dbo<Artist> 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, 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::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) 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, SortMethod sortMethod,
std::optional<Range> range, std::optional<Range> range,
bool& moreExpected); 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);
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults); 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> 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> getAllOrphans(Session& session); // No track related
static std::vector<pointer> getLastWritten(Session& session, static std::vector<pointer> getLastWritten(Session& session,
std::optional<Wt::WDateTime> after, std::optional<Wt::WDateTime> after,
const std::set<IdType>& clusters, 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>, std::optional<Range>,
bool& moreResults); bool& moreResults);
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {}); static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
static std::vector<pointer> getStarred(Session& session, static std::vector<pointer> getStarred(Session& session,
Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<User> user,
const std::set<IdType>& clusters, 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, SortMethod sortMethod,
std::optional<Range>, bool& moreResults); 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::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::size_t getReleaseCount() const;
std::vector<Wt::Dbo::ptr<Track>> getTracks(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<TrackArtistLink::Type> 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<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 // Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence // 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); ClusterType(std::string name);
static std::vector<pointer> getAllOrphans(Session& session); 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 getByName(Session& session, const std::string& name);
static pointer getById(Session& session, IdType id); static pointer getById(Session& session, IdType id);
static std::vector<pointer> getAll(Session& session); static std::vector<pointer> getAll(Session& session);
@@ -20,11 +20,12 @@
#pragma once #pragma once
#include <optional> #include <optional>
#include <set>
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/WDateTime.h>
#include <Wt/Dbo/Dbo.h>
#include "utils/UUID.hpp" #include "utils/UUID.hpp"
#include "TrackArtistLink.hpp"
#include "Types.hpp" #include "Types.hpp"
namespace Database namespace Database
@@ -34,6 +35,7 @@ class Artist;
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Release; class Release;
class Session;
class Track; class Track;
class User; 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<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<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> 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> 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); 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::vector<Wt::Dbo::ptr<Track>> getTracks(const std::set<IdType>& clusters = std::set<IdType>()) const;
std::size_t getTracksCount() const; std::size_t getTracksCount() const;
Wt::Dbo::ptr<Track> getFirstTrack() const;
// Get the cluster of the tracks that belong to this release // 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) // 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; Wt::WDateTime getLastWritten() const;
// Get the artists of this release // 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> > getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
std::vector<Wt::Dbo::ptr<Artist> > getReleaseArtists() const { return getArtists(TrackArtistLink::Type::ReleaseArtist); } std::vector<Wt::Dbo::ptr<Artist> > getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
bool hasVariousArtists() const; bool hasVariousArtists() const;
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const; std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
+8 -4
View File
@@ -26,12 +26,13 @@
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include <Wt/Dbo/Dbo.h>
#include <Wt/Dbo/WtSqlTraits.h>
#include "utils/EnumSet.hpp"
#include "utils/UUID.hpp" #include "utils/UUID.hpp"
#include "TrackArtistLink.hpp"
#include "Types.hpp" #include "Types.hpp"
namespace Database { namespace Database {
@@ -40,6 +41,8 @@ class Artist;
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Release; class Release;
class Session;
class TrackArtistLink;
class TrackFeatures; class TrackFeatures;
class TrackListEntry; class TrackListEntry;
class TrackStats; class TrackStats;
@@ -134,8 +137,9 @@ class Track : public Wt::Dbo::Dbo<Track>
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; } std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; } std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
std::vector<Wt::Dbo::ptr<Artist>> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; // no artistLinkTypes means get all
std::vector<IdType> getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; 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; std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() const;
Wt::Dbo::ptr<Release> getRelease() const { return _release; } Wt::Dbo::ptr<Release> getRelease() const { return _release; }
std::vector<Wt::Dbo::ptr<Cluster>> getClusters() const; std::vector<Wt::Dbo::ptr<Cluster>> getClusters() const;
@@ -19,64 +19,53 @@
#pragma once #pragma once
#include <string>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include "Types.hpp" #include "Types.hpp"
#include "utils/EnumSet.hpp"
namespace Database { namespace Database
class Artist;
class Session;
class Track;
class TrackArtistLink
{ {
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; class TrackArtistLink
TrackArtistLink(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, Type type); {
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; } static pointer create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, TrackArtistLinkType type);
Wt::Dbo::ptr<Artist> getArtist() const { return _artist; }
Type getType() const { return _type; }
template<class Action> static EnumSet<TrackArtistLinkType> getUsedTypes(Session& session);
void persist(Action& a)
{
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _type, "name");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); Wt::Dbo::ptr<Track> getTrack() const { return _track; }
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); 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; Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
std::string _name; Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
}
Wt::Dbo::ptr<Track> _track; private:
Wt::Dbo::ptr<Artist> _artist; 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 <Wt/Dbo/Dbo.h>
#include "TrackArtistLink.hpp"
#include "Types.hpp" #include "Types.hpp"
namespace Database { 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); TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
// Stats utility // 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<Release>> getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const; std::vector<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>> 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<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<Release>> getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
std::vector<Wt::Dbo::ptr<Track>> getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const; std::vector<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 offset {};
std::size_t limit {}; std::size_t limit {};
}; };
enum class TrackArtistLinkType
{
Artist, // regular artist
Arranger,
Composer,
Conductor,
Lyricist,
Mixer,
Performer,
Producer,
ReleaseArtist,
Remixer,
Writer,
};
} }
+2 -1
View File
@@ -132,8 +132,9 @@ class User : public Wt::Dbo::Dbo<User>
// Do not change enum values! // Do not change enum values!
enum class SubsonicArtistListMode enum class SubsonicArtistListMode
{ {
AllArtists = 0, AllArtists = 0,
ReleaseArtists = 1, ReleaseArtists = 1,
TrackArtists = 2,
}; };
static inline const std::size_t MinNameLength {3}; static inline const std::size_t MinNameLength {3};
+19 -47
View File
@@ -42,13 +42,13 @@ namespace MetaData
template<typename T> template<typename T>
std::vector<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; 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()) if (values.isEmpty())
continue; continue;
@@ -91,12 +91,13 @@ splitAndTrimString(const std::string& str, const std::string& delimiters)
static static
std::vector<Artist> 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")}; const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistTagNames)};
if (artistNames.empty())
artistNames = getPropertyValuesAs<std::string>(properties, "ARTIST");
if (artistNames.empty()) if (artistNames.empty())
return {}; return {};
@@ -106,7 +107,7 @@ getArtists(const TagLib::PropertyMap& properties)
[&](const std::string& name) { return Artist {name}; }); [&](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()) if (artistSortNames.size() == artists.size())
{ {
for (std::size_t i {}; i < artistSortNames.size(); ++i) 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()) if (artistNames.size() == artistsMBID.size())
{ {
@@ -128,41 +129,6 @@ getArtists(const TagLib::PropertyMap& properties)
return artists; 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 static
std::optional<Album> std::optional<Album>
getAlbum(const TagLib::PropertyMap& properties) getAlbum(const TagLib::PropertyMap& properties)
@@ -430,9 +396,15 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
processTag(track, tag, values, debug); processTag(track, tag, values, debug);
} }
track.artists = getArtists(properties);
track.albumArtists = getAlbumArtists(properties);
track.album = getAlbum(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; return track;
} }
@@ -78,6 +78,12 @@ namespace MetaData
std::optional<float> trackReplayGain; std::optional<float> trackReplayGain;
std::optional<float> albumReplayGain; std::optional<float> albumReplayGain;
std::string discSubtitle; 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 class IParser
+5 -2
View File
@@ -132,7 +132,10 @@ Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releas
} }
std::unordered_set<Database::IdType> 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; std::unordered_set<Database::IdType> res;
@@ -144,7 +147,7 @@ Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistI
continue; continue;
const IClassifier& classifier {*itClassifier->second}; const IClassifier& classifier {*itClassifier->second};
res = classifier.getSimilarArtists(dbSession, artistId, maxCount); res = classifier.getSimilarArtists(dbSession, artistId, linkTypes, maxCount);
if (!res.empty()) if (!res.empty())
{ {
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'"; LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'";
+7 -4
View File
@@ -56,10 +56,13 @@ namespace Recommendation
void load(bool forceReload, const ProgressCallback& progressCallback) override; void load(bool forceReload, const ProgressCallback& progressCallback) override;
void cancelLoad() override; void cancelLoad() override;
std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override; ResultContainer 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; ResultContainer 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; ResultContainer 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 getSimilarArtists(Database::Session& session,
Database::IdType artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) override;
void setClassifierPriorities(const std::vector<ClassifierType>& classifierTypes); void setClassifierPriorities(const std::vector<ClassifierType>& classifierTypes);
void clearClassifiers(); void clearClassifiers();
+9 -4
View File
@@ -24,6 +24,7 @@
#include <unordered_set> #include <unordered_set>
#include "database/Types.hpp" #include "database/Types.hpp"
#include "utils/EnumSet.hpp"
namespace Database namespace Database
{ {
@@ -49,10 +50,14 @@ namespace Recommendation
virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0; virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0;
virtual void requestCancelLoad() = 0; virtual void requestCancelLoad() = 0;
virtual std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0; using ResultContainer = std::unordered_set<Database::IdType>;
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 ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, 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; 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 } // ns Recommendation
@@ -83,7 +83,10 @@ ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::Id
} }
std::unordered_set<Database::IdType> 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; std::unordered_set<Database::IdType> res;
@@ -93,7 +96,7 @@ ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdT
if (!artist) if (!artist)
return res; 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)), std::transform(std::cbegin(artists), std::cend(artists), std::inserter(res, std::end(res)),
[](const auto& artist) { return artist.id(); }); [](const auto& artist) { return artist.id(); });
@@ -40,10 +40,13 @@ namespace Recommendation
bool load(Database::Session&, bool, const ProgressCallback&) override { return true; } bool load(Database::Session&, bool, const ProgressCallback&) override { return true; }
void requestCancelLoad() override {} void requestCancelLoad() override {}
std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; ResultContainer 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; ResultContainer 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; ResultContainer 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 getSimilarArtists(Database::Session& session,
Database::IdType artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) const override;
}; };
} // namespace Recommendation } // namespace Recommendation
@@ -25,6 +25,7 @@
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackFeatures.hpp" #include "database/TrackFeatures.hpp"
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "som/DataNormalizer.hpp" #include "som/DataNormalizer.hpp"
@@ -308,9 +309,34 @@ FeaturesClassifier::getSimilarReleases(Database::Session& session, Database::IdT
} }
std::unordered_set<Database::IdType> 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()) if (!similarArtistIds.empty())
{ {
// Report only existing ids // 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);) for (auto it {std::begin(similarArtistIds)}; it != std::end(similarArtistIds);)
{ {
const Database::IdType similarArtistId {*it}; const Database::IdType similarArtistId {*it};
if (!Database::Release::getById(session, similarArtistId)) if (!Database::Artist::getById(session, similarArtistId))
it = similarArtistIds.erase(it); it = similarArtistIds.erase(it);
else else
it++; it++;
} }
} }
while (similarArtistIds.size() > maxCount)
similarArtistIds.erase(Random::pickRandom(similarArtistIds));
return similarArtistIds; return similarArtistIds;
} }
@@ -339,6 +368,7 @@ bool
FeaturesClassifier::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) FeaturesClassifier::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback)
{ {
if (forceReload) if (forceReload)
{ {
FeaturesClassifierCache::invalidate(); FeaturesClassifierCache::invalidate();
} }
@@ -377,7 +407,6 @@ FeaturesClassifier::load(Database::Session& session,
const SOM::Coordinate width {network.getWidth()}; const SOM::Coordinate width {network.getWidth()};
const SOM::Coordinate height {network.getHeight()}; const SOM::Coordinate height {network.getHeight()};
_artistsMap = MatrixOfObjects {width, height};
_releasesMap = MatrixOfObjects {width, height}; _releasesMap = MatrixOfObjects {width, height};
_tracksMap = MatrixOfObjects {width, height}; _tracksMap = MatrixOfObjects {width, height};
@@ -407,10 +436,18 @@ FeaturesClassifier::load(Database::Session& session,
_releasePositions[track->getRelease().id()].insert(position); _releasePositions[track->getRelease().id()].insert(position);
_releasesMap[position].insert(track->getRelease().id()); _releasesMap[position].insert(track->getRelease().id());
} }
for (const auto& artist : track->getArtists()) for (const auto& artistLink : track->getArtistLinks())
{ {
_artistPositions[artist.id()].insert(position); _artistPositions[artistLink->getArtist().id()].insert(position);
_artistsMap[position].insert(artist.id()); 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> 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> 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> 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); bool loadFromCache(Database::Session& session, const FeaturesClassifierCache& cache);
@@ -99,8 +102,8 @@ class FeaturesClassifier : public IClassifier
std::unique_ptr<SOM::Network> _network; std::unique_ptr<SOM::Network> _network;
double _networkRefVectorsDistanceMedian {}; double _networkRefVectorsDistanceMedian {};
MatrixOfObjects _artistsMap; ObjectPositions _artistPositions;
ObjectPositions _artistPositions; std::unordered_map<Database::TrackArtistLinkType, MatrixOfObjects> _artistsMap;
MatrixOfObjects _releasesMap; MatrixOfObjects _releasesMap;
ObjectPositions _releasePositions; ObjectPositions _releasePositions;
@@ -20,9 +20,11 @@
#pragma once #pragma once
#include <functional> #include <functional>
#include <optional>
#include <unordered_set> #include <unordered_set>
#include "database/Types.hpp" #include "database/Types.hpp"
#include "utils/EnumSet.hpp"
namespace Database namespace Database
{ {
@@ -46,10 +48,15 @@ namespace Recommendation
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
virtual void cancelLoad() = 0; virtual void cancelLoad() = 0;
virtual std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0; using ResultContainer = std::unordered_set<Database::IdType>;
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 ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0;
virtual std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, 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); std::unique_ptr<IEngine> createEngine(Database::Db& db);
+27 -21
View File
@@ -29,6 +29,7 @@
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/ScanSettings.hpp" #include "database/ScanSettings.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackFeatures.hpp" #include "database/TrackFeatures.hpp"
#include "metadata/TagLibParser.hpp" #include "metadata/TagLibParser.hpp"
#include "recommendation/IEngine.hpp" #include "recommendation/IEngine.hpp"
@@ -750,20 +751,6 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
title = file.filename().string(); 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 // If file already exist, update data
// Otherwise, create it // Otherwise, create it
if (!track) if (!track)
@@ -784,15 +771,34 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
assert(track); assert(track);
track.modify()->clearArtistLinks(); track.modify()->clearArtistLinks();
for (const auto& artist : artists) for (const Artist::pointer& artist : getOrCreateArtists(_dbSession, trackInfo->artists))
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLink::Type::Artist)); track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLinkType::Artist));
for (const auto& releaseArtist : releaseArtists) for (const Artist::pointer& releaseArtist : getOrCreateArtists(_dbSession, trackInfo->albumArtists))
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLink::Type::ReleaseArtist)); 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()->setScanVersion(_scanVersion);
track.modify()->setRelease(release); if (trackInfo->album)
track.modify()->setClusters(clusters); track.modify()->setRelease(getOrCreateRelease(_dbSession, *trackInfo->album));
track.modify()->setClusters(getOrCreateClusters(_dbSession, trackInfo->clusters));
track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setLastWriteTime(lastWriteTime);
track.modify()->setName(title); track.modify()->setName(title);
track.modify()->setDuration(trackInfo->duration); track.modify()->setDuration(trackInfo->duration);
@@ -1012,7 +1018,7 @@ MediaScanner::checkDuplicatedAudioFiles(ScanStats& stats)
if (track->getMBID()) if (track->getMBID())
{ {
LMS_LOG(DBUPDATER, INFO) << "Found duplicated MBID [" << track->getMBID()->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName(); 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 <filesystem>
#include <vector> #include <vector>
#include "database/Types.hpp"
namespace Scanner { namespace Scanner {
enum class ScanErrorType enum class ScanErrorType
@@ -51,9 +53,8 @@ namespace Scanner {
struct ScanDuplicate struct ScanDuplicate
{ {
std::filesystem::path file; Database::IdType trackId;
DuplicateReason reason; DuplicateReason reason;
}; };
enum class ScanProgressStep : unsigned enum class ScanProgressStep : unsigned
+99 -46
View File
@@ -316,7 +316,8 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User:
trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()})); 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()) if (!artists.empty())
{ {
trackResponse.setAttribute("artist", getArtistNames(artists)); trackResponse.setAttribute("artist", getArtistNames(artists));
@@ -479,7 +480,7 @@ userToResponseNode(const User::pointer& user)
Response::Node userNode; Response::Node userNode;
userNode.setAttribute("username", user->getLoginName()); userNode.setAttribute("username", user->getLoginName());
userNode.setAttribute("scrobblingEnabled", false); userNode.setAttribute("scrobblingEnabled", true);
userNode.setAttribute("adminRole", user->isAdmin()); userNode.setAttribute("adminRole", user->isAdmin());
userNode.setAttribute("settingsRole", true); userNode.setAttribute("settingsRole", true);
userNode.setAttribute("downloadRole", true); userNode.setAttribute("downloadRole", true);
@@ -704,11 +705,13 @@ Response
handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
{ {
// Mandatory params // Mandatory params
std::string type {getMandatoryParameterAs<std::string>(context.parameters, "type")}; const std::string type {getMandatoryParameterAs<std::string>(context.parameters, "type")};
// Optional params // Optional params
std::size_t size {getParameterAs<std::size_t>(context.parameters, "size").value_or(10)}; const 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 offset {getParameterAs<std::size_t>(context.parameters, "offset").value_or(0)};
const Range range {offset, size};
std::vector<Release::pointer> releases; std::vector<Release::pointer> releases;
@@ -718,36 +721,14 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
if (!user) if (!user)
throw UserNotAuthorizedError {}; 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::getAll(context.dbSession, range);
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});
} }
else if (type == "alphabeticalByArtist") else if (type == "alphabeticalByArtist")
{ {
releases = Release::getAllOrderedByArtist(context.dbSession, offset, size); 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") else if (type == "byGenre")
{ {
// Mandatory param // Mandatory param
@@ -760,10 +741,42 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
if (cluster) if (cluster)
{ {
bool more; 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 else
throw NotImplementedGenericError {}; throw NotImplementedGenericError {};
@@ -881,7 +894,10 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString()); 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()}; auto transaction {context.dbSession.createSharedTransaction()};
@@ -931,13 +947,16 @@ handleGetArtistsRequest(RequestContext& context)
if (!user) if (!user)
throw UserNotAuthorizedError {}; throw UserNotAuthorizedError {};
std::optional<TrackArtistLink::Type> linkType; std::optional<TrackArtistLinkType> linkType;
switch (user->getSubsonicArtistListMode()) switch (user->getSubsonicArtistListMode())
{ {
case User::SubsonicArtistListMode::AllArtists: case User::SubsonicArtistListMode::AllArtists:
break; break;
case User::SubsonicArtistListMode::ReleaseArtists: case User::SubsonicArtistListMode::ReleaseArtists:
linkType = TrackArtistLink::Type::ReleaseArtist; linkType = TrackArtistLinkType::ReleaseArtist;
break;
case User::SubsonicArtistListMode::TrackArtists:
linkType = TrackArtistLinkType::Artist;
break; break;
} }
@@ -1075,13 +1094,16 @@ handleGetIndexesRequest(RequestContext& context)
if (!user) if (!user)
throw UserNotAuthorizedError {}; throw UserNotAuthorizedError {};
std::optional<TrackArtistLink::Type> linkType; std::optional<TrackArtistLinkType> linkType;
switch (user->getSubsonicArtistListMode()) switch (user->getSubsonicArtistListMode())
{ {
case User::SubsonicArtistListMode::AllArtists: case User::SubsonicArtistListMode::AllArtists:
break; break;
case User::SubsonicArtistListMode::ReleaseArtists: case User::SubsonicArtistListMode::ReleaseArtists:
linkType = TrackArtistLink::Type::ReleaseArtist; linkType = TrackArtistLinkType::ReleaseArtist;
break;
case User::SubsonicArtistListMode::TrackArtists:
linkType = TrackArtistLinkType::Artist;
break; break;
} }
@@ -1103,36 +1125,39 @@ Response
handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
{ {
// Mandatory params // Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")}; const Id artistId {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Artist) if (artistId.type != Id::Type::Artist)
throw BadParameterGenericError {"id"}; throw BadParameterGenericError {"id"};
// Optional params // Optional params
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)}; 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()}; 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) if (!artist)
throw RequestedDataNotFoundError {}; throw RequestedDataNotFoundError {};
User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; const User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
if (!user) if (!user)
throw UserNotAuthorizedError {}; throw UserNotAuthorizedError {};
// "Returns a random collection of songs from the given artist and similar artists" // "Returns a random collection of songs from the given artist and similar artists"
auto tracks {artist->getRandomTracks(count / 2)}; 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) if (!similarArtist)
continue; continue;
auto similarArtistTracks {similarArtist->getRandomTracks((count / 2) / 5)}; 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::begin(similarArtistTracks)),
std::make_move_iterator(std::end(similarArtistTracks))); std::make_move_iterator(std::end(similarArtistTracks)));
} }
@@ -1547,6 +1572,34 @@ handleUnstarRequest(RequestContext& context)
return Response::createOkResponse(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 static
Response Response
handleUpdateUserRequest(RequestContext& context) handleUpdateUserRequest(RequestContext& context)
@@ -1824,10 +1877,10 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
{"getAvatar", {handleNotImplemented, false}}, {"getAvatar", {handleNotImplemented, false}},
// Media annotation // Media annotation
{"star", {handleStarRequest, false}}, {"star", {handleStarRequest, false}},
{"unstar", {handleUnstarRequest, false}}, {"unstar", {handleUnstarRequest, false}},
{"setRating", {handleNotImplemented, false}}, {"setRating", {handleNotImplemented, false}},
{"scrobble", {handleNotImplemented, false}}, {"scrobble", {handleScrobble, false}},
// Sharing // Sharing
{"getShares", {handleNotImplemented, false}}, {"getShares", {handleNotImplemented, false}},
+1 -1
View File
@@ -60,7 +60,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())}; const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())};
ifs.seekg(0, std::ios::beg); 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)}; const Wt::Http::Request::ByteRangeSpecifier ranges {request.getRanges(fileSize)};
if (!ranges.isSatisfiable()) if (!ranges.isSatisfiable())
+155
View File
@@ -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
View File
@@ -77,7 +77,8 @@ generateWtConfig(std::string execPath)
args.push_back("--accesslog=" + wtAccessLogFilePath.string()); 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)); args.push_back("--threads=" + std::to_string(httpServerThreadCount));
} }
+3 -3
View File
@@ -541,7 +541,7 @@ LmsApplication::createHome()
{ {
const std::string sessionId {LmsApp->sessionId()}; 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, [=] 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(); _events.dbScanned.emit();
triggerUpdate(); triggerUpdate();
+2 -1
View File
@@ -30,6 +30,7 @@
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "database/Types.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "resource/ImageResource.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 transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)};
const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)}; const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)};
const auto artists {track->getArtists()}; const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
oss oss
<< "var params = {" << "var params = {"
+1 -1
View File
@@ -419,7 +419,7 @@ PlayQueue::addSome()
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); 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()}; const auto release {track->getRelease()};
if (!artists.empty() || release) if (!artists.empty() || release)
+3 -2
View File
@@ -333,6 +333,7 @@ class SettingsModel : public Wt::WFormModel
_subsonicArtistListModeModel = std::make_shared<ValueStringModel<User::SubsonicArtistListMode>>(); _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.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.release-artists"), User::SubsonicArtistListMode::ReleaseArtists);
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), User::SubsonicArtistListMode::TrackArtists);
} }
bool _withOldPassword {}; bool _withOldPassword {};
@@ -346,12 +347,12 @@ class SettingsModel : public Wt::WFormModel
SettingsView::SettingsView() SettingsView::SettingsView()
{ {
wApp->internalPathChanged().connect([=] wApp->internalPathChanged().connect(this, [this]
{ {
refreshView(); refreshView();
}); });
LmsApp->getMediaPlayer().settingsLoaded.connect([=]() LmsApp->getMediaPlayer().settingsLoaded.connect([this]
{ {
refreshView(); refreshView();
}); });
+2 -2
View File
@@ -167,10 +167,10 @@ class DatabaseSettingsModel : public Wt::WFormModel
DatabaseSettingsView::DatabaseSettingsView() DatabaseSettingsView::DatabaseSettingsView()
{ {
wApp->internalPathChanged().connect(std::bind([=] wApp->internalPathChanged().connect(this, [this]
{ {
refreshView(); refreshView();
})); });
refreshView(); refreshView();
} }
+19 -3
View File
@@ -28,6 +28,8 @@
#include <Wt/WResource.h> #include <Wt/WResource.h>
#include <Wt/WSplitButton.h> #include <Wt/WSplitButton.h>
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "LmsApplication.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; 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: private:
@@ -123,7 +139,7 @@ ScannerController::ScannerController()
auto onDbEvent = [&]() { refreshContents(); }; 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")); LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched"));
}); });
+1 -1
View File
@@ -242,7 +242,7 @@ class UserModel : public Wt::WFormModel
UserView::UserView() UserView::UserView()
{ {
wApp->internalPathChanged().connect([this]() wApp->internalPathChanged().connect(this, [this]()
{ {
refreshView(); refreshView();
}); });
+1 -1
View File
@@ -44,7 +44,7 @@ UsersView::UsersView()
LmsApp->setInternalPath("/admin/user", true); LmsApp->setInternalPath("/admin/user", true);
}); });
wApp->internalPathChanged().connect([this]() wApp->internalPathChanged().connect(this, [this]()
{ {
refreshView(); refreshView();
}); });
+6
View File
@@ -73,6 +73,12 @@ class ValueStringModel : public Wt::WStringListModel
setData(rowCount() - 1, 0, str, Wt::ItemDataRole::Display); setData(rowCount() - 1, 0, str, Wt::ItemDataRole::Display);
} }
void
clear()
{
removeRows(0, rowCount());
}
}; };
} // namespace UserInterface } // namespace UserInterface
+6 -3
View File
@@ -50,12 +50,12 @@ Artist::Artist(Filters* filters)
{ {
addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("tr", &Wt::WTemplate::Functions::tr);
LmsApp->internalPathChanged().connect([=] LmsApp->internalPathChanged().connect(this, [this]
{ {
refreshView(); refreshView();
}); });
filters->updated().connect([=] filters->updated().connect([this]
{ {
refreshView(); refreshView();
}); });
@@ -75,7 +75,10 @@ Artist::refreshView()
if (!artistId) if (!artistId)
throw ArtistNotFoundException {*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()}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+42 -10
View File
@@ -27,6 +27,7 @@
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -39,7 +40,7 @@ using namespace Database;
namespace UserInterface { namespace UserInterface {
using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLink::Type>>; using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLinkType>>;
Artists::Artists(Filters* filters) Artists::Artists(Filters* filters)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}, : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")},
@@ -68,14 +69,14 @@ Artists::Artists(Filters* filters)
} }
_linkType = bindNew<Wt::WComboBox>("link-type"); _linkType = bindNew<Wt::WComboBox>("link-type");
{ _linkType->setModel(std::make_shared<ArtistLinkModel>());
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->changed().connect([this] { refreshView(); }); _linkType->changed().connect([this] { refreshView(); });
refreshArtistLinkTypes();
LmsApp->getEvents().dbScanned.connect(this, [this]
{
refreshArtistLinkTypes();
});
_container = bindNew<Wt::WContainerWidget>("artists"); _container = bindNew<Wt::WContainerWidget>("artists");
hideLoadingIndicator(); hideLoadingIndicator();
@@ -100,6 +101,37 @@ Artists::refreshView(Mode mode)
refreshView(); 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 void
Artists::displayLoadingIndicator() Artists::displayLoadingIndicator()
{ {
@@ -125,7 +157,7 @@ Artists::getRandomArtists(std::optional<Range> range, bool& moreResults)
{ {
std::vector<Artist::pointer> artists; 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()) if (_randomArtists.empty())
_randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), linkType, maxItemsPerMode[Mode::Random]); _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; 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]}; const std::optional<std::size_t> modeLimit{maxItemsPerMode[_mode]};
if (modeLimit) if (modeLimit)
+1
View File
@@ -56,6 +56,7 @@ class Artists : public Wt::WTemplate
void refreshView(); void refreshView();
void refreshView(Mode mode); void refreshView(Mode mode);
void refreshArtistLinkTypes();
void displayLoadingIndicator(); void displayLoadingIndicator();
void hideLoadingIndicator(); void hideLoadingIndicator();
void addSome(); void addSome();
+1 -1
View File
@@ -113,7 +113,7 @@ Explore::Explore(Filters* filters)
tracks->tracksAction.connect(this, &Explore::handleTracksAction); tracks->tracksAction.connect(this, &Explore::handleTracksAction);
contentsStack->addWidget(std::move(tracks)); contentsStack->addWidget(std::move(tracks));
wApp->internalPathChanged().connect([=] wApp->internalPathChanged().connect(this, [=]
{ {
handleContentsPathChange(contentsStack); handleContentsPathChange(contentsStack);
}); });
+1 -1
View File
@@ -52,7 +52,7 @@ Filters::showDialog()
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; 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) for (const Database::ClusterType::pointer& type : types)
typeCombo->addItem(Wt::WString::fromUTF8(type->getName())); typeCombo->addItem(Wt::WString::fromUTF8(type->getName()));
+3 -3
View File
@@ -54,12 +54,12 @@ Release::Release(Filters* filters)
{ {
addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("tr", &Wt::WTemplate::Functions::tr);
wApp->internalPathChanged().connect([=] wApp->internalPathChanged().connect(this, [this]
{ {
refreshView(); refreshView();
}); });
filters->updated().connect([=] filters->updated().connect([this]
{ {
refreshView(); refreshView();
}); });
@@ -214,7 +214,7 @@ Release::refreshView()
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); 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()) if (variousArtists && !artists.empty())
{ {
entry->setCondition("if-has-artists", true); entry->setCondition("if-has-artists", true);
+1 -1
View File
@@ -47,7 +47,7 @@ namespace UserInterface::TrackListHelpers
Wt::WText* name {entry->bindNew<Wt::WText>("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain)}; Wt::WText* name {entry->bindNew<Wt::WText>("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain)};
name->setToolTip(Wt::WString::fromUTF8(track->getName())); 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 Release::pointer release {track->getRelease()};
const IdType trackId {track.id()}; const IdType trackId {track.id()};
@@ -19,7 +19,6 @@
#pragma once #pragma once
#include <filesystem>
#include <Wt/WResource.h> #include <Wt/WResource.h>
#include "database/Types.hpp" #include "database/Types.hpp"
+18 -23
View File
@@ -17,39 +17,34 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef COVER_RESOURCE_HPP_ #pragma once
#define COVER_RESOURCE_HPP_
#include <mutex>
#include <Wt/WResource.h> #include <Wt/WResource.h>
#include "database/Types.hpp" #include "database/Types.hpp"
namespace UserInterface { namespace UserInterface
class ImageResource : public Wt::WResource
{ {
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 ~ImageResource();
{
Small = 128,
Large = 512,
};
std::string getReleaseUrl(Database::IdType releaseId, Size size) const; enum class Size : std::size_t
std::string getTrackUrl(Database::IdType trackId, Size size) const; {
Small = 128,
Large = 512,
};
private: std::string getReleaseUrl(Database::IdType releaseId, Size size) const;
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; std::string getTrackUrl(Database::IdType trackId, Size size) const;
}; private:
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
};
} // namespace UserInterface } // namespace UserInterface
#endif
+124 -46
View File
@@ -28,6 +28,7 @@
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackBookmark.hpp" #include "database/TrackBookmark.hpp"
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "database/User.hpp" #include "database/User.hpp"
@@ -227,7 +228,6 @@ testSingleCluster(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
auto clusters {Cluster::getAll(session)}; auto clusters {Cluster::getAll(session)};
CHECK(clusters.size() == 1); CHECK(clusters.size() == 1);
CHECK(clusters.front().id() == cluster.getId()); CHECK(clusters.front().id() == cluster.getId());
@@ -241,6 +241,10 @@ testSingleCluster(Session& session)
CHECK(clusterTypes.size() == 1); CHECK(clusterTypes.size() == 1);
CHECK(clusterTypes.front().id() == clusterType.getId()); CHECK(clusterTypes.front().id() == clusterType.getId());
clusterTypes = ClusterType::getAllUsed(session);
CHECK(clusterTypes.size() == 1);
CHECK(clusterTypes.front().id() == clusterType.getId());
clusterTypes = ClusterType::getAllOrphans(session); clusterTypes = ClusterType::getAllOrphans(session);
CHECK(clusterTypes.empty()); CHECK(clusterTypes.empty());
} }
@@ -252,6 +256,8 @@ testSingleCluster(Session& session)
auto clusterTypes {ClusterType::getAllOrphans(session)}; auto clusterTypes {ClusterType::getAllOrphans(session)};
CHECK(clusterTypes.size() == 1); CHECK(clusterTypes.size() == 1);
CHECK(clusterTypes.front().id() == clusterType.getId()); CHECK(clusterTypes.front().id() == clusterType.getId());
CHECK(ClusterType::getAllUsed(session).empty());
} }
} }
@@ -265,7 +271,7 @@ testSingleTrackSingleArtist(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; 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 transaction {session.createSharedTransaction()};
auto artists {track->getArtists()}; auto artists {track->getArtists({TrackArtistLinkType::Artist})};
CHECK(artists.size() == 1); CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist.getId()); CHECK(artists.front().id() == artist.getId());
@@ -287,8 +293,9 @@ testSingleTrackSingleArtist(Session& session)
CHECK(artistLink->getTrack().id() == track.getId()); CHECK(artistLink->getTrack().id() == track.getId());
CHECK(artistLink->getArtist().id() == artist.getId()); CHECK(artistLink->getArtist().id() == artist.getId());
CHECK(track->getArtists(TrackArtistLink::Type::Artist).size() == 1); CHECK(track->getArtists({TrackArtistLinkType::Artist}).size() == 1);
CHECK(track->getArtists(TrackArtistLink::Type::ReleaseArtist).empty()); CHECK(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
CHECK(track->getArtists({}).size() == 1);
} }
{ {
@@ -298,8 +305,8 @@ testSingleTrackSingleArtist(Session& session)
CHECK(tracks.size() == 1); CHECK(tracks.size() == 1);
CHECK(tracks.front().id() == track.getId()); CHECK(tracks.front().id() == track.getId());
CHECK(artist->getTracks(TrackArtistLink::Type::ReleaseArtist).empty()); CHECK(artist->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
CHECK(artist->getTracks(TrackArtistLink::Type::Artist).size() == 1); CHECK(artist->getTracks(TrackArtistLinkType::Artist).size() == 1);
} }
} }
@@ -312,9 +319,9 @@ testSingleTrackSingleArtistMultiRoles(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::ReleaseArtist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Writer); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer);
} }
{ {
@@ -326,29 +333,29 @@ testSingleTrackSingleArtistMultiRoles(Session& session)
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
bool hasMore{}; bool hasMore{};
CHECK(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); 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, {}, {}, TrackArtistLinkType::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, {}, {}, TrackArtistLinkType::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, {}, {}, TrackArtistLinkType::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::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto artists {track->getArtists(TrackArtistLink::Type::Artist)}; auto artists {track->getArtists({TrackArtistLinkType::Artist})};
CHECK(artists.size() == 1); CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist.getId()); CHECK(artists.front().id() == artist.getId());
artists = track->getArtists(TrackArtistLink::Type::ReleaseArtist); artists = track->getArtists({TrackArtistLinkType::ReleaseArtist});
CHECK(artists.size() == 1); CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist.getId()); CHECK(artists.front().id() == artist.getId());
CHECK(track->getArtistLinks().size() == 3); CHECK(track->getArtistLinks().size() == 3);
CHECK(artist->getTracks().size() == 1); CHECK(artist->getTracks().size() == 1);
CHECK(artist->getTracks(TrackArtistLink::Type::ReleaseArtist).size() == 1); CHECK(artist->getTracks({TrackArtistLinkType::ReleaseArtist}).size() == 1);
CHECK(artist->getTracks(TrackArtistLink::Type::Artist).size() == 1); CHECK(artist->getTracks({TrackArtistLinkType::Artist}).size() == 1);
CHECK(artist->getTracks(TrackArtistLink::Type::Writer).size() == 1); CHECK(artist->getTracks({TrackArtistLinkType::Writer}).size() == 1);
} }
} }
@@ -364,8 +371,8 @@ testSingleTrackMultiArtists(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -376,13 +383,14 @@ testSingleTrackMultiArtists(Session& session)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto artists {track->getArtists()}; auto artists {track->getArtists({TrackArtistLinkType::Artist})};
CHECK(artists.size() == 2); CHECK(artists.size() == 2);
CHECK((artists[0].id() == artist1.getId() && artists[1].id() == artist2.getId()) CHECK((artists[0].id() == artist1.getId() && artists[1].id() == artist2.getId())
|| (artists[0].id() == artist2.getId() && artists[1].id() == artist1.getId())); || (artists[0].id() == artist2.getId() && artists[1].id() == artist1.getId()));
CHECK(track->getArtists(TrackArtistLink::Type::Artist).size() == 2); CHECK(track->getArtists({}).size() == 2);
CHECK(track->getArtists(TrackArtistLink::Type::ReleaseArtist).empty()); CHECK(track->getArtists({TrackArtistLinkType::Artist}).size() == 2);
CHECK(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
CHECK(Artist::getAll(session, Artist::SortMethod::ByName).size() == 2); CHECK(Artist::getAll(session, Artist::SortMethod::ByName).size() == 2);
CHECK(Artist::getAllIds(session).size() == 2); CHECK(Artist::getAllIds(session).size() == 2);
} }
@@ -393,10 +401,10 @@ testSingleTrackMultiArtists(Session& session)
CHECK(artist1->getTracks().front() == track.get()); CHECK(artist1->getTracks().front() == track.get());
CHECK(artist2->getTracks().front() == track.get()); CHECK(artist2->getTracks().front() == track.get());
CHECK(artist1->getTracks(TrackArtistLink::Type::ReleaseArtist).empty()); CHECK(artist1->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
CHECK(artist1->getTracks(TrackArtistLink::Type::Artist).size() == 1); CHECK(artist1->getTracks(TrackArtistLinkType::Artist).size() == 1);
CHECK(artist2->getTracks(TrackArtistLink::Type::ReleaseArtist).empty()); CHECK(artist2->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
CHECK(artist2->getTracks(TrackArtistLink::Type::Artist).size() == 1); CHECK(artist2->getTracks(TrackArtistLinkType::Artist).size() == 1);
} }
} }
@@ -410,7 +418,7 @@ testSingleArtistSearchByName(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
artist.get().modify()->setSortName("ZZZ"); 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 static
void void
testSingleTrackSingleCluster(Session& session) testSingleTrackSingleCluster(Session& session)
@@ -942,7 +997,7 @@ testSingleTrackSingleArtistMultiClusters(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; 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()); cluster1.get().modify()->addTrack(track.get());
} }
@@ -1004,8 +1059,8 @@ testSingleTrackSingleArtistMultiRolesMultiClusters(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::ReleaseArtist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
cluster.get().modify()->addTrack(track.get()); cluster.get().modify()->addTrack(track.get());
} }
@@ -1045,7 +1100,7 @@ testMultiTracksSingleArtistMultiClusters(Session& session)
tracks.emplace_back(session, "MyTrackFile" + std::to_string(i)); tracks.emplace_back(session, "MyTrackFile" + std::to_string(i));
auto transaction {session.createUniqueTransaction()}; 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) for (auto& cluster : clusters)
cluster.get().modify()->addTrack(tracks.back().get()); cluster.get().modify()->addTrack(tracks.back().get());
@@ -1084,7 +1139,7 @@ testMultiTracksSingleArtistSingleRelease(Session& session)
auto transaction {session.createUniqueTransaction()}; 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()); tracks.back().get().modify()->setRelease(release.get());
} }
@@ -1117,7 +1172,7 @@ testSingleTrackSingleReleaseSingleArtist(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; 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()); track.get().modify()->setRelease(release.get());
} }
@@ -1154,7 +1209,7 @@ testSingleTrackSingleReleaseSingleArtistSingleCluster(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; 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()); track.get().modify()->setRelease(release.get());
cluster.get().modify()->addTrack(track.get()); cluster.get().modify()->addTrack(track.get());
} }
@@ -1206,7 +1261,7 @@ testSingleTrackSingleReleaseSingleArtistMultiClusters(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; 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()); track.get().modify()->setRelease(release.get());
cluster1.get().modify()->addTrack(track.get()); cluster1.get().modify()->addTrack(track.get());
cluster2.get().modify()->addTrack(track.get()); cluster2.get().modify()->addTrack(track.get());
@@ -1254,7 +1309,7 @@ testSingleStarredArtist(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; 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()); user.get().modify()->starArtist(artist.get());
} }
@@ -1479,8 +1534,8 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session)
track1.get().modify()->setRelease(release1.get()); track1.get().modify()->setRelease(release1.get());
track2.get().modify()->setRelease(release2.get()); track2.get().modify()->setRelease(release2.get());
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
cluster1.get().modify()->addTrack(track1.get()); cluster1.get().modify()->addTrack(track1.get());
cluster2.get().modify()->addTrack(track2.get()); cluster2.get().modify()->addTrack(track2.get());
@@ -1558,7 +1613,7 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session)
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
bool moreResults {}; 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.size() == 1);
CHECK(artists.front().id() == artist1.getId()); CHECK(artists.front().id() == artist1.getId());
} }
@@ -1567,7 +1622,7 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session)
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
bool moreResults {}; 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.size() == 1);
CHECK(artists.front().id() == artist1.getId()); CHECK(artists.front().id() == artist1.getId());
} }
@@ -1580,7 +1635,7 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session)
CHECK(trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults).empty()); CHECK(trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults).empty());
CHECK(trackList->getTracksReverse({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()}; auto transaction {session.createUniqueTransaction()};
if (i < 5) 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 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()); cluster2.get().modify()->addTrack(tracks.back().get());
} }
@@ -1751,7 +1806,7 @@ testMultipleTracksMultipleArtistsMultiClusters(Session& session)
tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size())); tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size()));
{ {
auto transaction {session.createUniqueTransaction()}; 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()); cluster2.get().modify()->addTrack(tracks.back().get());
} }
@@ -1764,6 +1819,28 @@ testMultipleTracksMultipleArtistsMultiClusters(Session& session)
CHECK(artists.front().id() == artist2.getId()); 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()}; auto artists {artist2->getSimilarArtists()};
CHECK(artists.size() == 2); CHECK(artists.size() == 2);
@@ -1930,6 +2007,7 @@ int main()
RUN_TEST(testSingleTrackSingleRelease); RUN_TEST(testSingleTrackSingleRelease);
RUN_TEST(testMultiTracksSingleReleaseTotalDiscTrack); RUN_TEST(testMultiTracksSingleReleaseTotalDiscTrack);
RUN_TEST(testMultiTracksSingleReleaseFirstTrack);
RUN_TEST(testSingleTrackSingleCluster); RUN_TEST(testSingleTrackSingleCluster);
RUN_TEST(testMultipleTracksSingleCluster); RUN_TEST(testMultipleTracksSingleCluster);
+19 -1
View File
@@ -76,9 +76,27 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
for (const Artist& artist : track->artists) for (const Artist& artist : track->artists)
std::cout << "Artist: " << artist << std::endl; 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; 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) if (track->album)
std::cout << "Album: " << *track->album << std::endl; std::cout << "Album: " << *track->album << std::endl;
+16 -11
View File
@@ -35,10 +35,9 @@
#include "utils/StreamLogger.hpp" #include "utils/StreamLogger.hpp"
#include "recommendation/IEngine.hpp" #include "recommendation/IEngine.hpp"
static static
void void
dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& engine) dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount)
{ {
const std::vector<Database::IdType> trackIds {[&]() const std::vector<Database::IdType> trackIds {[&]()
{ {
@@ -58,7 +57,7 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng
res += track->getName(); res += track->getName();
if (track->getRelease()) if (track->getRelease())
res += " [" + track->getRelease()->getName() + "]"; res += " [" + track->getRelease()->getName() + "]";
for (auto artist : track->getArtists()) for (auto artist : track->getArtists({Database::TrackArtistLinkType::Artist}))
res += " - " + artist->getName(); res += " - " + artist->getName();
for (auto cluster : track->getClusters()) for (auto cluster : track->getClusters())
res += " {" + cluster->getType()->getName() + "-"+ cluster->getName() + "}"; 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; 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; std::cout << "\t- Similar track '" << trackToString(similarTrackId) << std::endl;
} }
} }
static static
void 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([&]() 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; 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; std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl;
} }
} }
static static
void 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([&]() 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; 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; 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") ("artists,a", "Display recommendation for artists")
("releases,r", "Display recommendation for releases") ("releases,r", "Display recommendation for releases")
("tracks,t", "Display recommendation for tracks") ("tracks,t", "Display recommendation for tracks")
("max,m", po::value<unsigned>()->default_value(3), "Max similarity result count")
; ;
po::variables_map vm; po::variables_map vm;
@@ -165,16 +167,19 @@ int main(int argc, char *argv[])
std::cout << "Loading recommendation engine..." << std::endl; std::cout << "Loading recommendation engine..." << std::endl;
engine->load(false); engine->load(false);
unsigned maxSimilarityCount {vm["max"].as<unsigned>()};
std::cout << "Recommendation engine loaded!" << std::endl; std::cout << "Recommendation engine loaded!" << std::endl;
if (vm.count("tracks")) if (vm.count("tracks"))
dumpTracksRecommendation(db, *engine); dumpTracksRecommendation(db, *engine, maxSimilarityCount);
if (vm.count("releases")) if (vm.count("releases"))
dumpReleasesRecommendation(db, *engine); dumpReleasesRecommendation(db, *engine, maxSimilarityCount);
if (vm.count("artists")) if (vm.count("artists"))
dumpArtistsRecommendation(db, *engine); dumpArtistsRecommendation(db, *engine, maxSimilarityCount);
} }
catch( std::exception& e) catch( std::exception& e)
{ {