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