From e2cb6917eb7d8a5e3b5c162c001cdd5490538a06 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 16 Apr 2019 13:13:01 +0200 Subject: [PATCH] API Subsonic: added getSimilarSongs functions, corrected a bug in the file path generation --- README.md | 2 +- src/api/subsonic/SubsonicResource.cpp | 77 +++++++++++++++++++++++++-- src/database/Artist.cpp | 15 ++++++ src/database/Artist.hpp | 1 + 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index db92f909..5dffba43 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/api/subsonic/SubsonicResource.cpp b/src/api/subsonic/SubsonicResource.cpp index 637a1741..8310af53 100644 --- a/src/api/subsonic/SubsonicResource.cpp +++ b/src/api/subsonic/SubsonicResource.cpp @@ -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 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 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(context.parameters, "id")}; + if (id.type != Id::Type::Artist) + throw Error {Error::CustomType::BadId}; + + // Optional params + std::size_t count {getParameterAs(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::chrono::duration_cast(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) diff --git a/src/database/Artist.cpp b/src/database/Artist.cpp index c898eb4f..2ba51ec9 100644 --- a/src/database/Artist.cpp +++ b/src/database/Artist.cpp @@ -212,6 +212,21 @@ Artist::getTracks() const return std::vector>(_tracks.begin(), _tracks.end()); } +std::vector> +Artist::getRandomTracks(boost::optional count) const +{ + assert(self()); + assert(IdIsValid(self()->id())); + assert(session()); + + Wt::Dbo::collection> tracks {session()->query>("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(*count) : -1)}; + + return std::vector>(tracks.begin(), tracks.end()); +} + std::vector>> Artist::getClusterGroups(std::vector clusterTypes, std::size_t size) const { diff --git a/src/database/Artist.hpp b/src/database/Artist.hpp index f60c4060..0b27f280 100644 --- a/src/database/Artist.hpp +++ b/src/database/Artist.hpp @@ -68,6 +68,7 @@ class Artist : public Wt::Dbo::Dbo // Get the releases that have at least one track for this artist that belongs to optional cluster filters std::vector> getReleases(const std::set& clusterIds = std::set()) const; std::vector> getTracks() const; + std::vector> getRandomTracks(boost::optional 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