Merge branch 'develop' for release v3.42.0

This commit is contained in:
emeric
2023-10-22 14:26:02 +02:00
61 changed files with 2847 additions and 2602 deletions
+5
View File
@@ -11,19 +11,24 @@ OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'l
## Extra fields
The following extra fields are implemented:
* `Album` response:
* `played`
* `musicBrainzId`
* `genres`
* `artists`
* `displayArtist`
* `releaseTypes`
* `moods`
* `originalReleaseDate`
* `isCompilation`
* `discTitles`: discs with no subtitle are omitted
* `Child` response:
* `played`
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
* `genres`
* `artists`
* `displayArtist`
* `albumArtists`
* `displayAlbumArtist`
* `contributors`
* `moods`
* `replayGain`
+2 -1
View File
@@ -241,6 +241,8 @@
<message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
<message id="Lms.Settings.change-password">Change password</message>
<message id="Lms.Settings.default-transcode-bitrate">Default transcode bitrate</message>
<message id="Lms.Settings.default-transcode-format">Default transcode format</message>
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
<message id="Lms.Settings.password-bad">Bad password</message>
@@ -265,7 +267,6 @@
<message id="Lms.Settings.subsonic-api">Subsonic API</message>
<message id="Lms.Settings.transcode">Transcoding</message>
<message id="Lms.Settings.transcode-bitrate">Transcode bitrate</message>
<message id="Lms.Settings.transcode-enable">Enable transcoding</message>
<message id="Lms.Settings.transcode-format">Transcode format</message>
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
+2 -1
View File
@@ -241,6 +241,8 @@
<message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message>
<message id="Lms.Settings.change-password">Changement de mot de passe</message>
<message id="Lms.Settings.default-transcode-bitrate">Bitrate par defaut du transcodage</message>
<message id="Lms.Settings.default-transcode-format">Format par defaut du transcodage</message>
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
@@ -265,7 +267,6 @@
<message id="Lms.Settings.subsonic-api">API Subsonic</message>
<message id="Lms.Settings.transcode">Transcodage</message>
<message id="Lms.Settings.transcode-bitrate">Bitrate du transcodage</message>
<message id="Lms.Settings.transcode-enable">Activer le transcodage</message>
<message id="Lms.Settings.transcode-format">Format du transcodage</message>
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
+2 -1
View File
@@ -241,6 +241,8 @@
<message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message>
<message id="Lms.Settings.change-password">Cambia password</message>
<message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
<message id="Lms.Settings.password-bad">Password errata</message>
@@ -265,7 +267,6 @@
<message id="Lms.Settings.subsonic-api">Subsonic API</message>
<message id="Lms.Settings.transcode">Transcodifica</message>
<message id="Lms.Settings.transcode-bitrate">Bitrate transcodifica</message>
<message id="Lms.Settings.transcode-enable">Abilita transcodifica</message>
<message id="Lms.Settings.transcode-format">Formato di transcodifica</message>
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
+2 -1
View File
@@ -241,6 +241,8 @@
<message id="Lms.Settings.audio">音频</message>
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
<message id="Lms.Settings.change-password">更改密码</message>
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
<message id="Lms.Settings.password-bad">无效密码</message>
@@ -265,7 +267,6 @@
<message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message>
<message id="Lms.Settings.transcode">转码</message>
<message id="Lms.Settings.transcode-bitrate">转码比特率</message>
<message id="Lms.Settings.transcode-enable">开启转码</message>
<message id="Lms.Settings.transcode-format">转码格式</message>
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
-1
View File
@@ -14,7 +14,6 @@
<div class="col">
<h2>${name}</h2>
${<if-has-release-artists>}${artists}${</if-has-release-artists>}
${<if-has-various-release-artists>}<div class="text-secondary">${tr:Lms.Explore.various-artists}</div>${</if-has-various-release-artists>}
<div class="small text-muted">${<if-has-year>}${year} · ${</if-has-year>}${duration}</div>
${clusters class="mb-2"}
<div class="btn-group">
+2 -3
View File
@@ -40,12 +40,11 @@
<message id="Lms.Explore.Releases.template.entry-grid">
<div class="col h-100 Lms-bg-dark-hover rounded">
<div class="p-2">
<div class="Lms-responsive-square Lms-cover-container">
<div class="Lms-responsive-square Lms-cover-container mb-1">
${cover class="shadow-sm"}
</div>
${release-name class="d-block text-truncate text-nowrap text-decoration-none link-success"}
${<if-has-artist>}${artist-name class="d-block text-truncate text-nowrap text-decoration-none link-secondary"}${</if-has-artist>}
${<if-has-various-artists>}<div class="text-truncate text-nowrap text-secondary">${tr:Lms.Explore.various-artists}</div>${</if-has-various-artists>}
${<if-has-artist>}${artist-name class="d-block text-truncate text-nowrap"}${</if-has-artist>}
${<if-has-year>}<div class="small text-muted">${year}</div>${</if-has-year>}
</div>
</div>
+3 -14
View File
@@ -74,20 +74,9 @@
</div>
${<if-has-subsonic-api>}
<legend>${tr:Lms.Settings.subsonic-api}</legend>
<div class="col-12">
<div class="form-check">
${subsonic-transcode-enable class="form-check-input"}
<label class="form-check-label" for="${id:subsonic-transcode-enable}">
${tr:Lms.Settings.transcode-enable class="form-control"}
</label>
<div class="invalid-feedback">
${subsonic-transcode-enable-info}
</div>
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:subsonic-transcode-format}">
${tr:Lms.Settings.transcode-format}
${tr:Lms.Settings.default-transcode-format}
</label>
${subsonic-transcode-format class="form-control"}
<div class="invalid-feedback">
@@ -95,8 +84,8 @@
</div>
</div>
<div class="col-lg-6">
<label class="col-lg-3 form-label" for="${id:subsonic-transcode-bitrate}">
${tr:Lms.Settings.transcode-bitrate}
<label class="form-label" for="${id:subsonic-transcode-bitrate}">
${tr:Lms.Settings.default-transcode-bitrate}
</label>
<div class="input-group mb-3">
${subsonic-transcode-bitrate class="form-control"}
+3
View File
@@ -210,6 +210,7 @@ getRelease(const TagMap& tags)
release.emplace();
release->name = std::move(*releaseName);
release->artistDisplayName = getPropertyValueAs<std::string_view>(tags, "ALBUMARTIST").value_or("");
release->mbid = getPropertyValueFirstMatchAs<UUID>(tags, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"});
release->artists = getArtists(tags, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"});
release->mediumCount = getPropertyValueAs<std::size_t>(tags, "DISCTOTAL");
@@ -353,6 +354,8 @@ TagLibParser::processTag(Track& track, const std::string& tag, const std::vector
track.copyrightURL = value;
else if (tag == "REPLAYGAIN_TRACK_GAIN")
track.replayGain = StringUtils::readAs<float>(value);
else if (tag == "ARTIST")
track.artistDisplayName = value;
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::set<std::string> clusterNames;
@@ -79,6 +79,7 @@ namespace MetaData
std::optional<UUID> mbid;
std::string name;
std::string artistDisplayName;
std::vector<Artist> artists;
std::optional<std::size_t> mediumCount;
std::optional<PrimaryType> primaryType;
@@ -117,6 +118,7 @@ namespace MetaData
std::string copyright;
std::string copyrightURL;
std::optional<float> replayGain;
std::string artistDisplayName;
std::vector<Artist> artists;
std::vector<Artist> conductorArtists;
std::vector<Artist> composerArtists;
+3 -3
View File
@@ -32,11 +32,11 @@ namespace Database
{
namespace
{
Wt::Dbo::Query<ClusterId> createQuery(Session& session, const Cluster::FindParameters& params)
Wt::Dbo::Query<Cluster::ClusterFindResult> createQuery(Session& session, const Cluster::FindParameters& params)
{
session.checkSharedLocked();
auto query{ session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c") };
auto query{ session.getDboSession().query<Cluster::ClusterFindResult>("SELECT DISTINCT c.id,c.name FROM cluster c") };
if (params.track.isValid() || params.release.isValid())
{
@@ -74,7 +74,7 @@ namespace Database
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
}
RangeResults<ClusterId> Cluster::find(Session& session, const FindParameters& params)
RangeResults<Cluster::ClusterFindResult> Cluster::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query{ createQuery(session, params) };
+76 -55
View File
@@ -26,73 +26,94 @@
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
namespace Database {
// Session living class handling the database and the login
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
namespace Database
{
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
namespace
{
class Connection : public Wt::Dbo::backend::Sqlite3
{
public:
Connection(const std::filesystem::path& dbPath)
: Wt::Dbo::backend::Sqlite3{ dbPath.string() }
, _dbPath{ dbPath }
{
prepare();
}
std::unique_ptr<Wt::Dbo::backend::Sqlite3> connection {std::make_unique<Wt::Dbo::backend::Sqlite3>(dbPath.string())};
// connection->setProperty("show-queries", "true");
connection->executeSql("pragma journal_mode=WAL");
connection->executeSql("pragma synchronous=normal");
private:
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
auto connectionPool = std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount);
connectionPool->setTimeout(std::chrono::seconds(10));
std::unique_ptr<SqlConnection> clone() const override
{
return std::make_unique<Connection>(_dbPath);
}
_connectionPool = std::move(connectionPool);
}
void prepare()
{
LMS_LOG(DB, DEBUG) << "Setting per-connection settings...";
executeSql("pragma journal_mode=WAL");
executeSql("pragma synchronous=normal");
executeSql("pragma analysis_limit=1000"); // to help make analyze command faster
LMS_LOG(DB, DEBUG) << "Setting per-connection settings done!";
}
Db::~Db()
{
LMS_LOG(DB, DEBUG) << "Optimizing db...";
executeSql("pragma optimize");
LMS_LOG(DB, DEBUG) << "Optimizing db DONE";
}
std::filesystem::path _dbPath;
};
}
void
Db::executeSql(const std::string& sql)
{
ScopedConnection connection {*_connectionPool};
connection->executeSql(sql);
}
// Session living class handling the database and the login
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
{
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
Session&
Db::getTLSSession()
{
static thread_local Session* tlsSession {};
auto connection{ std::make_unique<Connection>(dbPath.string()) };
// connection->setProperty("show-queries", "true");
if (!tlsSession)
{
auto newSession {std::make_unique<Session>(*this)};
tlsSession = newSession.get();
auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) };
connectionPool->setTimeout(std::chrono::seconds{ 10 });
{
std::scoped_lock lock {_tlsSessionsMutex};
_tlsSessions.push_back(std::move(newSession));
}
}
_connectionPool = std::move(connectionPool);
}
return *tlsSession;
}
void Db::executeSql(const std::string& sql)
{
ScopedConnection connection{ *_connectionPool };
connection->executeSql(sql);
}
Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool)
: _connectionPool {pool}
, _connection {_connectionPool.getConnection()}
{
}
Session& Db::getTLSSession()
{
static thread_local Session* tlsSession{};
Db::ScopedConnection::~ScopedConnection()
{
_connectionPool.returnConnection(std::move(_connection));
}
if (!tlsSession)
{
auto newSession{ std::make_unique<Session>(*this) };
tlsSession = newSession.get();
Wt::Dbo::SqlConnection* Db::ScopedConnection::operator->() const
{
return _connection.get();
}
{
std::scoped_lock lock{ _tlsSessionsMutex };
_tlsSessions.push_back(std::move(newSession));
}
}
return *tlsSession;
}
Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool)
: _connectionPool{ pool }
, _connection{ _connectionPool.getConnection() }
{
}
Db::ScopedConnection::~ScopedConnection()
{
_connectionPool.returnConnection(std::move(_connection));
}
Wt::Dbo::SqlConnection* Db::ScopedConnection::operator->() const
{
return _connection.get();
}
} // namespace Database
+191 -207
View File
@@ -27,261 +27,245 @@
namespace
{
using namespace Database;
using namespace Database;
Wt::Dbo::Query<ArtistId>
createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
{
auto query {session.query<ArtistId>("SELECT a.id from artist a")
.join("track t ON t.id = t_a_l.track_id")
.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id")
.join("listen l ON l.track_id = t.id")
.where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler)};
Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
{
auto query{ session.query<ArtistId>("SELECT a.id from artist a")
.join("track t ON t.id = t_a_l.track_id")
.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id")
.join("listen l ON l.track_id = t.id")
.where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler) };
if (linkType)
query.where("t_a_l.type = ?").bind(*linkType);
if (linkType)
query.where("t_a_l.type = ?").bind(*linkType);
if (!clusterIds.empty())
{
std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
if (!clusterIds.empty())
{
std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (auto id : clusterIds)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(id);
}
WhereClause clusterClause;
for (auto id : clusterIds)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(id);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
oss << " " << clusterClause.get();
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
query.where(oss.str());
}
query.where(oss.str());
}
return query;
}
return query;
}
Wt::Dbo::Query<ReleaseId>
createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
{
auto query {session.query<ReleaseId>("SELECT r.id from release r")
.join("track t ON t.release_id = r.id")
.join("listen l ON l.track_id = t.id")
.where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler)};
Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
{
auto query{ session.query<ReleaseId>("SELECT r.id from release r")
.join("track t ON t.release_id = r.id")
.join("listen l ON l.track_id = t.id")
.where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler) };
if (!clusterIds.empty())
{
std::ostringstream oss;
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
if (!clusterIds.empty())
{
std::ostringstream oss;
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (ClusterId id : clusterIds)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(id);
}
WhereClause clusterClause;
for (ClusterId id : clusterIds)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(id);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
query.where(oss.str());
}
query.where(oss.str());
}
return query;
}
return query;
}
Wt::Dbo::Query<TrackId>
createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
{
auto query {session.query<TrackId>("SELECT t.id from track t")
.join("listen l ON l.track_id = t.id")
.where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler)};
Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
{
auto query{ session.query<TrackId>("SELECT t.id from track t")
.join("listen l ON l.track_id = t.id")
.where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler) };
if (!clusterIds.empty())
{
std::ostringstream oss;
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
if (!clusterIds.empty())
{
std::ostringstream oss;
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
WhereClause clusterClause;
for (auto id : clusterIds)
{
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
query.bind(id);
}
WhereClause clusterClause;
for (auto id : clusterIds)
{
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
query.bind(id);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
query.where(oss.str());
}
query.where(oss.str());
}
return query;
}
return query;
}
}
namespace Database
{
Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())}
, _scrobbler {scrobbler}
, _user {getDboPtr(user)}
, _track {getDboPtr(track)}
{}
Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
: _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) }
, _scrobbler{ scrobbler }
, _user{ getDboPtr(user) }
, _track{ getDboPtr(track) }
{}
Listen::pointer
Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
{
return session.getDboSession().add(std::unique_ptr<Listen> {new Listen {user, track, scrobbler, dateTime}});
}
Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
{
return session.getDboSession().add(std::unique_ptr<Listen> {new Listen{ user, track, scrobbler, dateTime }});
}
std::size_t
Listen::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen");
}
std::size_t Listen::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen");
}
Listen::pointer
Listen::find(Session& session, ListenId id)
{
session.checkSharedLocked();
return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue();
}
Listen::pointer Listen::find(Session& session, ListenId id)
{
session.checkSharedLocked();
return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue();
}
RangeResults<ListenId>
Listen::find(Session& session, const FindParameters& parameters)
{
session.checkSharedLocked();
RangeResults<ListenId> Listen::find(Session& session, const FindParameters& parameters)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<ListenId>("SELECT id FROM listen")
.orderBy("date_time")};
auto query{ session.getDboSession().query<ListenId>("SELECT id FROM listen")
.orderBy("date_time") };
if (parameters.user.isValid())
query.where("user_id = ?").bind(parameters.user);
if (parameters.user.isValid())
query.where("user_id = ?").bind(parameters.user);
if (parameters.scrobbler)
query.where("scrobbler = ?").bind(*parameters.scrobbler);
if (parameters.scrobbler)
query.where("scrobbler = ?").bind(*parameters.scrobbler);
if (parameters.scrobblingState)
query.where("scrobbling_state = ?").bind(*parameters.scrobblingState);
if (parameters.scrobblingState)
query.where("scrobbling_state = ?").bind(*parameters.scrobblingState);
return Utils::execQuery(query, parameters.range);
}
return Utils::execQuery(query, parameters.range);
}
Listen::pointer
Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
{
session.checkSharedLocked();
Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
{
session.checkSharedLocked();
return session.getDboSession().find<Listen>()
.where("user_id = ?").bind(userId)
.where("track_id = ?").bind(trackId)
.where("scrobbler = ?").bind(scrobbler)
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
.resultValue();
}
return session.getDboSession().find<Listen>()
.where("user_id = ?").bind(userId)
.where("track_id = ?").bind(trackId)
.where("scrobbler = ?").bind(scrobbler)
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
.resultValue();
}
RangeResults<ArtistId>
Listen::getTopArtists(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
std::optional<TrackArtistLinkType> linkType,
Range range)
{
auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)};
RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
{
auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) };
auto collection {query
.orderBy("COUNT(a.id) DESC")
.groupBy("a.id")};
auto collection{ query
.orderBy("COUNT(a.id) DESC")
.groupBy("a.id") };
return Utils::execQuery(query, range);
}
return Utils::execQuery(query, range);
}
RangeResults<ReleaseId>
Listen::getTopReleases(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
Range range)
{
auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.orderBy("COUNT(r.id) DESC")
.groupBy("r.id")};
RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
{
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.orderBy("COUNT(r.id) DESC")
.groupBy("r.id") };
return Utils::execQuery(query, range);
}
return Utils::execQuery(query, range);
}
RangeResults<TrackId>
Listen::getTopTracks(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
Range range)
{
auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.orderBy("COUNT(t.id) DESC")
.groupBy("t.id")};
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
{
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.orderBy("COUNT(t.id) DESC")
.groupBy("t.id") };
return Utils::execQuery(query, range);
}
return Utils::execQuery(query, range);
}
RangeResults<ArtistId>
Listen::getRecentArtists(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
std::optional<TrackArtistLinkType> linkType,
Range range)
{
auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC")};
RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
{
auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") };
return Utils::execQuery(query, range);
}
return Utils::execQuery(query, range);
}
RangeResults<ReleaseId>
Listen::getRecentReleases(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
Range range)
{
auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC")};
RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
{
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") };
return Utils::execQuery(query, range);
}
return Utils::execQuery(query, range);
}
RangeResults<TrackId>
Listen::getRecentTracks(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
Range range)
{
auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC")};
RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
{
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") };
return Utils::execQuery(query, range);
}
return Utils::execQuery(query, range);
}
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId)
{
// TODO not pending remove?
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
.join("track t ON l.track_id = t.id")
.where("t.release_id = ?").bind(releaseId)
.where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler)
.orderBy("l.date_time DESC")
.limit(1)
.resultValue();
}
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId trackId)
{
// TODO not pending remove?
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
.join("track t ON track_id = t.id")
.where("t.id = ?").bind(trackId)
.where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler)
.orderBy("l.date_time DESC")
.limit(1)
.resultValue();
}
} // namespace Database
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -23,33 +23,33 @@
namespace Database
{
class Session;
class Session;
using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION {40};
class VersionInfo
{
public:
using pointer = Wt::Dbo::ptr<VersionInfo>;
using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION{ 42 };
class VersionInfo
{
public:
using pointer = Wt::Dbo::ptr<VersionInfo>;
static VersionInfo::pointer getOrCreate(Session& session);
static VersionInfo::pointer get(Session& session);
static VersionInfo::pointer getOrCreate(Session& session);
static VersionInfo::pointer get(Session& session);
Version getVersion() const { return _version; }
void setVersion(Version version) { _version = static_cast<int>(version); }
Version getVersion() const { return _version; }
void setVersion(Version version) { _version = static_cast<int>(version); }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _version, "db_version");
}
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _version, "db_version");
}
private:
int _version {LMS_DATABASE_VERSION};
};
private:
int _version{ LMS_DATABASE_VERSION };
};
namespace Migration
{
void doDbMigration(Session& session);
}
namespace Migration
{
void doDbMigration(Session& session);
}
}
+126 -126
View File
@@ -46,145 +46,145 @@
namespace Database
{
Session::Session(Db& db)
: _db {db}
{
_session.setConnectionPool(_db.getConnectionPool());
Session::Session(Db& db)
: _db{ db }
{
_session.setConnectionPool(_db.getConnectionPool());
_session.mapClass<VersionInfo>("version_info");
_session.mapClass<Artist>("artist");
_session.mapClass<AuthToken>("auth_token");
_session.mapClass<Cluster>("cluster");
_session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Listen>("listen");
_session.mapClass<Release>("release");
_session.mapClass<ScanSettings>("scan_settings");
_session.mapClass<StarredArtist>("starred_artist");
_session.mapClass<StarredRelease>("starred_release");
_session.mapClass<StarredTrack>("starred_track");
_session.mapClass<Track>("track");
_session.mapClass<TrackBookmark>("track_bookmark");
_session.mapClass<TrackArtistLink>("track_artist_link");
_session.mapClass<TrackFeatures>("track_features");
_session.mapClass<TrackList>("tracklist");
_session.mapClass<TrackListEntry>("tracklist_entry");
_session.mapClass<User>("user");
}
_session.mapClass<VersionInfo>("version_info");
_session.mapClass<Artist>("artist");
_session.mapClass<AuthToken>("auth_token");
_session.mapClass<Cluster>("cluster");
_session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Listen>("listen");
_session.mapClass<Release>("release");
_session.mapClass<ScanSettings>("scan_settings");
_session.mapClass<StarredArtist>("starred_artist");
_session.mapClass<StarredRelease>("starred_release");
_session.mapClass<StarredTrack>("starred_track");
_session.mapClass<Track>("track");
_session.mapClass<TrackBookmark>("track_bookmark");
_session.mapClass<TrackArtistLink>("track_artist_link");
_session.mapClass<TrackFeatures>("track_features");
_session.mapClass<TrackList>("tracklist");
_session.mapClass<TrackListEntry>("tracklist_entry");
_session.mapClass<User>("user");
}
UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
: _lock {mutex},
_transaction {session}
{
}
UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
: _lock{ mutex },
_transaction{ session }
{
}
SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
: _lock {mutex},
_transaction {session}
{
}
SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
: _lock{ mutex },
_transaction{ session }
{
}
void
Session::checkUniqueLocked()
{
assert(_db.getMutex().isUniqueLocked());
}
void Session::checkUniqueLocked()
{
assert(_db.getMutex().isUniqueLocked());
}
void
Session::checkSharedLocked()
{
assert(_db.getMutex().isSharedLocked());
}
void Session::checkSharedLocked()
{
assert(_db.getMutex().isSharedLocked());
}
UniqueTransaction
Session::createUniqueTransaction()
{
return UniqueTransaction {_db.getMutex(), _session};
}
UniqueTransaction Session::createUniqueTransaction()
{
return UniqueTransaction{ _db.getMutex(), _session };
}
SharedTransaction
Session::createSharedTransaction()
{
return SharedTransaction {_db.getMutex(), _session};
}
SharedTransaction Session::createSharedTransaction()
{
return SharedTransaction{ _db.getMutex(), _session };
}
void
Session::prepareTables()
{
// Creation case
try
{
_session.createTables();
void Session::prepareTables()
{
LMS_LOG(DB, INFO) << "Preparing tables...";
LMS_LOG(DB, INFO) << "Tables created";
}
catch (Wt::Dbo::Exception& e)
{
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
}
// Initial creation case
try
{
_session.createTables();
LMS_LOG(DB, INFO) << "Tables created";
}
catch (Wt::Dbo::Exception& e)
{
LMS_LOG(DB, DEBUG) << "Cannot create tables: " << e.what();
if (std::string_view{ e.what() }.find("already exists") == std::string_view::npos)
{
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
throw e;
}
}
Migration::doDbMigration(*this);
Migration::doDbMigration(*this);
// Indexes
{
auto uniqueTransaction {createUniqueTransaction()};
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
_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)");
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_scrobbler_idx ON listen(scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_scrobbler_idx ON listen(user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_scrobbler_date_time_idx ON listen(user_id,track_id,scrobbler,date_time)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_scrobbler_idx ON starred_artist(user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_scrobbler_idx ON starred_artist(artist_id,user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_scrobbler_idx ON starred_release(user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_scrobbler_idx ON starred_release(release_id,user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_scrobbler_idx ON starred_track(track_id,user_id,scrobbler)");
}
// Indexes
{
auto uniqueTransaction{ createUniqueTransaction() };
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
_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)");
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_scrobbler_idx ON listen(scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_scrobbler_idx ON listen(user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_scrobbler_date_time_idx ON listen(user_id,track_id,scrobbler,date_time)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_scrobbler_idx ON starred_artist(user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_scrobbler_idx ON starred_artist(artist_id,user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_scrobbler_idx ON starred_release(user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_scrobbler_idx ON starred_release(release_id,user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_scrobbler_idx ON starred_track(track_id,user_id,scrobbler)");
}
// Initial settings tables
{
auto uniqueTransaction {createUniqueTransaction()};
// Initial settings tables
{
auto uniqueTransaction{ createUniqueTransaction() };
ScanSettings::init(*this);
}
}
ScanSettings::init(*this);
}
}
void
Session::optimize()
{
LMS_LOG(DB, DEBUG) << "Optimizing db...";
{
auto uniqueTransaction {createUniqueTransaction()};
_session.execute("ANALYZE");
}
LMS_LOG(DB, DEBUG) << "Optimized db!";
}
void Session::analyze()
{
LMS_LOG(DB, INFO) << "Analyzing database...";
{
auto uniqueTransaction{ createUniqueTransaction() };
_session.execute("ANALYZE");
}
LMS_LOG(DB, INFO) << "Database Analyze complete";
}
} // namespace Database
+45 -55
View File
@@ -30,73 +30,63 @@
namespace Database {
User::User(std::string_view loginName)
: _loginName {loginName}
{
}
User::User(std::string_view loginName)
: _loginName{ loginName }
{
}
User::pointer
User::create(Session& session, std::string_view loginName)
{
return session.getDboSession().add(std::unique_ptr<User> {new User {loginName}});
}
User::pointer User::create(Session& session, std::string_view loginName)
{
return session.getDboSession().add(std::unique_ptr<User> {new User{ loginName }});
}
std::size_t
User::getCount(Session& session)
{
session.checkSharedLocked();
std::size_t User::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
}
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
}
RangeResults<UserId>
User::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
RangeResults<UserId> User::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<UserId>("SELECT id FROM user")};
auto query{ session.getDboSession().query<UserId>("SELECT id FROM user") };
if (params.scrobbler)
query.where("scrobbler = ?").bind(*params.scrobbler);
if (params.scrobbler)
query.where("scrobbler = ?").bind(*params.scrobbler);
return Utils::execQuery(query, params.range);
}
return Utils::execQuery(query, params.range);
}
User::pointer
User::findDemoUser(Session& session)
{
session.checkSharedLocked();
User::pointer User::findDemoUser(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue();
}
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue();
}
User::pointer
User::find(Session& session, UserId id)
{
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
}
User::pointer User::find(Session& session, UserId id)
{
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
}
User::pointer
User::find(Session& session, std::string_view name)
{
return session.getDboSession().find<User>()
.where("login_name = ?").bind(name)
.resultValue();
}
User::pointer User::find(Session& session, std::string_view name)
{
return session.getDboSession().find<User>()
.where("login_name = ?").bind(name)
.resultValue();
}
void
User::setSubsonicTranscodeBitrate(Bitrate bitrate)
{
assert(isAudioBitrateAllowed(bitrate));
_subsonicTranscodeBitrate = bitrate;
}
void User::setSubsonicDefaultTranscodeBitrate(Bitrate bitrate)
{
assert(isAudioBitrateAllowed(bitrate));
_subsonicDefaultTranscodeBitrate = bitrate;
}
void
User::clearAuthTokens()
{
_authTokens.clear();
}
void User::clearAuthTokens()
{
_authTokens.clear();
}
} // namespace Database
@@ -21,6 +21,7 @@
#include <string>
#include <string_view>
#include <tuple>
#include <vector>
#include <Wt/Dbo/Dbo.h>
@@ -58,10 +59,12 @@ namespace Database {
Cluster() = default;
// Find utility
static std::size_t getCount(Session& session);
static RangeResults<ClusterId> find(Session& session, const FindParameters& range);
static pointer find(Session& session, ClusterId id);
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
// As clusters only have a name, this is an optim to directly get the cluster names
using ClusterFindResult = std::tuple<ClusterId, std::string>;
static std::size_t getCount(Session& session);
static RangeResults<ClusterFindResult> find(Session& session, const FindParameters& range);
static pointer find(Session& session, ClusterId id);
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
// Accessors
const std::string& getName() const { return _name; }
@@ -27,52 +27,47 @@
namespace Database {
class Session;
class Db
{
public:
Db(const std::filesystem::path& dbPath, std::size_t connectionCount = 10);
~Db();
class Session;
class Db
{
public:
Db(const std::filesystem::path& dbPath, std::size_t connectionCount = 10);
Db(const Db&) = delete;
Db(Db&&) = delete;
Db& operator=(const Db&) = delete;
Db& operator=(Db&&) = delete;
Session& getTLSSession();
Session& getTLSSession();
void executeSql(const std::string& sql);
void executeSql(const std::string& sql);
private:
Db(const Db&) = delete;
Db& operator=(const Db&) = delete;
private:
friend class Session;
friend class Session;
RecursiveSharedMutex& getMutex() { return _sharedMutex; }
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
RecursiveSharedMutex& getMutex() { return _sharedMutex; }
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
class ScopedConnection
{
public:
ScopedConnection(Wt::Dbo::SqlConnectionPool& pool);
~ScopedConnection();
class ScopedConnection
{
public:
ScopedConnection(Wt::Dbo::SqlConnectionPool& pool);
~ScopedConnection();
ScopedConnection(const ScopedConnection& ) = delete;
ScopedConnection(ScopedConnection&& ) = delete;
ScopedConnection& operator=(const ScopedConnection& ) = delete;
ScopedConnection& operator=(ScopedConnection&& ) = delete;
Wt::Dbo::SqlConnection* operator->() const;
Wt::Dbo::SqlConnection* operator->() const;
private:
ScopedConnection(const ScopedConnection&) = delete;
ScopedConnection& operator=(const ScopedConnection&) = delete;
private:
Wt::Dbo::SqlConnectionPool& _connectionPool;
std::unique_ptr<Wt::Dbo::SqlConnection> _connection;
};
Wt::Dbo::SqlConnectionPool& _connectionPool;
std::unique_ptr<Wt::Dbo::SqlConnection> _connection;
};
RecursiveSharedMutex _sharedMutex;
std::unique_ptr<Wt::Dbo::SqlConnectionPool> _connectionPool;
RecursiveSharedMutex _sharedMutex;
std::unique_ptr<Wt::Dbo::SqlConnectionPool> _connectionPool;
std::mutex _tlsSessionsMutex;
std::vector<std::unique_ptr<Session>> _tlsSessions;
};
std::mutex _tlsSessionsMutex;
std::vector<std::unique_ptr<Session>> _tlsSessions;
};
} // namespace Database
@@ -34,99 +34,76 @@
namespace Database
{
class Session;
class Track;
class User;
class Session;
class Track;
class User;
class Listen final : public Object<Listen, ListenId>
{
public:
Listen() = default;
class Listen final : public Object<Listen, ListenId>
{
public:
Listen() = default;
struct FindParameters
{
UserId user;
std::optional<Scrobbler> scrobbler;
std::optional<ScrobblingState> scrobblingState;
Range range;
struct FindParameters
{
UserId user;
std::optional<Scrobbler> scrobbler;
std::optional<ScrobblingState> scrobblingState;
Range range;
FindParameters& setUser(UserId _user) { user = _user; return *this; }
FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; }
FindParameters& setScrobblingState(ScrobblingState _scrobblingState) { scrobblingState = _scrobblingState; return *this; }
FindParameters& setRange(Range _range) {range = _range; return *this; }
};
FindParameters& setUser(UserId _user) { user = _user; return *this; }
FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; }
FindParameters& setScrobblingState(ScrobblingState _scrobblingState) { scrobblingState = _scrobblingState; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; }
};
// Accessors
static std::size_t getCount(Session& session);
static pointer find(Session& session, ListenId id);
static pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters);
// Accessors
static std::size_t getCount(Session& session);
static pointer find(Session& session, ListenId id);
static pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters);
// Stats
static RangeResults<ArtistId> getTopArtists(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
std::optional<TrackArtistLinkType> linkType,
Range range = {});
static RangeResults<ReleaseId> getTopReleases(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
Range range = {});
static RangeResults<TrackId> getTopTracks(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
Range range = {});
// Stats
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<ArtistId> getRecentArtists(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
std::optional<TrackArtistLinkType> linkType,
Range range = {});
static RangeResults<ReleaseId> getRecentReleases(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
Range range = {});
static RangeResults<TrackId> getRecentTracks(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
Range range = {});
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
ScrobblingState getScrobblingState() const { return _scrobblingState; }
ObjectPtr<User> getUser() const { return _user; }
ObjectPtr<Track> getTrack() const { return _track; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId);
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId releaseId);
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
ScrobblingState getScrobblingState() const { return _scrobblingState; }
ObjectPtr<User> getUser() const { return _user; }
ObjectPtr<Track> getTrack() const { return _track; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
private:
friend class Session;
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
Wt::WDateTime _dateTime;
Scrobbler _scrobbler;
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
private:
friend class Session;
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
Wt::Dbo::ptr<User> _user;
Wt::Dbo::ptr<Track> _track;
};
Wt::WDateTime _dateTime;
Scrobbler _scrobbler;
ScrobblingState _scrobblingState{ ScrobblingState::PendingAdd };
Wt::Dbo::ptr<User> _user;
Wt::Dbo::ptr<Track> _track;
};
} // namespace Database
@@ -113,6 +113,7 @@ namespace Database
Wt::WDateTime getLastWritten() const;
std::optional<ReleaseTypePrimary> getPrimaryType() const { return _primaryType; }
EnumSet<ReleaseTypeSecondary> getSecondaryTypes() const { return _secondaryTypes; }
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
std::size_t getTracksCount() const;
// Setters
@@ -121,6 +122,7 @@ namespace Database
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
void setPrimaryType(std::optional<ReleaseTypePrimary> type) { _primaryType = type; }
void setSecondaryTypes(EnumSet<ReleaseTypeSecondary> types) { _secondaryTypes = types; }
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
// Get the artists of this release
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
@@ -137,7 +139,7 @@ namespace Database
Wt::Dbo::field(a, _totalDisc, "total_disc");
Wt::Dbo::field(a, _primaryType, "primary_type");
Wt::Dbo::field(a, _secondaryTypes, "secondary_types");
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
}
@@ -155,6 +157,7 @@ namespace Database
std::optional<int> _totalDisc{};
std::optional<ReleaseTypePrimary> _primaryType;
EnumSet<ReleaseTypeSecondary> _secondaryTypes;
std::string _artistDisplayName;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
};
@@ -28,68 +28,66 @@
namespace Database
{
class UniqueTransaction
{
private:
friend class Session;
UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
class UniqueTransaction
{
private:
friend class Session;
UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
std::unique_lock<RecursiveSharedMutex> _lock;
Wt::Dbo::Transaction _transaction;
};
std::unique_lock<RecursiveSharedMutex> _lock;
Wt::Dbo::Transaction _transaction;
};
class SharedTransaction
{
private:
friend class Session;
SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
class SharedTransaction
{
private:
friend class Session;
SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
std::shared_lock<RecursiveSharedMutex> _lock;
Wt::Dbo::Transaction _transaction;
};
std::shared_lock<RecursiveSharedMutex> _lock;
Wt::Dbo::Transaction _transaction;
};
class Db;
class Session
{
public:
Session (Db& database);
class Db;
class Session
{
public:
Session(Db& database);
Session(const Session&) = delete;
Session(Session&&) = delete;
Session& operator=(const Session&) = delete;
Session& operator=(Session&&) = delete;
[[nodiscard]] UniqueTransaction createUniqueTransaction();
[[nodiscard]] SharedTransaction createSharedTransaction();
[[nodiscard]] UniqueTransaction createUniqueTransaction();
[[nodiscard]] SharedTransaction createSharedTransaction();
void checkUniqueLocked();
void checkSharedLocked();
void checkUniqueLocked();
void checkSharedLocked();
void analyze();
void optimize();
void prepareTables(); // need to run only once at startup
void prepareTables(); // need to run only once at startup
Wt::Dbo::Session& getDboSession() { return _session; }
Db& getDb() { return _db; }
Wt::Dbo::Session& getDboSession() { return _session; }
Db& getDb() { return _db; }
template <typename Object, typename... Args>
typename Object::pointer create(Args&&... args)
{
checkUniqueLocked();
template <typename Object, typename... Args>
typename Object::pointer create(Args&&... args)
{
checkUniqueLocked();
typename Object::pointer res{ Object::create(*this, std::forward<Args>(args)...) };
getDboSession().flush();
typename Object::pointer res {Object::create(*this, std::forward<Args>(args)...)};
getDboSession().flush();
if (res->hasOnPostCreated())
res.modify()->onPostCreated();
if (res->hasOnPostCreated())
res.modify()->onPostCreated();
return res;
}
return res;
}
private:
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
private:
Db& _db;
Wt::Dbo::Session _session;
};
Db& _db;
Wt::Dbo::Session _session;
};
} // namespace Database
@@ -138,6 +138,7 @@ class Track final : public Object<Track, TrackId>
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
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 clearArtistLinks();
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
@@ -163,6 +164,7 @@ class Track final : public Object<Track, TrackId>
std::optional<std::string> getCopyrightURL() const;
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
// no artistLinkTypes means get all
std::vector<ObjectPtr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
std::vector<ArtistId> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
@@ -195,6 +197,7 @@ class Track final : public Object<Track, TrackId>
Wt::Dbo::field(a, _copyrightURL, "copyright_url");
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::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
@@ -228,6 +231,7 @@ class Track final : public Object<Track, TrackId>
std::string _copyrightURL;
std::optional<float> _trackReplayGain;
std::optional<float> _releaseReplayGain;
std::string _artistDisplayName;
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
@@ -33,134 +33,128 @@
namespace Database {
class AuthToken;
class Session;
class AuthToken;
class Session;
class User final : public Object<User, UserId>
{
public:
struct PasswordHash
{
std::string salt;
std::string hash;
};
class User final : public Object<User, UserId>
{
public:
struct PasswordHash
{
std::string salt;
std::string hash;
};
struct FindParameters
{
std::optional<Scrobbler> scrobbler;
Range range;
struct FindParameters
{
std::optional<Scrobbler> scrobbler;
Range range;
FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; }
FindParameters& setRange(Range _range) {range = _range; return *this; }
};
FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; }
};
static inline const std::size_t MinNameLength {3};
static inline const std::size_t MaxNameLength {15};
static inline const bool defaultSubsonicTranscodeEnable {true};
static inline const AudioFormat defaultSubsonicTranscodeFormat {AudioFormat::OGG_OPUS};
static inline const Bitrate defaultSubsonicTranscodeBitrate {128000};
static inline const UITheme defaultUITheme {UITheme::Dark};
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists};
static inline const Scrobbler defaultScrobbler {Scrobbler::Internal};
static inline const std::size_t MinNameLength{ 3 };
static inline const std::size_t MaxNameLength{ 15 };
static inline const AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS };
static inline const Bitrate defaultSubsonicTranscodeBitrate{ 128000 };
static inline const UITheme defaultUITheme{ UITheme::Dark };
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
static inline const Scrobbler defaultScrobbler{ Scrobbler::Internal };
User() = default;
User() = default;
static std::size_t getCount(Session& session);
static pointer find(Session& session, UserId id);
static pointer find(Session& session, std::string_view loginName);
static RangeResults<UserId> find(Session& session, const FindParameters& params);
static pointer findDemoUser(Session& session);
static std::size_t getCount(Session& session);
static pointer find(Session& session, UserId id);
static pointer find(Session& session, std::string_view loginName);
static RangeResults<UserId> find(Session& session, const FindParameters& params);
static pointer findDemoUser(Session& session);
// accessors
const std::string& getLoginName() const { return _loginName; }
PasswordHash getPasswordHash() const { return PasswordHash {_passwordSalt, _passwordHash}; }
const Wt::WDateTime& getLastLogin() const { return _lastLogin; }
std::size_t getAuthTokensCount() const { return _authTokens.size(); }
// accessors
const std::string& getLoginName() const { return _loginName; }
PasswordHash getPasswordHash() const { return PasswordHash{ _passwordSalt, _passwordHash }; }
const Wt::WDateTime& getLastLogin() const { return _lastLogin; }
std::size_t getAuthTokensCount() const { return _authTokens.size(); }
// write
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
void setType(UserType type) { _type = type; }
void setSubsonicTranscodeEnable(bool value) { _subsonicTranscodeEnable = value; }
void setSubsonicTranscodeFormat(AudioFormat encoding) { _subsonicTranscodeFormat = encoding; }
void setSubsonicTranscodeBitrate(Bitrate bitrate);
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
void setRadio(bool val) { _radio = val; }
void setRepeatAll(bool val) { _repeatAll = val; }
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setScrobbler(Scrobbler scrobbler) { _scrobbler = scrobbler; }
void setListenBrainzToken(const std::optional<UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; }
// write
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
void setType(UserType type) { _type = type; }
void setSubsonicDefaultTranscodeFormat(AudioFormat encoding) { _subsonicDefaultTranscodeFormat = encoding; }
void setSubsonicDefaultTranscodeBitrate(Bitrate bitrate);
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
void setRadio(bool val) { _radio = val; }
void setRepeatAll(bool val) { _repeatAll = val; }
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setScrobbler(Scrobbler scrobbler) { _scrobbler = scrobbler; }
void setListenBrainzToken(const std::optional<UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; }
// read
bool isAdmin() const { return _type == UserType::ADMIN; }
bool isDemo() const { return _type == UserType::DEMO; }
UserType getType() const { return _type; }
bool getSubsonicTranscodeEnable() const { return _subsonicTranscodeEnable; }
AudioFormat getSubsonicTranscodeFormat() const { return _subsonicTranscodeFormat; }
Bitrate getSubsonicTranscodeBitrate() const { return _subsonicTranscodeBitrate; }
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
bool isRepeatAllSet() const { return _repeatAll; }
bool isRadioSet() const { return _radio; }
UITheme getUITheme() const { return _uiTheme; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
Scrobbler getScrobbler() const { return _scrobbler; }
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
// read
bool isAdmin() const { return _type == UserType::ADMIN; }
bool isDemo() const { return _type == UserType::DEMO; }
UserType getType() const { return _type; }
AudioFormat getSubsonicDefaultTranscodeFormat() const { return _subsonicDefaultTranscodeFormat; }
Bitrate getSubsonicDefaultTranscodeBitrate() const { return _subsonicDefaultTranscodeBitrate; }
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
bool isRepeatAllSet() const { return _repeatAll; }
bool isRadioSet() const { return _radio; }
UITheme getUITheme() const { return _uiTheme; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
Scrobbler getScrobbler() const { return _scrobbler; }
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _loginName, "login_name");
Wt::Dbo::field(a, _passwordSalt, "password_salt");
Wt::Dbo::field(a, _passwordHash, "password_hash");
Wt::Dbo::field(a, _lastLogin, "last_login");
Wt::Dbo::field(a, _subsonicTranscodeEnable, "subsonic_transcode_enable");
Wt::Dbo::field(a, _subsonicTranscodeFormat, "subsonic_transcode_format");
Wt::Dbo::field(a, _subsonicTranscodeBitrate, "subsonic_transcode_bitrate");
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme");
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _loginName, "login_name");
Wt::Dbo::field(a, _passwordSalt, "password_salt");
Wt::Dbo::field(a, _passwordHash, "password_hash");
Wt::Dbo::field(a, _lastLogin, "last_login");
Wt::Dbo::field(a, _subsonicDefaultTranscodeFormat, "subsonic_default_transcode_format");
Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate");
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme");
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
// UI player settings
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
Wt::Dbo::field(a, _repeatAll, "repeat_all");
Wt::Dbo::field(a, _radio, "radio");
// UI player settings
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
Wt::Dbo::field(a, _repeatAll, "repeat_all");
Wt::Dbo::field(a, _radio, "radio");
Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user");
}
Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user");
}
private:
friend class Session;
User(std::string_view loginName);
static pointer create(Session& session, std::string_view loginName);
private:
friend class Session;
User(std::string_view loginName);
static pointer create(Session& session, std::string_view loginName);
std::string _loginName;
std::string _passwordSalt;
std::string _passwordHash;
Wt::WDateTime _lastLogin;
UITheme _uiTheme {defaultUITheme};
Scrobbler _scrobbler {defaultScrobbler};
std::string _listenbrainzToken; // Musicbrainz Identifier
std::string _loginName;
std::string _passwordSalt;
std::string _passwordHash;
Wt::WDateTime _lastLogin;
UITheme _uiTheme{ defaultUITheme };
Scrobbler _scrobbler{ defaultScrobbler };
std::string _listenbrainzToken; // Musicbrainz Identifier
// Admin defined settings
UserType _type {UserType::REGULAR};
// Admin defined settings
UserType _type{ UserType::REGULAR };
// User defined settings
SubsonicArtistListMode _subsonicArtistListMode {defaultSubsonicArtistListMode};
bool _subsonicTranscodeEnable {defaultSubsonicTranscodeEnable};
AudioFormat _subsonicTranscodeFormat {defaultSubsonicTranscodeFormat};
int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate};
// User defined settings
SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode };
AudioFormat _subsonicDefaultTranscodeFormat{ defaultSubsonicTranscodeFormat };
int _subsonicDefaultTranscodeBitrate{ defaultSubsonicTranscodeBitrate };
// User's dynamic data (UI)
int _curPlayingTrackPos {}; // Current track position in queue
bool _repeatAll {};
bool _radio {};
// User's dynamic data (UI)
int _curPlayingTrackPos{}; // Current track position in queue
bool _repeatAll{};
bool _radio{};
Wt::Dbo::collection<Wt::Dbo::ptr<AuthToken>> _authTokens;
};
Wt::Dbo::collection<Wt::Dbo::ptr<AuthToken>> _authTokens;
};
} // namespace Databas'
+13 -11
View File
@@ -48,13 +48,17 @@ TEST_F(DatabaseFixture, Cluster)
EXPECT_EQ(Cluster::getCount(session), 1);
EXPECT_EQ(cluster->getType()->getId(), clusterType.getId());
auto clusters{ Cluster::find(session, Cluster::FindParameters {}) };
ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster.getId());
{
const auto clusters{ Cluster::find(session, Cluster::FindParameters {}) };
ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(std::get<ClusterId>(clusters.results.front()), cluster.getId());
}
clusters = Cluster::findOrphans(session, Range{});
ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster.getId());
{
const auto clusters{ Cluster::findOrphans(session, Range{}) };
ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster.getId());
}
auto clusterTypes{ ClusterType::find(session, Range {}) };
ASSERT_EQ(clusterTypes.results.size(), 1);
@@ -114,7 +118,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
auto transaction{ session.createSharedTransaction() };
auto clusters{ Cluster::find(session, Cluster::FindParameters {}.setTrack(track.getId())) };
ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster1.getId());
EXPECT_EQ(std::get<ClusterId>(clusters.results.front()), cluster1.getId());
}
{
@@ -317,9 +321,9 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
{
auto transaction{ session.createSharedTransaction() };
auto clusters{ Cluster::find(session, Cluster::FindParameters{}.setRelease(release.getId())) };
const auto clusters{ Cluster::find(session, Cluster::FindParameters{}.setRelease(release.getId())) };
ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster.getId());
EXPECT_EQ(std::get<ClusterId>(clusters.results.front()), cluster.getId());
}
{
@@ -1106,5 +1110,3 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleReleasesMultiClusters)
}
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ DatabaseFixture::SetUpTestCase()
{
Database::Session s {_tmpDb->getDb()};
s.prepareTables();
s.optimize();
s.analyze();
// remove default created entries
{
File diff suppressed because it is too large Load Diff
@@ -29,80 +29,83 @@
namespace Recommendation {
using namespace Database;
using namespace Database;
std::unique_ptr<IEngine> createClustersEngine(Db& db)
{
return std::make_unique<ClusterEngine>(db);
}
std::unique_ptr<IEngine> createClustersEngine(Db& db)
{
return std::make_unique<ClusterEngine>(db);
}
TrackContainer
ClusterEngine::findSimilarTracks(const std::vector<TrackId>& trackIds, std::size_t maxCount) const
{
Session& dbSession {_db.getTLSSession()};
TrackContainer ClusterEngine::findSimilarTracks(const std::vector<TrackId>& trackIds, std::size_t maxCount) const
{
if (maxCount == 0)
return {};
auto transaction {dbSession.createSharedTransaction()};
Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createSharedTransaction() };
const auto similarTrackIds {Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount})};
return std::move(similarTrackIds.results);
}
const auto similarTrackIds{ Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount}) };
return std::move(similarTrackIds.results);
}
TrackContainer
ClusterEngine::findSimilarTracksFromTrackList(TrackListId tracklistId, std::size_t maxCount) const
{
Session& dbSession {_db.getTLSSession()};
TrackContainer ClusterEngine::findSimilarTracksFromTrackList(TrackListId tracklistId, std::size_t maxCount) const
{
TrackContainer res;
if (maxCount == 0)
return res;
TrackContainer res;
{
Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createSharedTransaction() };
{
auto transaction {dbSession.createSharedTransaction()};
const TrackList::pointer trackList{ TrackList::find(dbSession, tracklistId) };
if (!trackList)
return res;
const TrackList::pointer trackList {TrackList::find(dbSession, tracklistId)};
if (!trackList)
return res;
const auto tracks{ trackList->getSimilarTracks(0, maxCount) };
res.reserve(tracks.size());
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
}
const auto tracks {trackList->getSimilarTracks(0, maxCount)};
res.reserve(tracks.size());
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
}
return res;
}
return res;
}
ReleaseContainer ClusterEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
{
ReleaseContainer res;
if (maxCount == 0)
return res;
ReleaseContainer
ClusterEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
{
Session& dbSession {_db.getTLSSession()};
{
Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createSharedTransaction() };
ReleaseContainer res;
{
auto transaction {dbSession.createSharedTransaction()};
auto release{ Release::find(dbSession, releaseId) };
if (!release)
return res;
auto release {Release::find(dbSession, releaseId)};
if (!release)
return res;
const auto releases{ release->getSimilarReleases(0, maxCount) };
res.reserve(releases.size());
std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const auto& release) { return release->getId(); });
}
const auto releases {release->getSimilarReleases(0, maxCount)};
res.reserve(releases.size());
std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const auto& release) { return release->getId(); });
}
return res;
}
return res;
}
ArtistContainer ClusterEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> artistLinkTypes, std::size_t maxCount) const
{
if (maxCount == 0)
return {};
ArtistContainer
ClusterEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> artistLinkTypes, std::size_t maxCount) const
{
Session& dbSession {_db.getTLSSession()};
Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createSharedTransaction() };
auto transaction {dbSession.createSharedTransaction()};
auto artist{ Artist::find(dbSession, artistId) };
if (!artist)
return {};
auto artist {Artist::find(dbSession, artistId)};
if (!artist)
return {};
const auto similarArtistIds {artist->findSimilarArtists(artistLinkTypes, Range {0, maxCount})};
return std::move(similarArtistIds.results);
}
const auto similarArtistIds{ artist->findSimilarArtists(artistLinkTypes, Range {0, maxCount}) };
return std::move(similarArtistIds.results);
}
} // namespace Recommendation
@@ -189,9 +189,11 @@ namespace
if (release->getPrimaryType() != primaryType)
release.modify()->setPrimaryType(primaryType);
}
const EnumSet<ReleaseTypeSecondary> secondaryTypes {convertReleaseTypesSecondary(releaseInfo.secondaryTypes)};
const EnumSet<ReleaseTypeSecondary> secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) };
if (release->getSecondaryTypes() != secondaryTypes)
release.modify()->setSecondaryTypes(secondaryTypes);
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
}
Release::pointer
@@ -525,5 +527,6 @@ namespace Scanner
track.modify()->setCopyright(trackInfo->copyright);
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
track.modify()->setTrackReplayGain(trackInfo->replayGain);
track.modify()->setArtistDisplayName(trackInfo->artistDisplayName);
}
}
@@ -299,8 +299,7 @@ ScannerService::scan(bool forceScan)
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size();
// TODO make it a scan step
_dbSession.optimize();
_dbSession.analyze();
if (!_abortScan)
{
@@ -133,6 +133,32 @@ namespace Scrobbling
return res;
}
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId)
{
auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler)
return {};
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *scrobbler, releaseId) };
return listen ? listen->getDateTime() : Wt::WDateTime{};
}
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::TrackId trackId)
{
auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler)
return {};
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *scrobbler, trackId) };
return listen ? listen->getDateTime() : Wt::WDateTime{};
}
// Top
ScrobblingService::ArtistContainer ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
{
@@ -52,6 +52,9 @@ namespace Scrobbling
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) override;
Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) override;
Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) override;
ArtistContainer getTopArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType,
@@ -69,6 +69,9 @@ namespace Scrobbling
const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0;
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) = 0;
// Top
virtual ArtistContainer getTopArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds,
+1 -1
View File
@@ -31,7 +31,7 @@ namespace API::Subsonic
};
static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
static inline constexpr std::string_view serverVersion{ "1" };
static inline constexpr std::string_view serverVersion{ "2" };
}
namespace StringUtils
+2 -2
View File
@@ -288,12 +288,12 @@ namespace API::Subsonic
checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes);
Response resp{ (itEntryPoint->second.func)(requestContext) };
const Response resp{ (itEntryPoint->second.func)(requestContext) };
resp.write(response.out(), format);
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
return;
}
+189 -114
View File
@@ -20,12 +20,8 @@
#include "SubsonicResponse.hpp"
#include <cassert>
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Serializer.h>
#include <boost/property_tree/json_parser.hpp>
#include <cmath>
#include <climits>
#include <boost/property_tree/xml_parser.hpp>
#include "utils/Exception.hpp"
@@ -57,59 +53,66 @@ namespace API::Subsonic
_value = value;
}
void Response::Node::setAttribute(std::string_view key, std::string_view value)
void Response::Node::setAttribute(Key key, std::string_view value)
{
_attributes[std::string{ key }] = std::string{ value };
_attributes[key] = std::string{ value };
}
void Response::Node::addChild(const std::string& key, Node node)
void Response::Node::addChild(Key key, Node&& node)
{
assert(!_value);
_children[key].emplace_back(std::move(node));
assert(_children.find(key) == std::cend(_children));
_children[key] = std::move(node);
}
void Response::Node::createEmptyArrayChild(std::string_view key)
void Response::Node::createEmptyArrayChild(Key key)
{
assert(!_value);
assert(_children.find(key) == std::cend(_children));
_childrenArrays.emplace(key, std::vector<Node>{});
}
void Response::Node::addArrayChild(std::string_view key, Node node)
void Response::Node::addArrayChild(Key key, Node&& node)
{
assert(!_value);
_childrenArrays[std::string{ key }].emplace_back(std::move(node));
assert(_children.find(key) == std::cend(_children));
_childrenArrays[key].emplace_back(std::move(node));
}
void Response::Node::createEmptyArrayValue(std::string_view key)
void Response::Node::createEmptyArrayValue(Key key)
{
assert (!_value);
assert(!_value);
assert(_children.find(key) == std::cend(_children));
_childrenValues.emplace(key, ValuesType{});
}
void Response::Node::addArrayValue(std::string_view key, std::string_view value)
void Response::Node::addArrayValue(Key key, std::string_view value)
{
assert(!_value);
auto& values{ _childrenValues[std::string{ key }] };
assert(_children.find(key) == std::cend(_children));
auto& values{ _childrenValues[key] };
values.push_back(std::string{ value });
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
}
void Response::Node::addArrayValue(std::string_view key, long long value)
void Response::Node::addArrayValue(Key key, long long value)
{
assert(!_value);
auto& values {_childrenValues[std::string{ key }]};
auto& values{ _childrenValues[key] };
values.push_back(value);
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
}
Response::Node& Response::Node::createChild(const std::string& key)
Response::Node& Response::Node::createChild(Key key)
{
_children[key].emplace_back();
return _children[key].back();
assert(!_value);
return _children[key];
}
Response::Node& Response::Node::createArrayChild(const std::string& key)
Response::Node& Response::Node::createArrayChild(Key key)
{
assert(!_value);
assert(_children.find(key) == std::cend(_children));
_childrenArrays[key].emplace_back();
return _childrenArrays[key].back();
}
@@ -120,13 +123,30 @@ namespace API::Subsonic
}
Response Response::createOkResponse(ProtocolVersion protocolVersion)
{
return createResponseCommon(protocolVersion);
}
Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
{
return createResponseCommon(protocolVersion, &error);
}
Response Response::createResponseCommon(ProtocolVersion protocolVersion, const Error* error)
{
Response response;
Node& responseNode{ response._root.createChild("subsonic-response") };
responseNode.setAttribute("status", "ok");
responseNode.setAttribute("status", error ? "failed" : "ok");
responseNode.setVersionAttribute(protocolVersion);
if (error)
{
Node& errorNode{ responseNode.createChild("error") };
errorNode.setAttribute("code", static_cast<int>(error->getCode()));
errorNode.setAttribute("message", error->getMessage());
}
// OpenSubsonic mandatory fields
responseNode.setAttribute("type", "lms");
responseNode.setAttribute("serverVersion", serverVersion);
@@ -135,38 +155,22 @@ namespace API::Subsonic
return response;
}
Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
void Response::addNode(Node::Key key, Node&& node)
{
Response response;
Node& responseNode{ response._root.createChild("subsonic-response") };
responseNode.setAttribute("status", "failed");
responseNode.setVersionAttribute(protocolVersion);
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
Node& errorNode{ responseNode.createChild("error") };
errorNode.setAttribute("code", static_cast<int>(error.getCode()));
errorNode.setAttribute("message", error.getMessage());
return response;
return _root._children["subsonic-response"].addChild(key, std::move(node));
}
void Response::addNode(const std::string& key, Node node)
Response::Node& Response::createNode(Node::Key key)
{
return _root._children["subsonic-response"].front().addChild(key, std::move(node));
return _root._children["subsonic-response"].createChild(key);
}
Response::Node& Response::createNode(const std::string& key)
Response::Node& Response::createArrayNode(Node::Key key)
{
return _root._children["subsonic-response"].front().createChild(key);
return _root._children["subsonic-response"].createArrayChild(key);
}
Response::Node& Response::createArrayNode(const std::string& key)
{
return _root._children["subsonic-response"].front().createArrayChild(key);
}
void Response::write(std::ostream& os, ResponseFormat format)
void Response::write(std::ostream& os, ResponseFormat format) const
{
switch (format)
{
@@ -179,34 +183,34 @@ namespace API::Subsonic
}
}
void Response::writeXML(std::ostream& os)
void Response::writeXML(std::ostream& os) const
{
std::function<boost::property_tree::ptree(const Node&)> nodeToPropertyTree = [&](const Node& node)
{
boost::property_tree::ptree res;
for (auto itAttribute : node._attributes)
for (const auto& [key, value] : node._attributes)
{
if (std::holds_alternative<std::string>(itAttribute.second))
res.put("<xmlattr>." + itAttribute.first, std::get<std::string>(itAttribute.second));
else if (std::holds_alternative<bool>(itAttribute.second))
res.put("<xmlattr>." + itAttribute.first, std::get<bool>(itAttribute.second));
else if (std::holds_alternative<float>(itAttribute.second))
res.put("<xmlattr>." + itAttribute.first, std::get<float>(itAttribute.second));
else if (std::holds_alternative<long long>(itAttribute.second))
res.put("<xmlattr>." + itAttribute.first, std::get<long long>(itAttribute.second));
if (std::holds_alternative<std::string>(value))
res.put("<xmlattr>." + std::string{ key.get() }, std::get<std::string>(value));
else if (std::holds_alternative<bool>(value))
res.put("<xmlattr>." + std::string{ key.get() }, std::get<bool>(value));
else if (std::holds_alternative<float>(value))
res.put("<xmlattr>." + std::string{ key.get() }, std::get<float>(value));
else if (std::holds_alternative<long long>(value))
res.put("<xmlattr>." + std::string{ key.get() }, std::get<long long>(value));
}
auto valueToPropertyTree = [](const Node::ValueType& value)
{
boost::property_tree::ptree res;
std::visit([&](const auto& rawValue)
{
res.put_value(rawValue);
}, value);
boost::property_tree::ptree res;
std::visit([&](const auto& rawValue)
{
res.put_value(rawValue);
}, value);
return res;
};
return res;
};
if (node._value)
{
@@ -214,22 +218,21 @@ namespace API::Subsonic
}
else
{
for (const auto& [key, childNodes] : node._children)
for (const auto& [key, childNode] : node._children)
{
for (const Node& childNode : childNodes)
res.add_child(key, nodeToPropertyTree(childNode));
res.add_child(std::string{ key.get() }, nodeToPropertyTree(childNode));
}
for (const auto& [key, childArrayNodes] : node._childrenArrays)
{
for (const Node& childNode : childArrayNodes)
res.add_child(key, nodeToPropertyTree(childNode));
res.add_child(std::string{ key.get() }, nodeToPropertyTree(childNode));
}
for (const auto& [key, childArrayValues] : node._childrenValues)
{
for (const Response::Node::ValueType& value : childArrayValues)
res.add_child(key, valueToPropertyTree(value));
res.add_child(std::string{ key.get() }, valueToPropertyTree(value));
}
}
@@ -240,64 +243,136 @@ namespace API::Subsonic
boost::property_tree::write_xml(os, root);
}
void Response::writeJSON(std::ostream& os)
void Response::JsonSerializer::serializeNode(std::ostream& os, const Response::Node& node)
{
namespace Json = Wt::Json;
os << '{';
std::function<Json::Object(const Response::Node&)> nodeToJsonObject = [&](const Response::Node& node)
bool first{ true };
for (const auto& [key, value] : node._attributes)
{
if (!first)
os << ',';
serializeEscapedString(os, key.get());
os << ':';
serializeValue(os, value);
first = false;
}
if (node._value)
{
if (!first)
os << ',';
os << "\"value\":";
serializeValue(os, *node._value);
first = false;
}
else
{
for (const auto& [key, childNode] : node._children)
{
Json::Object res;
if (!first)
os << ',';
auto valueToJsonValue{ [](const Node::ValueType& value) -> Json::Value
serializeEscapedString(os, key.get());
os << ':';
serializeNode(os, childNode);
first = false;
}
for (const auto& [key, childArrayNodes] : node._childrenArrays)
{
if (!first)
os << ',';
serializeEscapedString(os, key.get());
os << ":[";
bool firstChild{ true };
for (const Response::Node& childNode : childArrayNodes)
{
Json::Value res;
std::visit([&](const auto& rawValue)
{
res = Json::Value{ rawValue };
}, value);
return res;
} };
if (!firstChild)
os << ",";
for (auto itAttribute : node._attributes)
res[itAttribute.first] = valueToJsonValue(itAttribute.second);
if (node._value)
{
res["value"] = valueToJsonValue(*node._value);
serializeNode(os, childNode);
firstChild = false;
}
else
os << ']';
first = false;
}
for (const auto& [key, childValues] : node._childrenValues)
{
if (!first)
os << ',';
serializeEscapedString(os, key.get());
os << ":[";
bool firstChild{ true };
for (const Node::ValueType& childValue : childValues)
{
for (const auto& [key, childNodes] : node._children)
{
for (const Response::Node& childNode : childNodes)
res[key] = nodeToJsonObject(childNode);
}
if (!firstChild)
os << ",";
for (const auto& [key, childArrayNodes] : node._childrenArrays)
{
Json::Array array;
for (const Response::Node& childNode : childArrayNodes)
array.emplace_back(nodeToJsonObject(childNode));
serializeValue(os, childValue);
res[key] = std::move(array);
}
for (const auto& [key, childValues] : node._childrenValues)
{
Json::Array array;
for (const Node::ValueType& childValue : childValues)
array.emplace_back(valueToJsonValue(childValue));
res[key] = std::move(array);
}
firstChild = false;
}
os << ']';
return res;
};
first = false;
}
}
Json::Object root{ nodeToJsonObject(_root) };
os << Json::serialize(root);
os << '}';
}
void Response::JsonSerializer::serializeValue(std::ostream& os, const Node::ValueType& value)
{
if (std::holds_alternative<std::string>(value))
{
serializeEscapedString(os, std::get<std::string>(value));
}
else if (std::holds_alternative<bool>(value))
{
os << (std::get<bool>(value) ? "true" : "false");
}
else if (std::holds_alternative<float>(value))
{
const float d{ std::get<float>(value) };
if (std::isnan(d) || std::fabs(d) == std::numeric_limits<float>::infinity())
os << "null";
else
os << d;
}
else if (std::holds_alternative<long long>(value))
{
os << std::get<long long>(value);
}
else
{
assert(false);
}
}
void Response::JsonSerializer::serializeEscapedString(std::ostream& os, std::string_view str)
{
os << '\"';
StringUtils::writeJSEscapedString(os, str);
os << '\"';
}
void Response::writeJSON(std::ostream& os) const
{
JsonSerializer serializer;
serializer.serializeNode(os, _root);
}
} // namespace
+46 -23
View File
@@ -191,17 +191,30 @@ namespace API::Subsonic
class Node
{
public:
void setAttribute(std::string_view key, std::string_view value);
class Key
{
public:
template<std::size_t N>
constexpr Key(const char (&str)[N]) : _str{ str } {}
constexpr std::string_view get() const { return _str; }
bool constexpr operator<(const Key& other) const { return _str < other._str; }
private:
const std::string_view _str;
};
void setAttribute(Key key, std::string_view value);
template <typename T, std::enable_if_t<std::is_arithmetic<T>::value>* = nullptr>
void setAttribute(std::string_view key, T value)
void setAttribute(Key key, T value)
{
if constexpr (std::is_same<bool, T>::value)
_attributes[std::string{ key }] = value;
_attributes[key] = value;
else if constexpr (std::is_floating_point<T>::value)
_attributes[std::string{ key }] = static_cast<float>(value);
_attributes[key] = static_cast<float>(value);
else if constexpr (std::is_integral<T>::value)
_attributes[std::string{ key }] = static_cast<long long>(value);
_attributes[key] = static_cast<long long>(value);
else
static_assert("Unhandled type");
}
@@ -209,28 +222,28 @@ namespace API::Subsonic
// A Node has either a single value or an array of values or some children
void setValue(std::string_view value);
void setValue(long long value);
Node& createChild(const std::string& key);
Node& createArrayChild(const std::string& key);
Node& createChild(Key key);
Node& createArrayChild(Key key);
void addChild(const std::string& key, Node node);
void createEmptyArrayChild(std::string_view key);
void addArrayChild(std::string_view key, Node node);
void createEmptyArrayValue(std::string_view key);
void addArrayValue(std::string_view key, std::string_view value);
void addArrayValue(std::string_view key, long long value);
void addChild(Key key, Node&& node);
void createEmptyArrayChild(Key key);
void addArrayChild(Key key, Node&& node);
void createEmptyArrayValue(Key key);
void addArrayValue(Key key, std::string_view value);
void addArrayValue(Key key, long long value);
private:
void setVersionAttribute(ProtocolVersion version);
friend class Response;
using ValueType = std::variant<std::string, bool, float, long long>;
std::map<std::string, ValueType> _attributes;
std::map<Key, ValueType> _attributes;
std::optional<ValueType> _value;
std::map<std::string, std::vector<Node>> _children;
std::map<std::string, std::vector<Node>> _childrenArrays;
std::map<Key, Node> _children;
std::map<Key, std::vector<Node>> _childrenArrays;
using ValuesType = std::vector<ValueType>;
std::map<std::string, ValuesType> _childrenValues;
std::map<Key, ValuesType> _childrenValues;
};
static Response createOkResponse(ProtocolVersion protocolVersion);
@@ -242,15 +255,25 @@ namespace API::Subsonic
Response(Response&&) = default;
Response& operator=(Response&&) = default;
void addNode(const std::string& key, Node node);
Node& createNode(const std::string& key);
Node& createArrayNode(const std::string& key);
void addNode(Node::Key key, Node&& node);
Node& createNode(Node::Key key);
Node& createArrayNode(Node::Key key);
void write(std::ostream& os, ResponseFormat format);
void write(std::ostream& os, ResponseFormat format) const;
private:
void writeJSON(std::ostream& os);
void writeXML(std::ostream& os);
static Response createResponseCommon(ProtocolVersion protocolVersion, const Error* error = nullptr);
class JsonSerializer
{
public:
void serializeNode(std::ostream& os, const Node& node);
void serializeValue(std::ostream& os, const Node::ValueType& node);
void serializeEscapedString(std::ostream&, std::string_view str);
};
void writeJSON(std::ostream& os) const;
void writeXML(std::ostream& os) const;
Response() = default;
Node _root;
@@ -133,7 +133,7 @@ namespace API::Subsonic
throw NotImplementedGenericError{};
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& albumListNode{ response.createNode(id3 ? "albumList2" : "albumList") };
Response::Node& albumListNode{ response.createNode(id3 ? Response::Node::Key{ "albumList2" } : Response::Node::Key{ "albumList" }) };
for (const ReleaseId releaseId : releases.results)
{
@@ -153,7 +153,7 @@ namespace API::Subsonic
throw UserNotAuthorizedError{};
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& starredNode{ response.createNode(id3 ? "starred2" : "starred") };
Response::Node& starredNode{ response.createNode(id3 ? Response::Node::Key{ "starred2" } : Response::Node::Key{ "starred" }) };
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() };
@@ -54,7 +54,7 @@ namespace API::Subsonic
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(20) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& artistInfoNode{ response.createNode(id3 ? "artistInfo2" : "artistInfo") };
Response::Node& artistInfoNode{ response.createNode(id3 ? Response::Node::Key{ "artistInfo2" } : Response::Node::Key{ "artistInfo" }) };
{
auto transaction{ context.dbSession.createSharedTransaction() };
@@ -236,7 +236,7 @@ namespace API::Subsonic
throw UserNotAuthorizedError{};
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& similarSongsNode{ response.createNode(id3 ? "similarSongs2" : "similarSongs") };
Response::Node& similarSongsNode{ response.createNode(id3 ? Response::Node::Key{ "similarSongs2" } : Response::Node::Key{ "similarSongs" }) };
for (const TrackId trackId : tracks)
{
const Track::pointer track{ Track::find(context.dbSession, trackId) };
@@ -28,8 +28,7 @@ namespace API::Subsonic::Scan
namespace
{
Response::Node
createStatusResponseNode()
Response::Node createStatusResponseNode()
{
Response::Node statusResponse;
@@ -19,6 +19,7 @@
#include "MediaRetrieval.hpp"
#include "av/IAudioFile.hpp"
#include "av/TranscodeParameters.hpp"
#include "av/TranscodeResourceHandlerCreator.hpp"
#include "av/Types.hpp"
@@ -30,25 +31,40 @@
#include "utils/Logger.hpp"
#include "utils/FileResourceHandlerCreator.hpp"
#include "utils/Utils.hpp"
#include "utils/String.hpp"
#include "ParameterParsing.hpp"
#include "SubsonicId.hpp"
using namespace Database;
namespace API::Subsonic
{
using namespace Database;
namespace {
std::optional<Av::Format> subsonicStreamFormatToAvFormat(std::string_view format)
{
for (const auto& [str, avFormat] : std::initializer_list<std::pair<std::string_view, Av::Format>>{
{"mp3", Av::Format::MP3},
{"opus", Av::Format::OGG_OPUS},
{"vorbis", Av::Format::OGG_VORBIS},
})
{
if (StringUtils::stringCaseInsensitiveEqual("str", format))
return avFormat;
}
return std::nullopt;
}
Av::Format userTranscodeFormatToAvFormat(AudioFormat format)
{
switch (format)
{
case AudioFormat::MP3: return Av::Format::MP3;
case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
default: return Av::Format::OGG_OPUS;
case Database::AudioFormat::MP3: return Av::Format::MP3;
case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
case Database::AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
}
return Av::Format::OGG_OPUS;
}
struct StreamParameters
@@ -64,9 +80,9 @@ namespace API::Subsonic
const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
// Optional params
std::optional<std::size_t> maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate") };
const std::optional<std::string> format{ getParameterAs<std::string>(context.parameters, "format") };
const std::optional<std::size_t> timeOffset{ getParameterAs<std::size_t>(context.parameters, "timeOffset") };
std::size_t maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate").value_or(0) }; // "If set to zero, no limit is imposed"
const std::string format{ getParameterAs<std::string>(context.parameters, "format").value_or("") };
std::size_t timeOffset{ getParameterAs<std::size_t>(context.parameters, "timeOffset").value_or(0) };
bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) };
StreamParameters parameters;
@@ -84,32 +100,41 @@ namespace API::Subsonic
parameters.inputFileParameters.duration = track->getDuration();
}
if (format == "raw") // raw => no transcode
return parameters;
const auto audioFile{ Av::parseAudioFile(parameters.inputFileParameters.trackPath) };
// check if transcode is really needed or not
// same format as requested, bitrate is lower than requested => no need to transcode
if (const auto streamInfo{ audioFile->getBestStreamInfo() })
{
const User::pointer user{ User::find(context.dbSession, context.userId) };
if (!user)
throw UserNotAuthorizedError{};
// format = "raw" => no transcode. Other format values will be ignored
const bool transcode{ (!format || (*format != "raw")) && user->getSubsonicTranscodeEnable() };
if (transcode)
// assume reported codec is "mp3", "opus", "vorbis", etc.
if (StringUtils::stringCaseInsensitiveEqual(streamInfo->codec, format) && (maxBitRate == 0 || (streamInfo->bitrate / 1000) <= maxBitRate))
{
std::size_t bitRate{ user->getSubsonicTranscodeBitrate() / 1000 };
// "If set to zero, no limit is imposed"
if (maxBitRate && *maxBitRate != 0)
bitRate = Utils::clamp(*maxBitRate, std::size_t{ 48 }, bitRate);
Av::TranscodeParameters transcodeParameters;
transcodeParameters.bitrate = bitRate * 1000;
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat());
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
transcodeParameters.offset = std::chrono::seconds{ timeOffset ? *timeOffset : 0 };
parameters.transcodeParameters = std::move(transcodeParameters);
LMS_LOG(API_SUBSONIC, DEBUG) << "stream parameters are compatible with actual file: no transcode";
return parameters;
}
}
const User::pointer user{ User::find(context.dbSession, context.userId) };
if (!user)
throw UserNotAuthorizedError{};
Av::TranscodeParameters& transcodeParameters{ parameters.transcodeParameters.emplace() };
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
transcodeParameters.offset = std::chrono::seconds{ timeOffset };
if (std::optional<Av::Format> requestedFormat{ subsonicStreamFormatToAvFormat(format) })
transcodeParameters.format = *requestedFormat;
else
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodeFormat());
transcodeParameters.bitrate = user->getSubsonicDefaultTranscodeBitrate();
if (maxBitRate != 0)
transcodeParameters.bitrate = Utils::clamp(transcodeParameters.bitrate, std::size_t{ 48000 }, maxBitRate * 1000);
return parameters;
}
}
@@ -75,7 +75,7 @@ namespace API::Subsonic
for (const TrackListEntry::pointer& entry : entries)
playlistNode.addArrayChild("entry", createSongNode(entry->getTrack(), context.dbSession, user));
response.addNode("playlist", playlistNode);
response.addNode("playlist", std::move(playlistNode));
return response;
}
+26 -27
View File
@@ -107,11 +107,14 @@ namespace API::Subsonic
}
else if (!artists.empty())
{
albumNode.setAttribute("artist", Utils::joinArtistNames(artists));
if (!release->getArtistDisplayName().empty())
albumNode.setAttribute("artist", release->getArtistDisplayName());
else
albumNode.setAttribute("artist", Utils::joinArtistNames(artists));
if (artists.size() == 1)
{
albumNode.setAttribute(id3 ? "artistId" : "parent", idToString(artists.front()->getId()));
albumNode.setAttribute(id3 ? Response::Node::Key{ "artistId" } : Response::Node::Key{ "parent" }, idToString(artists.front()->getId()));
}
else
{
@@ -121,26 +124,32 @@ namespace API::Subsonic
}
// Report the first GENRE for this track
if (ClusterType::pointer clusterType{ ClusterType::find(dbSession, "GENRE") })
const ClusterType::pointer genreClusterType{ ClusterType::find(dbSession, "GENRE") };
if (genreClusterType)
{
auto clusters{ release->getClusterGroups({clusterType}, 1) };
auto clusters{ release->getClusterGroups({genreClusterType}, 1) };
if (!clusters.empty() && !clusters.front().empty())
albumNode.setAttribute("genre", clusters.front().front()->getName());
}
if (const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getStarredDateTime(user->getId(), release->getId()) }; dateTime.isValid())
albumNode.setAttribute("starred", StringUtils::toISO8601String(dateTime)); // TODO report correct date/time
albumNode.setAttribute("starred", StringUtils::toISO8601String(dateTime));
// OpenSubsonic specific fields (must always be set)
if (!id3)
albumNode.setAttribute("mediaType", "album");
{
const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getLastListenDateTime(user->getId(), release->getId()) };
albumNode.setAttribute("played", dateTime.isValid() ? StringUtils::toISO8601String(dateTime) : std::string{ "" });
}
{
std::optional<UUID> mbid{ release->getMBID() };
albumNode.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : "");
}
auto addClusters{ [&](std::string_view field, std::string_view clusterTypeName)
auto addClusters{ [&](Response::Node::Key field, std::string_view clusterTypeName)
{
albumNode.createEmptyArrayValue(field);
@@ -151,41 +160,31 @@ namespace API::Subsonic
params.setRelease(release->getId());
params.setClusterType(clusterType->getId());
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
{
Cluster::pointer cluster{ Cluster::find(dbSession, clusterId) };
if (cluster)
albumNode.addArrayValue(field, cluster->getName());
}
for (const auto& cluster : Cluster::find(dbSession, params).results)
albumNode.addArrayValue(field, std::get<std::string>(cluster));
}
} };
addClusters("moods", "MOOD");
// Genres
albumNode.createEmptyArrayChild("genres");
if (genreClusterType)
{
albumNode.createEmptyArrayChild("genres");
Cluster::FindParameters params;
params.setRelease(release->getId());
params.setClusterType(genreClusterType->getId());
ClusterType::pointer clusterType{ ClusterType::find(dbSession, "GENRE") };
if (clusterType)
{
Cluster::FindParameters params;
params.setRelease(release->getId());
params.setClusterType(clusterType->getId());
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
{
Cluster::pointer cluster{ Cluster::find(dbSession, clusterId) };
if (cluster)
albumNode.addArrayChild("genres", createItemGenreNode(cluster));
}
}
for (const auto& cluster : Cluster::find(dbSession, params).results)
albumNode.addArrayChild("genres", createItemGenreNode(std::get<std::string>(cluster)));
}
albumNode.createEmptyArrayChild("artists");
for (const Artist::pointer& artist : release->getReleaseArtists())
albumNode.addArrayChild("artists", createArtistNode(artist));
albumNode.setAttribute("displayArtist", release->getArtistDisplayName());
{
const Wt::WDate originalReleaseDate{ release->getOriginalReleaseDate() };
albumNode.setAttribute("originalReleaseDate", originalReleaseDate.isValid() ? StringUtils::toISO8601String(originalReleaseDate) : "");
@@ -23,11 +23,11 @@
namespace API::Subsonic
{
Response::Node createItemGenreNode(const Database::Cluster::pointer& cluster)
Response::Node createItemGenreNode(std::string_view name)
{
Response::Node genreNode;
genreNode.setAttribute("name", cluster->getName());
genreNode.setAttribute("name", name);
return genreNode;
}
@@ -19,7 +19,7 @@
#pragma once
#include "services/database/Object.hpp"
#include <string_view>
#include "SubsonicResponse.hpp"
namespace Database
@@ -29,5 +29,5 @@ namespace Database
namespace API::Subsonic
{
Response::Node createItemGenreNode(const Database::ObjectPtr<Database::Cluster>& cluster);
Response::Node createItemGenreNode(std::string_view name);
}
+33 -32
View File
@@ -120,25 +120,28 @@ namespace API::Subsonic
trackResponse.setAttribute("suffix", extension.string().substr(1));
}
if (user->getSubsonicTranscodeEnable())
trackResponse.setAttribute("transcodedSuffix", formatToSuffix(user->getSubsonicTranscodeFormat()));
trackResponse.setAttribute("transcodedSuffix", formatToSuffix(user->getSubsonicDefaultTranscodeFormat()));
trackResponse.setAttribute("coverArt", idToString(track->getId()));
const std::vector<Artist::pointer>& artists{ track->getArtists({TrackArtistLinkType::Artist}) };
if (!artists.empty())
{
trackResponse.setAttribute("artist", Utils::joinArtistNames(artists));
if (!track->getArtistDisplayName().empty())
trackResponse.setAttribute("artist", track->getArtistDisplayName());
else
trackResponse.setAttribute("artist", Utils::joinArtistNames(artists));
if (artists.size() == 1)
trackResponse.setAttribute("artistId", idToString(artists.front()->getId()));
}
if (track->getRelease())
Release::pointer release{ track->getRelease() };
if (release)
{
trackResponse.setAttribute("album", track->getRelease()->getName());
trackResponse.setAttribute("albumId", idToString(track->getRelease()->getId()));
trackResponse.setAttribute("parent", idToString(track->getRelease()->getId()));
trackResponse.setAttribute("album", release->getName());
trackResponse.setAttribute("albumId", idToString(release->getId()));
trackResponse.setAttribute("parent", idToString(release->getId()));
}
trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count());
@@ -149,7 +152,8 @@ namespace API::Subsonic
trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime));
// Report the first GENRE for this track
if (ClusterType::pointer genreClusterType{ ClusterType::find(dbSession, "GENRE") })
const ClusterType::pointer genreClusterType{ ClusterType::find(dbSession, "GENRE") };
if (genreClusterType)
{
auto clusters{ track->getClusterGroups({genreClusterType}, 1) };
if (!clusters.empty() && !clusters.front().empty())
@@ -159,6 +163,11 @@ namespace API::Subsonic
// OpenSubsonic specific fields (must always be set)
trackResponse.setAttribute("mediaType", "song");
{
const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getLastListenDateTime(user->getId(), track->getId()) };
trackResponse.setAttribute("played", dateTime.isValid() ? StringUtils::toISO8601String(dateTime) : "");
}
{
std::optional<UUID> mbid{ track->getRecordingMBID() };
trackResponse.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : "");
@@ -178,7 +187,7 @@ namespace API::Subsonic
}
}
auto addArtistLinks{ [&](std::string_view nodeName, TrackArtistLinkType type)
auto addArtistLinks{ [&](Response::Node::Key nodeName, TrackArtistLinkType type)
{
trackResponse.createEmptyArrayChild(nodeName);
@@ -195,9 +204,13 @@ namespace API::Subsonic
} };
addArtistLinks("artists", TrackArtistLinkType::Artist);
addArtistLinks("albumartists", TrackArtistLinkType::ReleaseArtist);
trackResponse.setAttribute("displayArtist", track->getArtistDisplayName());
auto addClusters{ [&](std::string_view field, std::string_view clusterTypeName)
addArtistLinks("albumartists", TrackArtistLinkType::ReleaseArtist);
if (release)
trackResponse.setAttribute("displayAlbumArtist", release->getArtistDisplayName());
auto addClusters{ [&](Response::Node::Key field, std::string_view clusterTypeName)
{
trackResponse.createEmptyArrayValue(field);
@@ -208,35 +221,23 @@ namespace API::Subsonic
params.setTrack(track->getId());
params.setClusterType(clusterType->getId());
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
{
Cluster::pointer cluster {Cluster::find(dbSession, clusterId)};
if (cluster)
trackResponse.addArrayValue(field, cluster->getName());
}
for (const auto& cluster : Cluster::find(dbSession, params).results)
trackResponse.addArrayValue(field, std::get<std::string>(cluster));
}
} };
addClusters("moods", "MOOD");
// Genres
trackResponse.createEmptyArrayChild("genres");
if (genreClusterType)
{
trackResponse.createEmptyArrayChild("genres");
Cluster::FindParameters params;
params.setTrack(track->getId());
params.setClusterType(genreClusterType->getId());
ClusterType::pointer clusterType{ ClusterType::find(dbSession, "GENRE") };
if (clusterType)
{
Cluster::FindParameters params;
params.setTrack(track->getId());
params.setClusterType(clusterType->getId());
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
{
Cluster::pointer cluster{ Cluster::find(dbSession, clusterId) };
if (cluster)
trackResponse.addArrayChild("genres", createItemGenreNode(cluster));
}
}
for (const auto& cluster : Cluster::find(dbSession, params).results)
trackResponse.addArrayChild("genres", createItemGenreNode(std::get<std::string>(cluster)));
}
trackResponse.addChild("replayGain", createReplayGainNode(track));
+7 -6
View File
@@ -17,18 +17,19 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <thread>
#include "utils/StreamLogger.hpp"
StreamLogger::StreamLogger(std::ostream& os, EnumSet<Severity> severities)
: _os {os}
, _severities {severities}
: _os{ os }
, _severities{ severities }
{
}
void
StreamLogger::processLog(const Log& log)
void StreamLogger::processLog(const Log& log)
{
if (_severities.contains(log.getSeverity()))
_os << "[" << getSeverityName(log.getSeverity()) << "] [" << getModuleName(log.getModule()) << "] " << log.getMessage() << std::endl;
if (_severities.contains(log.getSeverity()))
_os << std::this_thread::get_id() << " [" << getSeverityName(log.getSeverity()) << "] [" << getModuleName(log.getModule()) << "] " << log.getMessage() << std::endl;
}
+37 -1
View File
@@ -177,6 +177,20 @@ namespace StringUtils
return oss.str();
}
bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB)
{
if (strA.size() != strB.size())
return false;
for (std::size_t i{}; i < strA.size(); ++i)
{
if (std::tolower(strA[i]) != std::tolower(strB[i]))
return false;
}
return true;
}
void capitalize(std::string& str)
{
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
@@ -205,7 +219,7 @@ namespace StringUtils
return res;
}
std::string jsEscape(const std::string& str)
std::string jsEscape(std::string_view str)
{
static const std::unordered_map<char, std::string_view> escapeMap
{
@@ -235,6 +249,28 @@ namespace StringUtils
return escaped;
}
void writeJSEscapedString(std::ostream& os, std::string_view str)
{
static constexpr std::pair<char, std::string_view> charsToEscape[]
{
{'\\', "\\\\" },
{ '\n', "\\n" },
{ '\r', "\\r" },
{ '\t', "\\t" },
{ '"', "\\\"" },
{ '\'', "\\\'" },
};
for (const char c : str)
{
auto itEntry{ std::find_if(std::cbegin(charsToEscape), std::cend(charsToEscape), [=](const auto& entry) { return entry.first == c;}) };
if (itEntry != std::cend(charsToEscape))
os << itEntry->second;
else
os << c;
}
}
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
{
std::string res;
+14 -3
View File
@@ -19,14 +19,25 @@
#include "utils/WtLogger.hpp"
#include <thread>
#include <sstream>
#include <Wt/WApplication.h>
#include <Wt/WLogger.h>
#include "utils/Logger.hpp"
void
WtLogger::processLog(const Log& log)
namespace
{
Wt::log(getSeverityName(log.getSeverity())) << Wt::WLogger::sep << "[" << getModuleName(log.getModule()) << "]" << Wt::WLogger::sep << log.getMessage();
std::string to_string(std::thread::id id)
{
std::ostringstream oss;
oss << id;
return oss.str();
}
}
void WtLogger::processLog(const Log& log)
{
Wt::log(getSeverityName(log.getSeverity())) << Wt::WLogger::sep << to_string(std::this_thread::get_id()) << Wt::WLogger::sep << "[" << getModuleName(log.getModule()) << "]" << Wt::WLogger::sep << log.getMessage();
}
+43 -41
View File
@@ -26,33 +26,33 @@
enum class Severity
{
FATAL,
ERROR,
WARNING,
INFO,
DEBUG,
FATAL,
ERROR,
WARNING,
INFO,
DEBUG,
};
enum class Module
{
API_SUBSONIC,
AUTH,
AV,
CHILDPROCESS,
COVER,
DB,
DBUPDATER,
FEATURE,
HTTP,
MAIN,
METADATA,
REMOTE,
SCROBBLING,
SERVICE,
RECOMMENDATION,
TRANSCODE,
UI,
UTILS,
API_SUBSONIC,
AUTH,
AV,
CHILDPROCESS,
COVER,
DB,
DBUPDATER,
FEATURE,
HTTP,
MAIN,
METADATA,
REMOTE,
SCROBBLING,
SERVICE,
RECOMMENDATION,
TRANSCODE,
UI,
UTILS,
};
const char* getModuleName(Module mod);
@@ -61,30 +61,32 @@ const char* getSeverityName(Severity sev);
class Logger;
class Log
{
public:
Log(Logger* logger, Module module, Severity severity);
~Log();
public:
Log(Logger* logger, Module module, Severity severity);
~Log();
Module getModule() const { return _module; }
Severity getSeverity() const { return _severity; }
std::string getMessage() const;
Module getModule() const { return _module; }
Severity getSeverity() const { return _severity; }
std::string getMessage() const;
std::ostringstream& getOstream() { return _oss; }
std::ostringstream& getOstream() { return _oss; }
private:
Module _module;
Severity _severity;
std::ostringstream _oss;
Logger* _logger {};
private:
Log(const Log&) = delete;
Log& operator=(const Log&) = delete;
Module _module;
Severity _severity;
std::ostringstream _oss;
Logger* _logger{};
};
class Logger
{
public:
virtual ~Logger() = default;
virtual void processLog(const Log& log) = 0;
public:
virtual ~Logger() = default;
virtual void processLog(const Log& log) = 0;
};
#define LMS_LOG(module, severity) Log(Service<Logger>::get(), Module::module, Severity::severity).getOstream()
#define LMS_LOG_EX(module, severity) Log(Service<Logger>::get(), module, severity).getOstream()
#define LMS_LOG(module, severity) Log{Service<Logger>::get(), Module::module, Severity::severity}.getOstream()
#define LMS_LOG_EX(module, severity) Log{Service<Logger>::get(), module, severity}.getOstream()
+4 -1
View File
@@ -55,6 +55,8 @@ namespace StringUtils {
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
[[nodiscard]] bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB);
void capitalize(std::string& str);
template<typename T>
@@ -81,7 +83,8 @@ namespace StringUtils {
[[nodiscard]] std::string replaceInString(std::string_view str, const std::string& from, const std::string& to);
[[nodiscard]] std::string jsEscape(const std::string& str);
[[nodiscard]] std::string jsEscape(std::string_view str);
void writeJSEscapedString(std::ostream& os, std::string_view str);
[[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
+4 -1
View File
@@ -238,7 +238,10 @@ int main(int argc, char* argv[])
{
Database::Session session {database};
session.prepareTables();
session.optimize();
// force optimize in case scanner aborted during a large import:
// queries may be too slow to even be able to relaunch a scan sing the web interface
session.analyze();
}
UserInterface::LmsApplicationManager appManager;
+2 -2
View File
@@ -535,8 +535,8 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
if (!artists.empty())
{
entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", Utils::createArtistContainer(artists));
entry->bindWidget("artists-md", Utils::createArtistContainer(artists));
entry->bindWidget("artists", Utils::createArtistAnchorList(artists));
entry->bindWidget("artists-md", Utils::createArtistAnchorList(artists));
}
const auto release {track->getRelease()};
+4 -27
View File
@@ -160,15 +160,13 @@ class SettingsModel : public Wt::WFormModel
}
{
user.modify()->setSubsonicTranscodeEnable(Wt::asNumber(value(SubsonicTranscodeEnableField)));
auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(SubsonicTranscodeBitrateField))};
if (subsonicTranscodeBitrateRow)
user.modify()->setSubsonicTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow));
user.modify()->setSubsonicDefaultTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow));
auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(SubsonicTranscodeFormatField))};
if (subsonicTranscodeFormatRow)
user.modify()->setSubsonicTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow));
user.modify()->setSubsonicDefaultTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow));
auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))};
if (subsonicArtistListModeRow)
@@ -224,19 +222,12 @@ class SettingsModel : public Wt::WFormModel
setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo);
}
setValue(SubsonicTranscodeEnableField, LmsApp->getUser()->getSubsonicTranscodeEnable());
{
const bool usesTranscode {LmsApp->getUser()->getSubsonicTranscodeEnable()};
setReadOnly(SubsonicTranscodeFormatField, !usesTranscode);
setReadOnly(SubsonicTranscodeBitrateField, !usesTranscode);
}
{
auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromValue(user->getSubsonicTranscodeBitrate())};
auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodeBitrate())};
if (subsonicTranscodeBitrateRow)
setValue(SubsonicTranscodeBitrateField, _transcodeBitrateModel->getString(*subsonicTranscodeBitrateRow));
auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromValue(user->getSubsonicTranscodeFormat())};
auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodeFormat())};
if (subsonicTranscodeFormatRow)
setValue(SubsonicTranscodeFormatField, _transcodeFormatModel->getString(*subsonicTranscodeFormatRow));
@@ -482,11 +473,6 @@ SettingsView::refreshView()
{
t->setCondition("if-has-subsonic-api", Service<IConfig>::get()->getBool("api-subsonic", true));
// Transcode
auto transcode {std::make_unique<Wt::WCheckBox>()};
auto* transcodeRaw {transcode.get()};
t->setFormWidget(SettingsModel::SubsonicTranscodeEnableField, std::move(transcode));
// Format
auto transcodeFormat {std::make_unique<Wt::WComboBox>()};
transcodeFormat->setModel(model->getTranscodeFormatModel());
@@ -501,15 +487,6 @@ SettingsView::refreshView()
auto artistListMode {std::make_unique<Wt::WComboBox>()};
artistListMode->setModel(model->getSubsonicArtistListModeModel());
t->setFormWidget(SettingsModel::SubsonicArtistListModeField, std::move(artistListMode));
transcodeRaw->changed().connect([=]
{
const bool enable {transcodeRaw->checkState() == Wt::CheckState::Checked};
model->setReadOnly(SettingsModel::SubsonicTranscodeFormatField, !enable);
model->setReadOnly(SettingsModel::SubsonicTranscodeBitrateField, !enable);
t->updateModel(model.get());
t->updateView(model.get());
});
}
// Scrobbling
+229 -164
View File
@@ -38,204 +38,269 @@
namespace UserInterface::Utils
{
std::string
durationToString(std::chrono::milliseconds msDuration)
{
const std::chrono::seconds duration {std::chrono::duration_cast<std::chrono::seconds>(msDuration)};
std::string durationToString(std::chrono::milliseconds msDuration)
{
const std::chrono::seconds duration{ std::chrono::duration_cast<std::chrono::seconds>(msDuration) };
std::ostringstream oss;
std::ostringstream oss;
if (duration.count() >= 3600)
{
oss << (duration.count() / 3600)
<< ":"
<< std::setfill('0') << std::setw(2) << (duration.count() % 3600) / 60
<< ":"
<< std::setfill('0') << std::setw(2) << duration.count() % 60;
}
else
{
oss << (duration.count() / 60)
<< ":"
<< std::setfill('0') << std::setw(2) << duration.count() % 60;
}
if (duration.count() >= 3600)
{
oss << (duration.count() / 3600)
<< ":"
<< std::setfill('0') << std::setw(2) << (duration.count() % 3600) / 60
<< ":"
<< std::setfill('0') << std::setw(2) << duration.count() % 60;
}
else
{
oss << (duration.count() / 60)
<< ":"
<< std::setfill('0') << std::setw(2) << duration.count() % 60;
}
return oss.str();
}
return oss.str();
}
std::unique_ptr<Wt::WImage>
createCover(Database::ReleaseId releaseId, CoverResource::Size size)
{
auto cover {std::make_unique<Wt::WImage>()};
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(releaseId, size));
cover->setStyleClass("Lms-cover img-fluid"); // HACK
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
return cover;
}
std::unique_ptr<Wt::WImage> createCover(Database::ReleaseId releaseId, CoverResource::Size size)
{
auto cover{ std::make_unique<Wt::WImage>() };
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(releaseId, size));
cover->setStyleClass("Lms-cover img-fluid"); // HACK
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
return cover;
}
std::unique_ptr<Wt::WImage>
createCover(Database::TrackId trackId, CoverResource::Size size)
{
auto cover {std::make_unique<Wt::WImage>()};
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(trackId, size));
cover->setStyleClass("Lms-cover img-fluid"); // HACK
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
return cover;
}
std::unique_ptr<Wt::WImage> createCover(Database::TrackId trackId, CoverResource::Size size)
{
auto cover{ std::make_unique<Wt::WImage>() };
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(trackId, size));
cover->setStyleClass("Lms-cover img-fluid"); // HACK
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
return cover;
}
std::unique_ptr<Wt::WInteractWidget>
createCluster(Database::ClusterId clusterId, bool canDelete)
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
std::unique_ptr<Wt::WInteractWidget> createCluster(Database::ClusterId clusterId, bool canDelete)
{
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
const Database::Cluster::pointer cluster {Database::Cluster::find(LmsApp->getDbSession(), clusterId)};
if (!cluster)
return {};
const Database::Cluster::pointer cluster{ Database::Cluster::find(LmsApp->getDbSession(), clusterId) };
if (!cluster)
return {};
auto getStyleClass {[](const Database::Cluster::pointer& cluster) -> const char*
{
switch (cluster->getType()->getId().getValue() % 8)
{
case 0: return "bg-primary";
case 1: return "bg-secondary";
case 2: return "bg-success";
case 3: return "bg-danger";
case 4: return "bg-warning text-dark";
case 5: return "bg-info text-dark";
case 6: return "bg-light text-dark";
case 7: return "bg-dark";
}
auto getStyleClass{ [](const Database::Cluster::pointer& cluster) -> const char*
{
switch (cluster->getType()->getId().getValue() % 8)
{
case 0: return "bg-primary";
case 1: return "bg-secondary";
case 2: return "bg-success";
case 3: return "bg-danger";
case 4: return "bg-warning text-dark";
case 5: return "bg-info text-dark";
case 6: return "bg-light text-dark";
case 7: return "bg-dark";
}
return "bg-primary";
}};
return "bg-primary";
} };
const std::string styleClass {getStyleClass(cluster)};
auto res {std::make_unique<Wt::WText>(std::string {} + (canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "") + Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::UnsafeXHTML)};
const std::string styleClass{ getStyleClass(cluster) };
auto res{ std::make_unique<Wt::WText>(std::string {} + (canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "") + Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::UnsafeXHTML) };
res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK
res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain);
res->setInline(true);
res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK
res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain);
res->setInline(true);
return res;
}
return res;
}
std::unique_ptr<Wt::WContainerWidget>
createArtistContainer(const std::vector<Database::ArtistId>& artistIds)
{
using namespace Database;
std::unique_ptr<Wt::WContainerWidget> createArtistAnchorList(const std::vector<Database::ArtistId>& artistIds, std::string_view cssAnchorClass)
{
using namespace Database;
std::unique_ptr<Wt::WContainerWidget> artistContainer {std::make_unique<Wt::WContainerWidget>()};
std::unique_ptr<Wt::WContainerWidget> artistContainer{ std::make_unique<Wt::WContainerWidget>() };
bool firstArtist {true};
bool firstArtist{ true };
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
for (const ArtistId artistId : artistIds)
{
const Artist::pointer artist {Artist::find(LmsApp->getDbSession(), artistId)};
if (!artist)
continue;
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
for (const ArtistId artistId : artistIds)
{
const Artist::pointer artist{ Artist::find(LmsApp->getDbSession(), artistId) };
if (!artist)
continue;
if (!firstArtist)
artistContainer->addNew<Wt::WText>(" · ");
if (!firstArtist)
artistContainer->addNew<Wt::WText>(" · ");
auto anchor {createArtistAnchor(artist)};
anchor->addStyleClass("link-success text-decoration-none"); // hack
artistContainer->addWidget(std::move(anchor));
firstArtist = false;
}
auto anchor{ createArtistAnchor(artist) };
anchor->addStyleClass("text-decoration-none"); // hack
anchor->addStyleClass(std::string{ cssAnchorClass });
artistContainer->addWidget(std::move(anchor));
firstArtist = false;
}
return artistContainer;
}
return artistContainer;
}
Wt::WLink
createArtistLink(Database::Artist::pointer artist)
{
if (const auto mbid {artist->getMBID()})
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()}};
else
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString()};
}
std::unique_ptr<Wt::WContainerWidget> createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector<Database::ArtistId>& artistIds, std::string_view cssAnchorClass)
{
using namespace Database;
std::unique_ptr<Wt::WAnchor>
createArtistAnchor(Database::Artist::pointer artist, bool setText)
{
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist));
std::size_t matchCount{};
std::string_view::size_type currentOffset{};
if (setText)
{
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(artist->getName()));
res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain);
}
auto result{ std::make_unique<Wt::WContainerWidget>() };
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
return res;
}
// consider order is guaranteed + we will likely succeed
for (const ArtistId artistId : artistIds)
{
const Artist::pointer artist{ Artist::find(LmsApp->getDbSession(), artistId) };
if (!artist)
break;
Wt::WLink
createReleaseLink(Database::Release::pointer release)
{
if (const auto mbid {release->getMBID()})
return Wt::WLink {Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()}};
else
return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + release->getId().toString()};
}
const auto pos{ displayName.find(artist->getName(), currentOffset) };
if (pos == std::string_view::npos)
break;
std::unique_ptr<Wt::WAnchor>
createReleaseAnchor(Database::Release::pointer release, bool setText)
{
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
assert(pos >= currentOffset);
if (pos != currentOffset)
result->addNew<Wt::WText>(std::string{ displayName.substr(currentOffset, pos - currentOffset) }, Wt::TextFormat::Plain);
if (setText)
{
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(release->getName()));
res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
}
auto anchor{ createArtistAnchor(artist) };
anchor->addStyleClass("text-decoration-none"); // hack
anchor->addStyleClass(std::string{ cssAnchorClass }); // hack
result->addWidget(std::move(anchor));
currentOffset = pos + artist->getName().size();
matchCount += 1;
}
return res;
}
if (matchCount != artistIds.size())
return createArtistAnchorList(artistIds, cssAnchorClass);
std::unique_ptr<Wt::WAnchor>
createTrackListAnchor(Database::TrackList::pointer trackList, bool setText)
{
Wt::WLink link {Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString()};
auto res {std::make_unique<Wt::WAnchor>(link)};
return result;
}
if (setText)
{
const Wt::WString name {Wt::WString::fromUTF8(std::string {trackList->getName()})};
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(name);
res->setToolTip(name, Wt::TextFormat::Plain);
}
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchorsForRelease(Database::ObjectPtr<Database::Release> release, Database::ArtistId omitIfMatchThisArtist, std::string_view cssAnchorClass)
{
using namespace Database;
return res;
}
Artist::FindParameters params;
params.setRelease(release->getId());
params.setLinkType(TrackArtistLinkType::ReleaseArtist);
std::unique_ptr<Wt::WContainerWidget>
createClustersForTrack(Database::Track::pointer track, Filters& filters)
{
using namespace Database;
if (const auto releaseArtists{ Artist::find(LmsApp->getDbSession(), params) }; !releaseArtists.results.empty())
{
if (releaseArtists.results.size() == 1 && releaseArtists.results.front() == omitIfMatchThisArtist)
return {};
std::unique_ptr<Wt::WContainerWidget> clusterContainer {std::make_unique<Wt::WContainerWidget>()};
return Utils::createArtistDisplayNameWithAnchors(release->getArtistDisplayName(), releaseArtists.results, cssAnchorClass);
}
const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()};
const auto clusterGroups {track->getClusterGroups(clusterTypes, 3)};
params.setLinkType(TrackArtistLinkType::Artist);
const auto artists{ Artist::find(LmsApp->getDbSession(), params) };
if (artists.results.size() == 1)
{
if (artists.results.front() == omitIfMatchThisArtist)
return {};
for (const auto& clusters : clusterGroups)
{
for (const Cluster::pointer& cluster : clusters)
{
const ClusterId clusterId {cluster->getId()};
Wt::WInteractWidget* entry {clusterContainer->addWidget(Utils::createCluster(clusterId))};
entry->clicked().connect([&filters, clusterId]
{
filters.add(clusterId);
});
}
}
return Utils::createArtistAnchorList({ artists.results.front() }, cssAnchorClass);
}
return clusterContainer;
}
if (artists.results.size() > 1)
{
auto res{ std::make_unique<Wt::WContainerWidget>() };
res->addNew<Wt::WText>(Wt::WString::tr("Lms.Explore.various-artists"));
return res;
}
return {};
}
Wt::WLink createArtistLink(Database::Artist::pointer artist)
{
if (const auto mbid{ artist->getMBID() })
return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()} };
else
return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString() };
}
std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::Artist::pointer artist, bool setText)
{
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist));
if (setText)
{
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(artist->getName()));
res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain);
}
return res;
}
Wt::WLink createReleaseLink(Database::Release::pointer release)
{
if (const auto mbid{ release->getMBID() })
return Wt::WLink{ Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()} };
else
return Wt::WLink{ Wt::LinkType::InternalPath, "/release/" + release->getId().toString() };
}
std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Database::Release::pointer release, bool setText)
{
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
if (setText)
{
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(release->getName()));
res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
}
return res;
}
std::unique_ptr<Wt::WAnchor> createTrackListAnchor(Database::TrackList::pointer trackList, bool setText)
{
Wt::WLink link{ Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString() };
auto res{ std::make_unique<Wt::WAnchor>(link) };
if (setText)
{
const Wt::WString name{ Wt::WString::fromUTF8(std::string {trackList->getName()}) };
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(name);
res->setToolTip(name, Wt::TextFormat::Plain);
}
return res;
}
std::unique_ptr<Wt::WContainerWidget> createClustersForTrack(Database::Track::pointer track, Filters& filters)
{
using namespace Database;
std::unique_ptr<Wt::WContainerWidget> clusterContainer{ std::make_unique<Wt::WContainerWidget>() };
const auto clusterTypes{ ScanSettings::get(LmsApp->getDbSession())->getClusterTypes() };
const auto clusterGroups{ track->getClusterGroups(clusterTypes, 3) };
for (const auto& clusters : clusterGroups)
{
for (const Cluster::pointer& cluster : clusters)
{
const ClusterId clusterId{ cluster->getId() };
Wt::WInteractWidget* entry{ clusterContainer->addWidget(Utils::createCluster(clusterId)) };
entry->clicked().connect([&filters, clusterId]
{
filters.add(clusterId);
});
}
}
return clusterContainer;
}
}
+4 -1
View File
@@ -21,6 +21,7 @@
#include <chrono>
#include <string>
#include <string_view>
#include <Wt/WContainerWidget.h>
#include <Wt/WImage.h>
@@ -57,7 +58,9 @@ namespace UserInterface::Utils
std::unique_ptr<Wt::WInteractWidget> createCluster(Database::ClusterId clusterId, bool canDelete = false);
std::unique_ptr<Wt::WContainerWidget> createArtistContainer(const std::vector<Database::ArtistId>& artists);
std::unique_ptr<Wt::WContainerWidget> createArtistAnchorList(const std::vector<Database::ArtistId>& artistIds, std::string_view cssAnchorClass = "link-success");
std::unique_ptr<Wt::WContainerWidget> createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector<Database::ArtistId>& artistIds, std::string_view cssAnchorClass = "link-success");
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchorsForRelease(Database::ObjectPtr<Database::Release> release, Database::ArtistId omitIfMatchThisArtist = {}, std::string_view cssAnchorClass = "link-success");
Wt::WLink createArtistLink(Database::ObjectPtr<Database::Artist> artist);
std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool setText = true);
+87 -99
View File
@@ -32,122 +32,110 @@ using namespace Database;
namespace UserInterface::ReleaseListHelpers
{
static
std::unique_ptr<Wt::WTemplate>
createEntryInternal(const Release::pointer& release, const std::string& templateKey, const Artist::pointer& artist, const bool showYear)
{
auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr(templateKey))};
namespace
{
std::unique_ptr<Wt::WTemplate> createEntryInternal(const Release::pointer& release, const std::string& templateKey, const Artist::pointer& artist, const bool showYear)
{
auto entry{ std::make_unique<Wt::WTemplate>(Wt::WString::tr(templateKey)) };
entry->bindWidget("release-name", Utils::createReleaseAnchor(release));
entry->addFunction("tr", &Wt::WTemplate::Functions::tr);
entry->bindWidget("release-name", Utils::createReleaseAnchor(release));
entry->addFunction("tr", &Wt::WTemplate::Functions::tr);
{
Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))};
auto cover {Utils::createCover(release->getId(), CoverResource::Size::Large)};
cover->addStyleClass("Lms-cover-release Lms-cover-anchor");
anchor->setImage(std::move(cover));
}
{
Wt::WAnchor* anchor{ entry->bindWidget("cover", Utils::createReleaseAnchor(release, false)) };
auto cover{ Utils::createCover(release->getId(), CoverResource::Size::Large) };
cover->addStyleClass("Lms-cover-release Lms-cover-anchor");
anchor->setImage(std::move(cover));
}
auto artists {release->getReleaseArtists()};
if (artists.empty())
artists = release->getArtists();
auto artistAnchors{ Utils::createArtistsAnchorsForRelease(release, artist ? artist->getId() : ArtistId{}, "link-secondary") };
if (artistAnchors)
{
entry->setCondition("if-has-artist", true);
entry->bindWidget("artist-name", std::move(artistAnchors));
}
const bool isSameArtist {(std::find(std::cbegin(artists), std::cend(artists), artist) != artists.end())};
if (showYear)
{
Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate()) };
if (!year.empty())
{
entry->setCondition("if-has-year", true);
entry->bindString("year", year, Wt::TextFormat::Plain);
}
}
if (artists.size() > 1)
{
entry->setCondition("if-has-various-artists", true);
}
else if (artists.size() == 1 && !isSameArtist)
{
entry->setCondition("if-has-artist", true);
entry->bindWidget("artist-name", Utils::createArtistAnchor(artists.front()));
}
return entry;
}
}
if (showYear)
{
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate())};
if (!year.empty())
{
entry->setCondition("if-has-year", true);
entry->bindString("year", year, Wt::TextFormat::Plain);
}
}
std::unique_ptr<Wt::WTemplate> createEntry(const Release::pointer& release, const Artist::pointer& artist, bool showYear)
{
return createEntryInternal(release, "Lms.Explore.Releases.template.entry-grid", artist, showYear);
}
return entry;
}
std::unique_ptr<Wt::WTemplate> createEntry(const Release::pointer& release)
{
return createEntry(release, Artist::pointer{}, false /*year*/);
}
std::unique_ptr<Wt::WTemplate>
createEntry(const Release::pointer& release, const Artist::pointer& artist, bool showYear)
{
return createEntryInternal(release, "Lms.Explore.Releases.template.entry-grid", artist, showYear);
}
std::unique_ptr<Wt::WTemplate>
createEntry(const Release::pointer& release)
{
return createEntry(release, Artist::pointer {}, false /*year*/);
}
std::unique_ptr<Wt::WTemplate>
createEntryForArtist(const Database::Release::pointer& release, const Database::Artist::pointer& artist)
{
return createEntry(release, artist, true);
}
std::unique_ptr<Wt::WTemplate> createEntryForArtist(const Database::Release::pointer& release, const Database::Artist::pointer& artist)
{
return createEntry(release, artist, true);
}
} // namespace UserInterface
namespace UserInterface::ReleaseHelpers
{
Wt::WString
buildReleaseTypeString(ReleaseTypePrimary primaryType, EnumSet<ReleaseTypeSecondary> secondaryTypes)
{
Wt::WString res;
Wt::WString buildReleaseTypeString(ReleaseTypePrimary primaryType, EnumSet<ReleaseTypeSecondary> secondaryTypes)
{
Wt::WString res;
switch (primaryType)
{
case ReleaseTypePrimary::Album: res = Wt::WString::tr("Lms.Explore.Release.type-primary-album"); break;
case ReleaseTypePrimary::Broadcast: res = Wt::WString::tr("Lms.Explore.Release.type-primary-broadcast"); break;
case ReleaseTypePrimary::EP: res = Wt::WString::tr("Lms.Explore.Release.type-primary-ep"); break;
case ReleaseTypePrimary::Single: res = Wt::WString::tr("Lms.Explore.Release.type-primary-single"); break;
case ReleaseTypePrimary::Other: res = Wt::WString::tr("Lms.Explore.Release.type-primary-other"); break;
}
switch (primaryType)
{
case ReleaseTypePrimary::Album: res = Wt::WString::tr("Lms.Explore.Release.type-primary-album"); break;
case ReleaseTypePrimary::Broadcast: res = Wt::WString::tr("Lms.Explore.Release.type-primary-broadcast"); break;
case ReleaseTypePrimary::EP: res = Wt::WString::tr("Lms.Explore.Release.type-primary-ep"); break;
case ReleaseTypePrimary::Single: res = Wt::WString::tr("Lms.Explore.Release.type-primary-single"); break;
case ReleaseTypePrimary::Other: res = Wt::WString::tr("Lms.Explore.Release.type-primary-other"); break;
}
for (ReleaseTypeSecondary secondaryType : secondaryTypes)
{
res += Wt::WString {" · "};
for (ReleaseTypeSecondary secondaryType : secondaryTypes)
{
res += Wt::WString{ " · " };
switch (secondaryType)
{
case ReleaseTypeSecondary::Compilation: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-compilation"); break;
case ReleaseTypeSecondary::Spokenword: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-spokenword"); break;
case ReleaseTypeSecondary::Soundtrack: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-soundtrack"); break;
case ReleaseTypeSecondary::Interview: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-interview"); break;
case ReleaseTypeSecondary::Audiobook: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiobook"); break;
case ReleaseTypeSecondary::AudioDrama: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiodrama"); break;
case ReleaseTypeSecondary::Live: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-live"); break;
case ReleaseTypeSecondary::Remix: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-remix"); break;
case ReleaseTypeSecondary::DJMix: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-djmix"); break;
case ReleaseTypeSecondary::Mixtape_Street: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-mixtape-street"); break;
case ReleaseTypeSecondary::Demo: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-demo"); break;
}
}
switch (secondaryType)
{
case ReleaseTypeSecondary::Compilation: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-compilation"); break;
case ReleaseTypeSecondary::Spokenword: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-spokenword"); break;
case ReleaseTypeSecondary::Soundtrack: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-soundtrack"); break;
case ReleaseTypeSecondary::Interview: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-interview"); break;
case ReleaseTypeSecondary::Audiobook: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiobook"); break;
case ReleaseTypeSecondary::AudioDrama: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiodrama"); break;
case ReleaseTypeSecondary::Live: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-live"); break;
case ReleaseTypeSecondary::Remix: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-remix"); break;
case ReleaseTypeSecondary::DJMix: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-djmix"); break;
case ReleaseTypeSecondary::Mixtape_Street: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-mixtape-street"); break;
case ReleaseTypeSecondary::Demo: res += Wt::WString::tr("Lms.Explore.Release.type-secondary-demo"); break;
}
}
return res;
}
return res;
}
Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate)
{
Wt::WString res;
Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate)
{
Wt::WString res;
// Year can be here, but originalYear can't be here without year (enforced by scanner)
if (!releaseDate.isValid())
return res;
// Year can be here, but originalYear can't be here without year (enforced by scanner)
if (!releaseDate.isValid())
return res;
if (originalReleaseDate.isValid() && originalReleaseDate != releaseDate)
res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")";
else
res = std::to_string(releaseDate.year());
if (originalReleaseDate.isValid() && originalReleaseDate.year() != releaseDate.year())
res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")";
else
res = std::to_string(releaseDate.year());
return res;
}
} // namespace UserInterface::ReleaseHelpers}
return res;
}
} // namespace UserInterface::ReleaseHelpers
+6 -6
View File
@@ -31,18 +31,18 @@
namespace Database
{
class Artist;
class Release;
class Artist;
class Release;
}
namespace UserInterface::ReleaseListHelpers
{
std::unique_ptr<Wt::WTemplate> createEntry(const Database::ObjectPtr<Database::Release>& release);
std::unique_ptr<Wt::WTemplate> createEntryForArtist(const Database::ObjectPtr<Database::Release>& release, const Database::ObjectPtr<Database::Artist>& artist);
std::unique_ptr<Wt::WTemplate> createEntry(const Database::ObjectPtr<Database::Release>& release);
std::unique_ptr<Wt::WTemplate> createEntryForArtist(const Database::ObjectPtr<Database::Release>& release, const Database::ObjectPtr<Database::Artist>& artist);
} // namespace UserInterface
namespace UserInterface::ReleaseHelpers
{
Wt::WString buildReleaseTypeString(Database::ReleaseTypePrimary primaryType, EnumSet<Database::ReleaseTypeSecondary> secondaryTypes);
Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate);
Wt::WString buildReleaseTypeString(Database::ReleaseTypePrimary primaryType, EnumSet<Database::ReleaseTypeSecondary> secondaryTypes);
Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate);
}
+6 -24
View File
@@ -128,7 +128,7 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
for (const auto& [role, artistIds] : artistMap)
{
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};
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->bindWidget("artist-container", std::move(artistContainer));
@@ -400,8 +400,8 @@ Release::refreshView()
if (variousArtists && !artists.empty())
{
entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", Utils::createArtistContainer(artists));
entry->bindWidget("artists-md", Utils::createArtistContainer(artists));
entry->bindWidget("artists", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
entry->bindWidget("artists-md", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(),artists));
}
auto trackNumber {track->getTrackNumber()};
@@ -480,29 +480,11 @@ Release::refreshView()
void
Release::refreshReleaseArtists(const Database::Release::pointer& release)
{
std::vector<ObjectPtr<Artist>> artists;
artists = release->getReleaseArtists();
if (artists.empty())
{
artists = release->getArtists(TrackArtistLinkType::Artist);
if (artists.size() > 1)
{
setCondition("if-has-various-release-artists", true);
return;
}
}
if (!artists.empty())
auto container{ Utils::createArtistsAnchorsForRelease(release) };
if (container)
{
setCondition("if-has-release-artists", true);
Wt::WContainerWidget* artistsContainer {bindNew<Wt::WContainerWidget>("artists")};
for (const auto& artist : artists)
{
Wt::WTemplate* artistTemplate {artistsContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry-release-artist"))};
artistTemplate->bindWidget("artist", Utils::createArtistAnchor(artist));
}
bindWidget("artists", std::move(container));
}
}
+3 -3
View File
@@ -117,7 +117,7 @@ namespace UserInterface::TrackListHelpers
for (const auto& [role, artistIds] : artistMap)
{
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};
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->bindWidget("artist-container", std::move(artistContainer));
@@ -171,8 +171,8 @@ namespace UserInterface::TrackListHelpers
if (!artists.empty())
{
entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", Utils::createArtistContainer(artists));
entry->bindWidget("artists-md", Utils::createArtistContainer(artists));
entry->bindWidget("artists", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
entry->bindWidget("artists-md", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
}
if (track->getRelease())
+6
View File
@@ -96,6 +96,9 @@ operator<<(std::ostream& os, const MetaData::Release& release)
if (release.mediumCount)
std::cout << "\tMediumCount: " << *release.mediumCount << std::endl;
if (!release.artistDisplayName.empty())
std::cout << "\tDisplay artist: " << release.artistDisplayName << std::endl;
for (const MetaData::Artist& artist : release.artists)
std::cout << "\tRelease artist: " << artist << std::endl;
@@ -154,6 +157,9 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
std::cout << "Parsed metadata:" << std::endl;
if (!track->artistDisplayName.empty())
std::cout << "Display artist: " << track->artistDisplayName << std::endl;
for (const Artist& artist : track->artists)
std::cout << "Artist: " << artist << std::endl;