Merge branch 'develop' for release v3.57.0
This commit is contained in:
+1
-1
@@ -7,7 +7,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
|
||||
if (UNIX)
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--no-undefined")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
|
||||
endif ()
|
||||
|
||||
option(ENABLE_TESTS "Enable tests" ON)
|
||||
|
||||
+24
-22
@@ -6,7 +6,7 @@ Given the API limitations of folder navigation commands, it is recommended to pl
|
||||
|
||||
The Subsonic API is enabled by default.
|
||||
|
||||
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method. Since logins/passwords are passed in plain text through URLs, it is highly recommended to use a unique password when using the Subsonic API. Note that this may affect the use of authentication via PAM. In any case, ensure that read access to the web server logs (and to the proxy, if applicable) is well protected.
|
||||
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method. Since logins/passwords are passed in plain text through URLs, it is highly recommended to use a unique password when using the Subsonic API. Note that this may affect the use of authentication via PAM. In any case, ensure the web server logs (and proxy logs, if applicable) are properly secured.
|
||||
|
||||
# OpenSubsonic API
|
||||
OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'll find more details in the [official documentation](https://opensubsonic.netlify.app/)
|
||||
@@ -14,32 +14,34 @@ OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'l
|
||||
## Extra fields
|
||||
The following extra fields are implemented:
|
||||
* `Album` response:
|
||||
* `mediaType`
|
||||
* `played`
|
||||
* `musicBrainzId`
|
||||
* `genres`
|
||||
* `artists`
|
||||
* `displayArtist`
|
||||
* `releaseTypes`
|
||||
* `moods`
|
||||
* `originalReleaseDate`
|
||||
* `isCompilation`
|
||||
* `discTitles`: discs with no subtitle are omitted
|
||||
* `Child` response:
|
||||
* `bitDepth`
|
||||
* `samplingRate`
|
||||
* `channelCount`
|
||||
* `mediaType`
|
||||
* `played`
|
||||
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
|
||||
* `genres`
|
||||
* `artists`
|
||||
* `displayArtist`
|
||||
* `albumArtists`
|
||||
* `displayAlbumArtist`
|
||||
* `contributors`
|
||||
* `genres`
|
||||
* `isCompilation`
|
||||
* `played`
|
||||
* `mediaType`
|
||||
* `moods`
|
||||
* `musicBrainzId`
|
||||
* `originalReleaseDate`
|
||||
* `releaseTypes`
|
||||
* `userRating`
|
||||
* `Child` response:
|
||||
* `albumArtists`
|
||||
* `artists`
|
||||
* `bitDepth`
|
||||
* `channelCount`
|
||||
* `comment`
|
||||
* `contributors`
|
||||
* `displayAlbumArtist`
|
||||
* `displayArtist`
|
||||
* `genres`
|
||||
* `mediaType`
|
||||
* `moods`
|
||||
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
|
||||
* `played`
|
||||
* `replayGain`
|
||||
* `samplingRate`
|
||||
* `Artist` response:
|
||||
* `mediaType`
|
||||
* `musicBrainzId`
|
||||
|
||||
@@ -125,6 +125,10 @@
|
||||
${playcount}
|
||||
</div>
|
||||
</div>
|
||||
${<if-has-comment>}
|
||||
<hr/>
|
||||
<pre>${comment}</pre>
|
||||
${</if-has-comment>}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
@@ -11,6 +11,9 @@ add_library(lmsdatabase SHARED
|
||||
impl/TrackArtistLink.cpp
|
||||
impl/TrackFeatures.cpp
|
||||
impl/TrackList.cpp
|
||||
impl/RatedArtist.cpp
|
||||
impl/RatedRelease.cpp
|
||||
impl/RatedTrack.cpp
|
||||
impl/Release.cpp
|
||||
impl/ScanSettings.cpp
|
||||
impl/Session.cpp
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 62 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 64 };
|
||||
}
|
||||
|
||||
VersionInfo::VersionInfo()
|
||||
@@ -664,6 +664,56 @@ SELECT
|
||||
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||
}
|
||||
|
||||
void migrateFromV62(Session& session)
|
||||
{
|
||||
// Add a new column comment
|
||||
session.getDboSession()->execute("ALTER TABLE track ADD comment TEXT NOT NULL DEFAULT ''");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||
}
|
||||
|
||||
void migrateFromV63(Session& session)
|
||||
{
|
||||
// Add a rated entities
|
||||
|
||||
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "rated_artist" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"rating" integer not null,
|
||||
"last_updated" text,
|
||||
"artist_id" bigint,
|
||||
"user_id" bigint,
|
||||
constraint "fk_rated_artist_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_rated_artist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "rated_release" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"rating" integer not null,
|
||||
"last_updated" text,
|
||||
"release_id" bigint,
|
||||
"user_id" bigint,
|
||||
constraint "fk_rated_release_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_rated_release_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "rated_track" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"rating" bigint not null,
|
||||
"last_updated" text,
|
||||
"track_id" bigint,
|
||||
"user_id" bigint,
|
||||
constraint "fk_rated_track_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_rated_track_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
// Drop badly named index, will be recreated
|
||||
session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_backend_date_time");
|
||||
}
|
||||
|
||||
bool doDbMigration(Session& session)
|
||||
{
|
||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -702,6 +752,8 @@ SELECT
|
||||
{ 59, migrateFromV59 },
|
||||
{ 60, migrateFromV60 },
|
||||
{ 61, migrateFromV61 },
|
||||
{ 62, migrateFromV62 },
|
||||
{ 63, migrateFromV63 },
|
||||
};
|
||||
|
||||
bool migrationPerformed{};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "database/RatedArtist.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
RatedArtist::RatedArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user)
|
||||
: _artist{ getDboPtr(artist) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
RatedArtist::pointer RatedArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<RatedArtist>{ new RatedArtist{ artist, user } });
|
||||
}
|
||||
|
||||
std::size_t RatedArtist::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM rated_artist"));
|
||||
}
|
||||
|
||||
RatedArtist::pointer RatedArtist::find(Session& session, RatedArtistId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<RatedArtist>().where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
RatedArtist::pointer RatedArtist::find(Session& session, ArtistId artistId, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<RatedArtist>().where("artist_id = ?").bind(artistId).where("user_id = ?").bind(userId));
|
||||
}
|
||||
|
||||
void RatedArtist::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<RatedArtist>>("SELECT r_a FROM rated_artist r_a") };
|
||||
|
||||
if (params.user.isValid())
|
||||
query.where("r_a.user_id = ?").bind(params.user);
|
||||
|
||||
utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
void RatedArtist::setLastUpdated(const Wt::WDateTime& lastUpdated)
|
||||
{
|
||||
_lastUpdated = utils::normalizeDateTime(lastUpdated);
|
||||
}
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "database/RatedRelease.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
RatedRelease::RatedRelease(ObjectPtr<Release> release, ObjectPtr<User> user)
|
||||
: _release{ getDboPtr(release) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
RatedRelease::pointer RatedRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<RatedRelease>{ new RatedRelease{ release, user } });
|
||||
}
|
||||
|
||||
std::size_t RatedRelease::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM rated_release"));
|
||||
}
|
||||
|
||||
RatedRelease::pointer RatedRelease::find(Session& session, RatedReleaseId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<RatedRelease>().where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
RatedRelease::pointer RatedRelease::find(Session& session, ReleaseId releaseId, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<RatedRelease>().where("release_id = ?").bind(releaseId).where("user_id = ?").bind(userId));
|
||||
}
|
||||
|
||||
void RatedRelease::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<RatedRelease>>("SELECT r_r FROM rated_release r_r") };
|
||||
|
||||
if (params.user.isValid())
|
||||
query.where("r_r.user_id = ?").bind(params.user);
|
||||
|
||||
utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
void RatedRelease::setLastUpdated(const Wt::WDateTime& lastUpdated)
|
||||
{
|
||||
_lastUpdated = utils::normalizeDateTime(lastUpdated);
|
||||
}
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "database/RatedTrack.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
RatedTrack::RatedTrack(ObjectPtr<Track> track, ObjectPtr<User> user)
|
||||
: _track{ getDboPtr(track) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
RatedTrack::pointer RatedTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<RatedTrack>{ new RatedTrack{ track, user } });
|
||||
}
|
||||
|
||||
std::size_t RatedTrack::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM rated_track"));
|
||||
}
|
||||
|
||||
RatedTrack::pointer RatedTrack::find(Session& session, RatedTrackId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<RatedTrack>().where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
RatedTrack::pointer RatedTrack::find(Session& session, TrackId trackId, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<RatedTrack>().where("track_id = ?").bind(trackId).where("user_id = ?").bind(userId));
|
||||
}
|
||||
|
||||
void RatedTrack::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<RatedTrack>>("SELECT r_t FROM rated_track r_t") };
|
||||
|
||||
if (params.user.isValid())
|
||||
query.where("r_t.user_id = ?").bind(params.user);
|
||||
|
||||
utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
void RatedTrack::setLastUpdated(const Wt::WDateTime& lastUpdated)
|
||||
{
|
||||
_lastUpdated = utils::normalizeDateTime(lastUpdated);
|
||||
}
|
||||
} // namespace lms::db
|
||||
@@ -30,6 +30,9 @@
|
||||
#include "database/Image.hpp"
|
||||
#include "database/Listen.hpp"
|
||||
#include "database/MediaLibrary.hpp"
|
||||
#include "database/RatedArtist.hpp"
|
||||
#include "database/RatedRelease.hpp"
|
||||
#include "database/RatedTrack.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/StarredArtist.hpp"
|
||||
@@ -98,6 +101,9 @@ namespace lms::db
|
||||
_session.mapClass<Image>("image");
|
||||
_session.mapClass<Listen>("listen");
|
||||
_session.mapClass<MediaLibrary>("media_library");
|
||||
_session.mapClass<RatedArtist>("rated_artist");
|
||||
_session.mapClass<RatedRelease>("rated_release");
|
||||
_session.mapClass<RatedTrack>("rated_track");
|
||||
_session.mapClass<Release>("release");
|
||||
_session.mapClass<ReleaseType>("release_type");
|
||||
_session.mapClass<ScanSettings>("scan_settings");
|
||||
@@ -194,10 +200,14 @@ namespace lms::db
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_date_time ON listen(user_id, backend, date_time DESC)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_date_time_idx ON listen(user_id, backend, date_time DESC)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)");
|
||||
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS rated_artist_user_artist_idx ON rated_artist(user_id,artist_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS rated_release_user_release_idx ON rated_release(user_id,release_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
|
||||
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/RatedArtistId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Artist;
|
||||
class Session;
|
||||
class User;
|
||||
|
||||
class RatedArtist final : public Object<RatedArtist, RatedArtistId>
|
||||
{
|
||||
public:
|
||||
RatedArtist() = default;
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
UserId user; // and this user
|
||||
std::optional<Range> range;
|
||||
|
||||
FindParameters& setUser(UserId _user)
|
||||
{
|
||||
user = _user;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
{
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, RatedArtistId id);
|
||||
static pointer find(Session& session, ArtistId artistId, UserId userId);
|
||||
static void find(Session& session, const FindParameters& findParams, std::function<void(const pointer&)> func);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
Rating getRating() const { return _rating; }
|
||||
const Wt::WDateTime& getLastUpdated() const { return _lastUpdated; }
|
||||
|
||||
// Setters
|
||||
void setRating(Rating rating) { _rating = rating; }
|
||||
void setLastUpdated(const Wt::WDateTime& lastUpdated);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _rating, "rating");
|
||||
Wt::Dbo::field(a, _lastUpdated, "last_updated");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
RatedArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user);
|
||||
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user);
|
||||
|
||||
Rating _rating{};
|
||||
Wt::WDateTime _lastUpdated; // when it was rated for the last time
|
||||
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(RatedArtistId)
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/RatedReleaseId.hpp"
|
||||
#include "database/ReleaseId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Release;
|
||||
class Session;
|
||||
class User;
|
||||
|
||||
class RatedRelease final : public Object<RatedRelease, RatedReleaseId>
|
||||
{
|
||||
public:
|
||||
RatedRelease() = default;
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
UserId user; // and this user
|
||||
std::optional<Range> range;
|
||||
|
||||
FindParameters& setUser(UserId _user)
|
||||
{
|
||||
user = _user;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
{
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, RatedReleaseId id);
|
||||
static pointer find(Session& session, ReleaseId releaseId, UserId userId);
|
||||
static void find(Session& session, const FindParameters& findParams, std::function<void(const pointer&)> func);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
Rating getRating() const { return _rating; }
|
||||
const Wt::WDateTime& getLastUpdated() const { return _lastUpdated; }
|
||||
|
||||
// Setters
|
||||
void setRating(Rating rating) { _rating = rating; }
|
||||
void setLastUpdated(const Wt::WDateTime& lastUpdated);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _rating, "rating");
|
||||
Wt::Dbo::field(a, _lastUpdated, "last_updated");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
RatedRelease(ObjectPtr<Release> release, ObjectPtr<User> user);
|
||||
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user);
|
||||
|
||||
Rating _rating{};
|
||||
Wt::WDateTime _lastUpdated; // when it was rated for the last time
|
||||
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(RatedReleaseId)
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/RatedTrackId.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Track;
|
||||
class Session;
|
||||
class User;
|
||||
|
||||
class RatedTrack final : public Object<RatedTrack, RatedTrackId>
|
||||
{
|
||||
public:
|
||||
RatedTrack() = default;
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
UserId user; // and this user
|
||||
std::optional<Range> range;
|
||||
|
||||
FindParameters& setUser(UserId _user)
|
||||
{
|
||||
user = _user;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
{
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, RatedTrackId id);
|
||||
static pointer find(Session& session, TrackId trackId, UserId userId);
|
||||
static void find(Session& session, const FindParameters& findParams, std::function<void(const pointer&)> func);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
Rating getRating() const { return _rating; }
|
||||
const Wt::WDateTime& getLastUpdated() const { return _lastUpdated; }
|
||||
|
||||
// Setters
|
||||
void setRating(Rating rating) { _rating = rating; }
|
||||
void setLastUpdated(const Wt::WDateTime& lastUpdated);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _rating, "rating");
|
||||
Wt::Dbo::field(a, _lastUpdated, "last_updated");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
RatedTrack(ObjectPtr<Track> track, ObjectPtr<User> user);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user);
|
||||
|
||||
Rating _rating{};
|
||||
Wt::WDateTime _lastUpdated; // when it was rated for the last time
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(RatedTrackId)
|
||||
@@ -22,7 +22,6 @@
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "core/EnumSet.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/StarredTrackId.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
|
||||
@@ -228,6 +228,7 @@ namespace lms::db
|
||||
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
||||
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
|
||||
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
|
||||
void setComment(std::string_view comment) { _comment = comment; }
|
||||
void clearArtistLinks();
|
||||
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
||||
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
||||
@@ -264,6 +265,8 @@ namespace lms::db
|
||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
|
||||
std::string_view getComment() const { return _comment; }
|
||||
|
||||
// no artistLinkTypes means get all
|
||||
std::vector<ObjectPtr<Artist>> getArtists(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||
std::vector<ArtistId> getArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||
@@ -307,6 +310,8 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
||||
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
|
||||
Wt::Dbo::field(a, _comment, "comment");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete track on media library removal, we want to wait for the next scan to have a chance to migrate files
|
||||
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -350,6 +355,7 @@ namespace lms::db
|
||||
std::optional<float> _trackReplayGain;
|
||||
std::optional<float> _releaseReplayGain;
|
||||
std::string _artistDisplayName;
|
||||
std::string _comment;
|
||||
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
||||
|
||||
@@ -196,6 +196,8 @@ namespace lms::db
|
||||
void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
|
||||
bool isAudioBitrateAllowed(Bitrate bitrate);
|
||||
|
||||
using Rating = int;
|
||||
|
||||
enum class ScrobblingBackend
|
||||
{
|
||||
Internal = 0,
|
||||
|
||||
@@ -8,6 +8,9 @@ add_executable(test-database
|
||||
Image.cpp
|
||||
Listen.cpp
|
||||
Migration.cpp
|
||||
RatedArtist.cpp
|
||||
RatedRelease.cpp
|
||||
RatedTrack.cpp
|
||||
Release.cpp
|
||||
StarredArtist.cpp
|
||||
StarredRelease.cpp
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Directory.hpp"
|
||||
#include "database/Image.hpp"
|
||||
#include "database/RatedArtist.hpp"
|
||||
#include "database/RatedRelease.hpp"
|
||||
#include "database/RatedTrack.hpp"
|
||||
#include "database/StarredArtist.hpp"
|
||||
#include "database/StarredRelease.hpp"
|
||||
#include "database/StarredTrack.hpp"
|
||||
@@ -335,6 +338,9 @@ VALUES
|
||||
EXPECT_FALSE(Directory::find(session, DirectoryId{}));
|
||||
EXPECT_FALSE(Image::find(session, ImageId{}));
|
||||
EXPECT_FALSE(Listen::find(session, ListenId{}));
|
||||
EXPECT_FALSE(RatedArtist::find(session, RatedArtistId{}));
|
||||
EXPECT_FALSE(RatedRelease::find(session, RatedReleaseId{}));
|
||||
EXPECT_FALSE(RatedTrack::find(session, RatedTrackId{}));
|
||||
EXPECT_FALSE(Release::find(session, ReleaseId{}));
|
||||
EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{}));
|
||||
EXPECT_FALSE(StarredRelease::find(session, StarredReleaseId{}));
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "database/RatedArtist.hpp"
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedRatedArtist = ScopedEntity<db::RatedArtist>;
|
||||
|
||||
TEST_F(DatabaseFixture, RatedArtist)
|
||||
{
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto starredArtist{ RatedArtist::find(session, artist->getId(), user->getId()) };
|
||||
EXPECT_FALSE(starredArtist);
|
||||
EXPECT_EQ(RatedArtist::getCount(session), 0);
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters{}) };
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
}
|
||||
|
||||
ScopedRatedArtist ratedArtist{ session, artist.lockAndGet(), user.lockAndGet() };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotArtist{ RatedArtist::find(session, artist->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotArtist->getId(), ratedArtist->getId());
|
||||
EXPECT_EQ(gotArtist->getRating(), 0);
|
||||
EXPECT_EQ(RatedArtist::getCount(session), 1);
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "database/RatedRelease.hpp"
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedRatedRelease = ScopedEntity<db::RatedRelease>;
|
||||
|
||||
TEST_F(DatabaseFixture, RatedRelease)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto starredRelease{ RatedRelease::find(session, release->getId(), user->getId()) };
|
||||
EXPECT_FALSE(starredRelease);
|
||||
EXPECT_EQ(RatedRelease::getCount(session), 0);
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters{}) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
}
|
||||
|
||||
ScopedRatedRelease ratedRelease{ session, release.lockAndGet(), user.lockAndGet() };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotRelease{ RatedRelease::find(session, release->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotRelease->getId(), ratedRelease->getId());
|
||||
EXPECT_EQ(gotRelease->getRating(), 0);
|
||||
EXPECT_EQ(RatedRelease::getCount(session), 1);
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "database/RatedTrack.hpp"
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedRatedTrack = ScopedEntity<db::RatedTrack>;
|
||||
|
||||
TEST_F(DatabaseFixture, RatedTrack)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto starredTrack{ RatedTrack::find(session, track->getId(), user->getId()) };
|
||||
EXPECT_FALSE(starredTrack);
|
||||
EXPECT_EQ(RatedTrack::getCount(session), 0);
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
|
||||
ScopedRatedTrack ratedTrack{ session, track.lockAndGet(), user.lockAndGet() };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotTrack{ RatedTrack::find(session, track->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotTrack->getId(), ratedTrack->getId());
|
||||
EXPECT_EQ(gotTrack->getRating(), 0);
|
||||
EXPECT_EQ(RatedTrack::getCount(session), 1);
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -363,4 +363,24 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(track->getSampleRate(), 44100);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Track_comment)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getComment(), "");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setComment("MyComment");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getComment(), "MyComment");
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -273,6 +273,7 @@ namespace lms::metadata
|
||||
track.originalYear = utils::parseYear(*dateStr);
|
||||
}
|
||||
|
||||
track.comments = getTagValuesAs<std::string>(tagReader, TagType::Comment, {} /* no custom delimiter on comments */);
|
||||
track.copyright = getTagValueAs<std::string>(tagReader, TagType::Copyright).value_or("");
|
||||
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
|
||||
track.replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainTrackGain);
|
||||
@@ -297,11 +298,26 @@ namespace lms::metadata
|
||||
track.medium = getMedium(tagReader);
|
||||
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters);
|
||||
|
||||
// We consider the artist display name is put in the Artist tag (picard case)
|
||||
// But to please most users, if we find a custom delimiter in the Artist tag, we construct the artist diplay string with a "nicer" join
|
||||
if (!_artistTagDelimiters.empty()
|
||||
&& track.artists.size() > 1
|
||||
&& getTagValuesAs<std::string>(tagReader, TagType::Artist, _artistTagDelimiters).size() > 1)
|
||||
auto needReconstructArtistDisplayName{ [&] {
|
||||
// We consider the artist display name is put in the Artist tag (picard case)
|
||||
|
||||
// To please most users, if we find a custom delimiter in the Artist tag, we construct the artist display string with a "nicer" join
|
||||
if (!_artistTagDelimiters.empty()
|
||||
&& track.artists.size() > 1
|
||||
&& getTagValuesAs<std::string>(tagReader, TagType::Artist, _artistTagDelimiters).size() > 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// We have (true) multiple entries in the Artist tag or nothing
|
||||
else if (getTagValuesAs<std::string>(tagReader, TagType::Artist, {}).size() != 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} };
|
||||
|
||||
if (needReconstructArtistDisplayName())
|
||||
{
|
||||
std::vector<std::string_view> artistNames;
|
||||
std::transform(std::cbegin(track.artists), std::cend(track.artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; });
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include <taglib/aifffile.h>
|
||||
#include <taglib/apeproperties.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
@@ -37,6 +38,7 @@
|
||||
#include <taglib/tag.h>
|
||||
#include <taglib/tpropertymap.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
@@ -273,6 +275,27 @@ namespace lms::metadata
|
||||
TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
|
||||
if (!coverArtList.isEmpty())
|
||||
_hasEmbeddedCover = true;
|
||||
|
||||
if (!_propertyMap.contains("ORIGINALDATE"))
|
||||
{
|
||||
// For now:
|
||||
// * TagLib 2.0 only parses ----:com.apple.iTunes:ORIGINALDATE
|
||||
// / TagLib <2.0 only parses ----:com.apple.iTunes:originaldate
|
||||
const auto& tags{ mp4File->tag()->itemMap() };
|
||||
for (const auto& origDateString : { "----:com.apple.iTunes:originaldate", "----:com.apple.iTunes:ORIGINALDATE" })
|
||||
{
|
||||
auto itOrigDateTag{ tags.find(origDateString) };
|
||||
if (itOrigDateTag != std::cend(tags))
|
||||
{
|
||||
const TagLib::StringList dates{ itOrigDateTag->second.toStringList() };
|
||||
if (!dates.isEmpty())
|
||||
{
|
||||
_propertyMap["ORIGINALDATE"] = dates.front();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// MPC
|
||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.file()) })
|
||||
@@ -300,6 +323,26 @@ namespace lms::metadata
|
||||
if (!opusFile->tag()->pictureList().isEmpty())
|
||||
_hasEmbeddedCover = true;
|
||||
}
|
||||
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.file()) })
|
||||
{
|
||||
if (aiffFile->hasID3v2Tag())
|
||||
{
|
||||
const auto& frameListMap{ aiffFile->tag()->frameListMap() };
|
||||
|
||||
if (!frameListMap["APIC"].isEmpty())
|
||||
_hasEmbeddedCover = true;
|
||||
}
|
||||
}
|
||||
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.file()) })
|
||||
{
|
||||
if (wavFile->hasID3v2Tag())
|
||||
{
|
||||
const auto& frameListMap{ wavFile->ID3v2Tag()->frameListMap() };
|
||||
|
||||
if (!frameListMap["APIC"].isEmpty())
|
||||
_hasEmbeddedCover = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
{
|
||||
@@ -334,6 +377,10 @@ namespace lms::metadata
|
||||
_audioProperties.bitsPerSample = mp4Properties->bitsPerSample();
|
||||
else if (const auto* wavePackProperties{ dynamic_cast<const TagLib::WavPack::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = wavePackProperties->bitsPerSample();
|
||||
else if (const auto* aiffProperties{ dynamic_cast<const TagLib::RIFF::AIFF::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = aiffProperties->bitsPerSample();
|
||||
else if (const auto* wavProperties{ dynamic_cast<const TagLib::RIFF::WAV::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = wavProperties->bitsPerSample();
|
||||
#if TAGLIB_MAJOR_VERSION >= 2
|
||||
else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSF::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
|
||||
|
||||
@@ -117,6 +117,7 @@ namespace lms::metadata
|
||||
std::optional<core::UUID> acoustID;
|
||||
std::string copyright;
|
||||
std::string copyrightURL;
|
||||
std::vector<std::string> comments;
|
||||
std::optional<float> replayGain;
|
||||
std::string artistDisplayName;
|
||||
std::vector<Artist> artists;
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace lms::metadata
|
||||
{ TagType::AlbumArtist, { "MyAlbumArtist1 & MyAlbumArtist2" } },
|
||||
{ TagType::AlbumArtists, { "MyAlbumArtist1", "MyAlbumArtist2" } },
|
||||
{ TagType::AlbumArtistsSortOrder, { "MyAlbumArtist1SortName", "MyAlbumArtist2SortName" } },
|
||||
{ TagType::Comment, { "Comment1", "Comment2" } },
|
||||
{ TagType::Composer, { "MyComposer1", "MyComposer2" } },
|
||||
{ TagType::ComposerSortOrder, { "MyComposerSortOrder1", "MyComposerSortOrder2" } },
|
||||
{ TagType::Conductor, { "MyConductor1", "MyConductor2" } },
|
||||
@@ -103,6 +104,9 @@ namespace lms::metadata
|
||||
EXPECT_EQ(track->artists[1].name, "MyArtist2");
|
||||
EXPECT_EQ(track->artists[1].sortName, "MyArtist2SortName");
|
||||
EXPECT_EQ(track->artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229"));
|
||||
ASSERT_EQ(track->comments.size(), 2);
|
||||
EXPECT_EQ(track->comments[0], "Comment1");
|
||||
EXPECT_EQ(track->comments[1], "Comment2");
|
||||
ASSERT_EQ(track->composerArtists.size(), 2);
|
||||
EXPECT_EQ(track->composerArtists[0].name, "MyComposer1");
|
||||
EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1");
|
||||
@@ -252,4 +256,68 @@ namespace lms::metadata
|
||||
EXPECT_EQ(track->artists[1].name, "Other Artist");
|
||||
EXPECT_EQ(track->artistDisplayName, "This / is ; One Artist, Other Artist"); // reconstruct artist display name since a custom delimiter is hit
|
||||
}
|
||||
|
||||
TEST(Parser, noArtistInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artist!
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 0);
|
||||
EXPECT_EQ(track->artistDisplayName, "");
|
||||
}
|
||||
|
||||
TEST(Parser, singleArtistInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artist!
|
||||
{ TagType::Artists, { "Artist1" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 1);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1");
|
||||
}
|
||||
|
||||
TEST(Parser, multipleArtistsInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artists!
|
||||
{ TagType::Artist, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
|
||||
}
|
||||
|
||||
TEST(Parser, multipleArtistsInArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artist!
|
||||
{ TagType::Artists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
|
||||
}
|
||||
} // namespace lms::metadata
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/RatedArtist.hpp"
|
||||
#include "database/RatedRelease.hpp"
|
||||
#include "database/RatedTrack.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/StarredArtist.hpp"
|
||||
@@ -108,6 +111,16 @@ namespace lms::feedback
|
||||
return Artist::findIds(session, searchParams);
|
||||
}
|
||||
|
||||
void FeedbackService::setRating(db::UserId userId, db::ArtistId artistId, std::optional<db::Rating> rating)
|
||||
{
|
||||
setRating<Artist, ArtistId, RatedArtist>(userId, artistId, rating);
|
||||
}
|
||||
|
||||
std::optional<db::Rating> FeedbackService::getRating(db::UserId userId, db::ArtistId artistId)
|
||||
{
|
||||
return getRating<Artist, ArtistId, RatedArtist>(userId, artistId);
|
||||
}
|
||||
|
||||
void FeedbackService::star(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
@@ -148,6 +161,16 @@ namespace lms::feedback
|
||||
return Release::findIds(session, searchParams);
|
||||
}
|
||||
|
||||
void FeedbackService::setRating(db::UserId userId, db::ReleaseId releaseId, std::optional<db::Rating> rating)
|
||||
{
|
||||
setRating<Release, ReleaseId, RatedRelease>(userId, releaseId, rating);
|
||||
}
|
||||
|
||||
std::optional<db::Rating> FeedbackService::getRating(db::UserId userId, db::ReleaseId releaseId)
|
||||
{
|
||||
return getRating<Release, ReleaseId, RatedRelease>(userId, releaseId);
|
||||
}
|
||||
|
||||
void FeedbackService::star(UserId userId, TrackId trackId)
|
||||
{
|
||||
star<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
@@ -187,4 +210,14 @@ namespace lms::feedback
|
||||
|
||||
return Track::findIds(session, searchParams);
|
||||
}
|
||||
|
||||
void FeedbackService::setRating(db::UserId userId, db::TrackId trackId, std::optional<db::Rating> rating)
|
||||
{
|
||||
setRating<db::Track, db::TrackId, db::RatedTrack>(userId, trackId, rating);
|
||||
}
|
||||
|
||||
std::optional<db::Rating> FeedbackService::getRating(db::UserId userId, db::TrackId trackId)
|
||||
{
|
||||
return getRating<db::Track, db::TrackId, db::RatedTrack>(userId, trackId);
|
||||
}
|
||||
} // namespace lms::feedback
|
||||
|
||||
@@ -49,18 +49,28 @@ namespace lms::feedback
|
||||
Wt::WDateTime getStarredDateTime(db::UserId userId, db::ArtistId artistId) override;
|
||||
ArtistContainer findStarredArtists(const ArtistFindParameters& params) override;
|
||||
|
||||
void setRating(db::UserId userId, db::ArtistId artistId, std::optional<db::Rating> rating) override;
|
||||
std::optional<db::Rating> getRating(db::UserId userId, db::ArtistId artistId) override;
|
||||
|
||||
void star(db::UserId userId, db::ReleaseId releaseId) override;
|
||||
void unstar(db::UserId userId, db::ReleaseId releaseId) override;
|
||||
bool isStarred(db::UserId userId, db::ReleaseId releasedId) override;
|
||||
Wt::WDateTime getStarredDateTime(db::UserId userId, db::ReleaseId releasedId) override;
|
||||
ReleaseContainer findStarredReleases(const FindParameters& params) override;
|
||||
|
||||
void setRating(db::UserId userId, db::ReleaseId releaseId, std::optional<db::Rating> rating) override;
|
||||
std::optional<db::Rating> getRating(db::UserId userId, db::ReleaseId releaseId) override;
|
||||
|
||||
void star(db::UserId userId, db::TrackId trackId) override;
|
||||
void unstar(db::UserId userId, db::TrackId trackId) override;
|
||||
bool isStarred(db::UserId userId, db::TrackId trackId) override;
|
||||
Wt::WDateTime getStarredDateTime(db::UserId userId, db::TrackId trackId) override;
|
||||
TrackContainer findStarredTracks(const FindParameters& params) override;
|
||||
|
||||
void setRating(db::UserId userId, db::TrackId trackId, std::optional<db::Rating> rating) override;
|
||||
std::optional<db::Rating> getRating(db::UserId userId, db::TrackId trackId) override;
|
||||
|
||||
private:
|
||||
std::optional<db::FeedbackBackend> getUserFeedbackBackend(db::UserId userId);
|
||||
|
||||
template<typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
@@ -72,6 +82,12 @@ namespace lms::feedback
|
||||
template<typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
Wt::WDateTime getStarredDateTime(db::UserId userId, ObjIdType id);
|
||||
|
||||
template<typename ObjType, typename ObjIdType, typename RatedObjType>
|
||||
void setRating(db::UserId userId, ObjIdType objectId, std::optional<db::Rating> rating);
|
||||
|
||||
template<typename ObjType, typename ObjIdType, typename RatedObjType>
|
||||
std::optional<db::Rating> getRating(db::UserId userId, ObjIdType objectId);
|
||||
|
||||
db::Db& _db;
|
||||
std::unordered_map<db::FeedbackBackend, std::unique_ptr<IFeedbackBackend>> _backends;
|
||||
};
|
||||
|
||||
@@ -102,4 +102,46 @@ namespace lms::feedback
|
||||
return {};
|
||||
}
|
||||
|
||||
template<typename ObjType, typename ObjIdType, typename RatedObjType>
|
||||
void FeedbackService::setRating(db::UserId userId, ObjIdType objectId, std::optional<db::Rating> rating)
|
||||
{
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
typename RatedObjType::pointer ratedObject{ RatedObjType::find(session, objectId, userId) };
|
||||
if (rating)
|
||||
{
|
||||
if (!ratedObject)
|
||||
{
|
||||
typename ObjType::pointer obj{ ObjType::find(session, objectId) };
|
||||
const User::pointer user{ User::find(session, userId) };
|
||||
|
||||
if (!obj || !user)
|
||||
return;
|
||||
|
||||
ratedObject = session.create<RatedObjType>(obj, user);
|
||||
}
|
||||
|
||||
ratedObject.modify()->setRating(*rating);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ratedObject)
|
||||
ratedObject.remove();
|
||||
}
|
||||
}
|
||||
|
||||
template<typename ObjType, typename ObjIdType, typename RatedObjType>
|
||||
std::optional<db::Rating> FeedbackService::getRating(db::UserId userId, ObjIdType objectId)
|
||||
{
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const typename RatedObjType::pointer ratedObj{ RatedObjType::find(session, objectId, userId) };
|
||||
if (!ratedObj)
|
||||
return std::nullopt;
|
||||
|
||||
return ratedObj->getRating();
|
||||
}
|
||||
|
||||
} // namespace lms::feedback
|
||||
@@ -109,6 +109,9 @@ namespace lms::feedback
|
||||
virtual Wt::WDateTime getStarredDateTime(db::UserId userId, db::ArtistId artistId) = 0;
|
||||
virtual ArtistContainer findStarredArtists(const ArtistFindParameters& params) = 0;
|
||||
|
||||
virtual void setRating(db::UserId userId, db::ArtistId artistId, std::optional<db::Rating> rating) = 0;
|
||||
virtual std::optional<db::Rating> getRating(db::UserId userId, db::ArtistId artistId) = 0;
|
||||
|
||||
// Releases
|
||||
virtual void star(db::UserId userId, db::ReleaseId releaseId) = 0;
|
||||
virtual void unstar(db::UserId userId, db::ReleaseId releaseId) = 0;
|
||||
@@ -116,12 +119,18 @@ namespace lms::feedback
|
||||
virtual Wt::WDateTime getStarredDateTime(db::UserId userId, db::ReleaseId artistId) = 0;
|
||||
virtual ReleaseContainer findStarredReleases(const FindParameters& params) = 0;
|
||||
|
||||
virtual void setRating(db::UserId userId, db::ReleaseId releaseId, std::optional<db::Rating> rating) = 0;
|
||||
virtual std::optional<db::Rating> getRating(db::UserId userId, db::ReleaseId releaseId) = 0;
|
||||
|
||||
// Tracks
|
||||
virtual void star(db::UserId userId, db::TrackId trackId) = 0;
|
||||
virtual void unstar(db::UserId userId, db::TrackId trackId) = 0;
|
||||
virtual bool isStarred(db::UserId userId, db::TrackId artistId) = 0;
|
||||
virtual Wt::WDateTime getStarredDateTime(db::UserId userId, db::TrackId artistId) = 0;
|
||||
virtual TrackContainer findStarredTracks(const FindParameters& params) = 0;
|
||||
|
||||
virtual void setRating(db::UserId userId, db::TrackId trackId, std::optional<db::Rating> rating) = 0;
|
||||
virtual std::optional<db::Rating> getRating(db::UserId userId, db::TrackId trackId) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IFeedbackService> createFeedbackService(boost::asio::io_service& ioService, db::Db& db);
|
||||
|
||||
@@ -710,6 +710,7 @@ namespace lms::scanner
|
||||
track.modify()->setHasCover(trackMetadata->hasCover);
|
||||
track.modify()->setCopyright(trackMetadata->copyright);
|
||||
track.modify()->setCopyrightURL(trackMetadata->copyrightURL);
|
||||
track.modify()->setComment(!trackMetadata->comments.empty() ? trackMetadata->comments.front() : ""); // only take the first one for now
|
||||
track.modify()->setTrackReplayGain(trackMetadata->replayGain);
|
||||
track.modify()->setArtistDisplayName(trackMetadata->artistDisplayName);
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ namespace lms::api::subsonic
|
||||
// Media annotation
|
||||
{ "/star", { handleStarRequest } },
|
||||
{ "/unstar", { handleUnstarRequest } },
|
||||
{ "/setRating", { handleNotImplemented } },
|
||||
{ "/setRating", { handleSetRating } },
|
||||
{ "/scrobble", { handleScrobble } },
|
||||
|
||||
// Sharing
|
||||
|
||||
@@ -580,23 +580,23 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
const auto artists{ Artist::find(context.dbSession, artistName) };
|
||||
if (artists.size() != 1)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& topSongs{ response.createNode("topSongs") };
|
||||
|
||||
scrobbling::IScrobblingService::FindParameters params;
|
||||
params.setUser(context.user->getId());
|
||||
params.setRange(db::Range{ 0, count });
|
||||
params.setArtist(artists.front()->getId());
|
||||
|
||||
const auto trackIds{ core::Service<scrobbling::IScrobblingService>::get()->getTopTracks(params) };
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
const auto artists{ Artist::find(context.dbSession, artistName) };
|
||||
if (artists.size() == 1)
|
||||
{
|
||||
if (Track::pointer track{ Track::find(context.dbSession, trackId) })
|
||||
topSongs.addArrayChild("song", createSongNode(context, track, context.user));
|
||||
scrobbling::IScrobblingService::FindParameters params;
|
||||
params.setUser(context.user->getId());
|
||||
params.setRange(db::Range{ 0, count });
|
||||
params.setArtist(artists.front()->getId());
|
||||
|
||||
const auto trackIds{ core::Service<scrobbling::IScrobblingService>::get()->getTopTracks(params) };
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
if (Track::pointer track{ Track::find(context.dbSession, trackId) })
|
||||
topSongs.addArrayChild("song", createSongNode(context, track, context.user));
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "MediaAnnotation.hpp"
|
||||
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "core/Service.hpp"
|
||||
@@ -77,6 +78,37 @@ namespace lms::api::subsonic
|
||||
return res ? res->getId() : ReleaseId{};
|
||||
}
|
||||
|
||||
struct RatingParameters
|
||||
{
|
||||
std::variant<ArtistId, ReleaseId, TrackId, DirectoryId> id;
|
||||
std::optional<Rating> rating;
|
||||
};
|
||||
|
||||
RatingParameters getRatingParameters(const Wt::Http::ParameterMap& parameters)
|
||||
{
|
||||
RatingParameters res;
|
||||
|
||||
if (const auto artistId{ getParameterAs<ArtistId>(parameters, "id") })
|
||||
res.id = *artistId;
|
||||
else if (const auto releaseId{ getParameterAs<ReleaseId>(parameters, "id") })
|
||||
res.id = *releaseId;
|
||||
else if (const auto trackId{ getParameterAs<TrackId>(parameters, "id") })
|
||||
res.id = *trackId;
|
||||
else if (const auto directoryId{ getParameterAs<DirectoryId>(parameters, "id") })
|
||||
res.id = *directoryId;
|
||||
else
|
||||
throw RequiredParameterMissingError{ "id" };
|
||||
|
||||
const int rating = getMandatoryParameterAs<int>(parameters, "rating"); // The rating between 1 and 5 (inclusive), or 0 to remove the rating
|
||||
if (rating < 0 || rating > 5)
|
||||
throw BadParameterGenericError{ "rating must be 0 or in range 1-5" };
|
||||
|
||||
if (rating > 0)
|
||||
res.rating = rating;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Response handleStarRequest(RequestContext& context)
|
||||
@@ -103,7 +135,7 @@ namespace lms::api::subsonic
|
||||
|
||||
Response handleUnstarRequest(RequestContext& context)
|
||||
{
|
||||
StarParameters params{ getStarParameters(context.parameters) };
|
||||
const StarParameters params{ getStarParameters(context.parameters) };
|
||||
|
||||
for (const DirectoryId id : params.directoryIds)
|
||||
{
|
||||
@@ -123,6 +155,25 @@ namespace lms::api::subsonic
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleSetRating(RequestContext& context)
|
||||
{
|
||||
const RatingParameters params{ getRatingParameters(context.parameters) };
|
||||
|
||||
if (const ArtistId * artistId{ std::get_if<ArtistId>(¶ms.id) })
|
||||
core::Service<feedback::IFeedbackService>::get()->setRating(context.user->getId(), *artistId, params.rating);
|
||||
else if (const DirectoryId * directoryId{ std::get_if<DirectoryId>(¶ms.id) })
|
||||
{
|
||||
if (const ReleaseId releaseId{ getReleaseFromDirectory(context.dbSession, *directoryId) }; releaseId.isValid())
|
||||
core::Service<feedback::IFeedbackService>::get()->setRating(context.user->getId(), releaseId, params.rating);
|
||||
}
|
||||
else if (const ReleaseId * releaseId{ std::get_if<ReleaseId>(¶ms.id) })
|
||||
core::Service<feedback::IFeedbackService>::get()->setRating(context.user->getId(), *releaseId, params.rating);
|
||||
else if (const TrackId * trackId{ std::get_if<TrackId>(¶ms.id) })
|
||||
core::Service<feedback::IFeedbackService>::get()->setRating(context.user->getId(), *trackId, params.rating);
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleScrobble(RequestContext& context)
|
||||
{
|
||||
const std::vector<TrackId> ids{ getMandatoryMultiParametersAs<TrackId>(context.parameters, "id") };
|
||||
|
||||
@@ -26,5 +26,6 @@ namespace lms::api::subsonic
|
||||
{
|
||||
Response handleStarRequest(RequestContext& context);
|
||||
Response handleUnstarRequest(RequestContext& context);
|
||||
Response handleSetRating(RequestContext& context);
|
||||
Response handleScrobble(RequestContext& context);
|
||||
} // namespace lms::api::subsonic
|
||||
@@ -116,6 +116,10 @@ namespace lms::api::subsonic
|
||||
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), release->getId()) }; dateTime.isValid())
|
||||
albumNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
||||
|
||||
// Always report user rating, even if legacy API only specified it for directories
|
||||
if (const auto rating{ core::Service<feedback::IFeedbackService>::get()->getRating(context.user->getId(), release->getId()) })
|
||||
albumNode.setAttribute("userRating", *rating);
|
||||
|
||||
if (!context.enableOpenSubsonic)
|
||||
return albumNode;
|
||||
|
||||
|
||||
@@ -103,6 +103,9 @@ namespace lms::api::subsonic
|
||||
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), artist->getId()) }; dateTime.isValid())
|
||||
artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
||||
|
||||
if (const auto rating{ core::Service<feedback::IFeedbackService>::get()->getRating(context.user->getId(), artist->getId()) })
|
||||
artistNode.setAttribute("userRating", *rating);
|
||||
|
||||
// OpenSubsonic specific fields (must always be set)
|
||||
if (context.enableOpenSubsonic)
|
||||
{
|
||||
|
||||
@@ -133,6 +133,8 @@ namespace lms::api::subsonic
|
||||
trackResponse.setAttribute("type", "music");
|
||||
trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getLastWritten()));
|
||||
trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension()));
|
||||
if (const auto rating{ core::Service<feedback::IFeedbackService>::get()->getRating(context.user->getId(), track->getId()) })
|
||||
trackResponse.setAttribute("userRating", *rating);
|
||||
|
||||
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), track->getId()) }; dateTime.isValid())
|
||||
trackResponse.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
||||
@@ -153,6 +155,7 @@ namespace lms::api::subsonic
|
||||
if (!context.enableOpenSubsonic)
|
||||
return trackResponse;
|
||||
|
||||
trackResponse.setAttribute("comment", track->getComment());
|
||||
trackResponse.setAttribute("bitDepth", track->getBitsPerSample());
|
||||
trackResponse.setAttribute("samplingRate", track->getSampleRate());
|
||||
trackResponse.setAttribute("channelCount", track->getChannelCount());
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace lms::ui::TrackListHelpers
|
||||
{
|
||||
std::unique_ptr<Wt::WContainerWidget> artistContainer{ utils::createArtistAnchorList(std::vector(std::cbegin(artistIds), std::cend(artistIds))) };
|
||||
auto artistsEntry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists")) };
|
||||
artistsEntry->bindString("type", role);
|
||||
artistsEntry->bindString("type", role, Wt::TextFormat::Plain);
|
||||
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
|
||||
artistTable->addWidget(std::move(artistsEntry));
|
||||
}
|
||||
@@ -124,7 +124,7 @@ namespace lms::ui::TrackListHelpers
|
||||
if (audioStream)
|
||||
{
|
||||
trackInfo->setCondition("if-has-codec", true);
|
||||
trackInfo->bindString("codec", audioStream->codecName);
|
||||
trackInfo->bindString("codec", audioStream->codecName, Wt::TextFormat::Plain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,12 @@ namespace lms::ui::TrackListHelpers
|
||||
|
||||
trackInfo->bindInt("playcount", core::Service<scrobbling::IScrobblingService>::get()->getCount(LmsApp->getUserId(), track->getId()));
|
||||
|
||||
if (std::string_view comment{ track->getComment() }; !comment.empty())
|
||||
{
|
||||
trackInfo->setCondition("if-has-comment", true);
|
||||
trackInfo->bindString("comment", Wt::WString::fromUTF8(std::string{ comment }), Wt::TextFormat::Plain);
|
||||
}
|
||||
|
||||
Wt::WContainerWidget* clusterContainer{ trackInfo->bindWidget("clusters", utils::createFilterClustersForTrack(track, filters)) };
|
||||
if (clusterContainer->count() > 0)
|
||||
trackInfo->setCondition("if-has-clusters", true);
|
||||
|
||||
@@ -216,6 +216,9 @@ namespace lms::metadata
|
||||
if (!track->copyright.empty())
|
||||
std::cout << "Copyright: " << track->copyright << std::endl;
|
||||
|
||||
for (const auto& comment : track->comments)
|
||||
std::cout << "Comment: '" << comment << "'" << std::endl;
|
||||
|
||||
if (!track->copyrightURL.empty())
|
||||
std::cout << "CopyrightURL: " << track->copyrightURL << std::endl;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user