API subsonic: Better report songs
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user