Subsonic API: added bookmark support. Fixes #24

This commit is contained in:
emeric
2020-01-29 17:02:18 +01:00
parent 0ef6f82732
commit 8d62df00ef
10 changed files with 351 additions and 7 deletions
+2
View File
@@ -46,6 +46,8 @@ lms_SOURCES = \
$(srcdir)/database/SqlQuery.hpp \
$(srcdir)/database/Track.cpp \
$(srcdir)/database/Track.hpp \
$(srcdir)/database/TrackBookmark.cpp \
$(srcdir)/database/TrackBookmark.hpp \
$(srcdir)/database/User.cpp \
$(srcdir)/database/User.hpp \
$(srcdir)/image/Image.cpp \
+110 -3
View File
@@ -35,6 +35,7 @@
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackBookmark.hpp"
#include "database/TrackList.hpp"
#include "database/User.hpp"
#include "similarity/SimilaritySearcher.hpp"
@@ -48,6 +49,8 @@ using namespace Database;
static const std::string genreClusterName {"GENRE"};
static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
static const std::string reportedCreatedBookmarkDate {"2000-01-01T00:00:00"};
static const std::string reportedChangedBookmarkDate {"2000-01-01T00:00:00"};
template<>
std::optional<API::Subsonic::Id>
@@ -412,6 +415,22 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User:
return trackResponse;
}
static
Response::Node
trackBookmarkToResponseNode(const TrackBookmark::pointer& trackBookmark)
{
Response::Node trackBookmarkNode;
trackBookmarkNode.setAttribute("position", std::to_string(trackBookmark->getOffset().count()));
if (!trackBookmark->getComment().empty())
trackBookmarkNode.setAttribute("comment", trackBookmark->getComment());
trackBookmarkNode.setAttribute("created", reportedCreatedBookmarkDate);
trackBookmarkNode.setAttribute("changed", reportedChangedBookmarkDate);
trackBookmarkNode.setAttribute("username", trackBookmark->getUser()->getLoginName());
return trackBookmarkNode;
}
static
Response::Node
releaseToResponseNode(const Release::pointer& release, Session& dbSession, const User::pointer& user, bool id3)
@@ -1656,6 +1675,94 @@ handleUpdatePlaylistRequest(RequestContext& context)
return Response::createOkResponse();
}
static
Response
handleGetBookmarks(RequestContext& context)
{
auto transaction {context.dbSession.createSharedTransaction()};
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
if (!user)
throw UserNotAuthorizedError {};
const auto bookmarks {TrackBookmark::getByUser(context.dbSession, user)};
Response response {Response::createOkResponse()};
Response::Node& bookmarksNode {response.createNode("bookmarks")};
for (const TrackBookmark::pointer& bookmark : bookmarks)
{
Response::Node bookmarkNode {trackBookmarkToResponseNode(bookmark)};
bookmarkNode.addArrayChild("entry", trackToResponseNode(bookmark->getTrack(), context.dbSession, user));
bookmarksNode.addArrayChild("bookmark", std::move(bookmarkNode));
}
return response ;
}
static
Response
handleCreateBookmark(RequestContext& context)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Track)
throw BadParameterGenericError {"id"};
unsigned long position {getMandatoryParameterAs<unsigned long>(context.parameters, "position")};
const std::optional<std::string> comment {getParameterAs<std::string>(context.parameters, "comment")};
auto transaction {context.dbSession.createUniqueTransaction()};
const User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
if (!user)
throw UserNotAuthorizedError {};
const Track::pointer track {Track::getById(context.dbSession, id.value)};
if (!track)
throw RequestedDataNotFoundError {};
// Replace any existing bookmark
auto bookmark {TrackBookmark::getByUser(context.dbSession, user, track)};
if (!bookmark)
bookmark = TrackBookmark::create(context.dbSession, user, track);
bookmark.modify()->setOffset(std::chrono::milliseconds {position});
if (comment)
bookmark.modify()->setComment(*comment);
return Response::createOkResponse();
}
static
Response
handleDeleteBookmark(RequestContext& context)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Track)
throw BadParameterGenericError {"id"};
auto transaction {context.dbSession.createUniqueTransaction()};
const User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
if (!user)
throw UserNotAuthorizedError {};
const Track::pointer track {Track::getById(context.dbSession, id.value)};
if (!track)
throw RequestedDataNotFoundError {};
auto bookmark {TrackBookmark::getByUser(context.dbSession, user, track)};
if (!bookmark)
throw RequestedDataNotFoundError {};
bookmark.remove();
return Response::createOkResponse();
}
static
Response
handleNotImplemented(RequestContext&)
@@ -1898,9 +2005,9 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
{"changePassword", {handleChangePassword, false}},
// Bookmarks
{"getBookmarks", {handleNotImplemented, false}},
{"createBookmarks", {handleNotImplemented, false}},
{"deleteBookmarks", {handleNotImplemented, false}},
{"getBookmarks", {handleGetBookmarks, false}},
{"createBookmark", {handleCreateBookmark, false}},
{"deleteBookmark", {handleDeleteBookmark, false}},
{"getPlayQueue", {handleNotImplemented, false}},
{"savePlayQueue", {handleNotImplemented, false}},
+2 -2
View File
@@ -54,9 +54,9 @@ Response::Node::setValue(const std::string& value)
}
void
Response::Node::setAttribute(const std::string& key, const std::string& value)
Response::Node::setAttribute(std::string_view key, std::string_view value)
{
_attributes[key] = value;
_attributes[std::string {key}] = value;
}
void
+2 -1
View File
@@ -19,6 +19,7 @@
#include <map>
#include <string>
#include <string_view>
#include <vector>
@@ -178,7 +179,7 @@ class Response
class Node
{
public:
void setAttribute(const std::string& key, const std::string& value);
void setAttribute(std::string_view key, std::string_view value);
// A Node has either a value or some children
void setValue(const std::string& value);
+19 -1
View File
@@ -32,6 +32,7 @@
#include "Release.hpp"
#include "ScanSettings.hpp"
#include "Track.hpp"
#include "TrackBookmark.hpp"
#include "TrackArtistLink.hpp"
#include "TrackList.hpp"
#include "TrackFeatures.hpp"
@@ -39,7 +40,7 @@
namespace Database {
#define LMS_DATABASE_VERSION 9
#define LMS_DATABASE_VERSION 10
using Version = std::size_t;
@@ -125,6 +126,20 @@ Session::doDatabaseMigrationIfNeeded()
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 9)
{
_session.execute(R"(
CREATE TABLE IF NOT EXISTS "track_bookmark" (
"id" integer primary key autoincrement,
"version" integer not null,
"offset" integer,
"comment" text not null,
"track_id" bigint,
"user_id" bigint,
constraint "fk_track_bookmark_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_bookmark_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
);)");
}
else
{
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
@@ -150,6 +165,7 @@ Session::Session(Db& db)
_session.mapClass<Release>("release");
_session.mapClass<ScanSettings>("scan_settings");
_session.mapClass<Track>("track");
_session.mapClass<TrackBookmark>("track_bookmark");
_session.mapClass<TrackArtistLink>("track_artist_link");
_session.mapClass<TrackFeatures>("track_features");
_session.mapClass<TrackList>("tracklist");
@@ -264,6 +280,8 @@ Session::prepareTables()
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_name_idx ON track_artist_link(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
}
// Initial settings tables
+91
View File
@@ -0,0 +1,91 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TrackBookmark.hpp"
#include "Session.hpp"
#include "Track.hpp"
#include "User.hpp"
namespace Database {
TrackBookmark::TrackBookmark(Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track)
: _user {user},
_track {track}
{
}
TrackBookmark::pointer
TrackBookmark::create(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track)
{
session.checkUniqueLocked();
TrackBookmark::pointer res {session.getDboSession().add(std::make_unique<TrackBookmark>(user, track))};
session.getDboSession().flush();
return res;
}
std::vector<TrackBookmark::pointer>
TrackBookmark::getAll(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<TrackBookmark::pointer> res {session.getDboSession().find<TrackBookmark>()};
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
}
std::vector<TrackBookmark::pointer>
TrackBookmark::getByUser(Session& session, Wt::Dbo::ptr<User> user)
{
session.checkSharedLocked();
Wt::Dbo::collection<TrackBookmark::pointer> res
{
session.getDboSession().find<TrackBookmark>()
.where("user_id = ?").bind(user.id())
};
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
}
TrackBookmark::pointer
TrackBookmark::getByUser(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track)
{
session.checkSharedLocked();
return session.getDboSession().find<TrackBookmark>()
.where("user_id = ?").bind(user.id())
.where("track_id = ?").bind(track.id());
}
TrackBookmark::pointer
TrackBookmark::getById(Session& session, IdType id)
{
session.checkSharedLocked();
return session.getDboSession().find<TrackBookmark>()
.where("id = ?").bind(id);
}
} // namespace Database
+82
View File
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <Wt/Dbo/Dbo.h>
#include "Types.hpp"
namespace Database {
class Session;
class Track;
class User;
class TrackBookmark : public Wt::Dbo::Dbo<TrackBookmark>
{
public:
using pointer = Wt::Dbo::ptr<TrackBookmark>;
TrackBookmark () = default;
TrackBookmark(Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track);
// utility
static pointer create(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track);
// Find utility functions
static std::vector<pointer> getAll(Session& session);
static std::vector<pointer> getByUser(Session& session, Wt::Dbo::ptr<User> user);
static pointer getByUser(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track);
static pointer getById(Session& session, IdType id);
// Setters
void setOffset(std::chrono::milliseconds offset) { _offset = offset; }
void setComment(std::string_view comment) { _comment = comment; }
// Getters
std::chrono::milliseconds getOffset() const { return _offset; }
std::string_view getComment() const { return _comment; }
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
Wt::Dbo::ptr<User> getUser() const { return _user; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _offset, "offset");
Wt::Dbo::field(a, _comment, "comment");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
private:
static const std::size_t _maxCommentLength = 128;
std::chrono::duration<int, std::milli> _offset;
std::string _comment;
Wt::Dbo::ptr<User> _user;
Wt::Dbo::ptr<Track> _track;
};
} // namespace database
+1
View File
@@ -164,6 +164,7 @@ class User : public Wt::Dbo::Dbo<User>
bool hasStarredRelease(Wt::Dbo::ptr<Release> release) const;
std::vector<Wt::Dbo::ptr<Release>> getStarredReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
// Stars
void starTrack(Wt::Dbo::ptr<Track> track);
void unstarTrack(Wt::Dbo::ptr<Track> track);
bool hasStarredTrack(Wt::Dbo::ptr<Track> track) const;
+1
View File
@@ -24,6 +24,7 @@ test_database_SOURCES = \
$(top_srcdir)/src/database/Session.cpp \
$(top_srcdir)/src/database/SqlQuery.cpp \
$(top_srcdir)/src/database/Track.cpp \
$(top_srcdir)/src/database/TrackBookmark.cpp \
$(top_srcdir)/src/database/User.cpp \
$(top_srcdir)/src/utils/Logger.cpp \
$(top_srcdir)/src/utils/StreamLogger.cpp \
+41
View File
@@ -28,6 +28,7 @@
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackBookmark.hpp"
#include "database/TrackList.hpp"
#include "database/User.hpp"
@@ -131,6 +132,7 @@ using ScopedCluster = ScopedEntity<Cluster>;
using ScopedClusterType = ScopedEntity<ClusterType>;
using ScopedRelease = ScopedEntity<Release>;
using ScopedTrack = ScopedEntity<Track>;
using ScopedTrackBookmark = ScopedEntity<TrackBookmark>;
using ScopedTrackList = ScopedEntity<TrackList>;
using ScopedUser = ScopedEntity<User>;
@@ -1266,6 +1268,42 @@ testMultipleTracksMultipleReleasesMultiClusters(Session& session)
}
}
static
void
testSingleTrackSingleUserSingleBookmark(Session& session)
{
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser", User::PasswordHash {}};
ScopedTrackBookmark bookmark {session, user.lockAndGet(), track.lockAndGet()};
{
auto transaction {session.createUniqueTransaction()};
bookmark.get().modify()->setComment("MyComment");
bookmark.get().modify()->setOffset(std::chrono::milliseconds {5});
}
{
auto transaction {session.createSharedTransaction()};
CHECK(TrackBookmark::getAll(session).size() == 1);
const auto bookmarks {TrackBookmark::getByUser(session, user.get())};
CHECK(bookmarks.size() == 1);
CHECK(bookmarks.back() == bookmark.get());
}
{
auto transaction {session.createSharedTransaction()};
auto userBookmark {TrackBookmark::getByUser(session, user.get(), track.get())};
CHECK(userBookmark);
CHECK(userBookmark == bookmark.get());
CHECK(userBookmark->getOffset() == std::chrono::milliseconds {5});
CHECK(userBookmark->getComment() == "MyComment");
}
}
static
void
testDatabaseEmpty(Session& session)
@@ -1277,6 +1315,7 @@ testDatabaseEmpty(Session& session)
CHECK(ClusterType::getAll(session).empty());
CHECK(Release::getAll(session).empty());
CHECK(Track::getAll(session).empty());
CHECK(TrackBookmark::getAll(session).empty());
CHECK(TrackList::getAll(session).empty());
CHECK(User::getAll(session).empty());
}
@@ -1353,6 +1392,8 @@ int main()
RUN_TEST(testSingleTrackListMultipleTrackMultiClusters);
RUN_TEST(testMultipleTracksMultipleArtistsMultiClusters);
RUN_TEST(testMultipleTracksMultipleReleasesMultiClusters);
RUN_TEST(testSingleTrackSingleUserSingleBookmark);
}
}
catch (std::exception& e)