Merge branch 'develop' for release v3.57.0

This commit is contained in:
emeric
2024-08-30 21:37:20 +02:00
42 changed files with 1218 additions and 48 deletions
+1 -1
View File
@@ -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
View File
@@ -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`
+4
View File
@@ -125,6 +125,10 @@
${playcount}
</div>
</div>
${<if-has-comment>}
<hr/>
<pre>${comment}</pre>
${</if-has-comment>}
</div>
</div>
<div class="modal-footer">
+3
View File
@@ -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
+53 -1
View File
@@ -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{};
+78
View File
@@ -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
+78
View File
@@ -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
+78
View File
@@ -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
+11 -1
View File
@@ -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,
+3
View File
@@ -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
+6
View File
@@ -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{}));
+55
View File
@@ -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
+55
View File
@@ -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
+55
View File
@@ -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
+20
View File
@@ -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
+21 -5
View File
@@ -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;
+68
View File
@@ -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);
+1 -1
View File
@@ -206,7 +206,7 @@ namespace lms::api::subsonic
// Media annotation
{ "/star", { handleStarRequest } },
{ "/unstar", { handleUnstarRequest } },
{ "/setRating", { handleNotImplemented } },
{ "/setRating", { handleSetRating } },
{ "/scrobble", { handleScrobble } },
// Sharing
+13 -13
View File
@@ -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>(&params.id) })
core::Service<feedback::IFeedbackService>::get()->setRating(context.user->getId(), *artistId, params.rating);
else if (const DirectoryId * directoryId{ std::get_if<DirectoryId>(&params.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>(&params.id) })
core::Service<feedback::IFeedbackService>::get()->setRating(context.user->getId(), *releaseId, params.rating);
else if (const TrackId * trackId{ std::get_if<TrackId>(&params.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());
+8 -2
View File
@@ -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);
+3
View File
@@ -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;