API Subsonic: added getSimilarSongs functions, corrected a bug in the file path generation

This commit is contained in:
emeric
2019-04-16 13:13:01 +02:00
parent cc437c7cfe
commit e2cb6917eb
4 changed files with 89 additions and 6 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ Please note this engine:
- makes use of computed data available on [AcousticBrainz](https://acousticbrainz.org/). Therefore your music must contain the [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) for the recommendation engine to work properly
## Subsonic API
For now, the API version implemented is 1.12.0 and has been tested using the official application and Ultrasonic on Android.
For now, the API version implemented is 1.12.0 and has been tested on Android using the official application, Ultrasonic and DSub.
As LMS does not aim to implement all the features of Subsonic, some commands are missing. Since LMS uses metadata tags to organize data, a compatibility mode is used to navigate through the collection using the directory browsing commands.
+72 -5
View File
@@ -59,6 +59,8 @@
#define GET_MUSIC_FOLDERS_URL "/rest/getMusicFolders.view"
#define GET_GENRES_URL "/rest/getGenres.view"
#define GET_INDEXES_URL "/rest/getIndexes.view"
#define GET_SIMILAR_SONGS_URL "/rest/getSimilarSongs.view"
#define GET_SIMILAR_SONGS2_URL "/rest/getSimilarSongs2.view"
#define GET_STARRED_URL "/rest/getStarred.view"
#define GET_STARRED2_URL "/rest/getStarred2.view"
#define GET_PLAYLIST_URL "/rest/getPlaylist.view"
@@ -125,6 +127,8 @@ static Response handleGetMusicDirectoryRequest(RequestContext& context);
static Response handleGetMusicFoldersRequest(RequestContext& context);
static Response handleGetGenresRequest(RequestContext& context);
static Response handleGetIndexesRequest(RequestContext& context);
static Response handleGetSimilarSongsRequest(RequestContext& context);
static Response handleGetSimilarSongs2Request(RequestContext& context);
static Response handleGetStarredRequest(RequestContext& context);
static Response handleGetStarred2Request(RequestContext& context);
static Response handleGetPlaylistRequest(RequestContext& context);
@@ -157,6 +161,8 @@ static std::map<std::string, RequestHandlerFunc> requestHandlers
{GET_MUSIC_FOLDERS_URL, handleGetMusicFoldersRequest},
{GET_GENRES_URL, handleGetGenresRequest},
{GET_INDEXES_URL, handleGetIndexesRequest},
{GET_SIMILAR_SONGS_URL, handleGetSimilarSongsRequest},
{GET_SIMILAR_SONGS2_URL, handleGetSimilarSongs2Request},
{GET_STARRED_URL, handleGetStarredRequest},
{GET_STARRED2_URL, handleGetStarred2Request},
{GET_PLAYLIST_URL, handleGetPlaylistRequest},
@@ -407,8 +413,6 @@ getTrackPath(const Database::Track::pointer& track)
{
std::string path;
// TODO encode '/'?
if (track->getRelease())
{
auto artists {track->getRelease()->getArtists()};
@@ -422,9 +426,9 @@ getTrackPath(const Database::Track::pointer& track)
}
if (track->getDiscNumber())
path += *track->getDiscNumber() + "-";
path += std::to_string(*track->getDiscNumber()) + "-";
if (track->getTrackNumber())
path += *track->getTrackNumber() + "-";
path += std::to_string(*track->getTrackNumber()) + "-";
return path + makeNameFilesystemCompatible(track->getName()) + ".mp3";
}
@@ -715,7 +719,6 @@ std::vector<Database::Release::pointer> getRandomAlbums(Wt::Dbo::Session& sessio
std::iota(std::begin(indexes), std::end(indexes), 1);
// As random results are paginated, we need to set a seed for it
std::random_device r;
std::seed_seq seed {1337};
std::mt19937 generator{seed};
@@ -1036,6 +1039,70 @@ handleGetIndexesRequest(RequestContext& context)
return response;
}
Response
handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Artist)
throw Error {Error::CustomType::BadId};
// Optional params
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").get_value_or(50)};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
if (!artist)
throw Error {Error::Code::RequestedDataNotFound};
// "Returns a random collection of songs from the given artist and similar artists"
auto tracks {artist->getRandomTracks(count / 2)};
LMS_LOG(API_SUBSONIC, DEBUG) << "Now have " << tracks.size() << " tracks";
auto similarArtistsId {getServices().similaritySearcher->getSimilarArtists(context.db.getSession(), artist.id(), 5)};
for ( const auto& similarArtistId : similarArtistsId )
{
Database::Artist::pointer similarArtist {Database::Artist::getById(context.db.getSession(), similarArtistId)};
if (!similarArtist)
continue;
auto similarArtistTracks {similarArtist->getRandomTracks((count / 2) / 5)};
LMS_LOG(API_SUBSONIC, DEBUG) << "Added " << similarArtistTracks.size() << " similar tracks from artist " << similarArtist->getName();
tracks.insert(tracks.end(),
std::make_move_iterator(std::begin(similarArtistTracks)),
std::make_move_iterator(std::end(similarArtistTracks)));
}
auto now {std::chrono::system_clock::now()};
std::mt19937 randGenerator {static_cast<std::mt19937::result_type>(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count())};
std::shuffle(std::begin(tracks), std::end(tracks), randGenerator);
LMS_LOG(API_SUBSONIC, DEBUG) << "FINAL Now have " << tracks.size() << " tracks";
Response response {Response::createOkResponse()};
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
for (const Database::Track::pointer& track : tracks)
similarSongsNode.addArrayChild("song", trackToResponseNode(track));
return response;
}
Response
handleGetSimilarSongsRequest(RequestContext& context)
{
return handleGetSimilarSongsRequestCommon(context, false /* no id3 */);
}
Response
handleGetSimilarSongs2Request(RequestContext& context)
{
return handleGetSimilarSongsRequestCommon(context, true /* id3 */);
}
Response
handleGetStarredRequest(RequestContext& context)
+15
View File
@@ -212,6 +212,21 @@ Artist::getTracks() const
return std::vector<Wt::Dbo::ptr<Track>>(_tracks.begin(), _tracks.end());
}
std::vector<Wt::Dbo::ptr<Track>>
Artist::getRandomTracks(boost::optional<std::size_t> count) const
{
assert(self());
assert(IdIsValid(self()->id()));
assert(session());
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> tracks {session()->query<Wt::Dbo::ptr<Track>>("SELECT t from Track t INNER JOIN artist a ON t_a.artist_id = a.id INNER JOIN track_artist t_a ON t_a.track_id = t.id")
.where("a.id = ?").bind(self()->id())
.orderBy("RANDOM()")
.limit(count ? static_cast<int>(*count) : -1)};
return std::vector<Wt::Dbo::ptr<Track>>(tracks.begin(), tracks.end());
}
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>>
Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
{
+1
View File
@@ -68,6 +68,7 @@ class Artist : public Wt::Dbo::Dbo<Artist>
// Get the releases that have at least one track for this artist that belongs to optional cluster filters
std::vector<Wt::Dbo::ptr<Release>> getReleases(const std::set<IdType>& clusterIds = std::set<IdType>()) const;
std::vector<Wt::Dbo::ptr<Track>> getTracks() const;
std::vector<Wt::Dbo::ptr<Track>> getRandomTracks(boost::optional<std::size_t> count) const;
// Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence