diff --git a/README.md b/README.md index c8f01a7b..9e039e50 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ # LMS - Lightweight Music Server -[![Build Status](https://travis-ci.org/epoupon/lms.svg?branch=master)](https://travis-ci.org/epoupon/lms) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/epoupon/lms) [![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) +![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 @@ -232,11 +232,3 @@ 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/) diff --git a/approot/messages.xml b/approot/messages.xml index 2bbb4907..7c792916 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -143,7 +143,12 @@ All artists Track artists +Composers +Lyricists +Mixers +Producers Album artists +Remixers Similar albums diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 0b66924a..c90d20c4 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -143,7 +143,12 @@ Tous les artistes Artistes de piste +Compositeurs +Paroliers +Mixers +Producteurs Artistes d'album +Remixers Albums similaires diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 2f41aa80..5ade2f65 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -81,7 +81,7 @@ createQuery(Session& session, const std::string& queryStr, const std::set& clusterIds, const std::vector& keywords, - std::optional linkType) + std::optional linkType) { session.checkSharedLocked(); @@ -214,7 +214,7 @@ Artist::getAllIds(Session& session) } std::vector -Artist::getAllIdsRandom(Session& session, const std::set& clusters, std::optional linkType, std::optional size) +Artist::getAllIdsRandom(Session& session, const std::set& clusters, std::optional linkType, std::optional size) { session.checkSharedLocked(); @@ -265,7 +265,7 @@ std::vector Artist::getByFilter(Session& session, const std::set& clusters, const std::vector& keywords, - std::optional linkType, + std::optional linkType, SortMethod sortMethod, std::optional range, bool& moreResults) @@ -306,7 +306,7 @@ std::vector Artist::getLastWritten(Session& session, std::optional after, const std::set& clusters, - std::optional linkType, + std::optional linkType, std::optional range, bool& moreResults) { session.checkSharedLocked(); @@ -338,7 +338,7 @@ std::vector Artist::getStarred(Session& session, User::pointer user, const std::set& clusters, - std::optional linkType, + std::optional linkType, SortMethod sortMethod, std::optional range, bool& moreResults) { @@ -443,7 +443,7 @@ Artist::getReleaseCount() const } std::vector> -Artist::getTracks(std::optional linkType) const +Artist::getTracks(std::optional linkType) const { assert(self()); assert(IdIsValid(self()->id())); @@ -462,7 +462,7 @@ Artist::getTracks(std::optional linkType) const } std::vector> -Artist::getTracksWithRelease(std::optional linkType) const +Artist::getTracksWithRelease(std::optional linkType) const { assert(self()); assert(IdIsValid(self()->id())); @@ -497,13 +497,14 @@ Artist::getRandomTracks(std::optional count) const } std::vector> -Artist::getSimilarArtists(std::optional offset, std::optional count) const +Artist::getSimilarArtists(EnumSet artistLinkTypes, std::optional range) const { assert(self()); assert(IdIsValid(self()->id())); assert(session()); - Wt::Dbo::Query query {session()->query( + std::ostringstream oss; + oss << "SELECT a FROM artist a" " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" " INNER JOIN track t ON t.id = t_a_l.track_id" @@ -515,14 +516,34 @@ Artist::getSimilarArtists(std::optional offset, std::optional ?" - ) + " AND a.id <> ?"; + + if (!artistLinkTypes.empty()) + { + oss << " AND t_a_l.type IN ("; + + bool first {true}; + for (TrackArtistLinkType type : artistLinkTypes) + { + (void) type; + if (!first) + oss << ", "; + oss << "?"; + first = false; + } + oss << ")"; + } + + Wt::Dbo::Query query {session()->query(oss.str()) .bind(self()->id()) .bind(self()->id()) .groupBy("a.id") .orderBy("COUNT(*) DESC, RANDOM()") - .limit(count ? static_cast(*count) : -1) - .offset(offset ? static_cast(*offset) : -1)}; + .limit(range ? static_cast(range->limit) : -1) + .offset(range ? static_cast(range->offset) : -1)}; + + for (TrackArtistLinkType type : artistLinkTypes) + query.bind(type); Wt::Dbo::collection res = query; return std::vector(res.begin(), res.end()); diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 6b476f91..473e7e08 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -19,13 +19,14 @@ #include "database/Release.hpp" -#include "utils/Logger.hpp" +#include #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/User.hpp" +#include "utils/Logger.hpp" #include "SqlQuery.hpp" namespace Database @@ -431,7 +432,7 @@ Release::getCopyrightURL() const } std::vector> -Release::getArtists(TrackArtistLink::Type linkType) const +Release::getArtists(TrackArtistLinkType linkType) const { assert(self()); assert(IdIsValid(self()->id())); diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 2782be74..2cbf47ac 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -40,7 +40,7 @@ namespace Database { -#define LMS_DATABASE_VERSION 26 +#define LMS_DATABASE_VERSION 27 using Version = std::size_t; @@ -278,6 +278,12 @@ CREATE TABLE "user_backup" ( // Just increment the scan version of the settings to make the next scheduled scan rescan everything ScanSettings::get(*this).modify()->incScanVersion(); } + else if (version == 26) + { + // Composer, mixer, etc. support + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(*this).modify()->incScanVersion(); + } else { LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index aaf4b53a..d71d410a 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -24,6 +24,7 @@ #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Release.hpp" +#include "database/TrackArtistLink.hpp" #include "database/TrackFeatures.hpp" #include "database/Session.hpp" #include "utils/Logger.hpp" @@ -480,31 +481,81 @@ Track::getCopyrightURL() const } std::vector> -Track::getArtists(TrackArtistLink::Type type) const +Track::getArtists(EnumSet linkTypes) const { assert(self()); assert(IdIsValid(self()->id())); assert(session()); - Wt::Dbo::collection> artists {session()->query("SELECT a from artist a INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id INNER JOIN track t ON t.id = t_a_l.track_id") - .where("t.id = ?").bind(self()->id()) - .where("t_a_l.type = ?").bind(type)}; + std::ostringstream oss; + oss << + "SELECT a from artist a" + " INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id" + " INNER JOIN track t ON t.id = t_a_l.track_id"; - return std::vector>(artists.begin(), artists.end()); + if (!linkTypes.empty()) + { + oss << " AND t_a_l.type IN ("; + + bool first {true}; + for (TrackArtistLinkType type : linkTypes) + { + (void) type; + if (!first) + oss << ", "; + oss << "?"; + first = false; + } + oss << ")"; + } + + Wt::Dbo::Query query {session()->query(oss.str()) + .where("t.id = ?").bind(self()->id())}; + + for (TrackArtistLinkType type : linkTypes) + query.bind(type); + + Wt::Dbo::collection res = query; + return std::vector(std::begin(res), std::end(res)); } std::vector -Track::getArtistIds(TrackArtistLink::Type type) const +Track::getArtistIds(EnumSet linkTypes) const { assert(self()); assert(IdIsValid(self()->id())); assert(session()); - Wt::Dbo::collection artists {session()->query("SELECT a.id from artist a INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id INNER JOIN track t ON t.id = t_a_l.track_id") - .where("t.id = ?").bind(self()->id()) - .where("t_a_l.type = ?").bind(type)}; + std::ostringstream oss; + oss << + "SELECT a.id from artist a" + " INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id" + " INNER JOIN track t ON t.id = t_a_l.track_id"; - return std::vector(artists.begin(), artists.end()); + if (!linkTypes.empty()) + { + oss << " AND t_a_l.type IN ("; + + bool first {true}; + for (TrackArtistLinkType type : linkTypes) + { + (void) type; + if (!first) + oss << ", "; + oss << "?"; + first = false; + } + oss << ")"; + } + + Wt::Dbo::Query query {session()->query(oss.str()) + .where("t.id = ?").bind(self()->id())}; + + for (TrackArtistLinkType type : linkTypes) + query.bind(type); + + Wt::Dbo::collection res = query; + return std::vector(std::begin(res), std::end(res)); } std::vector> diff --git a/src/libs/database/impl/TrackArtistLink.cpp b/src/libs/database/impl/TrackArtistLink.cpp index 57526eab..45b62bbf 100644 --- a/src/libs/database/impl/TrackArtistLink.cpp +++ b/src/libs/database/impl/TrackArtistLink.cpp @@ -25,7 +25,7 @@ namespace Database { -TrackArtistLink::TrackArtistLink(Wt::Dbo::ptr track, Wt::Dbo::ptr artist, Type type) +TrackArtistLink::TrackArtistLink(Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type) : _type {type}, _track {track}, _artist {artist} @@ -33,7 +33,7 @@ _artist {artist} } TrackArtistLink::pointer -TrackArtistLink::create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::ptr artist,Type type) +TrackArtistLink::create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type) { session.checkUniqueLocked(); @@ -43,5 +43,15 @@ TrackArtistLink::create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::pt return res; } +EnumSet +TrackArtistLink::getUsedTypes(Session& session) +{ + session.checkSharedLocked(); + + Wt::Dbo::collection collection = session.getDboSession().query("SELECT DISTINCT type from track_artist_link"); + + return EnumSet(std::begin(collection), std::end(collection)); +} + } diff --git a/src/libs/database/impl/TrackList.cpp b/src/libs/database/impl/TrackList.cpp index 981dd9cd..39cff1a8 100644 --- a/src/libs/database/impl/TrackList.cpp +++ b/src/libs/database/impl/TrackList.cpp @@ -165,7 +165,7 @@ TrackList::getEntriesReverse(std::optional offset, std::optional -createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set& clusterIds, std::optional linkType) +createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set& clusterIds, std::optional linkType) { auto query {session.query(queryStr)}; query.join("track t ON t.id = t_a_l.track_id"); @@ -270,7 +270,7 @@ createTracksQuery(Wt::Dbo::Session& session, IdType tracklistId, const std::set< } std::vector -TrackList::getArtistsReverse(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const +TrackList::getArtistsReverse(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const { assert(session()); assert(IdIsValid(self()->id())); @@ -417,7 +417,7 @@ TrackList::getDuration() const } std::vector -TrackList::getTopArtists(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const +TrackList::getTopArtists(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const { assert(session()); assert(IdIsValid(self()->id())); diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index 472ce5b4..2afe5a6c 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -21,14 +21,15 @@ #include #include +#include #include #include #include +#include "utils/EnumSet.hpp" #include "utils/UUID.hpp" -#include "TrackArtistLink.hpp" #include "Types.hpp" namespace Database @@ -39,6 +40,7 @@ class ClusterType; class Release; class Session; class Track; +class TrackArtistLink; class User; class Artist : public Wt::Dbo::Dbo @@ -68,7 +70,7 @@ class Artist : public Wt::Dbo::Dbo static std::vector getByFilter(Session& session, const std::set& clusters, // if non empty, at least one artist that belongs to these clusters const std::vector& keywords, // if non empty, name must match all of these keywords (name + sort name fields) - std::optional linkType, // if set, only artists that have produced at least one track with this link type + std::optional linkType, // if set, only artists that have produced at least one track with this link type SortMethod sortMethod, std::optional range, bool& moreExpected); @@ -77,19 +79,19 @@ class Artist : public Wt::Dbo::Dbo static std::vector getAll(Session& session, SortMethod sortMethod); static std::vector getAll(Session& session, SortMethod sortMethod, std::optional range, bool& moreResults); static std::vector getAllIds(Session& session); - static std::vector getAllIdsRandom(Session& session, const std::set& clusters, std::optional linkType, std::optional size = {}); + static std::vector getAllIdsRandom(Session& session, const std::set& clusters, std::optional linkType, std::optional size = {}); static std::vector getAllOrphans(Session& session); // No track related static std::vector getLastWritten(Session& session, std::optional after, const std::set& clusters, - std::optional linkType, // if set, only artists that have produced at least one track with this link type + std::optional linkType, // if set, only artists that have produced at least one track with this link type std::optional, bool& moreResults); static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); static std::vector getStarred(Session& session, Wt::Dbo::ptr user, const std::set& clusters, - std::optional linkType, // if set, only artists that have produced at least one track with this link type + std::optional linkType, // if set, only artists that have produced at least one track with this link type SortMethod sortMethod, std::optional, bool& moreResults); @@ -100,10 +102,12 @@ class Artist : public Wt::Dbo::Dbo std::vector> getReleases(const std::set& clusterIds = {}) const; // if non empty, get the releases that match all these clusters std::size_t getReleaseCount() const; - std::vector> getTracks(std::optional linkType = {}) const; - std::vector> getTracksWithRelease(std::optional linkType = {}) const; + std::vector> getTracks(std::optional linkType = {}) const; + std::vector> getTracksWithRelease(std::optional linkType = {}) const; std::vector> getRandomTracks(std::optional count) const; - std::vector getSimilarArtists(std::optional offset = {}, std::optional count = {}) const; + + // No artistLinkTypes means get them all + std::vector getSimilarArtists(EnumSet artistLinkTypes = {}, std::optional range = std::nullopt) const; // Get the cluster of the tracks made by this artist // Each clusters are grouped by cluster type, sorted by the number of occurence diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index 0354ff47..8149fff1 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -20,11 +20,12 @@ #pragma once #include +#include -#include +#include +#include #include "utils/UUID.hpp" -#include "TrackArtistLink.hpp" #include "Types.hpp" namespace Database @@ -34,6 +35,7 @@ class Artist; class Cluster; class ClusterType; class Release; +class Session; class Track; class User; @@ -95,8 +97,8 @@ class Release : public Wt::Dbo::Dbo Wt::WDateTime getLastWritten() const; // Get the artists of this release - std::vector > getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; - std::vector > getReleaseArtists() const { return getArtists(TrackArtistLink::Type::ReleaseArtist); } + std::vector > getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; + std::vector > getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); } bool hasVariousArtists() const; std::vector getSimilarReleases(std::optional offset = {}, std::optional count = {}) const; diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 4bbb4b5e..22e97643 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -26,12 +26,13 @@ #include #include -#include #include +#include +#include +#include "utils/EnumSet.hpp" #include "utils/UUID.hpp" -#include "TrackArtistLink.hpp" #include "Types.hpp" namespace Database { @@ -40,6 +41,8 @@ class Artist; class Cluster; class ClusterType; class Release; +class Session; +class TrackArtistLink; class TrackFeatures; class TrackListEntry; class TrackStats; @@ -134,8 +137,9 @@ class Track : public Wt::Dbo::Dbo std::optional getTrackReplayGain() const { return _trackReplayGain; } std::optional getReleaseReplayGain() const { return _releaseReplayGain; } - std::vector> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; - std::vector getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; + // no artistLinkTypes means get all + std::vector> getArtists(EnumSet artistLinkTypes) const; + std::vector getArtistIds(EnumSet artistLinkTypes) const; std::vector> getArtistLinks() const; Wt::Dbo::ptr getRelease() const { return _release; } std::vector> getClusters() const; diff --git a/src/libs/database/include/database/TrackArtistLink.hpp b/src/libs/database/include/database/TrackArtistLink.hpp index 7601d0c9..02a727e5 100644 --- a/src/libs/database/include/database/TrackArtistLink.hpp +++ b/src/libs/database/include/database/TrackArtistLink.hpp @@ -19,64 +19,53 @@ #pragma once +#include + #include #include "Types.hpp" +#include "utils/EnumSet.hpp" -namespace Database { - -class Artist; -class Session; -class Track; - -class TrackArtistLink +namespace Database { - public: - enum class Type - { - Artist, // regular artist - Arranger, - Composer, - Conductor, - Lyricist, - Mixer, - Performer, - Producer, - ReleaseArtist, - Remixer, - Writer, - }; - using pointer = Wt::Dbo::ptr; + class Artist; + class Session; + class Track; - TrackArtistLink() = default; - TrackArtistLink(Wt::Dbo::ptr track, Wt::Dbo::ptr artist, Type type); + class TrackArtistLink + { + public: + using pointer = Wt::Dbo::ptr; - static pointer create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::ptr artist,Type type); + TrackArtistLink() = default; + TrackArtistLink(Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type); - Wt::Dbo::ptr getTrack() const { return _track; } - Wt::Dbo::ptr getArtist() const { return _artist; } - Type getType() const { return _type; } + static pointer create(Session& session, Wt::Dbo::ptr track, Wt::Dbo::ptr artist, TrackArtistLinkType type); - template - void persist(Action& a) - { - Wt::Dbo::field(a, _type, "type"); - Wt::Dbo::field(a, _type, "name"); + static EnumSet getUsedTypes(Session& session); - Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); - } + Wt::Dbo::ptr getTrack() const { return _track; } + Wt::Dbo::ptr getArtist() const { return _artist; } + TrackArtistLinkType getType() const { return _type; } - private: + template + void persist(Action& a) + { + Wt::Dbo::field(a, _type, "type"); + Wt::Dbo::field(a, _type, "name"); - Type _type; - std::string _name; + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); + } - Wt::Dbo::ptr _track; - Wt::Dbo::ptr _artist; -}; + private: + TrackArtistLinkType _type; + std::string _name; + + Wt::Dbo::ptr _track; + Wt::Dbo::ptr _artist; + }; } - diff --git a/src/libs/database/include/database/TrackList.hpp b/src/libs/database/include/database/TrackList.hpp index f05f830b..b890391e 100644 --- a/src/libs/database/include/database/TrackList.hpp +++ b/src/libs/database/include/database/TrackList.hpp @@ -25,7 +25,6 @@ #include -#include "TrackArtistLink.hpp" #include "Types.hpp" namespace Database { @@ -53,7 +52,7 @@ class TrackList : public Wt::Dbo::Dbo TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr user); // Stats utility - std::vector> getTopArtists(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; + std::vector> getTopArtists(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; std::vector> getTopReleases(const std::set& clusterIds, std::optional range, bool& moreResults) const; std::vector> getTopTracks(const std::set& clusterIds, std::optional range, bool& moreResults) const; @@ -85,7 +84,7 @@ class TrackList : public Wt::Dbo::Dbo std::vector> getEntries(std::optional offset = {}, std::optional size = {}) const; std::vector> getEntriesReverse(std::optional offset = {}, std::optional size = {}) const; - std::vector> getArtistsReverse(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; + std::vector> getArtistsReverse(const std::set& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; std::vector> getReleasesReverse(const std::set& clusterIds, std::optional range, bool& moreResults) const; std::vector> getTracksReverse(const std::set& clusterIds, std::optional range, bool& moreResults) const; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 058b5052..15fdb0b3 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -35,5 +35,21 @@ namespace Database std::size_t offset {}; std::size_t limit {}; }; + + enum class TrackArtistLinkType + { + Artist, // regular artist + Arranger, + Composer, + Conductor, + Lyricist, + Mixer, + Performer, + Producer, + ReleaseArtist, + Remixer, + Writer, + }; + } diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index e4cbb4e2..044dde49 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -42,13 +42,13 @@ namespace MetaData template std::vector -getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::set& keys) +getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::vector& keys) { std::vector res; - for (const std::string& key : keys) + for (std::string_view key : keys) { - const TagLib::StringList& values {properties[key]}; + const TagLib::StringList& values {properties[std::string {key}]}; if (values.isEmpty()) continue; @@ -91,12 +91,13 @@ splitAndTrimString(const std::string& str, const std::string& delimiters) static std::vector -getArtists(const TagLib::PropertyMap& properties) +getArtists(const TagLib::PropertyMap& properties, + const std::vector& artistTagNames, + const std::vector& artistSortTagNames, + const std::vector& artistMBIDTagNames + ) { - std::vector artistNames {getPropertyValuesAs(properties, "ARTISTS")}; - if (artistNames.empty()) - artistNames = getPropertyValuesAs(properties, "ARTIST"); - + const std::vector artistNames {getPropertyValuesFirstMatchAs(properties, artistTagNames)}; if (artistNames.empty()) return {}; @@ -106,7 +107,7 @@ getArtists(const TagLib::PropertyMap& properties) [&](const std::string& name) { return Artist {name}; }); { - const std::vector artistSortNames {getPropertyValuesAs(properties, "ARTISTSORT")}; + const std::vector artistSortNames {getPropertyValuesFirstMatchAs(properties, artistSortTagNames)}; if (artistSortNames.size() == artists.size()) { for (std::size_t i {}; i < artistSortNames.size(); ++i) @@ -115,7 +116,7 @@ getArtists(const TagLib::PropertyMap& properties) } { - const std::vector artistsMBID {getPropertyValuesFirstMatchAs(properties, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"})}; + const std::vector artistsMBID {getPropertyValuesFirstMatchAs(properties, artistMBIDTagNames)}; if (artistNames.size() == artistsMBID.size()) { @@ -128,41 +129,6 @@ getArtists(const TagLib::PropertyMap& properties) return artists; } -static -std::vector -getAlbumArtists(const TagLib::PropertyMap& properties) -{ - std::vector artistNames {getPropertyValuesAs(properties, "ALBUMARTIST")}; - if (artistNames.empty()) - return {}; - - std::vector artists; - artists.reserve(artistNames.size()); - std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists), - [&](const std::string& name) { return Artist {name}; }); - - { - const std::vector artistSortNames {getPropertyValuesAs(properties, "ALBUMARTISTSORT")}; - if (artistSortNames.size() == artists.size()) - { - for (std::size_t i {}; i < artistSortNames.size(); ++i) - artists[i].sortName = artistSortNames[i]; - } - } - - { - const std::vector artistsMBID {getPropertyValuesFirstMatchAs(properties, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"})}; - - if (artistsMBID.size() == artists.size()) - { - for (std::size_t i {}; i < artistsMBID.size(); ++i) - artists[i].musicBrainzArtistID = artistsMBID[i]; - } - } - - return artists; -} - static std::optional getAlbum(const TagLib::PropertyMap& properties) @@ -430,9 +396,15 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) processTag(track, tag, values, debug); } - track.artists = getArtists(properties); - track.albumArtists = getAlbumArtists(properties); track.album = getAlbum(properties); + track.artists = getArtists(properties, {"ARTIST", "ARTISTS"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"}); + track.albumArtists = getArtists(properties, {"ALBUMARTIST"}, {"ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"}); + track.conductorArtists = getArtists(properties, {"CONDUCTOR"}, {""}, {}); + track.composerArtists = getArtists(properties, {"COMPOSER"}, {"COMPOSERSORT"}, {}); + track.lyricistArtists = getArtists(properties, {"LYRICIST"}, {"LYRICISTSORT"}, {}); + track.mixerArtists = getArtists(properties, {"MIXER"}, {""}, {}); + track.producerArtists = getArtists(properties, {"PRODUCER"}, {""}, {}); + track.remixerArtists = getArtists(properties, {"REMIXER", "ModifiedBy"}, {""}, {}); return track; } diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 624af218..06a521bf 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -78,6 +78,12 @@ namespace MetaData std::optional trackReplayGain; std::optional albumReplayGain; std::string discSubtitle; + std::vector conductorArtists; + std::vector composerArtists; + std::vector lyricistArtists; + std::vector mixerArtists; + std::vector producerArtists; + std::vector remixerArtists; }; class IParser diff --git a/src/libs/recommendation/impl/Engine.cpp b/src/libs/recommendation/impl/Engine.cpp index e3986ce5..717b087c 100644 --- a/src/libs/recommendation/impl/Engine.cpp +++ b/src/libs/recommendation/impl/Engine.cpp @@ -132,7 +132,10 @@ Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releas } std::unordered_set -Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount) +Engine::getSimilarArtists(Database::Session& dbSession, + Database::IdType artistId, + EnumSet linkTypes, + std::size_t maxCount) { std::unordered_set res; @@ -144,7 +147,7 @@ Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistI continue; const IClassifier& classifier {*itClassifier->second}; - res = classifier.getSimilarArtists(dbSession, artistId, maxCount); + res = classifier.getSimilarArtists(dbSession, artistId, linkTypes, maxCount); if (!res.empty()) { LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'"; diff --git a/src/libs/recommendation/impl/Engine.hpp b/src/libs/recommendation/impl/Engine.hpp index dc516863..2e48e539 100644 --- a/src/libs/recommendation/impl/Engine.hpp +++ b/src/libs/recommendation/impl/Engine.hpp @@ -56,10 +56,13 @@ namespace Recommendation void load(bool forceReload, const ProgressCallback& progressCallback) override; void cancelLoad() override; - std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override; - std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) override; - std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override; - std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) override; + ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override; + ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) override; + ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override; + ResultContainer getSimilarArtists(Database::Session& session, + Database::IdType artistId, + EnumSet linkTypes, + std::size_t maxCount) override; void setClassifierPriorities(const std::vector& classifierTypes); void clearClassifiers(); diff --git a/src/libs/recommendation/impl/IClassifier.hpp b/src/libs/recommendation/impl/IClassifier.hpp index 4c53df48..4670a424 100644 --- a/src/libs/recommendation/impl/IClassifier.hpp +++ b/src/libs/recommendation/impl/IClassifier.hpp @@ -24,6 +24,7 @@ #include #include "database/Types.hpp" +#include "utils/EnumSet.hpp" namespace Database { @@ -49,10 +50,14 @@ namespace Recommendation virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0; virtual void requestCancelLoad() = 0; - virtual std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0; - virtual std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const = 0; - virtual std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0; - virtual std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const = 0; + using ResultContainer = std::unordered_set; + + virtual ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0; + virtual ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const = 0; + virtual ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0; + virtual ResultContainer getSimilarArtists(Database::Session& session, + Database::IdType artistId, + EnumSet linkTypes, std::size_t maxCount) const = 0; }; } // ns Recommendation diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp index c03f517e..7a9c9ea9 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp @@ -83,7 +83,10 @@ ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::Id } std::unordered_set -ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount) const +ClusterClassifier::getSimilarArtists(Database::Session& dbSession, + Database::IdType artistId, + EnumSet artistLinkTypes, + std::size_t maxCount) const { std::unordered_set res; @@ -93,7 +96,7 @@ ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdT if (!artist) return res; - const auto artists {artist->getSimilarArtists(0, maxCount)}; + const auto artists {artist->getSimilarArtists(artistLinkTypes, Database::Range {0, maxCount})}; std::transform(std::cbegin(artists), std::cend(artists), std::inserter(res, std::end(res)), [](const auto& artist) { return artist.id(); }); diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp index 7f96a828..dcb1a8dc 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp @@ -40,10 +40,13 @@ namespace Recommendation bool load(Database::Session&, bool, const ProgressCallback&) override { return true; } void requestCancelLoad() override {} - std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; - std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override; - std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override; - std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override; + ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; + ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override; + ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override; + ResultContainer getSimilarArtists(Database::Session& session, + Database::IdType artistId, + EnumSet linkTypes, + std::size_t maxCount) const override; }; } // namespace Recommendation diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp index a98011de..b28f2160 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp @@ -25,6 +25,7 @@ #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/TrackArtistLink.hpp" #include "database/TrackFeatures.hpp" #include "database/TrackList.hpp" #include "som/DataNormalizer.hpp" @@ -308,9 +309,34 @@ FeaturesClassifier::getSimilarReleases(Database::Session& session, Database::IdT } std::unordered_set -FeaturesClassifier::getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const +FeaturesClassifier::getSimilarArtists(Database::Session& session, + Database::IdType artistId, + EnumSet linkTypes, + std::size_t maxCount) const { - auto similarArtistIds {getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount)}; + auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType) + { + std::unordered_set similarArtistIds; + + const auto itArtists {_artistsMap.find(linkType)}; + if (itArtists == std::cend(_artistsMap)) + { + return similarArtistIds; + } + + similarArtistIds = getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount); + + return similarArtistIds; + }}; + + std::unordered_set similarArtistIds; + + for (Database::TrackArtistLinkType linkType : linkTypes) + { + const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)}; + similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType)); + } + if (!similarArtistIds.empty()) { // Report only existing ids @@ -319,13 +345,16 @@ FeaturesClassifier::getSimilarArtists(Database::Session& session, Database::IdTy for (auto it {std::begin(similarArtistIds)}; it != std::end(similarArtistIds);) { const Database::IdType similarArtistId {*it}; - if (!Database::Release::getById(session, similarArtistId)) + if (!Database::Artist::getById(session, similarArtistId)) it = similarArtistIds.erase(it); else it++; } } + while (similarArtistIds.size() > maxCount) + similarArtistIds.erase(Random::pickRandom(similarArtistIds)); + return similarArtistIds; } @@ -339,6 +368,7 @@ bool FeaturesClassifier::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) { if (forceReload) + { FeaturesClassifierCache::invalidate(); } @@ -377,7 +407,6 @@ FeaturesClassifier::load(Database::Session& session, const SOM::Coordinate width {network.getWidth()}; const SOM::Coordinate height {network.getHeight()}; - _artistsMap = MatrixOfObjects {width, height}; _releasesMap = MatrixOfObjects {width, height}; _tracksMap = MatrixOfObjects {width, height}; @@ -407,10 +436,18 @@ FeaturesClassifier::load(Database::Session& session, _releasePositions[track->getRelease().id()].insert(position); _releasesMap[position].insert(track->getRelease().id()); } - for (const auto& artist : track->getArtists()) + for (const auto& artistLink : track->getArtistLinks()) { - _artistPositions[artist.id()].insert(position); - _artistsMap[position].insert(artist.id()); + _artistPositions[artistLink->getArtist().id()].insert(position); + auto itArtists {_artistsMap.find(artistLink->getType())}; + if (itArtists == std::cend(_artistsMap)) + { + auto [it, inserted] = _artistsMap.try_emplace(artistLink->getType(), MatrixOfObjects {}); + assert(inserted); + itArtists = it; + itArtists->second = MatrixOfObjects {width, height}; + } + itArtists->second[position].insert(artistLink->getArtist().id()); } } } diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp index 2d5b3992..177962ac 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp @@ -65,7 +65,10 @@ class FeaturesClassifier : public IClassifier std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override; std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override; - std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override; + std::unordered_set getSimilarArtists(Database::Session& session, + Database::IdType artistId, + EnumSet linkTypes, + std::size_t maxCount) const override; bool loadFromCache(Database::Session& session, const FeaturesClassifierCache& cache); @@ -99,8 +102,8 @@ class FeaturesClassifier : public IClassifier std::unique_ptr _network; double _networkRefVectorsDistanceMedian {}; - MatrixOfObjects _artistsMap; - ObjectPositions _artistPositions; + ObjectPositions _artistPositions; + std::unordered_map _artistsMap; MatrixOfObjects _releasesMap; ObjectPositions _releasePositions; diff --git a/src/libs/recommendation/include/recommendation/IEngine.hpp b/src/libs/recommendation/include/recommendation/IEngine.hpp index f1b4ce2b..bd7ce05d 100644 --- a/src/libs/recommendation/include/recommendation/IEngine.hpp +++ b/src/libs/recommendation/include/recommendation/IEngine.hpp @@ -20,9 +20,11 @@ #pragma once #include +#include #include #include "database/Types.hpp" +#include "utils/EnumSet.hpp" namespace Database { @@ -46,10 +48,15 @@ namespace Recommendation virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; virtual void cancelLoad() = 0; - virtual std::unordered_set getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0; - virtual std::unordered_set getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) = 0; - virtual std::unordered_set getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0; - virtual std::unordered_set getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) = 0; + using ResultContainer = std::unordered_set; + + virtual ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0; + virtual ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) = 0; + virtual ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0; + virtual ResultContainer getSimilarArtists(Database::Session& session, + Database::IdType artistId, + EnumSet linkTypes, + std::size_t maxCount) = 0; }; std::unique_ptr createEngine(Database::Db& db); diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index c3ac4c25..3bc6984e 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -29,6 +29,7 @@ #include "database/Release.hpp" #include "database/ScanSettings.hpp" #include "database/Track.hpp" +#include "database/TrackArtistLink.hpp" #include "database/TrackFeatures.hpp" #include "metadata/TagLibParser.hpp" #include "recommendation/IEngine.hpp" @@ -750,20 +751,6 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S title = file.filename().string(); } - // ***** Clusters - std::vector clusters {getOrCreateClusters(_dbSession, trackInfo->clusters)}; - - // ***** Artists - std::vector artists {getOrCreateArtists(_dbSession, trackInfo->artists)}; - - // ***** Release artists - std::vector releaseArtists {getOrCreateArtists(_dbSession, trackInfo->albumArtists)}; - - // ***** Release - Release::pointer release; - if (trackInfo->album) - release = getOrCreateRelease(_dbSession, *trackInfo->album); - // If file already exist, update data // Otherwise, create it if (!track) @@ -784,15 +771,34 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S assert(track); track.modify()->clearArtistLinks(); - for (const auto& artist : artists) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLink::Type::Artist)); + for (const Artist::pointer& artist : getOrCreateArtists(_dbSession, trackInfo->artists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLinkType::Artist)); - for (const auto& releaseArtist : releaseArtists) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLink::Type::ReleaseArtist)); + for (const Artist::pointer& releaseArtist : getOrCreateArtists(_dbSession, trackInfo->albumArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLinkType::ReleaseArtist)); + + for (const Artist::pointer& conductor : getOrCreateArtists(_dbSession, trackInfo->conductorArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, conductor, Database::TrackArtistLinkType::Conductor)); + + for (const Artist::pointer& composer : getOrCreateArtists(_dbSession, trackInfo->composerArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, composer, Database::TrackArtistLinkType::Composer)); + + for (const Artist::pointer& lyricist : getOrCreateArtists(_dbSession, trackInfo->lyricistArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, lyricist, Database::TrackArtistLinkType::Lyricist)); + + for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, mixer, Database::TrackArtistLinkType::Mixer)); + + for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, producer, Database::TrackArtistLinkType::Producer)); + + for (const Artist::pointer& remixer : getOrCreateArtists(_dbSession, trackInfo->remixerArtists)) + track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, remixer, Database::TrackArtistLinkType::Remixer)); track.modify()->setScanVersion(_scanVersion); - track.modify()->setRelease(release); - track.modify()->setClusters(clusters); + if (trackInfo->album) + track.modify()->setRelease(getOrCreateRelease(_dbSession, *trackInfo->album)); + track.modify()->setClusters(getOrCreateClusters(_dbSession, trackInfo->clusters)); track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setName(title); track.modify()->setDuration(trackInfo->duration); diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 6d1a6091..d59a5db2 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -316,7 +316,7 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()})); - auto artists {track->getArtists()}; + auto artists {track->getArtists({TrackArtistLinkType::Artist})}; if (!artists.empty()) { trackResponse.setAttribute("artist", getArtistNames(artists)); @@ -893,7 +893,10 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString()); } - auto similarArtistsId {Service::get()->getSimilarArtists(context.dbSession, id.value, count)}; + auto similarArtistsId {Service::get()->getSimilarArtists(context.dbSession, + id.value, + {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, + count)}; { auto transaction {context.dbSession.createSharedTransaction()}; @@ -943,13 +946,13 @@ handleGetArtistsRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - std::optional linkType; + std::optional linkType; switch (user->getSubsonicArtistListMode()) { case User::SubsonicArtistListMode::AllArtists: break; case User::SubsonicArtistListMode::ReleaseArtists: - linkType = TrackArtistLink::Type::ReleaseArtist; + linkType = TrackArtistLinkType::ReleaseArtist; break; } @@ -1087,13 +1090,13 @@ handleGetIndexesRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - std::optional linkType; + std::optional linkType; switch (user->getSubsonicArtistListMode()) { case User::SubsonicArtistListMode::AllArtists: break; case User::SubsonicArtistListMode::ReleaseArtists: - linkType = TrackArtistLink::Type::ReleaseArtist; + linkType = TrackArtistLinkType::ReleaseArtist; break; } @@ -1115,36 +1118,39 @@ Response handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) { // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; - if (id.type != Id::Type::Artist) + const Id artistId {getMandatoryParameterAs(context.parameters, "id")}; + if (artistId.type != Id::Type::Artist) throw BadParameterGenericError {"id"}; // Optional params std::size_t count {getParameterAs(context.parameters, "count").value_or(50)}; - auto similarArtistsId {Service::get()->getSimilarArtists(context.dbSession, id.value, 5)}; + auto similarArtistIds {Service::get()->getSimilarArtists(context.dbSession, + artistId.value, + {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, + 5)}; auto transaction {context.dbSession.createSharedTransaction()}; - Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; + const Artist::pointer artist {Artist::getById(context.dbSession, artistId.value)}; if (!artist) throw RequestedDataNotFoundError {}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + const User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw UserNotAuthorizedError {}; // "Returns a random collection of songs from the given artist and similar artists" auto tracks {artist->getRandomTracks(count / 2)}; - for ( const auto& similarArtistId : similarArtistsId ) + for (const Database::IdType similarArtistId : similarArtistIds) { - Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; + const Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; if (!similarArtist) continue; auto similarArtistTracks {similarArtist->getRandomTracks((count / 2) / 5)}; - tracks.insert(tracks.end(), + tracks.insert(std::end(tracks), std::make_move_iterator(std::begin(similarArtistTracks)), std::make_move_iterator(std::end(similarArtistTracks))); } diff --git a/src/libs/utils/include/utils/EnumSet.hpp b/src/libs/utils/include/utils/EnumSet.hpp new file mode 100644 index 00000000..c4699c0a --- /dev/null +++ b/src/libs/utils/include/utils/EnumSet.hpp @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +template +class EnumSet +{ + static_assert(std::is_enum::value); + static_assert(std::is_same::value || std::is_same::value); + + using index_type = std::uint_fast8_t; + + public: + + EnumSet() = default; + constexpr EnumSet(std::initializer_list values) + { + for (T value : values) + insert(value); + } + + template + constexpr EnumSet(It begin, It end) + { + for (It it {begin}; it != end; ++it) + insert(*it); + } + + constexpr void insert(T value) + { + assert(static_cast(value) < sizeof(_bitfield) * 8); + _bitfield |= (underlying_type{ 1 } << static_cast(value)); + } + + constexpr void erase(T value) + { + assert(static_cast(value) < sizeof(_bitfield) * 8); + _bitfield &= ~(underlying_type{ 1 } << static_cast(value)); + } + + constexpr bool empty() const + { + return _bitfield == 0; + } + + constexpr bool contains(T value) const + { + assert(static_cast(value) < sizeof(_bitfield) * 8); + return _bitfield & (underlying_type{ 1 } << static_cast(value)); + } + + class iterator + { + public: + using value_type = T; + + constexpr value_type operator*() const + { + return static_cast(_index); + } + + constexpr bool operator==(const iterator& _other) const + { + return &_container == &_other._container && _index == _other._index; + } + + constexpr bool operator!=(const iterator& _other) const + { + return !(*this == _other); + } + + constexpr iterator& operator++() + { + _index = _container.getFirstBitSetIndex(_index + 1); + return *this; + } + + private: + friend class EnumSet; + + constexpr iterator(const EnumSet& _container, index_type _index) + : _container {_container} + , _index {_index} + { + } + + const EnumSet& _container; + index_type _index; + }; + + constexpr iterator begin() const + { + return iterator {*this, getFirstBitSetIndex()}; + } + + constexpr iterator end() const + { + return iterator {*this, npos}; + } + + private: + + static_assert(std::numeric_limits::max() >= sizeof(underlying_type) * 8); + enum : index_type { npos = sizeof(underlying_type) * 8 }; + + constexpr index_type getFirstBitSetIndex(index_type start = {}) const + { + assert(start < npos); + + // return npos if no bit found + index_type res {countTrailingZero(_bitfield >> start)}; + if (res == npos) + return res; + + return res + start; + } + + static constexpr index_type countTrailingZero(underlying_type bitField) + { + index_type res {}; + + while (res < (sizeof(underlying_type) * 8) && (bitField & 1) == 0) + { + ++res; + bitField >>= 1; + } + + if (res == sizeof(underlying_type) * 8) + res = npos; + + return res; + } + + underlying_type _bitfield{}; +}; + + diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index ca96f30f..abebac64 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -30,6 +30,7 @@ #include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackList.hpp" +#include "database/Types.hpp" #include "database/User.hpp" #include "resource/ImageResource.hpp" @@ -242,7 +243,7 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain) const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)}; const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)}; - const auto artists {track->getArtists()}; + const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; oss << "var params = {" diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index ff0e5db1..7b8e89cb 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -419,7 +419,7 @@ PlayQueue::addSome() entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); - const auto artists {track->getArtists()}; + const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; const auto release {track->getRelease()}; if (!artists.empty() || release) diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 5b16c86f..978c6278 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -75,7 +75,10 @@ Artist::refreshView() if (!artistId) throw ArtistNotFoundException {*artistId}; - const auto similarArtistIds {Service::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)}; + const auto similarArtistIds {Service::get()->getSimilarArtists(LmsApp->getDbSession(), + *artistId, + {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, + 5)}; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; diff --git a/src/lms/ui/explore/ArtistsView.cpp b/src/lms/ui/explore/ArtistsView.cpp index 28752654..7cb61753 100644 --- a/src/lms/ui/explore/ArtistsView.cpp +++ b/src/lms/ui/explore/ArtistsView.cpp @@ -27,6 +27,7 @@ #include "database/Artist.hpp" #include "database/Session.hpp" #include "database/User.hpp" +#include "database/TrackArtistLink.hpp" #include "database/TrackList.hpp" #include "utils/Logger.hpp" @@ -39,7 +40,7 @@ using namespace Database; namespace UserInterface { -using ArtistLinkModel = ValueStringModel>; +using ArtistLinkModel = ValueStringModel>; Artists::Artists(Filters* filters) : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}, @@ -68,11 +69,32 @@ Artists::Artists(Filters* filters) } _linkType = bindNew("link-type"); + { auto linkTypeModel {std::make_shared()}; + EnumSet usedLinkTypes; + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + usedLinkTypes = Database::TrackArtistLink::getUsedTypes(LmsApp->getDbSession()); + } + + auto addTypeIfUsed {[&](Database::TrackArtistLinkType linkType, std::string_view stringKey) + { + if (!usedLinkTypes.contains(linkType)) + return; + + linkTypeModel->add(Wt::WString::tr(std::string {stringKey}), linkType); + }}; + linkTypeModel->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); + 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"); + _linkType->setModel(linkTypeModel); } _linkType->changed().connect([this] { refreshView(); }); @@ -125,7 +147,7 @@ Artists::getRandomArtists(std::optional range, bool& moreResults) { std::vector artists; - const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())}; + const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())}; if (_randomArtists.empty()) _randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), linkType, maxItemsPerMode[Mode::Random]); @@ -152,7 +174,7 @@ Artists::getArtists(std::optional range, bool& moreResults) { std::vector artists; - const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())}; + const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())}; const std::optional modeLimit{maxItemsPerMode[_mode]}; if (modeLimit) diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 59ed4dc3..f3356679 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -214,7 +214,7 @@ Release::refreshView() entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); - auto artists {track->getArtists()}; + const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; if (variousArtists && !artists.empty()) { entry->setCondition("if-has-artists", true); diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 9a8930e7..7d241b3c 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -47,7 +47,7 @@ namespace UserInterface::TrackListHelpers Wt::WText* name {entry->bindNew("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain)}; name->setToolTip(Wt::WString::fromUTF8(track->getName())); - const auto artists {track->getArtists()}; + const auto artists {track->getArtists({TrackArtistLinkType::Artist})}; const Release::pointer release {track->getRelease()}; const IdType trackId {track.id()}; diff --git a/src/test/database/DatabaseTest.cpp b/src/test/database/DatabaseTest.cpp index 6ce4fd4e..f3e76efa 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/test/database/DatabaseTest.cpp @@ -28,6 +28,7 @@ #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/TrackArtistLink.hpp" #include "database/TrackBookmark.hpp" #include "database/TrackList.hpp" #include "database/User.hpp" @@ -265,7 +266,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 +277,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 +288,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({}).empty()); } { @@ -298,8 +300,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 +314,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 +328,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 +366,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 +378,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 +396,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 +413,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); } { @@ -989,7 +992,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()); } @@ -1051,8 +1054,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()); } @@ -1092,7 +1095,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()); @@ -1131,7 +1134,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()); } @@ -1164,7 +1167,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()); } @@ -1201,7 +1204,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()); } @@ -1253,7 +1256,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()); @@ -1301,7 +1304,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()); } @@ -1526,8 +1529,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()); @@ -1605,7 +1608,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()); } @@ -1614,7 +1617,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()); } @@ -1627,7 +1630,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()); } { @@ -1785,10 +1788,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()); } @@ -1798,7 +1801,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()); } @@ -1811,6 +1814,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); diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 963d44b0..f7103ee1 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -76,9 +76,27 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file) for (const Artist& artist : track->artists) std::cout << "Artist: " << artist << std::endl; - for (const Artist& artist: track->albumArtists) + for (const Artist& artist : track->albumArtists) std::cout << "Album artist: " << artist << std::endl; + for (const Artist& artist : track->conductorArtists) + std::cout << "Conductor: " << artist << std::endl; + + for (const Artist& artist : track->composerArtists) + std::cout << "Composer: " << artist << std::endl; + + for (const Artist& artist : track->lyricistArtists) + std::cout << "Lyricist: " << artist << std::endl; + + for (const Artist& artist : track->mixerArtists) + std::cout << "Mixer: " << artist << std::endl; + + for (const Artist& artist : track->producerArtists) + std::cout << "Producer: " << artist << std::endl; + + for (const Artist& artist : track->remixerArtists) + std::cout << "Remixer: " << artist << std::endl; + if (track->album) std::cout << "Album: " << *track->album << std::endl; diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index 5c722c7f..bea308e8 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -35,10 +35,9 @@ #include "utils/StreamLogger.hpp" #include "recommendation/IEngine.hpp" - static void -dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& engine) +dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) { const std::vector trackIds {[&]() { @@ -58,7 +57,7 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng res += track->getName(); if (track->getRelease()) res += " [" + track->getRelease()->getName() + "]"; - for (auto artist : track->getArtists()) + for (auto artist : track->getArtists({Database::TrackArtistLinkType::Artist})) res += " - " + artist->getName(); for (auto cluster : track->getClusters()) res += " {" + cluster->getType()->getName() + "-"+ cluster->getName() + "}"; @@ -67,14 +66,14 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng }; std::cout << "Processing track '" << trackToString(trackId) << std::endl; - for (Database::IdType similarTrackId : engine.getSimilarTracks(session, {trackId}, 3)) + for (Database::IdType similarTrackId : engine.getSimilarTracks(session, {trackId}, maxSimilarityCount)) std::cout << "\t- Similar track '" << trackToString(similarTrackId) << std::endl; } } static void -dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& engine) +dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) { const std::vector releaseIds = std::invoke([&]() { @@ -94,14 +93,14 @@ dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& e }; std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl; - for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, releaseId, 3)) + for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, releaseId, maxSimilarityCount)) std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl; } } static void -dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& engine) +dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) { const std::vector artistIds = std::invoke([&]() { @@ -121,8 +120,10 @@ dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& en }; std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl; - for (Database::IdType similarArtistId : engine.getSimilarArtists(session, artistId, 3)) + for (Database::IdType similarArtistId : engine.getSimilarArtists(session, artistId, {Database::TrackArtistLinkType::Artist, Database::TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount)) + { std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl; + } } } @@ -143,6 +144,7 @@ int main(int argc, char *argv[]) ("artists,a", "Display recommendation for artists") ("releases,r", "Display recommendation for releases") ("tracks,t", "Display recommendation for tracks") + ("max,m", po::value()->default_value(3), "Max similarity result count") ; po::variables_map vm; @@ -165,16 +167,19 @@ int main(int argc, char *argv[]) std::cout << "Loading recommendation engine..." << std::endl; engine->load(false); + + unsigned maxSimilarityCount {vm["max"].as()}; + std::cout << "Recommendation engine loaded!" << std::endl; if (vm.count("tracks")) - dumpTracksRecommendation(db, *engine); + dumpTracksRecommendation(db, *engine, maxSimilarityCount); if (vm.count("releases")) - dumpReleasesRecommendation(db, *engine); + dumpReleasesRecommendation(db, *engine, maxSimilarityCount); if (vm.count("artists")) - dumpArtistsRecommendation(db, *engine); + dumpArtistsRecommendation(db, *engine, maxSimilarityCount); } catch( std::exception& e) {