Subsonic API: added star support

This commit is contained in:
emeric
2019-06-16 15:11:42 +02:00
parent 08988b17a2
commit ef648d5cb6
12 changed files with 611 additions and 55 deletions
+1
View File
@@ -19,6 +19,7 @@ A [demo](http://lms.demo.poupon.io) instance is available, with the following li
- Custom tags (ex: "mood", "genre", "albummood", "albumgrouping", ...)
- MusicBrainzID support to handle duplicated artist and release names
- Playlists (only using Subsonic API for now)
- Starred Album/Artist/Tracks (only using Subsonic API for now)
## Recommendation engine
LMS provides several ways to help you find the music you like:
+1
View File
@@ -94,6 +94,7 @@ lms_SOURCES = \
$(srcdir)/ui/admin/UsersView.hpp \
$(srcdir)/ui/common/Validators.cpp \
$(srcdir)/ui/common/Validators.hpp \
$(srcdir)/ui/common/ValueStringModel.hpp \
$(srcdir)/ui/explore/ArtistInfoView.cpp \
$(srcdir)/ui/explore/ArtistInfoView.hpp \
$(srcdir)/ui/explore/ArtistLink.cpp \
+255 -39
View File
@@ -45,7 +45,7 @@ static const std::size_t reportedBitrate {128};
static const std::string reportedFileSuffix {"mp3"};
static const Av::Encoding reportedEncoding {Av::Encoding::MP3};
static const Av::Encoding transcodeEncoding {Av::Encoding::MP3};
static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
// Requests
#define PING_URL "/rest/ping.view"
@@ -73,6 +73,8 @@ static const Av::Encoding transcodeEncoding {Av::Encoding::MP3};
#define GET_SONGS_BY_GENRE_URL "/rest/getSongsByGenre.view"
#define SEARCH2_URL "/rest/search2.view"
#define SEARCH3_URL "/rest/search3.view"
#define STAR_URL "/rest/star.view"
#define UNSTAR_URL "/rest/unstar.view"
#define UPDATE_PLAYLIST_URL "/rest/updatePlaylist.view"
// MediaRetrievals
@@ -141,6 +143,8 @@ static Response handleGetPlaylistsRequest(RequestContext& context);
static Response handleGetSongsByGenreRequest(RequestContext& context);
static Response handleSearch2Request(RequestContext& context);
static Response handleSearch3Request(RequestContext& context);
static Response handleStarRequest(RequestContext& context);
static Response handleUnstarRequest(RequestContext& context);
static Response handleUpdatePlaylistRequest(RequestContext& context);
// MediaRetrievals
@@ -179,8 +183,10 @@ static std::map<std::string, RequestHandlerFunc> requestHandlers
{GET_PLAYLIST_URL, handleGetPlaylistRequest},
{GET_PLAYLISTS_URL, handleGetPlaylistsRequest},
{GET_SONGS_BY_GENRE_URL, handleGetSongsByGenreRequest},
{STAR_URL, handleStarRequest},
{SEARCH2_URL, handleSearch2Request},
{SEARCH3_URL, handleSearch3Request},
{UNSTAR_URL, handleUnstarRequest},
{UPDATE_PLAYLIST_URL, handleUpdatePlaylistRequest},
};
@@ -380,6 +386,8 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
resp.write(response.out(), format);
response.setMimeType(ResponseFormatToMimeType(format));
LMS_LOG(API_SUBSONIC, DEBUG) << "Request '" << request.path() << "' handled!";
return;
}
@@ -406,15 +414,19 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
{
auto continuation {response.createContinuation()};
continuation->setData(std::move(res.continuationData));
return;
}
LMS_LOG(API_SUBSONIC, DEBUG) << "Request '" << request.path() << "' handled!";
return;
}
LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << request.path() << "'";
}
catch (const Error& e)
{
LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing command. code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'";
LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << request.path() << "'"
<< ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]"
<< ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'";
Response resp {Response::createFailedResponse(e)};
resp.write(response.out(), format);
response.setMimeType(ResponseFormatToMimeType(format));
@@ -471,7 +483,7 @@ getTrackPath(const Database::Track::pointer& track)
static
Response::Node
trackToResponseNode(const Database::Track::pointer& track)
trackToResponseNode(const Database::User::pointer& user, const Database::Track::pointer& track)
{
Response::Node trackResponse;
@@ -511,6 +523,9 @@ trackToResponseNode(const Database::Track::pointer& track)
trackResponse.setAttribute("contentType", Av::encodingToMimetype(reportedEncoding));
trackResponse.setAttribute("type", "music");
if (user->hasStarredTrack(track))
trackResponse.setAttribute("starred", reportedStarredDate);
// Report the first GENRE for this track
Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(*track.session(), genreClusterName)};
if (clusterType)
@@ -525,7 +540,7 @@ trackToResponseNode(const Database::Track::pointer& track)
static
Response::Node
releaseToResponseNode(const Database::Release::pointer& release, bool id3)
releaseToResponseNode(const Database::User::pointer& user, const Database::Release::pointer& release, bool id3)
{
Response::Node albumNode;
@@ -586,12 +601,15 @@ releaseToResponseNode(const Database::Release::pointer& release, bool id3)
}
}
if (user->hasStarredRelease(release))
albumNode.setAttribute("starred", reportedStarredDate);
return albumNode;
}
static
Response::Node
artistToResponseNode(const Database::Artist::pointer& artist, bool id3)
artistToResponseNode(const Database::User::pointer& user, const Database::Artist::pointer& artist, bool id3)
{
Response::Node artistNode;
@@ -601,6 +619,9 @@ artistToResponseNode(const Database::Artist::pointer& artist, bool id3)
if (id3)
artistNode.setAttribute("albumCount", std::to_string(artist->getReleases().size()));
if (user->hasStarredArtist(artist))
artistNode.setAttribute("starred", reportedStarredDate);
return artistNode;
}
@@ -730,13 +751,17 @@ handleGetRandomSongsRequest(RequestContext& context)
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
auto tracks {Database::Track::getAllRandom(context.db.getSession(), size)};
Response response {Response::createOkResponse()};
Response::Node& randomSongsNode {response.createNode("randomSongs")};
for (const Database::Track::pointer& track : tracks)
randomSongsNode.addArrayChild("song", trackToResponseNode(track));
randomSongsNode.addArrayChild("song", trackToResponseNode(user, track));
return response;
}
@@ -788,6 +813,10 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
if (type == "random")
{
releases = getRandomAlbums(context.db.getSession(), offset, size);
@@ -824,7 +853,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")};
for (const Database::Release::pointer& release : releases)
albumListNode.addArrayChild("album", releaseToResponseNode(release, id3));
albumListNode.addArrayChild("album", releaseToResponseNode(user, release, id3));
return response;
}
@@ -856,12 +885,16 @@ handleGetAlbumRequest(RequestContext& context)
if (!release)
throw Error {Error::Code::RequestedDataNotFound};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
Response::Node releaseNode {releaseToResponseNode(release, true /* id3 */)};
Response::Node releaseNode {releaseToResponseNode(user, release, true /* id3 */)};
auto tracks {release->getTracks()};
for (const Database::Track::pointer& track : tracks)
releaseNode.addArrayChild("song", trackToResponseNode(track));
releaseNode.addArrayChild("song", trackToResponseNode(user, track));
response.addNode("album", std::move(releaseNode));
@@ -880,15 +913,17 @@ handleGetArtistRequest(RequestContext& context)
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
if (!artist)
Database::User::pointer user {context.db.getUser(context.userName)};
if (!artist || !user)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
Response::Node artistNode {artistToResponseNode(artist, true /* id3 */)};
Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)};
auto releases {artist->getReleases()};
for (const Database::Release::pointer& release : releases)
artistNode.addArrayChild("album", releaseToResponseNode(release, true /* id3 */));
artistNode.addArrayChild("album", releaseToResponseNode(user, release, true /* id3 */));
response.addNode("artist", std::move(artistNode));
@@ -910,7 +945,9 @@ Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
if (!artist)
Database::User::pointer user {context.db.getUser(context.userName)};
if (!artist || !user)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
@@ -925,7 +962,7 @@ Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
Database::Artist::pointer similarArtist {Database::Artist::getById(context.db.getSession(), similarArtistId)};
if (similarArtist)
artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(similarArtist, id3));
artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(user, similarArtist, id3));
}
return response;
@@ -951,9 +988,13 @@ handleGetArtistsRequest(RequestContext& context)
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
auto artists {Database::Artist::getAll(context.db.getSession())};
for (const Database::Artist::pointer& artist : artists)
indexNode.addArrayChild("artist", artistToResponseNode(artist, true /* id3 */));
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */));
return response;
}
@@ -970,25 +1011,27 @@ handleGetMusicDirectoryRequest(RequestContext& context)
directoryNode.setAttribute("id", IdToString(id));
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
switch (id.type)
{
case Id::Type::Root:
{
Wt::Dbo::Transaction transaction {context.db.getSession()};
directoryNode.setAttribute("name", "Music");
auto artists {Database::Artist::getAll(context.db.getSession())};
for (const Database::Artist::pointer& artist : artists)
directoryNode.addArrayChild("child", artistToResponseNode(artist, false /* no id3 */));
directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */));
break;
}
case Id::Type::Artist:
{
Wt::Dbo::Transaction transaction {context.db.getSession()};
auto artist {Database::Artist::getById(context.db.getSession(), id.value)};
if (!artist)
throw Error {Error::Code::RequestedDataNotFound};
@@ -997,15 +1040,13 @@ handleGetMusicDirectoryRequest(RequestContext& context)
auto releases {artist->getReleases()};
for (const Database::Release::pointer& release : releases)
directoryNode.addArrayChild("child", releaseToResponseNode(release, false /* no id3 */));
directoryNode.addArrayChild("child", releaseToResponseNode(user, release, false /* no id3 */));
break;
}
case Id::Type::Release:
{
Wt::Dbo::Transaction transaction {context.db.getSession()};
auto release {Database::Release::getById(context.db.getSession(), id.value)};
if (!release)
throw Error {Error::Code::RequestedDataNotFound};
@@ -1014,7 +1055,7 @@ handleGetMusicDirectoryRequest(RequestContext& context)
auto tracks {release->getTracks()};
for (const Database::Track::pointer& track : tracks)
directoryNode.addArrayChild("child", trackToResponseNode(track));
directoryNode.addArrayChild("child", trackToResponseNode(user, track));
break;
}
@@ -1071,9 +1112,13 @@ handleGetIndexesRequest(RequestContext& context)
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
auto artists {Database::Artist::getAll(context.db.getSession())};
for (const Database::Artist::pointer& artist : artists)
indexNode.addArrayChild("artist", artistToResponseNode(artist, false /* no id3 */));
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */));
return response;
}
@@ -1092,7 +1137,9 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
if (!artist)
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user || !artist)
throw Error {Error::Code::RequestedDataNotFound};
// "Returns a random collection of songs from the given artist and similar artists"
@@ -1119,7 +1166,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
Response response {Response::createOkResponse()};
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
for (const Database::Track::pointer& track : tracks)
similarSongsNode.addArrayChild("song", trackToResponseNode(track));
similarSongsNode.addArrayChild("song", trackToResponseNode(user, track));
return response;
}
@@ -1137,16 +1184,51 @@ handleGetSimilarSongs2Request(RequestContext& context)
}
static
Response
handleGetStarredRequestCommon(RequestContext& context, bool id3)
{
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")};
{
auto artists {user->getStarredArtists()};
for (const Database::Artist::pointer& artist : artists)
starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3));
}
{
auto releases {user->getStarredReleases()};
for (const Database::Release::pointer& release : releases)
starredNode.addArrayChild("album", releaseToResponseNode(user, release, id3));
}
{
auto tracks {user->getStarredTracks()};
for (const Database::Track::pointer& track : tracks)
starredNode.addArrayChild("song", trackToResponseNode(user, track));
}
return response;
}
Response
handleGetStarredRequest(RequestContext& context)
{
throw Error{Error::CustomType::NotImplemented};
return handleGetStarredRequestCommon(context, false /* no id3 */);
}
Response
handleGetStarred2Request(RequestContext& context)
{
throw Error{Error::CustomType::NotImplemented};
return handleGetStarredRequestCommon(context, true /* id3 */);
}
Response::Node
@@ -1184,11 +1266,8 @@ handleGetPlaylistRequest(RequestContext& context)
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Database::TrackList::pointer tracklist {Database::TrackList::getById(context.db.getSession(), id.value)};
if (!tracklist)
if (!user || !tracklist)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
@@ -1196,7 +1275,7 @@ handleGetPlaylistRequest(RequestContext& context)
auto entries {tracklist->getEntries()};
for (const Database::TrackListEntry::pointer& entry : entries)
playlistNode.addArrayChild("entry", trackToResponseNode(entry->getTrack()));
playlistNode.addArrayChild("entry", trackToResponseNode(user, entry->getTrack()));
response.addNode("playlist", playlistNode );
@@ -1244,17 +1323,22 @@ handleGetSongsByGenreRequest(RequestContext& context)
if (!cluster)
throw Error {Error::Code::RequestedDataNotFound};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
Response::Node& songsByGenreNode {response.createNode("songsByGenre")};
bool more;
auto tracks {Database::Track::getByFilter(context.db.getSession(), {cluster.id()}, {}, offset, size, more)};
for (const Database::Track::pointer& track : tracks)
songsByGenreNode.addArrayChild("song", trackToResponseNode(track));
songsByGenreNode.addArrayChild("song", trackToResponseNode(user, track));
return response;
}
static
Response
handleSearchRequestCommon(RequestContext& context, bool id3)
{
@@ -1273,6 +1357,10 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")};
@@ -1280,24 +1368,110 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
{
auto artists {Database::Artist::getByFilter(context.db.getSession(), {}, keywords, artistOffset, artistCount, more)};
for (const Database::Artist::pointer& artist : artists)
searchResult2Node.addArrayChild("artist", artistToResponseNode(artist, id3));
searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3));
}
{
auto releases {Database::Release::getByFilter(context.db.getSession(), {}, keywords, albumOffset, albumCount, more)};
for (const Database::Release::pointer& release : releases)
searchResult2Node.addArrayChild("album", releaseToResponseNode(release, id3));
searchResult2Node.addArrayChild("album", releaseToResponseNode(user, release, id3));
}
{
auto tracks {Database::Track::getByFilter(context.db.getSession(), {}, keywords, songOffset, songCount, more)};
for (const Database::Track::pointer& track : tracks)
searchResult2Node.addArrayChild("song", trackToResponseNode(track));
searchResult2Node.addArrayChild("song", trackToResponseNode(user, track));
}
return response;
}
struct StarParameters
{
std::vector<Id> artistIds;
std::vector<Id> releaseIds;
std::vector<Id> trackIds;
};
static
StarParameters
getStarParameters(const Wt::Http::ParameterMap& parameters)
{
StarParameters res;
std::vector<Id> ids {getMultiParametersAs<Id>(parameters, "id")};
res.artistIds = getMultiParametersAs<Id>(parameters, "artistId");
res.releaseIds = getMultiParametersAs<Id>(parameters, "albumId");
if (!std::all_of(std::cbegin(res.releaseIds ), std::cend(res.releaseIds ), [](const Id& id) { return id.type == Id::Type::Release; }))
throw Error {Error::CustomType::BadId};
if (!std::all_of(std::cbegin(res.artistIds ), std::cend(res.artistIds ), [](const Id& id) { return id.type == Id::Type::Artist; }))
throw Error {Error::CustomType::BadId};
// Redispatch the old "id" parameter in new lists
for (const Id& id : ids)
{
switch (id.type)
{
case Id::Type::Artist:
res.artistIds.emplace_back(id);
break;
case Id::Type::Release:
res.releaseIds.emplace_back(id);
break;
case Id::Type::Track:
res.trackIds.emplace_back(id);
break;
default:
throw Error {Error::CustomType::BadId};
}
}
return res;
}
Response
handleStarRequest(RequestContext& context)
{
StarParameters params {getStarParameters(context.parameters)};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
for (const Id& id : params.artistIds)
{
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
if (!artist)
continue;
user.modify()->starArtist(artist);
}
for (const Id& id : params.releaseIds)
{
Database::Release::pointer release {Database::Release::getById(context.db.getSession(), id.value)};
if (!release)
continue;
user.modify()->starRelease(release);
}
for (const Id& id : params.trackIds)
{
Database::Track::pointer track {Database::Track::getById(context.db.getSession(), id.value)};
if (!track)
continue;
user.modify()->starTrack(track);
}
return Response::createOkResponse();
}
Response
handleSearch2Request(RequestContext& context)
{
@@ -1310,6 +1484,48 @@ handleSearch3Request(RequestContext& context)
return handleSearchRequestCommon(context, true /* id3 */);
}
Response
handleUnstarRequest(RequestContext& context)
{
StarParameters params {getStarParameters(context.parameters)};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
for (const Id& id : params.artistIds)
{
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
if (!artist)
continue;
user.modify()->unstarArtist(artist);
}
for (const Id& id : params.releaseIds)
{
Database::Release::pointer release {Database::Release::getById(context.db.getSession(), id.value)};
if (!release)
continue;
user.modify()->unstarRelease(release);
}
for (const Id& id : params.trackIds)
{
Database::Track::pointer track {Database::Track::getById(context.db.getSession(), id.value)};
if (!track)
continue;
user.modify()->unstarTrack(track);
}
return Response::createOkResponse();
}
Response
handleUpdatePlaylistRequest(RequestContext& context)
{
+1
View File
@@ -26,6 +26,7 @@
#include "Release.hpp"
#include "SqlQuery.hpp"
#include "Track.hpp"
#include "User.hpp"
namespace Database
{
+3
View File
@@ -37,6 +37,7 @@ class Cluster;
class ClusterType;
class Release;
class Track;
class User;
class Artist : public Wt::Dbo::Dbo<Artist>
{
@@ -93,6 +94,7 @@ class Artist : public Wt::Dbo::Dbo<Artist>
Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
}
private:
@@ -104,6 +106,7 @@ class Artist : public Wt::Dbo::Dbo<Artist>
std::string _MBID; // Musicbrainz Identifier
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
Wt::Dbo::collection<Wt::Dbo::ptr<User>> _starringUsers; // Users that starred this artist
};
} // namespace Database
+63 -13
View File
@@ -46,7 +46,7 @@
namespace Database {
#define LMS_DATABASE_VERSION 3
#define LMS_DATABASE_VERSION 4
namespace {
Wt::Auth::AuthService authService;
@@ -82,6 +82,67 @@ class VersionInfo
int _version {LMS_DATABASE_VERSION};
};
static
void
doDatabaseMigrationIfNeeded(Wt::Dbo::Session& session)
{
Wt::Dbo::Transaction transaction {session};
static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"};
Version version;
try
{
version = VersionInfo::get(session)->getVersion();
if (version == LMS_DATABASE_VERSION)
return;
}
catch (std::exception& e)
{
LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what();
throw LmsException {outdatedMsg};
}
switch (version)
{
case 3:
LMS_LOG(DB, INFO) << "Migrating database from version 3...";
session.execute(R"(CREATE TABLE IF NOT EXISTS "user_artist_starred" (
"user_id" bigint,
"artist_id" bigint,
primary key ("user_id", "artist_id"),
constraint "fk_user_artist_starred_key1" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred,
constraint "fk_user_artist_starred_key2" foreign key ("artist_id") references "artist" ("id") deferrable initially deferred);)");
session.execute(R"(CREATE INDEX "user_artist_starred_user" on "user_artist_starred" ("user_id");)");
session.execute(R"(CREATE INDEX "user_artist_starred_artist" on "user_artist_starred" ("artist_id");)");
session.execute(R"(CREATE TABLE IF NOT EXISTS "user_release_starred" (
"user_id" bigint,
"release_id" bigint,
primary key ("user_id", "release_id"),
constraint "fk_user_release_starred_key1" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred,
constraint "fk_user_release_starred_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred);)");
session.execute(R"(CREATE INDEX "user_release_starred_user" on "user_release_starred" ("user_id");)");
session.execute(R"(CREATE INDEX "user_release_starred_release" on "user_release_starred" ("release_id");)");
session.execute(R"(CREATE TABLE IF NOT EXISTS "user_track_starred" (
"user_id" bigint,
"track_id" bigint,
primary key ("user_id", "track_id"),
constraint "fk_user_track_starred_key1" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred,
constraint "fk_user_track_starred_key2" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred);)");
session.execute(R"(CREATE INDEX "user_track_starred_user" on "user_track_starred" ("user_id");)");
session.execute(R"(CREATE INDEX "user_track_starred_track" on "user_track_starred" ("track_id");)");
break;
default:
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
throw LmsException {outdatedMsg};
}
VersionInfo::get(session).modify()->setVersion(LMS_DATABASE_VERSION);
}
void
Handler::configureAuth(void)
@@ -163,18 +224,7 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool)
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
}
try
{
Wt::Dbo::Transaction transaction {_session};
Version version {VersionInfo::get(_session)->getVersion()};
if (version != LMS_DATABASE_VERSION)
throw LmsException {"Outdated database, please rebuild it (database migration not yet implemented)"};
}
catch (std::exception& e)
{
throw LmsException {"Cannot get database version, please rebuild it (database migration not yet implemented)"};
}
doDatabaseMigrationIfNeeded(_session);
{
Wt::Dbo::Transaction transaction {_session};
+1
View File
@@ -25,6 +25,7 @@
#include "Cluster.hpp"
#include "SqlQuery.hpp"
#include "Track.hpp"
#include "User.hpp"
namespace Database
{
+5 -2
View File
@@ -29,11 +29,12 @@
namespace Database
{
class Track;
class Release;
class Artist;
class Cluster;
class ClusterType;
class Release;
class Track;
class User;
class Release : public Wt::Dbo::Dbo<Release>
{
@@ -104,6 +105,7 @@ class Release : public Wt::Dbo::Dbo<Release>
Wt::Dbo::field(a, _totalTrackNumber, "total_track_number");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
}
private:
@@ -115,6 +117,7 @@ class Release : public Wt::Dbo::Dbo<Release>
int _totalTrackNumber {};
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
Wt::Dbo::collection<Wt::Dbo::ptr<User>> _starringUsers; // Users that starred this release
};
} // namespace Database
+3
View File
@@ -41,6 +41,7 @@ class Release;
class TrackFeatures;
class TrackListEntry;
class TrackStats;
class User;
class Track : public Wt::Dbo::Dbo<Track>
{
@@ -144,6 +145,7 @@ class Track : public Wt::Dbo::Dbo<Track>
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _playlistEntries, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasOne(a, _trackFeatures);
}
@@ -175,6 +177,7 @@ class Track : public Wt::Dbo::Dbo<Track>
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _playlistEntries;
Wt::Dbo::collection<Wt::Dbo::ptr<User>> _starringUsers;
Wt::Dbo::weak_ptr<TrackFeatures> _trackFeatures;
};
+82
View File
@@ -19,6 +19,9 @@
#include "User.hpp"
#include "Artist.hpp"
#include "Release.hpp"
#include "Track.hpp"
#include "TrackList.hpp"
namespace Database {
@@ -123,6 +126,85 @@ User::getQueuedTrackList() const
return res;
}
void
User::starArtist(Wt::Dbo::ptr<Artist> artist)
{
if (_starredArtists.count(artist) == 0)
_starredArtists.insert(artist);
}
void
User::unstarArtist(Wt::Dbo::ptr<Artist> artist)
{
if (_starredArtists.count(artist) != 0)
_starredArtists.erase(artist);
}
bool
User::hasStarredArtist(Wt::Dbo::ptr<Artist> artist) const
{
return _starredArtists.count(artist) != 0;
}
std::vector<Wt::Dbo::ptr<Artist>>
User::getStarredArtists() const
{
return std::vector<Wt::Dbo::ptr<Artist>>(_starredArtists.begin(), _starredArtists.end());
}
void
User::starRelease(Wt::Dbo::ptr<Release> release)
{
if (_starredReleases.count(release) == 0)
_starredReleases.insert(release);
}
void
User::unstarRelease(Wt::Dbo::ptr<Release> release)
{
if (_starredReleases.count(release) != 0)
_starredReleases.erase(release);
}
bool
User::hasStarredRelease(Wt::Dbo::ptr<Release> release) const
{
return _starredReleases.count(release) != 0;
}
std::vector<Wt::Dbo::ptr<Release>>
User::getStarredReleases() const
{
return std::vector<Wt::Dbo::ptr<Release>>(_starredReleases.begin(), _starredReleases.end());
}
void
User::starTrack(Wt::Dbo::ptr<Track> track)
{
if (_starredTracks.count(track) == 0)
_starredTracks.insert(track);
}
void
User::unstarTrack(Wt::Dbo::ptr<Track> track)
{
if (_starredTracks.count(track) != 0)
_starredTracks.erase(track);
}
bool
User::hasStarredTrack(Wt::Dbo::ptr<Track> track) const
{
return _starredTracks.count(track) != 0;
}
std::vector<Wt::Dbo::ptr<Track>>
User::getStarredTracks() const
{
return std::vector<Wt::Dbo::ptr<Track>>(_starredTracks.begin(), _starredTracks.end());
}
} // namespace Database
+25 -1
View File
@@ -31,7 +31,10 @@ namespace Database {
class User;
using AuthInfo = Wt::Auth::Dbo::AuthInfo<User>;
class Artist;
class Release;
class TrackList;
class Track;
// User selectable audio formats
// Do not change values
@@ -97,6 +100,21 @@ class User : public Wt::Dbo::Dbo<User>
Wt::Dbo::ptr<TrackList> getQueuedTrackList() const;
Wt::Dbo::ptr<TrackList> getPlayedTrackList() const;
void starArtist(Wt::Dbo::ptr<Artist> artist);
void unstarArtist(Wt::Dbo::ptr<Artist> artist);
bool hasStarredArtist(Wt::Dbo::ptr<Artist> artist) const;
std::vector<Wt::Dbo::ptr<Artist>> getStarredArtists() const;
void starRelease(Wt::Dbo::ptr<Release> release);
void unstarRelease(Wt::Dbo::ptr<Release> release);
bool hasStarredRelease(Wt::Dbo::ptr<Release> release) const;
std::vector<Wt::Dbo::ptr<Release>> getStarredReleases() const;
void starTrack(Wt::Dbo::ptr<Track> track);
void unstarTrack(Wt::Dbo::ptr<Track> track);
bool hasStarredTrack(Wt::Dbo::ptr<Track> track) const;
std::vector<Wt::Dbo::ptr<Track>> getStarredTracks() const;
template<class Action>
void persist(Action& a)
{
@@ -110,6 +128,9 @@ class User : public Wt::Dbo::Dbo<User>
Wt::Dbo::field(a, _repeatAll, "repeat_all");
Wt::Dbo::field(a, _radio, "radio");
Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _starredTracks, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade);
}
private:
@@ -132,7 +153,10 @@ class User : public Wt::Dbo::Dbo<User>
bool _repeatAll {};
bool _radio {};
Wt::Dbo::collection< Wt::Dbo::ptr<TrackList> > _tracklists;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackList>> _tracklists;
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> _starredArtists;
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> _starredReleases;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _starredTracks;
};
+171
View File
@@ -964,6 +964,174 @@ testSingleTrackSingleReleaseSingleArtistMultiClusters(Wt::Dbo::Session& session)
}
}
static
void
testSingleStarredArtist(Wt::Dbo::Session& session)
{
IdType artistId {};
IdType userId {};
{
Wt::Dbo::Transaction transaction {session};
auto artist {Artist::create(session, "MyArtist")};
CHECK(artist);
auto user {User::create(session)};
CHECK(user);
session.flush();
artistId = artist.id();
userId = user.id();
}
{
Wt::Dbo::Transaction transaction {session};
auto user {User::getById(session, userId)};
CHECK(user);
auto artist {Artist::getById(session, artistId)};
CHECK(artist);
user.modify()->starArtist(artist);
}
{
Wt::Dbo::Transaction transaction {session};
auto user {User::getById(session, userId)};
CHECK(user);
auto artists {user->getStarredArtists()};
CHECK(artists.size() == 1);
CHECK(artists.front().id() == artistId);
auto artist {Artist::getById(session, artistId)};
CHECK(user->hasStarredArtist(artist));
}
{
Wt::Dbo::Transaction transaction {session};
auto user {User::getById(session, userId)};
CHECK(user);
auto artist {Artist::getById(session, artistId)};
CHECK(artist);
user.remove();
artist.remove();
}
}
static
void
testSingleStarredRelease(Wt::Dbo::Session& session)
{
IdType releaseId {};
IdType userId {};
{
Wt::Dbo::Transaction transaction {session};
auto release {Release::create(session, "MyRelease")};
CHECK(release);
auto user {User::create(session)};
CHECK(user);
session.flush();
releaseId = release.id();
userId = user.id();
}
{
Wt::Dbo::Transaction transaction {session};
auto user {User::getById(session, userId)};
CHECK(user);
auto release {Release::getById(session, releaseId)};
CHECK(release);
user.modify()->starRelease(release);
}
{
Wt::Dbo::Transaction transaction {session};
auto user {User::getById(session, userId)};
CHECK(user);
auto releases {user->getStarredReleases()};
CHECK(releases.size() == 1);
CHECK(releases.front().id() == releaseId);
auto release {Release::getById(session, releaseId)};
CHECK(user->hasStarredRelease(release));
}
{
Wt::Dbo::Transaction transaction {session};
auto user {User::getById(session, userId)};
CHECK(user);
auto release {Release::getById(session, releaseId)};
CHECK(release);
user.remove();
release.remove();
}
}
static
void
testSingleStarredTrack(Wt::Dbo::Session& session)
{
IdType trackId {};
IdType userId {};
{
Wt::Dbo::Transaction transaction {session};
auto track {Track::create(session, "MyTrackFile")};
CHECK(track);
auto user {User::create(session)};
CHECK(user);
session.flush();
trackId = track.id();
userId = user.id();
}
{
Wt::Dbo::Transaction transaction {session};
auto user {User::getById(session, userId)};
CHECK(user);
auto track {Track::getById(session, trackId)};
CHECK(track);
user.modify()->starTrack(track);
}
{
Wt::Dbo::Transaction transaction {session};
auto user {User::getById(session, userId)};
CHECK(user);
auto tracks {user->getStarredTracks()};
CHECK(tracks.size() == 1);
CHECK(tracks.front().id() == trackId);
auto track {Track::getById(session, trackId)};
CHECK(user->hasStarredTrack(track));
}
{
Wt::Dbo::Transaction transaction {session};
auto user {User::getById(session, userId)};
CHECK(user);
auto track {Track::getById(session, trackId)};
CHECK(track);
user.remove();
track.remove();
}
}
static
void
testDatabaseEmpty(Wt::Dbo::Session& session)
@@ -1025,6 +1193,9 @@ int main(int argc, char* argv[])
RUN_TEST(testSingleTrackSingleReleaseSingleArtistSingleCluster);
RUN_TEST(testSingleTrackSingleReleaseSingleArtistMultiClusters);
RUN_TEST(testSingleStarredArtist);
RUN_TEST(testSingleStarredRelease);
RUN_TEST(testSingleStarredTrack);
}
catch (std::exception& e)
{