API subsonic: Better report songs

This commit is contained in:
emeric
2019-04-06 18:37:07 +02:00
parent 1f344aaaae
commit edc59fb2db
2 changed files with 40 additions and 33 deletions
+2 -3
View File
@@ -30,10 +30,9 @@ 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 Android application and Ultrasonic. For now, the API version implemented is 1.12.0 and has been tested using the official application and Ultrasonic on Android.
Please note some commands are not implemented (stars, shares, playlists). 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 uses metadata tags to organize data, a compatibility mode is used to navigate through the collection using the directory browsing commands.
The Subsonic API is enabled by default. The Subsonic API is enabled by default.
+38 -30
View File
@@ -37,6 +37,9 @@
#include "SubsonicId.hpp" #include "SubsonicId.hpp"
#include "SubsonicResponse.hpp" #include "SubsonicResponse.hpp"
#define CLUSTER_TYPE_GENRE "GENRE"
#define REPORTED_BITRATE 128
// Requests // Requests
#define PING_URL "/rest/ping.view" #define PING_URL "/rest/ping.view"
#define GET_LICENSE_URL "/rest/getLicense.view" #define GET_LICENSE_URL "/rest/getLicense.view"
@@ -294,7 +297,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
static static
std::string std::string
getArtistNames(const std::vector<Database::Artist::pointer> artists) getArtistNames(const std::vector<Database::Artist::pointer>& artists)
{ {
if (artists.size() == 1) if (artists.size() == 1)
return artists.front()->getName(); return artists.front()->getName();
@@ -313,16 +316,21 @@ getArtistNames(const std::vector<Database::Artist::pointer> artists)
static static
Response::Node Response::Node
trackToResponseNode(const Database::Track::pointer& track, bool id3) trackToResponseNode(const Database::Track::pointer& track)
{ {
Response::Node trackResponse; Response::Node trackResponse;
trackResponse.setAttribute("title", track->getName());
if (!id3)
trackResponse.setAttribute("isDir", "false");
trackResponse.setAttribute("id", IdToString({Id::Type::Track, track.id()})); trackResponse.setAttribute("id", IdToString({Id::Type::Track, track.id()}));
trackResponse.setAttribute("isDir", "false");
trackResponse.setAttribute("title", track->getName());
if (track->getTrackNumber())
trackResponse.setAttribute("track", std::to_string(*track->getTrackNumber()));
if (track->getDiscNumber())
trackResponse.setAttribute("discNumber", std::to_string(*track->getDiscNumber()));
if (track->getYear())
trackResponse.setAttribute("year", std::to_string(*track->getYear()));
trackResponse.setAttribute("size", std::to_string(REPORTED_BITRATE*1000/8 * std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count()));
trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()})); trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()}));
auto artists {track->getArtists()}; auto artists {track->getArtists()};
@@ -330,31 +338,31 @@ trackToResponseNode(const Database::Track::pointer& track, bool id3)
{ {
trackResponse.setAttribute("artist", getArtistNames(artists)); trackResponse.setAttribute("artist", getArtistNames(artists));
if (artists.size() == 1 && id3) if (artists.size() == 1)
trackResponse.setAttribute("artistId", IdToString({Id::Type::Artist, artists.front().id()})); trackResponse.setAttribute("artistId", IdToString({Id::Type::Artist, artists.front().id()}));
} }
if (track->getRelease())
{
trackResponse.setAttribute("album", track->getRelease()->getName());
trackResponse.setAttribute("albumId", IdToString({Id::Type::Release, track->getRelease().id()}));
trackResponse.setAttribute("parent", IdToString({Id::Type::Release, track->getRelease().id()}));
}
trackResponse.setAttribute("path", track->getName() + ".mp3"); trackResponse.setAttribute("path", track->getName() + ".mp3");
trackResponse.setAttribute("bitRate", "128"); trackResponse.setAttribute("bitRate", std::to_string(REPORTED_BITRATE));
trackResponse.setAttribute("duration", std::to_string(std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count())); trackResponse.setAttribute("duration", std::to_string(std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count()));
trackResponse.setAttribute("suffix", "mp3"); trackResponse.setAttribute("suffix", "mp3");
trackResponse.setAttribute("contentType", "audio/mpeg"); trackResponse.setAttribute("contentType", "audio/mpeg");
trackResponse.setAttribute("size", std::to_string(128000/8 * std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count())); trackResponse.setAttribute("type", "music");
if (track->getYear()) // Report the first GENRE for this track
trackResponse.setAttribute("year", std::to_string(*track->getYear())); Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(*track.session(), CLUSTER_TYPE_GENRE)};
if (clusterType)
if (track->getTrackNumber())
trackResponse.setAttribute("track", std::to_string(*track->getTrackNumber()));
if (track->getRelease())
{ {
if (id3) auto clusters {track->getClusterGroups({clusterType}, 1)};
trackResponse.setAttribute("albumId", IdToString({Id::Type::Release, track->getRelease().id()})); if (!clusters.empty() && !clusters.front().empty())
else trackResponse.setAttribute("genre", clusters.front().front()->getName());
trackResponse.setAttribute("parent", IdToString({Id::Type::Release, track->getRelease().id()}));
trackResponse.setAttribute("album", track->getRelease()->getName());
} }
return trackResponse; return trackResponse;
@@ -474,7 +482,7 @@ handleGetRandomSongsRequest(const Wt::Http::ParameterMap& parameters, Database::
Response::Node& randomSongsNode {response.createNode("randomSongs")}; Response::Node& randomSongsNode {response.createNode("randomSongs")};
for (const Database::Track::pointer& track : tracks) for (const Database::Track::pointer& track : tracks)
randomSongsNode.addArrayChild("song", trackToResponseNode(track, true)); randomSongsNode.addArrayChild("song", trackToResponseNode(track));
return response; return response;
} }
@@ -586,7 +594,7 @@ handleGetAlbumRequest(const Wt::Http::ParameterMap& request, Database::Handler&
auto tracks {release->getTracks()}; auto tracks {release->getTracks()};
for (const Database::Track::pointer& track : tracks) for (const Database::Track::pointer& track : tracks)
releaseNode.addArrayChild("song", trackToResponseNode(track, true /* id3 */)); releaseNode.addArrayChild("song", trackToResponseNode(track));
response.addNode("album", std::move(releaseNode)); response.addNode("album", std::move(releaseNode));
@@ -692,7 +700,7 @@ handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database::
auto tracks {release->getTracks()}; auto tracks {release->getTracks()};
for (const Database::Track::pointer& track : tracks) for (const Database::Track::pointer& track : tracks)
directoryNode.addArrayChild("child", trackToResponseNode(track, false)); directoryNode.addArrayChild("child", trackToResponseNode(track));
break; break;
} }
@@ -726,7 +734,7 @@ handleGetGenresRequest(const Wt::Http::ParameterMap& request, Database::Handler&
Wt::Dbo::Transaction transaction {db.getSession()}; Wt::Dbo::Transaction transaction {db.getSession()};
auto clusterType {Database::ClusterType::getByName(db.getSession(), "GENRE")}; auto clusterType {Database::ClusterType::getByName(db.getSession(), CLUSTER_TYPE_GENRE)};
if (clusterType) if (clusterType)
{ {
auto clusters {clusterType->getClusters()}; auto clusters {clusterType->getClusters()};
@@ -800,7 +808,7 @@ handleGetSongsByGenreRequest(const Wt::Http::ParameterMap& request, Database::Ha
Wt::Dbo::Transaction transaction {db.getSession()}; Wt::Dbo::Transaction transaction {db.getSession()};
auto clusterType {Database::ClusterType::getByName(db.getSession(), "GENRE")}; auto clusterType {Database::ClusterType::getByName(db.getSession(), CLUSTER_TYPE_GENRE)};
if (!clusterType) if (!clusterType)
throw Error {Error::Code::RequestedDataNotFound}; throw Error {Error::Code::RequestedDataNotFound};
@@ -814,7 +822,7 @@ handleGetSongsByGenreRequest(const Wt::Http::ParameterMap& request, Database::Ha
bool more; bool more;
auto tracks {Database::Track::getByFilter(db.getSession(), {cluster.id()}, {}, offset, size, more)}; auto tracks {Database::Track::getByFilter(db.getSession(), {cluster.id()}, {}, offset, size, more)};
for (const Database::Track::pointer& track : tracks) for (const Database::Track::pointer& track : tracks)
songsByGenreNode.addArrayChild("song", trackToResponseNode(track, true)); songsByGenreNode.addArrayChild("song", trackToResponseNode(track));
return response; return response;
} }
@@ -858,7 +866,7 @@ handleSearchRequestCommon(const Wt::Http::ParameterMap& request, Database::Handl
{ {
auto tracks {Database::Track::getByFilter(db.getSession(), {}, keywords, songOffset, songCount, more)}; auto tracks {Database::Track::getByFilter(db.getSession(), {}, keywords, songOffset, songCount, more)};
for (const Database::Track::pointer& track : tracks) for (const Database::Track::pointer& track : tracks)
searchResult2Node.addArrayChild("song", trackToResponseNode(track, id3)); searchResult2Node.addArrayChild("song", trackToResponseNode(track));
} }
return response; return response;