API Subsonic: added getSimilarSongs functions, corrected a bug in the file path generation
This commit is contained in:
@@ -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
|
- 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
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,8 @@
|
|||||||
#define GET_MUSIC_FOLDERS_URL "/rest/getMusicFolders.view"
|
#define GET_MUSIC_FOLDERS_URL "/rest/getMusicFolders.view"
|
||||||
#define GET_GENRES_URL "/rest/getGenres.view"
|
#define GET_GENRES_URL "/rest/getGenres.view"
|
||||||
#define GET_INDEXES_URL "/rest/getIndexes.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_STARRED_URL "/rest/getStarred.view"
|
||||||
#define GET_STARRED2_URL "/rest/getStarred2.view"
|
#define GET_STARRED2_URL "/rest/getStarred2.view"
|
||||||
#define GET_PLAYLIST_URL "/rest/getPlaylist.view"
|
#define GET_PLAYLIST_URL "/rest/getPlaylist.view"
|
||||||
@@ -125,6 +127,8 @@ static Response handleGetMusicDirectoryRequest(RequestContext& context);
|
|||||||
static Response handleGetMusicFoldersRequest(RequestContext& context);
|
static Response handleGetMusicFoldersRequest(RequestContext& context);
|
||||||
static Response handleGetGenresRequest(RequestContext& context);
|
static Response handleGetGenresRequest(RequestContext& context);
|
||||||
static Response handleGetIndexesRequest(RequestContext& context);
|
static Response handleGetIndexesRequest(RequestContext& context);
|
||||||
|
static Response handleGetSimilarSongsRequest(RequestContext& context);
|
||||||
|
static Response handleGetSimilarSongs2Request(RequestContext& context);
|
||||||
static Response handleGetStarredRequest(RequestContext& context);
|
static Response handleGetStarredRequest(RequestContext& context);
|
||||||
static Response handleGetStarred2Request(RequestContext& context);
|
static Response handleGetStarred2Request(RequestContext& context);
|
||||||
static Response handleGetPlaylistRequest(RequestContext& context);
|
static Response handleGetPlaylistRequest(RequestContext& context);
|
||||||
@@ -157,6 +161,8 @@ static std::map<std::string, RequestHandlerFunc> requestHandlers
|
|||||||
{GET_MUSIC_FOLDERS_URL, handleGetMusicFoldersRequest},
|
{GET_MUSIC_FOLDERS_URL, handleGetMusicFoldersRequest},
|
||||||
{GET_GENRES_URL, handleGetGenresRequest},
|
{GET_GENRES_URL, handleGetGenresRequest},
|
||||||
{GET_INDEXES_URL, handleGetIndexesRequest},
|
{GET_INDEXES_URL, handleGetIndexesRequest},
|
||||||
|
{GET_SIMILAR_SONGS_URL, handleGetSimilarSongsRequest},
|
||||||
|
{GET_SIMILAR_SONGS2_URL, handleGetSimilarSongs2Request},
|
||||||
{GET_STARRED_URL, handleGetStarredRequest},
|
{GET_STARRED_URL, handleGetStarredRequest},
|
||||||
{GET_STARRED2_URL, handleGetStarred2Request},
|
{GET_STARRED2_URL, handleGetStarred2Request},
|
||||||
{GET_PLAYLIST_URL, handleGetPlaylistRequest},
|
{GET_PLAYLIST_URL, handleGetPlaylistRequest},
|
||||||
@@ -407,8 +413,6 @@ getTrackPath(const Database::Track::pointer& track)
|
|||||||
{
|
{
|
||||||
std::string path;
|
std::string path;
|
||||||
|
|
||||||
// TODO encode '/'?
|
|
||||||
|
|
||||||
if (track->getRelease())
|
if (track->getRelease())
|
||||||
{
|
{
|
||||||
auto artists {track->getRelease()->getArtists()};
|
auto artists {track->getRelease()->getArtists()};
|
||||||
@@ -422,9 +426,9 @@ getTrackPath(const Database::Track::pointer& track)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (track->getDiscNumber())
|
if (track->getDiscNumber())
|
||||||
path += *track->getDiscNumber() + "-";
|
path += std::to_string(*track->getDiscNumber()) + "-";
|
||||||
if (track->getTrackNumber())
|
if (track->getTrackNumber())
|
||||||
path += *track->getTrackNumber() + "-";
|
path += std::to_string(*track->getTrackNumber()) + "-";
|
||||||
|
|
||||||
return path + makeNameFilesystemCompatible(track->getName()) + ".mp3";
|
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);
|
std::iota(std::begin(indexes), std::end(indexes), 1);
|
||||||
|
|
||||||
// As random results are paginated, we need to set a seed for it
|
// As random results are paginated, we need to set a seed for it
|
||||||
std::random_device r;
|
|
||||||
std::seed_seq seed {1337};
|
std::seed_seq seed {1337};
|
||||||
std::mt19937 generator{seed};
|
std::mt19937 generator{seed};
|
||||||
|
|
||||||
@@ -1036,6 +1039,70 @@ handleGetIndexesRequest(RequestContext& context)
|
|||||||
return response;
|
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
|
Response
|
||||||
handleGetStarredRequest(RequestContext& context)
|
handleGetStarredRequest(RequestContext& context)
|
||||||
|
|||||||
@@ -212,6 +212,21 @@ Artist::getTracks() const
|
|||||||
return std::vector<Wt::Dbo::ptr<Track>>(_tracks.begin(), _tracks.end());
|
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>>>
|
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>>
|
||||||
Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
|
Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
// 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<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>> 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
|
// Get the cluster of the tracks made by this artist
|
||||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||||
|
|||||||
Reference in New Issue
Block a user