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