Added composer, conductor, lyricist, mixer, producer, remixer tag support

This commit is contained in:
emeric
2020-11-12 13:19:22 +01:00
parent cf6ebe0911
commit b78967262c
37 changed files with 675 additions and 285 deletions
+5 -13
View File
@@ -1,17 +1,16 @@
# LMS - Lightweight Music Server # LMS - Lightweight Music Server
[![Build Status](https://travis-ci.org/epoupon/lms.svg?branch=master)](https://travis-ci.org/epoupon/lms) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/epoupon/lms) [![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! _LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface!
A [demo](http://lms.demo.poupon.io) instance is available. Note the administration panel is not available. A [demo instance](http://lms.demo.poupon.io) is available. Note the administration panel is not available.
## Main features ## Main features
* Low memory requirements: the demo instance runs on a Raspberry Pi3B+ * Low memory requirements: the demo instance runs on a _Raspberry Pi Zero W_
* Recommendation engine * Recommendation engine
* Audio transcode for maximum interoperability and low bandwith requirements * Audio transcode for maximum interoperability and low bandwith requirements
* Multi-value tags: artists, genres, ... * Multi-value tags: artists, genres, composers, lyricists, moods, ...
* Custom tags support: _mood_, _genre_, _albummood_, _albumgrouping_, ...
* Compilation support * Compilation support
* [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names * [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names
* Disc subtitles support * Disc subtitles support
@@ -31,6 +30,7 @@ _LMS_ provides several ways to help you find the music you like:
* Radio mode, based on what is in the current playqueue * Radio mode, based on what is in the current playqueue
* Searches in album, artist and track names (including sort names) * Searches in album, artist and track names (including sort names)
* Starred Albums/Artists/Tracks * Starred Albums/Artists/Tracks
* Custom tags support to help you filter your music: _mood_, _albummood_, _albumgenre_, _albumgrouping_, ...
* Random/Starred/Most played/Recently played/Recently added for Artist/Albums/Tracks, allowing you to search for things like: * Random/Starred/Most played/Recently played/Recently added for Artist/Albums/Tracks, allowing you to search for things like:
* Recently added _Electronic_ artists * Recently added _Electronic_ artists
* Random _Metal_ and _Aggressive_ albums * Random _Metal_ and _Aggressive_ albums
@@ -232,11 +232,3 @@ journalctl -u lms.service
To connect to _LMS_, just open your favorite browser and go to http://localhost:5082 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/)
+5
View File
@@ -143,7 +143,12 @@
<!--Explore:Artists--> <!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">All artists</message> <message id="Lms.Explore.Artists.linktype-all">All artists</message>
<message id="Lms.Explore.Artists.linktype-artist">Track artists</message> <message id="Lms.Explore.Artists.linktype-artist">Track artists</message>
<message id="Lms.Explore.Artists.linktype-composer">Composers</message>
<message id="Lms.Explore.Artists.linktype-lyricist">Lyricists</message>
<message id="Lms.Explore.Artists.linktype-mixer">Mixers</message>
<message id="Lms.Explore.Artists.linktype-producer">Producers</message>
<message id="Lms.Explore.Artists.linktype-releaseartist">Album artists</message> <message id="Lms.Explore.Artists.linktype-releaseartist">Album artists</message>
<message id="Lms.Explore.Artists.linktype-remixer">Remixers</message>
<!--Explore:Release--> <!--Explore:Release-->
<message id="Lms.Explore.Release.similar-releases">Similar albums</message> <message id="Lms.Explore.Release.similar-releases">Similar albums</message>
+5
View File
@@ -143,7 +143,12 @@
<!--Explore:Artists--> <!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">Tous les artistes</message> <message id="Lms.Explore.Artists.linktype-all">Tous les artistes</message>
<message id="Lms.Explore.Artists.linktype-artist">Artistes de piste</message> <message id="Lms.Explore.Artists.linktype-artist">Artistes de piste</message>
<message id="Lms.Explore.Artists.linktype-composer">Compositeurs</message>
<message id="Lms.Explore.Artists.linktype-lyricist">Paroliers</message>
<message id="Lms.Explore.Artists.linktype-mixer">Mixers</message>
<message id="Lms.Explore.Artists.linktype-producer">Producteurs</message>
<message id="Lms.Explore.Artists.linktype-releaseartist">Artistes d'album</message> <message id="Lms.Explore.Artists.linktype-releaseartist">Artistes d'album</message>
<message id="Lms.Explore.Artists.linktype-remixer">Remixers</message>
<!--Explore:Release--> <!--Explore:Release-->
<message id="Lms.Explore.Release.similar-releases">Albums similaires</message> <message id="Lms.Explore.Release.similar-releases">Albums similaires</message>
+34 -13
View File
@@ -81,7 +81,7 @@ createQuery(Session& session,
const std::string& queryStr, const std::string& queryStr,
const std::set<IdType>& clusterIds, const std::set<IdType>& clusterIds,
const std::vector<std::string>& keywords, const std::vector<std::string>& keywords,
std::optional<TrackArtistLink::Type> linkType) std::optional<TrackArtistLinkType> linkType)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -214,7 +214,7 @@ Artist::getAllIds(Session& session)
} }
std::vector<IdType> std::vector<IdType>
Artist::getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<std::size_t> size) Artist::getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -265,7 +265,7 @@ std::vector<Artist::pointer>
Artist::getByFilter(Session& session, Artist::getByFilter(Session& session,
const std::set<IdType>& clusters, const std::set<IdType>& clusters,
const std::vector<std::string>& keywords, const std::vector<std::string>& keywords,
std::optional<TrackArtistLink::Type> linkType, std::optional<TrackArtistLinkType> linkType,
SortMethod sortMethod, SortMethod sortMethod,
std::optional<Range> range, std::optional<Range> range,
bool& moreResults) bool& moreResults)
@@ -306,7 +306,7 @@ std::vector<Artist::pointer>
Artist::getLastWritten(Session& session, Artist::getLastWritten(Session& session,
std::optional<Wt::WDateTime> after, std::optional<Wt::WDateTime> after,
const std::set<IdType>& clusters, const std::set<IdType>& clusters,
std::optional<TrackArtistLink::Type> linkType, std::optional<TrackArtistLinkType> linkType,
std::optional<Range> range, bool& moreResults) std::optional<Range> range, bool& moreResults)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -338,7 +338,7 @@ std::vector<Artist::pointer>
Artist::getStarred(Session& session, Artist::getStarred(Session& session,
User::pointer user, User::pointer user,
const std::set<IdType>& clusters, const std::set<IdType>& clusters,
std::optional<TrackArtistLink::Type> linkType, std::optional<TrackArtistLinkType> linkType,
SortMethod sortMethod, SortMethod sortMethod,
std::optional<Range> range, bool& moreResults) std::optional<Range> range, bool& moreResults)
{ {
@@ -443,7 +443,7 @@ Artist::getReleaseCount() const
} }
std::vector<Wt::Dbo::ptr<Track>> std::vector<Wt::Dbo::ptr<Track>>
Artist::getTracks(std::optional<TrackArtistLink::Type> linkType) const Artist::getTracks(std::optional<TrackArtistLinkType> linkType) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
@@ -462,7 +462,7 @@ Artist::getTracks(std::optional<TrackArtistLink::Type> linkType) const
} }
std::vector<Wt::Dbo::ptr<Track>> std::vector<Wt::Dbo::ptr<Track>>
Artist::getTracksWithRelease(std::optional<TrackArtistLink::Type> linkType) const Artist::getTracksWithRelease(std::optional<TrackArtistLinkType> linkType) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
@@ -497,13 +497,14 @@ Artist::getRandomTracks(std::optional<std::size_t> count) const
} }
std::vector<Wt::Dbo::ptr<Artist>> std::vector<Wt::Dbo::ptr<Artist>>
Artist::getSimilarArtists(std::optional<std::size_t> offset, std::optional<std::size_t> count) const Artist::getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
assert(session()); assert(session());
Wt::Dbo::Query<pointer> query {session()->query<pointer>( std::ostringstream oss;
oss <<
"SELECT a FROM artist a" "SELECT a FROM artist a"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN track t ON t.id = t_a_l.track_id" " INNER JOIN track t ON t.id = t_a_l.track_id"
@@ -515,14 +516,34 @@ Artist::getSimilarArtists(std::optional<std::size_t> offset, std::optional<std::
" INNER JOIN artist a ON a.id = t_a_l.artist_id" " INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" WHERE a.id = ?)" " WHERE a.id = ?)"
" AND a.id <> ?" " AND a.id <> ?";
)
if (!artistLinkTypes.empty())
{
oss << " AND t_a_l.type IN (";
bool first {true};
for (TrackArtistLinkType type : artistLinkTypes)
{
(void) type;
if (!first)
oss << ", ";
oss << "?";
first = false;
}
oss << ")";
}
Wt::Dbo::Query<pointer> query {session()->query<pointer>(oss.str())
.bind(self()->id()) .bind(self()->id())
.bind(self()->id()) .bind(self()->id())
.groupBy("a.id") .groupBy("a.id")
.orderBy("COUNT(*) DESC, RANDOM()") .orderBy("COUNT(*) DESC, RANDOM()")
.limit(count ? static_cast<int>(*count) : -1) .limit(range ? static_cast<int>(range->limit) : -1)
.offset(offset ? static_cast<int>(*offset) : -1)}; .offset(range ? static_cast<int>(range->offset) : -1)};
for (TrackArtistLinkType type : artistLinkTypes)
query.bind(type);
Wt::Dbo::collection<pointer> res = query; Wt::Dbo::collection<pointer> res = query;
return std::vector<pointer>(res.begin(), res.end()); return std::vector<pointer>(res.begin(), res.end());
+3 -2
View File
@@ -19,13 +19,14 @@
#include "database/Release.hpp" #include "database/Release.hpp"
#include "utils/Logger.hpp" #include <Wt/Dbo/WtSqlTraits.h>
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "utils/Logger.hpp"
#include "SqlQuery.hpp" #include "SqlQuery.hpp"
namespace Database namespace Database
@@ -431,7 +432,7 @@ Release::getCopyrightURL() const
} }
std::vector<Wt::Dbo::ptr<Artist>> std::vector<Wt::Dbo::ptr<Artist>>
Release::getArtists(TrackArtistLink::Type linkType) const Release::getArtists(TrackArtistLinkType linkType) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
+7 -1
View File
@@ -40,7 +40,7 @@
namespace Database { namespace Database {
#define LMS_DATABASE_VERSION 26 #define LMS_DATABASE_VERSION 27
using Version = std::size_t; using Version = std::size_t;
@@ -278,6 +278,12 @@ CREATE TABLE "user_backup" (
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion(); ScanSettings::get(*this).modify()->incScanVersion();
} }
else if (version == 26)
{
// Composer, mixer, etc. support
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else else
{ {
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
+61 -10
View File
@@ -24,6 +24,7 @@
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackFeatures.hpp" #include "database/TrackFeatures.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -480,31 +481,81 @@ Track::getCopyrightURL() const
} }
std::vector<Wt::Dbo::ptr<Artist>> std::vector<Wt::Dbo::ptr<Artist>>
Track::getArtists(TrackArtistLink::Type type) const Track::getArtists(EnumSet<TrackArtistLinkType> linkTypes) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
assert(session()); assert(session());
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> artists {session()->query<Artist::pointer>("SELECT a from artist a INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id INNER JOIN track t ON t.id = t_a_l.track_id") std::ostringstream oss;
.where("t.id = ?").bind(self()->id()) oss <<
.where("t_a_l.type = ?").bind(type)}; "SELECT a from artist a"
" INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id"
" INNER JOIN track t ON t.id = t_a_l.track_id";
return std::vector<Wt::Dbo::ptr<Artist>>(artists.begin(), artists.end()); if (!linkTypes.empty())
{
oss << " AND t_a_l.type IN (";
bool first {true};
for (TrackArtistLinkType type : linkTypes)
{
(void) type;
if (!first)
oss << ", ";
oss << "?";
first = false;
}
oss << ")";
}
Wt::Dbo::Query<Artist::pointer> query {session()->query<Artist::pointer>(oss.str())
.where("t.id = ?").bind(self()->id())};
for (TrackArtistLinkType type : linkTypes)
query.bind(type);
Wt::Dbo::collection<Artist::pointer> res = query;
return std::vector<Artist::pointer>(std::begin(res), std::end(res));
} }
std::vector<IdType> std::vector<IdType>
Track::getArtistIds(TrackArtistLink::Type type) const Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
{ {
assert(self()); assert(self());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
assert(session()); assert(session());
Wt::Dbo::collection<IdType> artists {session()->query<IdType>("SELECT a.id from artist a INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id INNER JOIN track t ON t.id = t_a_l.track_id") std::ostringstream oss;
.where("t.id = ?").bind(self()->id()) oss <<
.where("t_a_l.type = ?").bind(type)}; "SELECT a.id from artist a"
" INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id"
" INNER JOIN track t ON t.id = t_a_l.track_id";
return std::vector<IdType>(artists.begin(), artists.end()); if (!linkTypes.empty())
{
oss << " AND t_a_l.type IN (";
bool first {true};
for (TrackArtistLinkType type : linkTypes)
{
(void) type;
if (!first)
oss << ", ";
oss << "?";
first = false;
}
oss << ")";
}
Wt::Dbo::Query<IdType> query {session()->query<IdType>(oss.str())
.where("t.id = ?").bind(self()->id())};
for (TrackArtistLinkType type : linkTypes)
query.bind(type);
Wt::Dbo::collection<IdType> res = query;
return std::vector<IdType>(std::begin(res), std::end(res));
} }
std::vector<Wt::Dbo::ptr<TrackArtistLink>> std::vector<Wt::Dbo::ptr<TrackArtistLink>>
+12 -2
View File
@@ -25,7 +25,7 @@
namespace Database { namespace Database {
TrackArtistLink::TrackArtistLink(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, Type type) TrackArtistLink::TrackArtistLink(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, TrackArtistLinkType type)
: _type {type}, : _type {type},
_track {track}, _track {track},
_artist {artist} _artist {artist}
@@ -33,7 +33,7 @@ _artist {artist}
} }
TrackArtistLink::pointer TrackArtistLink::pointer
TrackArtistLink::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist,Type type) TrackArtistLink::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Artist> artist, TrackArtistLinkType type)
{ {
session.checkUniqueLocked(); session.checkUniqueLocked();
@@ -43,5 +43,15 @@ TrackArtistLink::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::pt
return res; return res;
} }
EnumSet<TrackArtistLinkType>
TrackArtistLink::getUsedTypes(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<TrackArtistLinkType> collection = session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link");
return EnumSet<TrackArtistLinkType>(std::begin(collection), std::end(collection));
}
} }
+3 -3
View File
@@ -165,7 +165,7 @@ TrackList::getEntriesReverse(std::optional<std::size_t> offset, std::optional<st
static static
Wt::Dbo::Query<Artist::pointer> Wt::Dbo::Query<Artist::pointer>
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType) createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType)
{ {
auto query {session.query<Artist::pointer>(queryStr)}; auto query {session.query<Artist::pointer>(queryStr)};
query.join("track t ON t.id = t_a_l.track_id"); query.join("track t ON t.id = t_a_l.track_id");
@@ -270,7 +270,7 @@ createTracksQuery(Wt::Dbo::Session& session, IdType tracklistId, const std::set<
} }
std::vector<Artist::pointer> std::vector<Artist::pointer>
TrackList::getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const TrackList::getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
{ {
assert(session()); assert(session());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
@@ -417,7 +417,7 @@ TrackList::getDuration() const
} }
std::vector<Artist::pointer> std::vector<Artist::pointer>
TrackList::getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const TrackList::getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
{ {
assert(session()); assert(session());
assert(IdIsValid(self()->id())); assert(IdIsValid(self()->id()));
+12 -8
View File
@@ -21,14 +21,15 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <unordered_set>
#include <vector> #include <vector>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include "utils/EnumSet.hpp"
#include "utils/UUID.hpp" #include "utils/UUID.hpp"
#include "TrackArtistLink.hpp"
#include "Types.hpp" #include "Types.hpp"
namespace Database namespace Database
@@ -39,6 +40,7 @@ class ClusterType;
class Release; class Release;
class Session; class Session;
class Track; class Track;
class TrackArtistLink;
class User; class User;
class Artist : public Wt::Dbo::Dbo<Artist> class Artist : public Wt::Dbo::Dbo<Artist>
@@ -68,7 +70,7 @@ class Artist : public Wt::Dbo::Dbo<Artist>
static std::vector<pointer> getByFilter(Session& session, static std::vector<pointer> getByFilter(Session& session,
const std::set<IdType>& clusters, // if non empty, at least one artist that belongs to these clusters const std::set<IdType>& clusters, // if non empty, at least one artist that belongs to these clusters
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords (name + sort name fields) const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
SortMethod sortMethod, SortMethod sortMethod,
std::optional<Range> range, std::optional<Range> range,
bool& moreExpected); bool& moreExpected);
@@ -77,19 +79,19 @@ class Artist : public Wt::Dbo::Dbo<Artist>
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod); static std::vector<pointer> getAll(Session& session, SortMethod sortMethod);
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults); static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults);
static std::vector<IdType> getAllIds(Session& session); static std::vector<IdType> getAllIds(Session& session);
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<std::size_t> size = {}); static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size = {});
static std::vector<pointer> getAllOrphans(Session& session); // No track related static std::vector<pointer> getAllOrphans(Session& session); // No track related
static std::vector<pointer> getLastWritten(Session& session, static std::vector<pointer> getLastWritten(Session& session,
std::optional<Wt::WDateTime> after, std::optional<Wt::WDateTime> after,
const std::set<IdType>& clusters, const std::set<IdType>& clusters,
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
std::optional<Range>, std::optional<Range>,
bool& moreResults); bool& moreResults);
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {}); static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
static std::vector<pointer> getStarred(Session& session, static std::vector<pointer> getStarred(Session& session,
Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<User> user,
const std::set<IdType>& clusters, const std::set<IdType>& clusters,
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
SortMethod sortMethod, SortMethod sortMethod,
std::optional<Range>, bool& moreResults); std::optional<Range>, bool& moreResults);
@@ -100,10 +102,12 @@ class Artist : public Wt::Dbo::Dbo<Artist>
std::vector<Wt::Dbo::ptr<Release>> getReleases(const std::set<IdType>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters std::vector<Wt::Dbo::ptr<Release>> getReleases(const std::set<IdType>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
std::size_t getReleaseCount() const; std::size_t getReleaseCount() const;
std::vector<Wt::Dbo::ptr<Track>> getTracks(std::optional<TrackArtistLink::Type> linkType = {}) const; std::vector<Wt::Dbo::ptr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
std::vector<Wt::Dbo::ptr<Track>> getTracksWithRelease(std::optional<TrackArtistLink::Type> linkType = {}) const; std::vector<Wt::Dbo::ptr<Track>> getTracksWithRelease(std::optional<TrackArtistLinkType> linkType = {}) const;
std::vector<Wt::Dbo::ptr<Track>> getRandomTracks(std::optional<std::size_t> count) const; std::vector<Wt::Dbo::ptr<Track>> getRandomTracks(std::optional<std::size_t> count) const;
std::vector<pointer> getSimilarArtists(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
// No artistLinkTypes means get them all
std::vector<pointer> getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
// Get the cluster of the tracks made by this artist // Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence // Each clusters are grouped by cluster type, sorted by the number of occurence
@@ -20,11 +20,12 @@
#pragma once #pragma once
#include <optional> #include <optional>
#include <set>
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/WDateTime.h>
#include <Wt/Dbo/Dbo.h>
#include "utils/UUID.hpp" #include "utils/UUID.hpp"
#include "TrackArtistLink.hpp"
#include "Types.hpp" #include "Types.hpp"
namespace Database namespace Database
@@ -34,6 +35,7 @@ class Artist;
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Release; class Release;
class Session;
class Track; class Track;
class User; class User;
@@ -95,8 +97,8 @@ class Release : public Wt::Dbo::Dbo<Release>
Wt::WDateTime getLastWritten() const; Wt::WDateTime getLastWritten() const;
// Get the artists of this release // Get the artists of this release
std::vector<Wt::Dbo::ptr<Artist> > getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; std::vector<Wt::Dbo::ptr<Artist> > getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
std::vector<Wt::Dbo::ptr<Artist> > getReleaseArtists() const { return getArtists(TrackArtistLink::Type::ReleaseArtist); } std::vector<Wt::Dbo::ptr<Artist> > getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
bool hasVariousArtists() const; bool hasVariousArtists() const;
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const; std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
+8 -4
View File
@@ -26,12 +26,13 @@
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include <Wt/Dbo/Dbo.h>
#include <Wt/Dbo/WtSqlTraits.h>
#include "utils/EnumSet.hpp"
#include "utils/UUID.hpp" #include "utils/UUID.hpp"
#include "TrackArtistLink.hpp"
#include "Types.hpp" #include "Types.hpp"
namespace Database { namespace Database {
@@ -40,6 +41,8 @@ class Artist;
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Release; class Release;
class Session;
class TrackArtistLink;
class TrackFeatures; class TrackFeatures;
class TrackListEntry; class TrackListEntry;
class TrackStats; class TrackStats;
@@ -134,8 +137,9 @@ class Track : public Wt::Dbo::Dbo<Track>
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; } std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; } std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
std::vector<Wt::Dbo::ptr<Artist>> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; // no artistLinkTypes means get all
std::vector<IdType> getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; std::vector<Wt::Dbo::ptr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const;
std::vector<IdType> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const;
std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() const; std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() const;
Wt::Dbo::ptr<Release> getRelease() const { return _release; } Wt::Dbo::ptr<Release> getRelease() const { return _release; }
std::vector<Wt::Dbo::ptr<Cluster>> getClusters() const; std::vector<Wt::Dbo::ptr<Cluster>> getClusters() const;
@@ -19,44 +19,35 @@
#pragma once #pragma once
#include <string>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include "Types.hpp" #include "Types.hpp"
#include "utils/EnumSet.hpp"
namespace Database { namespace Database
class Artist;
class Session;
class Track;
class TrackArtistLink
{ {
public:
enum class Type
{
Artist, // regular artist
Arranger,
Composer,
Conductor,
Lyricist,
Mixer,
Performer,
Producer,
ReleaseArtist,
Remixer,
Writer,
};
class Artist;
class Session;
class Track;
class TrackArtistLink
{
public:
using pointer = Wt::Dbo::ptr<TrackArtistLink>; using pointer = Wt::Dbo::ptr<TrackArtistLink>;
TrackArtistLink() = default; 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<Track> getTrack() const { return _track; }
Wt::Dbo::ptr<Artist> getArtist() const { return _artist; } Wt::Dbo::ptr<Artist> getArtist() const { return _artist; }
Type getType() const { return _type; } TrackArtistLinkType getType() const { return _type; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
@@ -69,14 +60,12 @@ class TrackArtistLink
} }
private: private:
TrackArtistLinkType _type;
Type _type;
std::string _name; std::string _name;
Wt::Dbo::ptr<Track> _track; Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<Artist> _artist; Wt::Dbo::ptr<Artist> _artist;
}; };
} }
@@ -25,7 +25,6 @@
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include "TrackArtistLink.hpp"
#include "Types.hpp" #include "Types.hpp"
namespace Database { namespace Database {
@@ -53,7 +52,7 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user); TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
// Stats utility // Stats utility
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const; std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
std::vector<Wt::Dbo::ptr<Release>> getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const; std::vector<Wt::Dbo::ptr<Release>> getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const; std::vector<Wt::Dbo::ptr<Track>> getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
@@ -85,7 +84,7 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const; std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntriesReverse(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const; std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntriesReverse(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
std::vector<Wt::Dbo::ptr<Artist>> getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const; std::vector<Wt::Dbo::ptr<Artist>> getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
std::vector<Wt::Dbo::ptr<Release>> getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const; std::vector<Wt::Dbo::ptr<Release>> getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
std::vector<Wt::Dbo::ptr<Track>> getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const; std::vector<Wt::Dbo::ptr<Track>> getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
@@ -35,5 +35,21 @@ namespace Database
std::size_t offset {}; std::size_t offset {};
std::size_t limit {}; std::size_t limit {};
}; };
enum class TrackArtistLinkType
{
Artist, // regular artist
Arranger,
Composer,
Conductor,
Lyricist,
Mixer,
Performer,
Producer,
ReleaseArtist,
Remixer,
Writer,
};
} }
+19 -47
View File
@@ -42,13 +42,13 @@ namespace MetaData
template<typename T> template<typename T>
std::vector<T> std::vector<T>
getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::set<std::string>& keys) getPropertyValuesFirstMatchAs(const TagLib::PropertyMap& properties, const std::vector<std::string_view>& keys)
{ {
std::vector<T> res; std::vector<T> res;
for (const std::string& key : keys) for (std::string_view key : keys)
{ {
const TagLib::StringList& values {properties[key]}; const TagLib::StringList& values {properties[std::string {key}]};
if (values.isEmpty()) if (values.isEmpty())
continue; continue;
@@ -91,12 +91,13 @@ splitAndTrimString(const std::string& str, const std::string& delimiters)
static static
std::vector<Artist> std::vector<Artist>
getArtists(const TagLib::PropertyMap& properties) getArtists(const TagLib::PropertyMap& properties,
const std::vector<std::string_view>& artistTagNames,
const std::vector<std::string_view>& artistSortTagNames,
const std::vector<std::string_view>& artistMBIDTagNames
)
{ {
std::vector<std::string> artistNames {getPropertyValuesAs<std::string>(properties, "ARTISTS")}; const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistTagNames)};
if (artistNames.empty())
artistNames = getPropertyValuesAs<std::string>(properties, "ARTIST");
if (artistNames.empty()) if (artistNames.empty())
return {}; return {};
@@ -106,7 +107,7 @@ getArtists(const TagLib::PropertyMap& properties)
[&](const std::string& name) { return Artist {name}; }); [&](const std::string& name) { return Artist {name}; });
{ {
const std::vector<std::string> artistSortNames {getPropertyValuesAs<std::string>(properties, "ARTISTSORT")}; const std::vector<std::string> artistSortNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistSortTagNames)};
if (artistSortNames.size() == artists.size()) if (artistSortNames.size() == artists.size())
{ {
for (std::size_t i {}; i < artistSortNames.size(); ++i) for (std::size_t i {}; i < artistSortNames.size(); ++i)
@@ -115,7 +116,7 @@ getArtists(const TagLib::PropertyMap& properties)
} }
{ {
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"})}; const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(properties, artistMBIDTagNames)};
if (artistNames.size() == artistsMBID.size()) if (artistNames.size() == artistsMBID.size())
{ {
@@ -128,41 +129,6 @@ getArtists(const TagLib::PropertyMap& properties)
return artists; return artists;
} }
static
std::vector<Artist>
getAlbumArtists(const TagLib::PropertyMap& properties)
{
std::vector<std::string> artistNames {getPropertyValuesAs<std::string>(properties, "ALBUMARTIST")};
if (artistNames.empty())
return {};
std::vector<Artist> artists;
artists.reserve(artistNames.size());
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists),
[&](const std::string& name) { return Artist {name}; });
{
const std::vector<std::string> artistSortNames {getPropertyValuesAs<std::string>(properties, "ALBUMARTISTSORT")};
if (artistSortNames.size() == artists.size())
{
for (std::size_t i {}; i < artistSortNames.size(); ++i)
artists[i].sortName = artistSortNames[i];
}
}
{
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"})};
if (artistsMBID.size() == artists.size())
{
for (std::size_t i {}; i < artistsMBID.size(); ++i)
artists[i].musicBrainzArtistID = artistsMBID[i];
}
}
return artists;
}
static static
std::optional<Album> std::optional<Album>
getAlbum(const TagLib::PropertyMap& properties) getAlbum(const TagLib::PropertyMap& properties)
@@ -430,9 +396,15 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
processTag(track, tag, values, debug); processTag(track, tag, values, debug);
} }
track.artists = getArtists(properties);
track.albumArtists = getAlbumArtists(properties);
track.album = getAlbum(properties); track.album = getAlbum(properties);
track.artists = getArtists(properties, {"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; return track;
} }
@@ -78,6 +78,12 @@ namespace MetaData
std::optional<float> trackReplayGain; std::optional<float> trackReplayGain;
std::optional<float> albumReplayGain; std::optional<float> albumReplayGain;
std::string discSubtitle; std::string discSubtitle;
std::vector<Artist> conductorArtists;
std::vector<Artist> composerArtists;
std::vector<Artist> lyricistArtists;
std::vector<Artist> mixerArtists;
std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists;
}; };
class IParser class IParser
+5 -2
View File
@@ -132,7 +132,10 @@ Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releas
} }
std::unordered_set<Database::IdType> std::unordered_set<Database::IdType>
Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount) Engine::getSimilarArtists(Database::Session& dbSession,
Database::IdType artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount)
{ {
std::unordered_set<Database::IdType> res; std::unordered_set<Database::IdType> res;
@@ -144,7 +147,7 @@ Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistI
continue; continue;
const IClassifier& classifier {*itClassifier->second}; const IClassifier& classifier {*itClassifier->second};
res = classifier.getSimilarArtists(dbSession, artistId, maxCount); res = classifier.getSimilarArtists(dbSession, artistId, linkTypes, maxCount);
if (!res.empty()) if (!res.empty())
{ {
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'"; LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'";
+7 -4
View File
@@ -56,10 +56,13 @@ namespace Recommendation
void load(bool forceReload, const ProgressCallback& progressCallback) override; void load(bool forceReload, const ProgressCallback& progressCallback) override;
void cancelLoad() override; void cancelLoad() override;
std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override; ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override;
std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) override; ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) override;
std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override; ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override;
std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) override; ResultContainer getSimilarArtists(Database::Session& session,
Database::IdType artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) override;
void setClassifierPriorities(const std::vector<ClassifierType>& classifierTypes); void setClassifierPriorities(const std::vector<ClassifierType>& classifierTypes);
void clearClassifiers(); void clearClassifiers();
+9 -4
View File
@@ -24,6 +24,7 @@
#include <unordered_set> #include <unordered_set>
#include "database/Types.hpp" #include "database/Types.hpp"
#include "utils/EnumSet.hpp"
namespace Database namespace Database
{ {
@@ -49,10 +50,14 @@ namespace Recommendation
virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0; virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0;
virtual void requestCancelLoad() = 0; virtual void requestCancelLoad() = 0;
virtual std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0; using ResultContainer = std::unordered_set<Database::IdType>;
virtual std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const = 0;
virtual std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0; virtual ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0;
virtual std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const = 0; virtual ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const = 0;
virtual ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0;
virtual ResultContainer getSimilarArtists(Database::Session& session,
Database::IdType artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
}; };
} // ns Recommendation } // ns Recommendation
@@ -83,7 +83,10 @@ ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::Id
} }
std::unordered_set<Database::IdType> std::unordered_set<Database::IdType>
ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount) const ClusterClassifier::getSimilarArtists(Database::Session& dbSession,
Database::IdType artistId,
EnumSet<Database::TrackArtistLinkType> artistLinkTypes,
std::size_t maxCount) const
{ {
std::unordered_set<Database::IdType> res; std::unordered_set<Database::IdType> res;
@@ -93,7 +96,7 @@ ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdT
if (!artist) if (!artist)
return res; return res;
const auto artists {artist->getSimilarArtists(0, maxCount)}; const auto artists {artist->getSimilarArtists(artistLinkTypes, Database::Range {0, maxCount})};
std::transform(std::cbegin(artists), std::cend(artists), std::inserter(res, std::end(res)), std::transform(std::cbegin(artists), std::cend(artists), std::inserter(res, std::end(res)),
[](const auto& artist) { return artist.id(); }); [](const auto& artist) { return artist.id(); });
@@ -40,10 +40,13 @@ namespace Recommendation
bool load(Database::Session&, bool, const ProgressCallback&) override { return true; } bool load(Database::Session&, bool, const ProgressCallback&) override { return true; }
void requestCancelLoad() override {} void requestCancelLoad() override {}
std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override;
std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const override; ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const override;
std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override; ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override;
std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override; ResultContainer getSimilarArtists(Database::Session& session,
Database::IdType artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) const override;
}; };
} // namespace Recommendation } // namespace Recommendation
@@ -25,6 +25,7 @@
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackFeatures.hpp" #include "database/TrackFeatures.hpp"
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "som/DataNormalizer.hpp" #include "som/DataNormalizer.hpp"
@@ -308,9 +309,34 @@ FeaturesClassifier::getSimilarReleases(Database::Session& session, Database::IdT
} }
std::unordered_set<Database::IdType> std::unordered_set<Database::IdType>
FeaturesClassifier::getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const FeaturesClassifier::getSimilarArtists(Database::Session& session,
Database::IdType artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) const
{ {
auto similarArtistIds {getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount)}; auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType)
{
std::unordered_set<Database::IdType> similarArtistIds;
const auto itArtists {_artistsMap.find(linkType)};
if (itArtists == std::cend(_artistsMap))
{
return similarArtistIds;
}
similarArtistIds = getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
return similarArtistIds;
}};
std::unordered_set<Database::IdType> similarArtistIds;
for (Database::TrackArtistLinkType linkType : linkTypes)
{
const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)};
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
}
if (!similarArtistIds.empty()) if (!similarArtistIds.empty())
{ {
// Report only existing ids // Report only existing ids
@@ -319,13 +345,16 @@ FeaturesClassifier::getSimilarArtists(Database::Session& session, Database::IdTy
for (auto it {std::begin(similarArtistIds)}; it != std::end(similarArtistIds);) for (auto it {std::begin(similarArtistIds)}; it != std::end(similarArtistIds);)
{ {
const Database::IdType similarArtistId {*it}; const Database::IdType similarArtistId {*it};
if (!Database::Release::getById(session, similarArtistId)) if (!Database::Artist::getById(session, similarArtistId))
it = similarArtistIds.erase(it); it = similarArtistIds.erase(it);
else else
it++; it++;
} }
} }
while (similarArtistIds.size() > maxCount)
similarArtistIds.erase(Random::pickRandom(similarArtistIds));
return similarArtistIds; return similarArtistIds;
} }
@@ -339,6 +368,7 @@ bool
FeaturesClassifier::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) FeaturesClassifier::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback)
{ {
if (forceReload) if (forceReload)
{ {
FeaturesClassifierCache::invalidate(); FeaturesClassifierCache::invalidate();
} }
@@ -377,7 +407,6 @@ FeaturesClassifier::load(Database::Session& session,
const SOM::Coordinate width {network.getWidth()}; const SOM::Coordinate width {network.getWidth()};
const SOM::Coordinate height {network.getHeight()}; const SOM::Coordinate height {network.getHeight()};
_artistsMap = MatrixOfObjects {width, height};
_releasesMap = MatrixOfObjects {width, height}; _releasesMap = MatrixOfObjects {width, height};
_tracksMap = MatrixOfObjects {width, height}; _tracksMap = MatrixOfObjects {width, height};
@@ -407,10 +436,18 @@ FeaturesClassifier::load(Database::Session& session,
_releasePositions[track->getRelease().id()].insert(position); _releasePositions[track->getRelease().id()].insert(position);
_releasesMap[position].insert(track->getRelease().id()); _releasesMap[position].insert(track->getRelease().id());
} }
for (const auto& artist : track->getArtists()) for (const auto& artistLink : track->getArtistLinks())
{ {
_artistPositions[artist.id()].insert(position); _artistPositions[artistLink->getArtist().id()].insert(position);
_artistsMap[position].insert(artist.id()); auto itArtists {_artistsMap.find(artistLink->getType())};
if (itArtists == std::cend(_artistsMap))
{
auto [it, inserted] = _artistsMap.try_emplace(artistLink->getType(), MatrixOfObjects {});
assert(inserted);
itArtists = it;
itArtists->second = MatrixOfObjects {width, height};
}
itArtists->second[position].insert(artistLink->getArtist().id());
} }
} }
} }
@@ -65,7 +65,10 @@ class FeaturesClassifier : public IClassifier
std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override;
std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const override; std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const override;
std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override; std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override;
std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override; std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session,
Database::IdType artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) const override;
bool loadFromCache(Database::Session& session, const FeaturesClassifierCache& cache); bool loadFromCache(Database::Session& session, const FeaturesClassifierCache& cache);
@@ -99,8 +102,8 @@ class FeaturesClassifier : public IClassifier
std::unique_ptr<SOM::Network> _network; std::unique_ptr<SOM::Network> _network;
double _networkRefVectorsDistanceMedian {}; double _networkRefVectorsDistanceMedian {};
MatrixOfObjects _artistsMap;
ObjectPositions _artistPositions; ObjectPositions _artistPositions;
std::unordered_map<Database::TrackArtistLinkType, MatrixOfObjects> _artistsMap;
MatrixOfObjects _releasesMap; MatrixOfObjects _releasesMap;
ObjectPositions _releasePositions; ObjectPositions _releasePositions;
@@ -20,9 +20,11 @@
#pragma once #pragma once
#include <functional> #include <functional>
#include <optional>
#include <unordered_set> #include <unordered_set>
#include "database/Types.hpp" #include "database/Types.hpp"
#include "utils/EnumSet.hpp"
namespace Database namespace Database
{ {
@@ -46,10 +48,15 @@ namespace Recommendation
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
virtual void cancelLoad() = 0; virtual void cancelLoad() = 0;
virtual std::unordered_set<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0; using ResultContainer = std::unordered_set<Database::IdType>;
virtual std::unordered_set<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) = 0;
virtual std::unordered_set<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0; virtual ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0;
virtual std::unordered_set<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) = 0; virtual ResultContainer getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) = 0;
virtual ResultContainer getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0;
virtual ResultContainer getSimilarArtists(Database::Session& session,
Database::IdType artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) = 0;
}; };
std::unique_ptr<IEngine> createEngine(Database::Db& db); std::unique_ptr<IEngine> createEngine(Database::Db& db);
+26 -20
View File
@@ -29,6 +29,7 @@
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/ScanSettings.hpp" #include "database/ScanSettings.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackFeatures.hpp" #include "database/TrackFeatures.hpp"
#include "metadata/TagLibParser.hpp" #include "metadata/TagLibParser.hpp"
#include "recommendation/IEngine.hpp" #include "recommendation/IEngine.hpp"
@@ -750,20 +751,6 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
title = file.filename().string(); title = file.filename().string();
} }
// ***** Clusters
std::vector<Cluster::pointer> clusters {getOrCreateClusters(_dbSession, trackInfo->clusters)};
// ***** Artists
std::vector<Artist::pointer> artists {getOrCreateArtists(_dbSession, trackInfo->artists)};
// ***** Release artists
std::vector<Artist::pointer> releaseArtists {getOrCreateArtists(_dbSession, trackInfo->albumArtists)};
// ***** Release
Release::pointer release;
if (trackInfo->album)
release = getOrCreateRelease(_dbSession, *trackInfo->album);
// If file already exist, update data // If file already exist, update data
// Otherwise, create it // Otherwise, create it
if (!track) if (!track)
@@ -784,15 +771,34 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
assert(track); assert(track);
track.modify()->clearArtistLinks(); track.modify()->clearArtistLinks();
for (const auto& artist : artists) for (const Artist::pointer& artist : getOrCreateArtists(_dbSession, trackInfo->artists))
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLink::Type::Artist)); track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLinkType::Artist));
for (const auto& releaseArtist : releaseArtists) for (const Artist::pointer& releaseArtist : getOrCreateArtists(_dbSession, trackInfo->albumArtists))
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLink::Type::ReleaseArtist)); track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLinkType::ReleaseArtist));
for (const Artist::pointer& conductor : getOrCreateArtists(_dbSession, trackInfo->conductorArtists))
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, conductor, Database::TrackArtistLinkType::Conductor));
for (const Artist::pointer& composer : getOrCreateArtists(_dbSession, trackInfo->composerArtists))
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, composer, Database::TrackArtistLinkType::Composer));
for (const Artist::pointer& lyricist : getOrCreateArtists(_dbSession, trackInfo->lyricistArtists))
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, lyricist, Database::TrackArtistLinkType::Lyricist));
for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists))
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, mixer, Database::TrackArtistLinkType::Mixer));
for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists))
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, producer, Database::TrackArtistLinkType::Producer));
for (const Artist::pointer& remixer : getOrCreateArtists(_dbSession, trackInfo->remixerArtists))
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, remixer, Database::TrackArtistLinkType::Remixer));
track.modify()->setScanVersion(_scanVersion); track.modify()->setScanVersion(_scanVersion);
track.modify()->setRelease(release); if (trackInfo->album)
track.modify()->setClusters(clusters); track.modify()->setRelease(getOrCreateRelease(_dbSession, *trackInfo->album));
track.modify()->setClusters(getOrCreateClusters(_dbSession, trackInfo->clusters));
track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setLastWriteTime(lastWriteTime);
track.modify()->setName(title); track.modify()->setName(title);
track.modify()->setDuration(trackInfo->duration); track.modify()->setDuration(trackInfo->duration);
+20 -14
View File
@@ -316,7 +316,7 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User:
trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()})); trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()}));
auto artists {track->getArtists()}; auto artists {track->getArtists({TrackArtistLinkType::Artist})};
if (!artists.empty()) if (!artists.empty())
{ {
trackResponse.setAttribute("artist", getArtistNames(artists)); trackResponse.setAttribute("artist", getArtistNames(artists));
@@ -893,7 +893,10 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString()); artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
} }
auto similarArtistsId {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession, id.value, count)}; auto similarArtistsId {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession,
id.value,
{TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist},
count)};
{ {
auto transaction {context.dbSession.createSharedTransaction()}; auto transaction {context.dbSession.createSharedTransaction()};
@@ -943,13 +946,13 @@ handleGetArtistsRequest(RequestContext& context)
if (!user) if (!user)
throw UserNotAuthorizedError {}; throw UserNotAuthorizedError {};
std::optional<TrackArtistLink::Type> linkType; std::optional<TrackArtistLinkType> linkType;
switch (user->getSubsonicArtistListMode()) switch (user->getSubsonicArtistListMode())
{ {
case User::SubsonicArtistListMode::AllArtists: case User::SubsonicArtistListMode::AllArtists:
break; break;
case User::SubsonicArtistListMode::ReleaseArtists: case User::SubsonicArtistListMode::ReleaseArtists:
linkType = TrackArtistLink::Type::ReleaseArtist; linkType = TrackArtistLinkType::ReleaseArtist;
break; break;
} }
@@ -1087,13 +1090,13 @@ handleGetIndexesRequest(RequestContext& context)
if (!user) if (!user)
throw UserNotAuthorizedError {}; throw UserNotAuthorizedError {};
std::optional<TrackArtistLink::Type> linkType; std::optional<TrackArtistLinkType> linkType;
switch (user->getSubsonicArtistListMode()) switch (user->getSubsonicArtistListMode())
{ {
case User::SubsonicArtistListMode::AllArtists: case User::SubsonicArtistListMode::AllArtists:
break; break;
case User::SubsonicArtistListMode::ReleaseArtists: case User::SubsonicArtistListMode::ReleaseArtists:
linkType = TrackArtistLink::Type::ReleaseArtist; linkType = TrackArtistLinkType::ReleaseArtist;
break; break;
} }
@@ -1115,36 +1118,39 @@ Response
handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
{ {
// Mandatory params // Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")}; const Id artistId {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Artist) if (artistId.type != Id::Type::Artist)
throw BadParameterGenericError {"id"}; throw BadParameterGenericError {"id"};
// Optional params // Optional params
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)}; std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)};
auto similarArtistsId {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession, id.value, 5)}; auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession,
artistId.value,
{TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist},
5)};
auto transaction {context.dbSession.createSharedTransaction()}; auto transaction {context.dbSession.createSharedTransaction()};
Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; const Artist::pointer artist {Artist::getById(context.dbSession, artistId.value)};
if (!artist) if (!artist)
throw RequestedDataNotFoundError {}; throw RequestedDataNotFoundError {};
User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; const User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
if (!user) if (!user)
throw UserNotAuthorizedError {}; throw UserNotAuthorizedError {};
// "Returns a random collection of songs from the given artist and similar artists" // "Returns a random collection of songs from the given artist and similar artists"
auto tracks {artist->getRandomTracks(count / 2)}; auto tracks {artist->getRandomTracks(count / 2)};
for ( const auto& similarArtistId : similarArtistsId ) for (const Database::IdType similarArtistId : similarArtistIds)
{ {
Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; const Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)};
if (!similarArtist) if (!similarArtist)
continue; continue;
auto similarArtistTracks {similarArtist->getRandomTracks((count / 2) / 5)}; auto similarArtistTracks {similarArtist->getRandomTracks((count / 2) / 5)};
tracks.insert(tracks.end(), tracks.insert(std::end(tracks),
std::make_move_iterator(std::begin(similarArtistTracks)), std::make_move_iterator(std::begin(similarArtistTracks)),
std::make_move_iterator(std::end(similarArtistTracks))); std::make_move_iterator(std::end(similarArtistTracks)));
} }
+157
View File
@@ -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 <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
@@ -30,6 +30,7 @@
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "database/Types.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
@@ -242,7 +243,7 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain)
const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)}; const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)};
const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)}; const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)};
const auto artists {track->getArtists()}; const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
oss oss
<< "var params = {" << "var params = {"
+1 -1
View File
@@ -419,7 +419,7 @@ PlayQueue::addSome()
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
const auto artists {track->getArtists()}; const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
const auto release {track->getRelease()}; const auto release {track->getRelease()};
if (!artists.empty() || release) if (!artists.empty() || release)
+4 -1
View File
@@ -75,7 +75,10 @@ Artist::refreshView()
if (!artistId) if (!artistId)
throw ArtistNotFoundException {*artistId}; throw ArtistNotFoundException {*artistId};
const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)}; const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(),
*artistId,
{TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist},
5)};
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+27 -5
View File
@@ -27,6 +27,7 @@
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -39,7 +40,7 @@ using namespace Database;
namespace UserInterface { namespace UserInterface {
using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLink::Type>>; using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLinkType>>;
Artists::Artists(Filters* filters) Artists::Artists(Filters* filters)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}, : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")},
@@ -68,11 +69,32 @@ Artists::Artists(Filters* filters)
} }
_linkType = bindNew<Wt::WComboBox>("link-type"); _linkType = bindNew<Wt::WComboBox>("link-type");
{ {
auto linkTypeModel {std::make_shared<ArtistLinkModel>()}; auto linkTypeModel {std::make_shared<ArtistLinkModel>()};
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->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), {}); linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), {});
linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-artist"), TrackArtistLink::Type::Artist); addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist");
linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-releaseartist"), TrackArtistLink::Type::ReleaseArtist); 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->setModel(linkTypeModel);
} }
_linkType->changed().connect([this] { refreshView(); }); _linkType->changed().connect([this] { refreshView(); });
@@ -125,7 +147,7 @@ Artists::getRandomArtists(std::optional<Range> range, bool& moreResults)
{ {
std::vector<Artist::pointer> artists; std::vector<Artist::pointer> artists;
const std::optional<TrackArtistLink::Type> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())}; const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
if (_randomArtists.empty()) if (_randomArtists.empty())
_randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), linkType, maxItemsPerMode[Mode::Random]); _randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), linkType, maxItemsPerMode[Mode::Random]);
@@ -152,7 +174,7 @@ Artists::getArtists(std::optional<Range> range, bool& moreResults)
{ {
std::vector<Artist::pointer> artists; std::vector<Artist::pointer> artists;
const std::optional<TrackArtistLink::Type> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())}; const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
const std::optional<std::size_t> modeLimit{maxItemsPerMode[_mode]}; const std::optional<std::size_t> modeLimit{maxItemsPerMode[_mode]};
if (modeLimit) if (modeLimit)
+1 -1
View File
@@ -214,7 +214,7 @@ Release::refreshView()
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
auto artists {track->getArtists()}; const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
if (variousArtists && !artists.empty()) if (variousArtists && !artists.empty())
{ {
entry->setCondition("if-has-artists", true); entry->setCondition("if-has-artists", true);
+1 -1
View File
@@ -47,7 +47,7 @@ namespace UserInterface::TrackListHelpers
Wt::WText* name {entry->bindNew<Wt::WText>("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain)}; Wt::WText* name {entry->bindNew<Wt::WText>("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain)};
name->setToolTip(Wt::WString::fromUTF8(track->getName())); name->setToolTip(Wt::WString::fromUTF8(track->getName()));
const auto artists {track->getArtists()}; const auto artists {track->getArtists({TrackArtistLinkType::Artist})};
const Release::pointer release {track->getRelease()}; const Release::pointer release {track->getRelease()};
const IdType trackId {track.id()}; const IdType trackId {track.id()};
+70 -45
View File
@@ -28,6 +28,7 @@
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackBookmark.hpp" #include "database/TrackBookmark.hpp"
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "database/User.hpp" #include "database/User.hpp"
@@ -265,7 +266,7 @@ testSingleTrackSingleArtist(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -276,7 +277,7 @@ testSingleTrackSingleArtist(Session& session)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto artists {track->getArtists()}; auto artists {track->getArtists({TrackArtistLinkType::Artist})};
CHECK(artists.size() == 1); CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist.getId()); CHECK(artists.front().id() == artist.getId());
@@ -287,8 +288,9 @@ testSingleTrackSingleArtist(Session& session)
CHECK(artistLink->getTrack().id() == track.getId()); CHECK(artistLink->getTrack().id() == track.getId());
CHECK(artistLink->getArtist().id() == artist.getId()); CHECK(artistLink->getArtist().id() == artist.getId());
CHECK(track->getArtists(TrackArtistLink::Type::Artist).size() == 1); CHECK(track->getArtists({TrackArtistLinkType::Artist}).size() == 1);
CHECK(track->getArtists(TrackArtistLink::Type::ReleaseArtist).empty()); CHECK(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
CHECK(track->getArtists({}).empty());
} }
{ {
@@ -298,8 +300,8 @@ testSingleTrackSingleArtist(Session& session)
CHECK(tracks.size() == 1); CHECK(tracks.size() == 1);
CHECK(tracks.front().id() == track.getId()); CHECK(tracks.front().id() == track.getId());
CHECK(artist->getTracks(TrackArtistLink::Type::ReleaseArtist).empty()); CHECK(artist->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
CHECK(artist->getTracks(TrackArtistLink::Type::Artist).size() == 1); CHECK(artist->getTracks(TrackArtistLinkType::Artist).size() == 1);
} }
} }
@@ -312,9 +314,9 @@ testSingleTrackSingleArtistMultiRoles(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::ReleaseArtist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Writer); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer);
} }
{ {
@@ -326,29 +328,29 @@ testSingleTrackSingleArtistMultiRoles(Session& session)
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
bool hasMore{}; bool hasMore{};
CHECK(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); CHECK(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Artist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Artist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::ReleaseArtist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::ReleaseArtist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Writer, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1); CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Writer, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty()); CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto artists {track->getArtists(TrackArtistLink::Type::Artist)}; auto artists {track->getArtists({TrackArtistLinkType::Artist})};
CHECK(artists.size() == 1); CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist.getId()); CHECK(artists.front().id() == artist.getId());
artists = track->getArtists(TrackArtistLink::Type::ReleaseArtist); artists = track->getArtists({TrackArtistLinkType::ReleaseArtist});
CHECK(artists.size() == 1); CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist.getId()); CHECK(artists.front().id() == artist.getId());
CHECK(track->getArtistLinks().size() == 3); CHECK(track->getArtistLinks().size() == 3);
CHECK(artist->getTracks().size() == 1); CHECK(artist->getTracks().size() == 1);
CHECK(artist->getTracks(TrackArtistLink::Type::ReleaseArtist).size() == 1); CHECK(artist->getTracks({TrackArtistLinkType::ReleaseArtist}).size() == 1);
CHECK(artist->getTracks(TrackArtistLink::Type::Artist).size() == 1); CHECK(artist->getTracks({TrackArtistLinkType::Artist}).size() == 1);
CHECK(artist->getTracks(TrackArtistLink::Type::Writer).size() == 1); CHECK(artist->getTracks({TrackArtistLinkType::Writer}).size() == 1);
} }
} }
@@ -364,8 +366,8 @@ testSingleTrackMultiArtists(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -376,13 +378,14 @@ testSingleTrackMultiArtists(Session& session)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto artists {track->getArtists()}; auto artists {track->getArtists({TrackArtistLinkType::Artist})};
CHECK(artists.size() == 2); CHECK(artists.size() == 2);
CHECK((artists[0].id() == artist1.getId() && artists[1].id() == artist2.getId()) CHECK((artists[0].id() == artist1.getId() && artists[1].id() == artist2.getId())
|| (artists[0].id() == artist2.getId() && artists[1].id() == artist1.getId())); || (artists[0].id() == artist2.getId() && artists[1].id() == artist1.getId()));
CHECK(track->getArtists(TrackArtistLink::Type::Artist).size() == 2); CHECK(track->getArtists({}).size() == 2);
CHECK(track->getArtists(TrackArtistLink::Type::ReleaseArtist).empty()); CHECK(track->getArtists({TrackArtistLinkType::Artist}).size() == 2);
CHECK(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
CHECK(Artist::getAll(session, Artist::SortMethod::ByName).size() == 2); CHECK(Artist::getAll(session, Artist::SortMethod::ByName).size() == 2);
CHECK(Artist::getAllIds(session).size() == 2); CHECK(Artist::getAllIds(session).size() == 2);
} }
@@ -393,10 +396,10 @@ testSingleTrackMultiArtists(Session& session)
CHECK(artist1->getTracks().front() == track.get()); CHECK(artist1->getTracks().front() == track.get());
CHECK(artist2->getTracks().front() == track.get()); CHECK(artist2->getTracks().front() == track.get());
CHECK(artist1->getTracks(TrackArtistLink::Type::ReleaseArtist).empty()); CHECK(artist1->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
CHECK(artist1->getTracks(TrackArtistLink::Type::Artist).size() == 1); CHECK(artist1->getTracks(TrackArtistLinkType::Artist).size() == 1);
CHECK(artist2->getTracks(TrackArtistLink::Type::ReleaseArtist).empty()); CHECK(artist2->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
CHECK(artist2->getTracks(TrackArtistLink::Type::Artist).size() == 1); CHECK(artist2->getTracks(TrackArtistLinkType::Artist).size() == 1);
} }
} }
@@ -410,7 +413,7 @@ testSingleArtistSearchByName(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
artist.get().modify()->setSortName("ZZZ"); artist.get().modify()->setSortName("ZZZ");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -989,7 +992,7 @@ testSingleTrackSingleArtistMultiClusters(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist)}; auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)};
cluster1.get().modify()->addTrack(track.get()); cluster1.get().modify()->addTrack(track.get());
} }
@@ -1051,8 +1054,8 @@ testSingleTrackSingleArtistMultiRolesMultiClusters(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::ReleaseArtist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
cluster.get().modify()->addTrack(track.get()); cluster.get().modify()->addTrack(track.get());
} }
@@ -1092,7 +1095,7 @@ testMultiTracksSingleArtistMultiClusters(Session& session)
tracks.emplace_back(session, "MyTrackFile" + std::to_string(i)); tracks.emplace_back(session, "MyTrackFile" + std::to_string(i));
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
for (auto& cluster : clusters) for (auto& cluster : clusters)
cluster.get().modify()->addTrack(tracks.back().get()); cluster.get().modify()->addTrack(tracks.back().get());
@@ -1131,7 +1134,7 @@ testMultiTracksSingleArtistSingleRelease(Session& session)
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
tracks.back().get().modify()->setRelease(release.get()); tracks.back().get().modify()->setRelease(release.get());
} }
@@ -1164,7 +1167,7 @@ testSingleTrackSingleReleaseSingleArtist(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist)}; auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)};
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
} }
@@ -1201,7 +1204,7 @@ testSingleTrackSingleReleaseSingleArtistSingleCluster(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
cluster.get().modify()->addTrack(track.get()); cluster.get().modify()->addTrack(track.get());
} }
@@ -1253,7 +1256,7 @@ testSingleTrackSingleReleaseSingleArtistMultiClusters(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist)}; auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)};
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
cluster1.get().modify()->addTrack(track.get()); cluster1.get().modify()->addTrack(track.get());
cluster2.get().modify()->addTrack(track.get()); cluster2.get().modify()->addTrack(track.get());
@@ -1301,7 +1304,7 @@ testSingleStarredArtist(Session& session)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist)}; auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)};
user.get().modify()->starArtist(artist.get()); user.get().modify()->starArtist(artist.get());
} }
@@ -1526,8 +1529,8 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session)
track1.get().modify()->setRelease(release1.get()); track1.get().modify()->setRelease(release1.get());
track2.get().modify()->setRelease(release2.get()); track2.get().modify()->setRelease(release2.get());
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
cluster1.get().modify()->addTrack(track1.get()); cluster1.get().modify()->addTrack(track1.get());
cluster2.get().modify()->addTrack(track2.get()); cluster2.get().modify()->addTrack(track2.get());
@@ -1605,7 +1608,7 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session)
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
bool moreResults {}; bool moreResults {};
const auto artists {trackList->getArtistsReverse({cluster1.getId()}, TrackArtistLink::Type::Artist, std::nullopt, moreResults)}; const auto artists {trackList->getArtistsReverse({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults)};
CHECK(artists.size() == 1); CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist1.getId()); CHECK(artists.front().id() == artist1.getId());
} }
@@ -1614,7 +1617,7 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session)
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
bool moreResults {}; bool moreResults {};
const auto artists {trackList->getArtistsReverse({}, TrackArtistLink::Type::Artist, std::nullopt, moreResults)}; const auto artists {trackList->getArtistsReverse({}, TrackArtistLinkType::Artist, std::nullopt, moreResults)};
CHECK(artists.size() == 1); CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist1.getId()); CHECK(artists.front().id() == artist1.getId());
} }
@@ -1627,7 +1630,7 @@ testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session)
CHECK(trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults).empty()); CHECK(trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults).empty());
CHECK(trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults).empty()); CHECK(trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults).empty());
CHECK(trackList->getArtistsReverse({}, TrackArtistLink::Type::ReleaseArtist, std::nullopt, moreResults).empty()); CHECK(trackList->getArtistsReverse({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty());
} }
{ {
@@ -1785,10 +1788,10 @@ testMultipleTracksMultipleArtistsMultiClusters(Session& session)
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
if (i < 5) if (i < 5)
TrackArtistLink::create(session, tracks.back().get(), artist1.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, tracks.back().get(), artist1.get(), TrackArtistLinkType::Artist);
else else
{ {
TrackArtistLink::create(session, tracks.back().get(), artist2.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, tracks.back().get(), artist2.get(), TrackArtistLinkType::Artist);
cluster2.get().modify()->addTrack(tracks.back().get()); cluster2.get().modify()->addTrack(tracks.back().get());
} }
@@ -1798,7 +1801,7 @@ testMultipleTracksMultipleArtistsMultiClusters(Session& session)
tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size())); tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size()));
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLink::Type::Artist); TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist);
cluster2.get().modify()->addTrack(tracks.back().get()); cluster2.get().modify()->addTrack(tracks.back().get());
} }
@@ -1811,6 +1814,28 @@ testMultipleTracksMultipleArtistsMultiClusters(Session& session)
CHECK(artists.front().id() == artist2.getId()); CHECK(artists.front().id() == artist2.getId());
} }
{
auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist})};
CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist2.getId());
}
{
auto artists {artist1->getSimilarArtists({TrackArtistLinkType::ReleaseArtist})};
CHECK(artists.empty() == 1);
}
{
auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist})};
CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist2.getId());
}
{
auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Composer})};
CHECK(artists.empty());
}
{ {
auto artists {artist2->getSimilarArtists()}; auto artists {artist2->getSimilarArtists()};
CHECK(artists.size() == 2); CHECK(artists.size() == 2);
+19 -1
View File
@@ -76,9 +76,27 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
for (const Artist& artist : track->artists) for (const Artist& artist : track->artists)
std::cout << "Artist: " << artist << std::endl; std::cout << "Artist: " << artist << std::endl;
for (const Artist& artist: track->albumArtists) for (const Artist& artist : track->albumArtists)
std::cout << "Album artist: " << artist << std::endl; std::cout << "Album artist: " << artist << std::endl;
for (const Artist& artist : track->conductorArtists)
std::cout << "Conductor: " << artist << std::endl;
for (const Artist& artist : track->composerArtists)
std::cout << "Composer: " << artist << std::endl;
for (const Artist& artist : track->lyricistArtists)
std::cout << "Lyricist: " << artist << std::endl;
for (const Artist& artist : track->mixerArtists)
std::cout << "Mixer: " << artist << std::endl;
for (const Artist& artist : track->producerArtists)
std::cout << "Producer: " << artist << std::endl;
for (const Artist& artist : track->remixerArtists)
std::cout << "Remixer: " << artist << std::endl;
if (track->album) if (track->album)
std::cout << "Album: " << *track->album << std::endl; std::cout << "Album: " << *track->album << std::endl;
+16 -11
View File
@@ -35,10 +35,9 @@
#include "utils/StreamLogger.hpp" #include "utils/StreamLogger.hpp"
#include "recommendation/IEngine.hpp" #include "recommendation/IEngine.hpp"
static static
void void
dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& engine) dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount)
{ {
const std::vector<Database::IdType> trackIds {[&]() const std::vector<Database::IdType> trackIds {[&]()
{ {
@@ -58,7 +57,7 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng
res += track->getName(); res += track->getName();
if (track->getRelease()) if (track->getRelease())
res += " [" + track->getRelease()->getName() + "]"; res += " [" + track->getRelease()->getName() + "]";
for (auto artist : track->getArtists()) for (auto artist : track->getArtists({Database::TrackArtistLinkType::Artist}))
res += " - " + artist->getName(); res += " - " + artist->getName();
for (auto cluster : track->getClusters()) for (auto cluster : track->getClusters())
res += " {" + cluster->getType()->getName() + "-"+ cluster->getName() + "}"; res += " {" + cluster->getType()->getName() + "-"+ cluster->getName() + "}";
@@ -67,14 +66,14 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng
}; };
std::cout << "Processing track '" << trackToString(trackId) << std::endl; std::cout << "Processing track '" << trackToString(trackId) << std::endl;
for (Database::IdType similarTrackId : engine.getSimilarTracks(session, {trackId}, 3)) for (Database::IdType similarTrackId : engine.getSimilarTracks(session, {trackId}, maxSimilarityCount))
std::cout << "\t- Similar track '" << trackToString(similarTrackId) << std::endl; std::cout << "\t- Similar track '" << trackToString(similarTrackId) << std::endl;
} }
} }
static static
void void
dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& engine) dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount)
{ {
const std::vector<Database::IdType> releaseIds = std::invoke([&]() const std::vector<Database::IdType> releaseIds = std::invoke([&]()
{ {
@@ -94,14 +93,14 @@ dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& e
}; };
std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl; std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl;
for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, releaseId, 3)) for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, releaseId, maxSimilarityCount))
std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl; std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl;
} }
} }
static static
void void
dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& engine) dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount)
{ {
const std::vector<Database::IdType> artistIds = std::invoke([&]() const std::vector<Database::IdType> artistIds = std::invoke([&]()
{ {
@@ -121,9 +120,11 @@ dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& en
}; };
std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl; std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl;
for (Database::IdType similarArtistId : engine.getSimilarArtists(session, artistId, 3)) for (Database::IdType similarArtistId : engine.getSimilarArtists(session, artistId, {Database::TrackArtistLinkType::Artist, Database::TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount))
{
std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl; std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl;
} }
}
} }
@@ -143,6 +144,7 @@ int main(int argc, char *argv[])
("artists,a", "Display recommendation for artists") ("artists,a", "Display recommendation for artists")
("releases,r", "Display recommendation for releases") ("releases,r", "Display recommendation for releases")
("tracks,t", "Display recommendation for tracks") ("tracks,t", "Display recommendation for tracks")
("max,m", po::value<unsigned>()->default_value(3), "Max similarity result count")
; ;
po::variables_map vm; po::variables_map vm;
@@ -165,16 +167,19 @@ int main(int argc, char *argv[])
std::cout << "Loading recommendation engine..." << std::endl; std::cout << "Loading recommendation engine..." << std::endl;
engine->load(false); engine->load(false);
unsigned maxSimilarityCount {vm["max"].as<unsigned>()};
std::cout << "Recommendation engine loaded!" << std::endl; std::cout << "Recommendation engine loaded!" << std::endl;
if (vm.count("tracks")) if (vm.count("tracks"))
dumpTracksRecommendation(db, *engine); dumpTracksRecommendation(db, *engine, maxSimilarityCount);
if (vm.count("releases")) if (vm.count("releases"))
dumpReleasesRecommendation(db, *engine); dumpReleasesRecommendation(db, *engine, maxSimilarityCount);
if (vm.count("artists")) if (vm.count("artists"))
dumpArtistsRecommendation(db, *engine); dumpArtistsRecommendation(db, *engine, maxSimilarityCount);
} }
catch( std::exception& e) catch( std::exception& e)
{ {