Merge branch 'develop' for release v3.42.0
This commit is contained in:
@@ -11,19 +11,24 @@ OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'l
|
|||||||
## Extra fields
|
## Extra fields
|
||||||
The following extra fields are implemented:
|
The following extra fields are implemented:
|
||||||
* `Album` response:
|
* `Album` response:
|
||||||
|
* `played`
|
||||||
* `musicBrainzId`
|
* `musicBrainzId`
|
||||||
* `genres`
|
* `genres`
|
||||||
* `artists`
|
* `artists`
|
||||||
|
* `displayArtist`
|
||||||
* `releaseTypes`
|
* `releaseTypes`
|
||||||
* `moods`
|
* `moods`
|
||||||
* `originalReleaseDate`
|
* `originalReleaseDate`
|
||||||
* `isCompilation`
|
* `isCompilation`
|
||||||
* `discTitles`: discs with no subtitle are omitted
|
* `discTitles`: discs with no subtitle are omitted
|
||||||
* `Child` response:
|
* `Child` response:
|
||||||
|
* `played`
|
||||||
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
|
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
|
||||||
* `genres`
|
* `genres`
|
||||||
* `artists`
|
* `artists`
|
||||||
|
* `displayArtist`
|
||||||
* `albumArtists`
|
* `albumArtists`
|
||||||
|
* `displayAlbumArtist`
|
||||||
* `contributors`
|
* `contributors`
|
||||||
* `moods`
|
* `moods`
|
||||||
* `replayGain`
|
* `replayGain`
|
||||||
|
|||||||
@@ -241,6 +241,8 @@
|
|||||||
<message id="Lms.Settings.audio">Audio</message>
|
<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.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.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.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.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
|
||||||
<message id="Lms.Settings.password-bad">Bad password</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.subsonic-api">Subsonic API</message>
|
||||||
<message id="Lms.Settings.transcode">Transcoding</message>
|
<message id="Lms.Settings.transcode">Transcoding</message>
|
||||||
<message id="Lms.Settings.transcode-bitrate">Transcode bitrate</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">Transcode format</message>
|
||||||
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
|
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
|
||||||
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
|
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
|
||||||
|
|||||||
@@ -241,6 +241,8 @@
|
|||||||
<message id="Lms.Settings.audio">Audio</message>
|
<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.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.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.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.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>
|
<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.subsonic-api">API Subsonic</message>
|
||||||
<message id="Lms.Settings.transcode">Transcodage</message>
|
<message id="Lms.Settings.transcode">Transcodage</message>
|
||||||
<message id="Lms.Settings.transcode-bitrate">Bitrate du 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">Format du transcodage</message>
|
||||||
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
|
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
|
||||||
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
|
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
|
||||||
|
|||||||
@@ -241,6 +241,8 @@
|
|||||||
<message id="Lms.Settings.audio">Audio</message>
|
<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.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.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.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.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
|
||||||
<message id="Lms.Settings.password-bad">Password errata</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.subsonic-api">Subsonic API</message>
|
||||||
<message id="Lms.Settings.transcode">Transcodifica</message>
|
<message id="Lms.Settings.transcode">Transcodifica</message>
|
||||||
<message id="Lms.Settings.transcode-bitrate">Bitrate 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">Formato di transcodifica</message>
|
||||||
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
|
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
|
||||||
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
|
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
|
||||||
|
|||||||
@@ -241,6 +241,8 @@
|
|||||||
<message id="Lms.Settings.audio">音频</message>
|
<message id="Lms.Settings.audio">音频</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
|
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
|
||||||
<message id="Lms.Settings.change-password">更改密码</message>
|
<message id="Lms.Settings.change-password">更改密码</message>
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</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.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
|
||||||
<message id="Lms.Settings.password-bad">无效密码</message>
|
<message id="Lms.Settings.password-bad">无效密码</message>
|
||||||
@@ -265,7 +267,6 @@
|
|||||||
<message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message>
|
<message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message>
|
||||||
<message id="Lms.Settings.transcode">转码</message>
|
<message id="Lms.Settings.transcode">转码</message>
|
||||||
<message id="Lms.Settings.transcode-bitrate">转码比特率</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">转码格式</message>
|
||||||
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
|
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
|
||||||
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
|
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<h2>${name}</h2>
|
<h2>${name}</h2>
|
||||||
${<if-has-release-artists>}${artists}${</if-has-release-artists>}
|
${<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>
|
<div class="small text-muted">${<if-has-year>}${year} · ${</if-has-year>}${duration}</div>
|
||||||
${clusters class="mb-2"}
|
${clusters class="mb-2"}
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
|
|||||||
@@ -40,12 +40,11 @@
|
|||||||
<message id="Lms.Explore.Releases.template.entry-grid">
|
<message id="Lms.Explore.Releases.template.entry-grid">
|
||||||
<div class="col h-100 Lms-bg-dark-hover rounded">
|
<div class="col h-100 Lms-bg-dark-hover rounded">
|
||||||
<div class="p-2">
|
<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"}
|
${cover class="shadow-sm"}
|
||||||
</div>
|
</div>
|
||||||
${release-name class="d-block text-truncate text-nowrap text-decoration-none link-success"}
|
${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-artist>}${artist-name class="d-block text-truncate text-nowrap"}${</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-year>}<div class="small text-muted">${year}</div>${</if-has-year>}
|
${<if-has-year>}<div class="small text-muted">${year}</div>${</if-has-year>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-14
@@ -74,20 +74,9 @@
|
|||||||
</div>
|
</div>
|
||||||
${<if-has-subsonic-api>}
|
${<if-has-subsonic-api>}
|
||||||
<legend>${tr:Lms.Settings.subsonic-api}</legend>
|
<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">
|
<div class="col-lg-6">
|
||||||
<label class="form-label" for="${id:subsonic-transcode-format}">
|
<label class="form-label" for="${id:subsonic-transcode-format}">
|
||||||
${tr:Lms.Settings.transcode-format}
|
${tr:Lms.Settings.default-transcode-format}
|
||||||
</label>
|
</label>
|
||||||
${subsonic-transcode-format class="form-control"}
|
${subsonic-transcode-format class="form-control"}
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
@@ -95,8 +84,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<label class="col-lg-3 form-label" for="${id:subsonic-transcode-bitrate}">
|
<label class="form-label" for="${id:subsonic-transcode-bitrate}">
|
||||||
${tr:Lms.Settings.transcode-bitrate}
|
${tr:Lms.Settings.default-transcode-bitrate}
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
${subsonic-transcode-bitrate class="form-control"}
|
${subsonic-transcode-bitrate class="form-control"}
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ getRelease(const TagMap& tags)
|
|||||||
|
|
||||||
release.emplace();
|
release.emplace();
|
||||||
release->name = std::move(*releaseName);
|
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->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->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");
|
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;
|
track.copyrightURL = value;
|
||||||
else if (tag == "REPLAYGAIN_TRACK_GAIN")
|
else if (tag == "REPLAYGAIN_TRACK_GAIN")
|
||||||
track.replayGain = StringUtils::readAs<float>(value);
|
track.replayGain = StringUtils::readAs<float>(value);
|
||||||
|
else if (tag == "ARTIST")
|
||||||
|
track.artistDisplayName = value;
|
||||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||||
{
|
{
|
||||||
std::set<std::string> clusterNames;
|
std::set<std::string> clusterNames;
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ namespace MetaData
|
|||||||
|
|
||||||
std::optional<UUID> mbid;
|
std::optional<UUID> mbid;
|
||||||
std::string name;
|
std::string name;
|
||||||
|
std::string artistDisplayName;
|
||||||
std::vector<Artist> artists;
|
std::vector<Artist> artists;
|
||||||
std::optional<std::size_t> mediumCount;
|
std::optional<std::size_t> mediumCount;
|
||||||
std::optional<PrimaryType> primaryType;
|
std::optional<PrimaryType> primaryType;
|
||||||
@@ -117,6 +118,7 @@ namespace MetaData
|
|||||||
std::string copyright;
|
std::string copyright;
|
||||||
std::string copyrightURL;
|
std::string copyrightURL;
|
||||||
std::optional<float> replayGain;
|
std::optional<float> replayGain;
|
||||||
|
std::string artistDisplayName;
|
||||||
std::vector<Artist> artists;
|
std::vector<Artist> artists;
|
||||||
std::vector<Artist> conductorArtists;
|
std::vector<Artist> conductorArtists;
|
||||||
std::vector<Artist> composerArtists;
|
std::vector<Artist> composerArtists;
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ namespace Database
|
|||||||
{
|
{
|
||||||
namespace
|
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();
|
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())
|
if (params.track.isValid() || params.release.isValid())
|
||||||
{
|
{
|
||||||
@@ -74,7 +74,7 @@ namespace Database
|
|||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
|
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();
|
session.checkSharedLocked();
|
||||||
auto query{ createQuery(session, params) };
|
auto query{ createQuery(session, params) };
|
||||||
|
|||||||
@@ -26,40 +26,63 @@
|
|||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class Connection : public Wt::Dbo::backend::Sqlite3
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Connection(const std::filesystem::path& dbPath)
|
||||||
|
: Wt::Dbo::backend::Sqlite3{ dbPath.string() }
|
||||||
|
, _dbPath{ dbPath }
|
||||||
|
{
|
||||||
|
prepare();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Connection(const Connection&) = delete;
|
||||||
|
Connection& operator=(const Connection&) = delete;
|
||||||
|
|
||||||
|
std::unique_ptr<SqlConnection> clone() const override
|
||||||
|
{
|
||||||
|
return std::make_unique<Connection>(_dbPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
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!";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path _dbPath;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Session living class handling the database and the login
|
// Session living class handling the database and the login
|
||||||
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
|
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
|
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
|
||||||
|
|
||||||
std::unique_ptr<Wt::Dbo::backend::Sqlite3> connection {std::make_unique<Wt::Dbo::backend::Sqlite3>(dbPath.string())};
|
auto connection{ std::make_unique<Connection>(dbPath.string()) };
|
||||||
// connection->setProperty("show-queries", "true");
|
// connection->setProperty("show-queries", "true");
|
||||||
connection->executeSql("pragma journal_mode=WAL");
|
|
||||||
connection->executeSql("pragma synchronous=normal");
|
|
||||||
|
|
||||||
auto connectionPool = std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount);
|
auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) };
|
||||||
connectionPool->setTimeout(std::chrono::seconds(10));
|
connectionPool->setTimeout(std::chrono::seconds{ 10 });
|
||||||
|
|
||||||
_connectionPool = std::move(connectionPool);
|
_connectionPool = std::move(connectionPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
Db::~Db()
|
void Db::executeSql(const std::string& sql)
|
||||||
{
|
|
||||||
LMS_LOG(DB, DEBUG) << "Optimizing db...";
|
|
||||||
executeSql("pragma optimize");
|
|
||||||
LMS_LOG(DB, DEBUG) << "Optimizing db DONE";
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Db::executeSql(const std::string& sql)
|
|
||||||
{
|
{
|
||||||
ScopedConnection connection{ *_connectionPool };
|
ScopedConnection connection{ *_connectionPool };
|
||||||
connection->executeSql(sql);
|
connection->executeSql(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
Session&
|
Session& Db::getTLSSession()
|
||||||
Db::getTLSSession()
|
|
||||||
{
|
{
|
||||||
static thread_local Session* tlsSession{};
|
static thread_local Session* tlsSession{};
|
||||||
|
|
||||||
@@ -94,5 +117,3 @@ Wt::Dbo::SqlConnection* Db::ScopedConnection::operator->() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ namespace
|
|||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
Wt::Dbo::Query<ArtistId>
|
Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
||||||
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")
|
auto query{ session.query<ArtistId>("SELECT a.id from artist a")
|
||||||
.join("track t ON t.id = t_a_l.track_id")
|
.join("track t ON t.id = t_a_l.track_id")
|
||||||
@@ -67,8 +66,7 @@ namespace
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::Dbo::Query<ReleaseId>
|
Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
|
||||||
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")
|
auto query{ session.query<ReleaseId>("SELECT r.id from release r")
|
||||||
.join("track t ON t.release_id = r.id")
|
.join("track t ON t.release_id = r.id")
|
||||||
@@ -100,8 +98,7 @@ namespace
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::Dbo::Query<TrackId>
|
Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
|
||||||
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")
|
auto query{ session.query<TrackId>("SELECT t.id from track t")
|
||||||
.join("listen l ON l.track_id = t.id")
|
.join("listen l ON l.track_id = t.id")
|
||||||
@@ -141,28 +138,24 @@ namespace Database
|
|||||||
, _track{ getDboPtr(track) }
|
, _track{ getDboPtr(track) }
|
||||||
{}
|
{}
|
||||||
|
|
||||||
Listen::pointer
|
Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
||||||
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 }});
|
return session.getDboSession().add(std::unique_ptr<Listen> {new Listen{ user, track, scrobbler, dateTime }});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t
|
std::size_t Listen::getCount(Session& session)
|
||||||
Listen::getCount(Session& session)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen");
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen");
|
||||||
}
|
}
|
||||||
|
|
||||||
Listen::pointer
|
Listen::pointer Listen::find(Session& session, ListenId id)
|
||||||
Listen::find(Session& session, ListenId id)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue();
|
return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ListenId>
|
RangeResults<ListenId> Listen::find(Session& session, const FindParameters& parameters)
|
||||||
Listen::find(Session& session, const FindParameters& parameters)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -181,8 +174,7 @@ namespace Database
|
|||||||
return Utils::execQuery(query, parameters.range);
|
return Utils::execQuery(query, parameters.range);
|
||||||
}
|
}
|
||||||
|
|
||||||
Listen::pointer
|
Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
||||||
Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -194,13 +186,7 @@ namespace Database
|
|||||||
.resultValue();
|
.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ArtistId>
|
RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||||
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 query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) };
|
||||||
|
|
||||||
@@ -211,12 +197,7 @@ namespace Database
|
|||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ReleaseId>
|
RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
Listen::getTopReleases(Session& session,
|
|
||||||
UserId userId,
|
|
||||||
Scrobbler scrobbler,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
Range range)
|
|
||||||
{
|
{
|
||||||
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
.orderBy("COUNT(r.id) DESC")
|
.orderBy("COUNT(r.id) DESC")
|
||||||
@@ -225,12 +206,7 @@ namespace Database
|
|||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<TrackId>
|
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
Listen::getTopTracks(Session& session,
|
|
||||||
UserId userId,
|
|
||||||
Scrobbler scrobbler,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
Range range)
|
|
||||||
{
|
{
|
||||||
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
.orderBy("COUNT(t.id) DESC")
|
.orderBy("COUNT(t.id) DESC")
|
||||||
@@ -239,13 +215,7 @@ namespace Database
|
|||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ArtistId>
|
RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||||
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)
|
auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)
|
||||||
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
|
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
|
||||||
@@ -254,12 +224,7 @@ namespace Database
|
|||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ReleaseId>
|
RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
Listen::getRecentReleases(Session& session,
|
|
||||||
UserId userId,
|
|
||||||
Scrobbler scrobbler,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
Range range)
|
|
||||||
{
|
{
|
||||||
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
|
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
|
||||||
@@ -268,12 +233,7 @@ namespace Database
|
|||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<TrackId>
|
RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
Listen::getRecentTracks(Session& session,
|
|
||||||
UserId userId,
|
|
||||||
Scrobbler scrobbler,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
Range range)
|
|
||||||
{
|
{
|
||||||
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
||||||
@@ -282,6 +242,30 @@ namespace Database
|
|||||||
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
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,7 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
VersionInfo::pointer
|
VersionInfo::pointer VersionInfo::getOrCreate(Session& session)
|
||||||
VersionInfo::getOrCreate(Session& session)
|
|
||||||
{
|
{
|
||||||
session.checkUniqueLocked();
|
session.checkUniqueLocked();
|
||||||
|
|
||||||
@@ -42,14 +41,12 @@ namespace Database
|
|||||||
return versionInfo;
|
return versionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionInfo::pointer
|
VersionInfo::pointer VersionInfo::get(Session& session)
|
||||||
VersionInfo::get(Session& session)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
return session.getDboSession().find<VersionInfo>();
|
return session.getDboSession().find<VersionInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Database::Migration
|
namespace Database::Migration
|
||||||
@@ -75,50 +72,38 @@ namespace Database::Migration
|
|||||||
Db& _db;
|
Db& _db;
|
||||||
};
|
};
|
||||||
|
|
||||||
static
|
static std::string dateTimeToDbFormat(const Wt::WDateTime& dateTime)
|
||||||
std::string
|
|
||||||
dateTimeToDbFormat(const Wt::WDateTime& dateTime)
|
|
||||||
{
|
{
|
||||||
return dateTime.toString("yyyy'-'MM'-'dd'T'hh':'mm':'ss'.000'", false).toUTF8();
|
return dateTime.toString("yyyy'-'MM'-'dd'T'hh':'mm':'ss'.000'", false).toUTF8();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV5(Session& session)
|
||||||
void
|
|
||||||
migrateFromV5(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute("DELETE FROM auth_token"); // format has changed
|
session.getDboSession().execute("DELETE FROM auth_token"); // format has changed
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV6(Session& session)
|
||||||
void
|
|
||||||
migrateFromV6(Session& session)
|
|
||||||
{
|
{
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static
|
static void migrateFromV7(Session& session)
|
||||||
void
|
|
||||||
migrateFromV7(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute("DROP TABLE similarity_settings");
|
session.getDboSession().execute("DROP TABLE similarity_settings");
|
||||||
session.getDboSession().execute("DROP TABLE similarity_settings_feature");
|
session.getDboSession().execute("DROP TABLE similarity_settings_feature");
|
||||||
session.getDboSession().execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(ScanSettings::RecommendationEngineType::Clusters)) + ")");
|
session.getDboSession().execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(ScanSettings::RecommendationEngineType::Clusters)) + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV8(Session& session)
|
||||||
void
|
|
||||||
migrateFromV8(Session& session)
|
|
||||||
{
|
{
|
||||||
// Better cover handling, need to rescan the whole files
|
// Better cover handling, need to rescan the whole files
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV9(Session& session)
|
||||||
void
|
|
||||||
migrateFromV9(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute(R"(
|
session.getDboSession().execute(R"(
|
||||||
CREATE TABLE IF NOT EXISTS "track_bookmark" (
|
CREATE TABLE IF NOT EXISTS "track_bookmark" (
|
||||||
@@ -133,60 +118,46 @@ CREATE TABLE IF NOT EXISTS "track_bookmark" (
|
|||||||
);)");
|
);)");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV10(Session& session)
|
||||||
void
|
|
||||||
migrateFromV10(Session& session)
|
|
||||||
{
|
{
|
||||||
ScanSettings::get(session).modify()->addAudioFileExtension(".m4b");
|
ScanSettings::get(session).modify()->addAudioFileExtension(".m4b");
|
||||||
ScanSettings::get(session).modify()->addAudioFileExtension(".alac");
|
ScanSettings::get(session).modify()->addAudioFileExtension(".alac");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV11(Session& session)
|
||||||
void
|
|
||||||
migrateFromV11(Session& session)
|
|
||||||
{
|
{
|
||||||
// Sanitize bad MBID, need to rescan the whole files
|
// Sanitize bad MBID, need to rescan the whole files
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV12(Session& session)
|
||||||
void
|
|
||||||
migrateFromV12(Session& session)
|
|
||||||
{
|
{
|
||||||
// Artist and release that have a badly parsed name but a MBID had no chance to updat the name
|
// Artist and release that have a badly parsed name but a MBID had no chance to updat the name
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV13(Session& session)
|
||||||
void
|
|
||||||
migrateFromV13(Session& session)
|
|
||||||
{
|
{
|
||||||
// Always store UUID in lower case + better WMA parsing
|
// Always store UUID in lower case + better WMA parsing
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV14(Session& session)
|
||||||
void
|
|
||||||
migrateFromV14(Session& session)
|
|
||||||
{
|
{
|
||||||
// SortName now set from metadata
|
// SortName now set from metadata
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV15(Session& session)
|
||||||
void
|
|
||||||
migrateFromV15(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultUITheme)) + ")");
|
session.getDboSession().execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultUITheme)) + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV16(Session& session)
|
||||||
void
|
|
||||||
migrateFromV16(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)");
|
session.getDboSession().execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)");
|
||||||
session.getDboSession().execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)");
|
session.getDboSession().execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)");
|
||||||
@@ -195,9 +166,7 @@ CREATE TABLE IF NOT EXISTS "track_bookmark" (
|
|||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV17(Session& session)
|
||||||
void
|
|
||||||
migrateFromV17(Session& session)
|
|
||||||
{
|
{
|
||||||
// Drop colums total_disc/total_track from release
|
// Drop colums total_disc/total_track from release
|
||||||
session.getDboSession().execute(R"(
|
session.getDboSession().execute(R"(
|
||||||
@@ -215,10 +184,7 @@ CREATE TABLE "release_backup" (
|
|||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void migrateFromV18(Session& session)
|
||||||
static
|
|
||||||
void
|
|
||||||
migrateFromV18(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute(R"(
|
session.getDboSession().execute(R"(
|
||||||
CREATE TABLE IF NOT EXISTS "subsonic_settings" (
|
CREATE TABLE IF NOT EXISTS "subsonic_settings" (
|
||||||
@@ -229,9 +195,7 @@ CREATE TABLE IF NOT EXISTS "subsonic_settings" (
|
|||||||
))");
|
))");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV19(Session& session)
|
||||||
void
|
|
||||||
migrateFromV19(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute(R"(
|
session.getDboSession().execute(R"(
|
||||||
CREATE TABLE "user_backup" (
|
CREATE TABLE "user_backup" (
|
||||||
@@ -252,7 +216,7 @@ CREATE TABLE "user_backup" (
|
|||||||
"radio" boolean not null
|
"radio" boolean not null
|
||||||
))");
|
))");
|
||||||
session.getDboSession().execute(std::string{ "INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, " }
|
session.getDboSession().execute(std::string{ "INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, " }
|
||||||
+ (User::defaultSubsonicTranscodeEnable ? "1" : "0")
|
+ "1" // default enable transcode
|
||||||
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicTranscodeFormat))
|
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicTranscodeFormat))
|
||||||
+ ", " + std::to_string(User::defaultSubsonicTranscodeBitrate)
|
+ ", " + std::to_string(User::defaultSubsonicTranscodeBitrate)
|
||||||
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicArtistListMode))
|
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicArtistListMode))
|
||||||
@@ -261,16 +225,12 @@ CREATE TABLE "user_backup" (
|
|||||||
session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user");
|
session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV20(Session& session)
|
||||||
void
|
|
||||||
migrateFromV20(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute("DROP TABLE subsonic_settings");
|
session.getDboSession().execute("DROP TABLE subsonic_settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV21(Session& session)
|
||||||
void
|
|
||||||
migrateFromV21(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute("ALTER TABLE track ADD track_replay_gain REAL");
|
session.getDboSession().execute("ALTER TABLE track ADD track_replay_gain REAL");
|
||||||
session.getDboSession().execute("ALTER TABLE track ADD release_replay_gain REAL");
|
session.getDboSession().execute("ALTER TABLE track ADD release_replay_gain REAL");
|
||||||
@@ -279,10 +239,7 @@ CREATE TABLE "user_backup" (
|
|||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void migrateFromV22(Session& session)
|
||||||
static
|
|
||||||
void
|
|
||||||
migrateFromV22(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''");
|
session.getDboSession().execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
@@ -290,54 +247,41 @@ CREATE TABLE "user_backup" (
|
|||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void migrateFromV23(Session& session)
|
||||||
static
|
|
||||||
void
|
|
||||||
migrateFromV23(Session& session)
|
|
||||||
{
|
{
|
||||||
// Better cover detection
|
// Better cover detection
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV24(Session& session)
|
||||||
void
|
|
||||||
migrateFromV24(Session& session)
|
|
||||||
{
|
{
|
||||||
// User's AuthMode
|
// User's AuthMode
|
||||||
session.getDboSession().execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultAuthMode*/0)) + ")");
|
session.getDboSession().execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultAuthMode*/0)) + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV25(Session& session)
|
||||||
void
|
|
||||||
migrateFromV25(Session& session)
|
|
||||||
{
|
{
|
||||||
// Better cover detection
|
// Better cover detection
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV26(Session& session)
|
||||||
void
|
|
||||||
migrateFromV26(Session& session)
|
|
||||||
{
|
{
|
||||||
// Composer, mixer, etc. support
|
// Composer, mixer, etc. support
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV27(Session& session)
|
||||||
void
|
|
||||||
migrateFromV27(Session& session)
|
|
||||||
{
|
{
|
||||||
// Composer, mixer, etc. support, now fallback on MBID tagged entries as there is no mean to provide MBID by tags for these kinf od artists
|
// Composer, mixer, etc. support, now fallback on MBID tagged entries as there is no mean to provide MBID by tags for these kinf od artists
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV28(Session& session)
|
||||||
void
|
|
||||||
migrateFromV28(Session& session)
|
|
||||||
{
|
{
|
||||||
// Drop Auth mode
|
// Drop Auth mode
|
||||||
session.getDboSession().execute(R"(
|
session.getDboSession().execute(R"(
|
||||||
@@ -363,9 +307,7 @@ CREATE TABLE "user_backup" (
|
|||||||
session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user");
|
session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV29(Session& session)
|
||||||
void
|
|
||||||
migrateFromV29(Session& session)
|
|
||||||
{
|
{
|
||||||
session.getDboSession().execute("ALTER TABLE tracklist_entry ADD date_time TEXT");
|
session.getDboSession().execute("ALTER TABLE tracklist_entry ADD date_time TEXT");
|
||||||
session.getDboSession().execute("ALTER TABLE user ADD listenbrainz_token TEXT");
|
session.getDboSession().execute("ALTER TABLE user ADD listenbrainz_token TEXT");
|
||||||
@@ -379,9 +321,7 @@ CREATE TABLE "user_backup" (
|
|||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV30(Session& session)
|
||||||
void
|
|
||||||
migrateFromV30(Session& session)
|
|
||||||
{
|
{
|
||||||
// drop "year" and "original_year" (rescan needed to convert them into dates)
|
// drop "year" and "original_year" (rescan needed to convert them into dates)
|
||||||
session.getDboSession().execute(R"(
|
session.getDboSession().execute(R"(
|
||||||
@@ -413,9 +353,7 @@ CREATE TABLE "track_backup" (
|
|||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV31(Session& session)
|
||||||
void
|
|
||||||
migrateFromV31(Session& session)
|
|
||||||
{
|
{
|
||||||
// new star system, using dedicated entries per scrobbler and date time
|
// new star system, using dedicated entries per scrobbler and date time
|
||||||
session.getDboSession().execute(R"(
|
session.getDboSession().execute(R"(
|
||||||
@@ -542,16 +480,12 @@ CREATE TABLE "listen" (
|
|||||||
migrateListens("__scrobbler_listenbrainz_history__", Scrobbler::ListenBrainz);
|
migrateListens("__scrobbler_listenbrainz_history__", Scrobbler::ListenBrainz);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV32(Session& session)
|
||||||
void
|
|
||||||
migrateFromV32(Session& session)
|
|
||||||
{
|
{
|
||||||
ScanSettings::get(session).modify()->addAudioFileExtension(".wv");
|
ScanSettings::get(session).modify()->addAudioFileExtension(".wv");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV33(Session& session)
|
||||||
void
|
|
||||||
migrateFromV33(Session& session)
|
|
||||||
{
|
{
|
||||||
// remove name from track_artist_link
|
// remove name from track_artist_link
|
||||||
// Drop Auth mode
|
// Drop Auth mode
|
||||||
@@ -571,9 +505,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
|||||||
session.getDboSession().execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link");
|
session.getDboSession().execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV34(Session& session)
|
||||||
void
|
|
||||||
migrateFromV34(Session& session)
|
|
||||||
{
|
{
|
||||||
// Add scrobbling state
|
// Add scrobbling state
|
||||||
// By default, everything needs to be sent
|
// By default, everything needs to be sent
|
||||||
@@ -582,27 +514,21 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
|||||||
session.getDboSession().execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")");
|
session.getDboSession().execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV35(Session& session)
|
||||||
void
|
|
||||||
migrateFromV35(Session& session)
|
|
||||||
{
|
{
|
||||||
// Add creattion/last modif date time for tracklists
|
// Add creattion/last modif date time for tracklists
|
||||||
session.getDboSession().execute("ALTER TABLE tracklist ADD creation_date_time TEXT");
|
session.getDboSession().execute("ALTER TABLE tracklist ADD creation_date_time TEXT");
|
||||||
session.getDboSession().execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT");
|
session.getDboSession().execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT");
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV36(Session& session)
|
||||||
void
|
|
||||||
migrateFromV36(Session& session)
|
|
||||||
{
|
{
|
||||||
// Increased precision for track durations (now in milliseconds instead of secodns)
|
// Increased precision for track durations (now in milliseconds instead of secodns)
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV37(Session& session)
|
||||||
void
|
|
||||||
migrateFromV37(Session& session)
|
|
||||||
{
|
{
|
||||||
// Support Performer tags (via subtypes)
|
// Support Performer tags (via subtypes)
|
||||||
session.getDboSession().execute("ALTER TABLE track_artist_link ADD subtype TEXT");
|
session.getDboSession().execute("ALTER TABLE track_artist_link ADD subtype TEXT");
|
||||||
@@ -611,9 +537,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
|||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV38(Session& session)
|
||||||
void
|
|
||||||
migrateFromV38(Session& session)
|
|
||||||
{
|
{
|
||||||
// migrate release-specific tags from Track to Release
|
// migrate release-specific tags from Track to Release
|
||||||
session.getDboSession().execute("ALTER TABLE release ADD total_disc INTEGER");
|
session.getDboSession().execute("ALTER TABLE release ADD total_disc INTEGER");
|
||||||
@@ -653,9 +577,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static void migrateFromV39(Session& session)
|
||||||
void
|
|
||||||
migrateFromV39(Session& session)
|
|
||||||
{
|
{
|
||||||
// add release type
|
// add release type
|
||||||
session.getDboSession().execute("ALTER TABLE release ADD primary_type INTEGER");
|
session.getDboSession().execute("ALTER TABLE release ADD primary_type INTEGER");
|
||||||
@@ -665,8 +587,25 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
ScanSettings::get(session).modify()->incScanVersion();
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
static void migrateFromV40(Session& session)
|
||||||
doDbMigration(Session& session)
|
{
|
||||||
|
// add artist_display_name in Release and Track
|
||||||
|
session.getDboSession().execute("ALTER TABLE release ADD artist_display_name TEXT");
|
||||||
|
session.getDboSession().execute("ALTER TABLE track ADD artist_display_name TEXT");
|
||||||
|
|
||||||
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void migrateFromV41(Session& session)
|
||||||
|
{
|
||||||
|
// add artist_display_name in Release and Track
|
||||||
|
session.getDboSession().execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_format TO subsonic_default_transcode_format");
|
||||||
|
session.getDboSession().execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_bitrate TO subsonic_default_transcode_bitrate");
|
||||||
|
session.getDboSession().execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable");
|
||||||
|
}
|
||||||
|
|
||||||
|
void doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||||
|
|
||||||
@@ -711,9 +650,10 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
{37, migrateFromV37},
|
{37, migrateFromV37},
|
||||||
{38, migrateFromV38},
|
{38, migrateFromV38},
|
||||||
{39, migrateFromV39},
|
{39, migrateFromV39},
|
||||||
|
{40, migrateFromV40},
|
||||||
|
{41, migrateFromV41},
|
||||||
};
|
};
|
||||||
|
|
||||||
while (1)
|
|
||||||
{
|
{
|
||||||
auto uniqueTransaction{ session.createUniqueTransaction() };
|
auto uniqueTransaction{ session.createUniqueTransaction() };
|
||||||
|
|
||||||
@@ -729,26 +669,24 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
throw LmsException{ outdatedMsg };
|
throw LmsException{ outdatedMsg };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version == LMS_DATABASE_VERSION)
|
if (version > LMS_DATABASE_VERSION)
|
||||||
{
|
|
||||||
LMS_LOG(DB, DEBUG) << "Lms database version " << LMS_DATABASE_VERSION << ": up to date!";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (version > LMS_DATABASE_VERSION)
|
|
||||||
{
|
|
||||||
throw LmsException{ "Server binary outdated, please upgrade it to handle this database" };
|
throw LmsException{ "Server binary outdated, please upgrade it to handle this database" };
|
||||||
}
|
|
||||||
|
|
||||||
if (version < migrationFunctions.begin()->first)
|
if (version < migrationFunctions.begin()->first)
|
||||||
throw LmsException{ outdatedMsg };
|
throw LmsException{ outdatedMsg };
|
||||||
|
|
||||||
LMS_LOG(DB, INFO) << "Migrating database from version " << version << "...";
|
while (version < LMS_DATABASE_VERSION)
|
||||||
|
{
|
||||||
|
LMS_LOG(DB, INFO) << "Migrating database from version " << version << " to " << version + 1 << "...";
|
||||||
|
|
||||||
auto itMigrationFunc{ migrationFunctions.find(version) };
|
auto itMigrationFunc{ migrationFunctions.find(version) };
|
||||||
assert(itMigrationFunc != std::cend(migrationFunctions));
|
assert(itMigrationFunc != std::cend(migrationFunctions));
|
||||||
itMigrationFunc->second(session);
|
itMigrationFunc->second(session);
|
||||||
|
|
||||||
VersionInfo::get(session).modify()->setVersion(++version);
|
VersionInfo::get(session).modify()->setVersion(++version);
|
||||||
|
|
||||||
|
LMS_LOG(DB, INFO) << "Migration complete to version " << version;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace Database
|
|||||||
class Session;
|
class Session;
|
||||||
|
|
||||||
using Version = std::size_t;
|
using Version = std::size_t;
|
||||||
static constexpr Version LMS_DATABASE_VERSION {40};
|
static constexpr Version LMS_DATABASE_VERSION{ 42 };
|
||||||
class VersionInfo
|
class VersionInfo
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -83,43 +83,44 @@ SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Sessi
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Session::checkUniqueLocked()
|
||||||
Session::checkUniqueLocked()
|
|
||||||
{
|
{
|
||||||
assert(_db.getMutex().isUniqueLocked());
|
assert(_db.getMutex().isUniqueLocked());
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Session::checkSharedLocked()
|
||||||
Session::checkSharedLocked()
|
|
||||||
{
|
{
|
||||||
assert(_db.getMutex().isSharedLocked());
|
assert(_db.getMutex().isSharedLocked());
|
||||||
}
|
}
|
||||||
|
|
||||||
UniqueTransaction
|
UniqueTransaction Session::createUniqueTransaction()
|
||||||
Session::createUniqueTransaction()
|
|
||||||
{
|
{
|
||||||
return UniqueTransaction{ _db.getMutex(), _session };
|
return UniqueTransaction{ _db.getMutex(), _session };
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedTransaction
|
SharedTransaction Session::createSharedTransaction()
|
||||||
Session::createSharedTransaction()
|
|
||||||
{
|
{
|
||||||
return SharedTransaction{ _db.getMutex(), _session };
|
return SharedTransaction{ _db.getMutex(), _session };
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Session::prepareTables()
|
||||||
Session::prepareTables()
|
|
||||||
{
|
{
|
||||||
// Creation case
|
LMS_LOG(DB, INFO) << "Preparing tables...";
|
||||||
|
|
||||||
|
// Initial creation case
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_session.createTables();
|
_session.createTables();
|
||||||
|
|
||||||
LMS_LOG(DB, INFO) << "Tables created";
|
LMS_LOG(DB, INFO) << "Tables created";
|
||||||
}
|
}
|
||||||
catch (Wt::Dbo::Exception& e)
|
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();
|
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Migration::doDbMigration(*this);
|
Migration::doDbMigration(*this);
|
||||||
@@ -176,15 +177,14 @@ Session::prepareTables()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Session::analyze()
|
||||||
Session::optimize()
|
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, DEBUG) << "Optimizing db...";
|
LMS_LOG(DB, INFO) << "Analyzing database...";
|
||||||
{
|
{
|
||||||
auto uniqueTransaction{ createUniqueTransaction() };
|
auto uniqueTransaction{ createUniqueTransaction() };
|
||||||
_session.execute("ANALYZE");
|
_session.execute("ANALYZE");
|
||||||
}
|
}
|
||||||
LMS_LOG(DB, DEBUG) << "Optimized db!";
|
LMS_LOG(DB, INFO) << "Database Analyze complete";
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|||||||
@@ -35,22 +35,19 @@ User::User(std::string_view loginName)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
User::pointer
|
User::pointer User::create(Session& session, std::string_view loginName)
|
||||||
User::create(Session& session, std::string_view loginName)
|
|
||||||
{
|
{
|
||||||
return session.getDboSession().add(std::unique_ptr<User> {new User{ loginName }});
|
return session.getDboSession().add(std::unique_ptr<User> {new User{ loginName }});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t
|
std::size_t User::getCount(Session& session)
|
||||||
User::getCount(Session& session)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<UserId>
|
RangeResults<UserId> User::find(Session& session, const FindParameters& params)
|
||||||
User::find(Session& session, const FindParameters& params)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -62,41 +59,34 @@ User::find(Session& session, const FindParameters& params)
|
|||||||
return Utils::execQuery(query, params.range);
|
return Utils::execQuery(query, params.range);
|
||||||
}
|
}
|
||||||
|
|
||||||
User::pointer
|
User::pointer User::findDemoUser(Session& session)
|
||||||
User::findDemoUser(Session& session)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
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::pointer User::find(Session& session, UserId id)
|
||||||
User::find(Session& session, UserId id)
|
|
||||||
{
|
{
|
||||||
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
|
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
User::pointer
|
User::pointer User::find(Session& session, std::string_view name)
|
||||||
User::find(Session& session, std::string_view name)
|
|
||||||
{
|
{
|
||||||
return session.getDboSession().find<User>()
|
return session.getDboSession().find<User>()
|
||||||
.where("login_name = ?").bind(name)
|
.where("login_name = ?").bind(name)
|
||||||
.resultValue();
|
.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void User::setSubsonicDefaultTranscodeBitrate(Bitrate bitrate)
|
||||||
User::setSubsonicTranscodeBitrate(Bitrate bitrate)
|
|
||||||
{
|
{
|
||||||
assert(isAudioBitrateAllowed(bitrate));
|
assert(isAudioBitrateAllowed(bitrate));
|
||||||
_subsonicTranscodeBitrate = bitrate;
|
_subsonicDefaultTranscodeBitrate = bitrate;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void User::clearAuthTokens()
|
||||||
User::clearAuthTokens()
|
|
||||||
{
|
{
|
||||||
_authTokens.clear();
|
_authTokens.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <tuple>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
@@ -58,8 +59,10 @@ namespace Database {
|
|||||||
Cluster() = default;
|
Cluster() = default;
|
||||||
|
|
||||||
// Find utility
|
// Find utility
|
||||||
|
// 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 std::size_t getCount(Session& session);
|
||||||
static RangeResults<ClusterId> find(Session& session, const FindParameters& range);
|
static RangeResults<ClusterFindResult> find(Session& session, const FindParameters& range);
|
||||||
static pointer find(Session& session, ClusterId id);
|
static pointer find(Session& session, ClusterId id);
|
||||||
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
|
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
|
||||||
|
|
||||||
|
|||||||
@@ -32,18 +32,15 @@ class Db
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Db(const std::filesystem::path& dbPath, std::size_t connectionCount = 10);
|
Db(const std::filesystem::path& dbPath, std::size_t connectionCount = 10);
|
||||||
~Db();
|
|
||||||
|
|
||||||
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:
|
private:
|
||||||
|
Db(const Db&) = delete;
|
||||||
|
Db& operator=(const Db&) = delete;
|
||||||
|
|
||||||
friend class Session;
|
friend class Session;
|
||||||
|
|
||||||
RecursiveSharedMutex& getMutex() { return _sharedMutex; }
|
RecursiveSharedMutex& getMutex() { return _sharedMutex; }
|
||||||
@@ -55,14 +52,12 @@ class Db
|
|||||||
ScopedConnection(Wt::Dbo::SqlConnectionPool& pool);
|
ScopedConnection(Wt::Dbo::SqlConnectionPool& pool);
|
||||||
~ScopedConnection();
|
~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:
|
private:
|
||||||
|
ScopedConnection(const ScopedConnection&) = delete;
|
||||||
|
ScopedConnection& operator=(const ScopedConnection&) = delete;
|
||||||
|
|
||||||
Wt::Dbo::SqlConnectionPool& _connectionPool;
|
Wt::Dbo::SqlConnectionPool& _connectionPool;
|
||||||
std::unique_ptr<Wt::Dbo::SqlConnection> _connection;
|
std::unique_ptr<Wt::Dbo::SqlConnection> _connection;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -63,39 +63,16 @@ class Listen final : public Object<Listen, ListenId>
|
|||||||
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters);
|
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters);
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
static RangeResults<ArtistId> getTopArtists(Session& session,
|
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
|
||||||
UserId userId,
|
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
|
||||||
Scrobbler scrobbler,
|
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
|
||||||
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,
|
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
|
||||||
UserId userId,
|
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
|
||||||
Scrobbler scrobbler,
|
static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
std::optional<TrackArtistLinkType> linkType,
|
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId);
|
||||||
Range range = {});
|
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId releaseId);
|
||||||
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; }
|
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
||||||
ObjectPtr<User> getUser() const { return _user; }
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ namespace Database
|
|||||||
Wt::WDateTime getLastWritten() const;
|
Wt::WDateTime getLastWritten() const;
|
||||||
std::optional<ReleaseTypePrimary> getPrimaryType() const { return _primaryType; }
|
std::optional<ReleaseTypePrimary> getPrimaryType() const { return _primaryType; }
|
||||||
EnumSet<ReleaseTypeSecondary> getSecondaryTypes() const { return _secondaryTypes; }
|
EnumSet<ReleaseTypeSecondary> getSecondaryTypes() const { return _secondaryTypes; }
|
||||||
|
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
|
||||||
std::size_t getTracksCount() const;
|
std::size_t getTracksCount() const;
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
@@ -121,6 +122,7 @@ namespace Database
|
|||||||
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
|
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
|
||||||
void setPrimaryType(std::optional<ReleaseTypePrimary> type) { _primaryType = type; }
|
void setPrimaryType(std::optional<ReleaseTypePrimary> type) { _primaryType = type; }
|
||||||
void setSecondaryTypes(EnumSet<ReleaseTypeSecondary> types) { _secondaryTypes = types; }
|
void setSecondaryTypes(EnumSet<ReleaseTypeSecondary> types) { _secondaryTypes = types; }
|
||||||
|
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
|
||||||
|
|
||||||
// Get the artists of this release
|
// Get the artists of this release
|
||||||
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
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, _totalDisc, "total_disc");
|
||||||
Wt::Dbo::field(a, _primaryType, "primary_type");
|
Wt::Dbo::field(a, _primaryType, "primary_type");
|
||||||
Wt::Dbo::field(a, _secondaryTypes, "secondary_types");
|
Wt::Dbo::field(a, _secondaryTypes, "secondary_types");
|
||||||
|
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
|
||||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +157,7 @@ namespace Database
|
|||||||
std::optional<int> _totalDisc{};
|
std::optional<int> _totalDisc{};
|
||||||
std::optional<ReleaseTypePrimary> _primaryType;
|
std::optional<ReleaseTypePrimary> _primaryType;
|
||||||
EnumSet<ReleaseTypeSecondary> _secondaryTypes;
|
EnumSet<ReleaseTypeSecondary> _secondaryTypes;
|
||||||
|
std::string _artistDisplayName;
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
|
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,18 +54,13 @@ namespace Database
|
|||||||
public:
|
public:
|
||||||
Session(Db& database);
|
Session(Db& database);
|
||||||
|
|
||||||
Session(const Session&) = delete;
|
|
||||||
Session(Session&&) = delete;
|
|
||||||
Session& operator=(const Session&) = delete;
|
|
||||||
Session& operator=(Session&&) = delete;
|
|
||||||
|
|
||||||
[[nodiscard]] UniqueTransaction createUniqueTransaction();
|
[[nodiscard]] UniqueTransaction createUniqueTransaction();
|
||||||
[[nodiscard]] SharedTransaction createSharedTransaction();
|
[[nodiscard]] SharedTransaction createSharedTransaction();
|
||||||
|
|
||||||
void checkUniqueLocked();
|
void checkUniqueLocked();
|
||||||
void checkSharedLocked();
|
void checkSharedLocked();
|
||||||
|
|
||||||
void optimize();
|
void analyze();
|
||||||
|
|
||||||
void prepareTables(); // need to run only once at startup
|
void prepareTables(); // need to run only once at startup
|
||||||
|
|
||||||
@@ -87,6 +82,9 @@ namespace Database
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
Session(const Session&) = delete;
|
||||||
|
Session& operator=(const Session&) = delete;
|
||||||
|
|
||||||
Db& _db;
|
Db& _db;
|
||||||
Wt::Dbo::Session _session;
|
Wt::Dbo::Session _session;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ class Track final : public Object<Track, TrackId>
|
|||||||
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
|
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
|
||||||
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
||||||
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
|
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
|
||||||
|
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
|
||||||
void clearArtistLinks();
|
void clearArtistLinks();
|
||||||
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
||||||
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
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<std::string> getCopyrightURL() const;
|
||||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||||
|
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
|
||||||
// no artistLinkTypes means get all
|
// no artistLinkTypes means get all
|
||||||
std::vector<ObjectPtr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means 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
|
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, _copyrightURL, "copyright_url");
|
||||||
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
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, _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::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
||||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||||
@@ -228,6 +231,7 @@ class Track final : public Object<Track, TrackId>
|
|||||||
std::string _copyrightURL;
|
std::string _copyrightURL;
|
||||||
std::optional<float> _trackReplayGain;
|
std::optional<float> _trackReplayGain;
|
||||||
std::optional<float> _releaseReplayGain;
|
std::optional<float> _releaseReplayGain;
|
||||||
|
std::string _artistDisplayName;
|
||||||
|
|
||||||
Wt::Dbo::ptr<Release> _release;
|
Wt::Dbo::ptr<Release> _release;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ class User final : public Object<User, UserId>
|
|||||||
|
|
||||||
static inline const std::size_t MinNameLength{ 3 };
|
static inline const std::size_t MinNameLength{ 3 };
|
||||||
static inline const std::size_t MaxNameLength{ 15 };
|
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 AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS };
|
||||||
static inline const Bitrate defaultSubsonicTranscodeBitrate{ 128000 };
|
static inline const Bitrate defaultSubsonicTranscodeBitrate{ 128000 };
|
||||||
static inline const UITheme defaultUITheme{ UITheme::Dark };
|
static inline const UITheme defaultUITheme{ UITheme::Dark };
|
||||||
@@ -81,9 +80,8 @@ class User final : public Object<User, UserId>
|
|||||||
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
|
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
|
||||||
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
|
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
|
||||||
void setType(UserType type) { _type = type; }
|
void setType(UserType type) { _type = type; }
|
||||||
void setSubsonicTranscodeEnable(bool value) { _subsonicTranscodeEnable = value; }
|
void setSubsonicDefaultTranscodeFormat(AudioFormat encoding) { _subsonicDefaultTranscodeFormat = encoding; }
|
||||||
void setSubsonicTranscodeFormat(AudioFormat encoding) { _subsonicTranscodeFormat = encoding; }
|
void setSubsonicDefaultTranscodeBitrate(Bitrate bitrate);
|
||||||
void setSubsonicTranscodeBitrate(Bitrate bitrate);
|
|
||||||
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
|
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
|
||||||
void setRadio(bool val) { _radio = val; }
|
void setRadio(bool val) { _radio = val; }
|
||||||
void setRepeatAll(bool val) { _repeatAll = val; }
|
void setRepeatAll(bool val) { _repeatAll = val; }
|
||||||
@@ -97,9 +95,8 @@ class User final : public Object<User, UserId>
|
|||||||
bool isAdmin() const { return _type == UserType::ADMIN; }
|
bool isAdmin() const { return _type == UserType::ADMIN; }
|
||||||
bool isDemo() const { return _type == UserType::DEMO; }
|
bool isDemo() const { return _type == UserType::DEMO; }
|
||||||
UserType getType() const { return _type; }
|
UserType getType() const { return _type; }
|
||||||
bool getSubsonicTranscodeEnable() const { return _subsonicTranscodeEnable; }
|
AudioFormat getSubsonicDefaultTranscodeFormat() const { return _subsonicDefaultTranscodeFormat; }
|
||||||
AudioFormat getSubsonicTranscodeFormat() const { return _subsonicTranscodeFormat; }
|
Bitrate getSubsonicDefaultTranscodeBitrate() const { return _subsonicDefaultTranscodeBitrate; }
|
||||||
Bitrate getSubsonicTranscodeBitrate() const { return _subsonicTranscodeBitrate; }
|
|
||||||
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
|
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
|
||||||
bool isRepeatAllSet() const { return _repeatAll; }
|
bool isRepeatAllSet() const { return _repeatAll; }
|
||||||
bool isRadioSet() const { return _radio; }
|
bool isRadioSet() const { return _radio; }
|
||||||
@@ -116,9 +113,8 @@ class User final : public Object<User, UserId>
|
|||||||
Wt::Dbo::field(a, _passwordSalt, "password_salt");
|
Wt::Dbo::field(a, _passwordSalt, "password_salt");
|
||||||
Wt::Dbo::field(a, _passwordHash, "password_hash");
|
Wt::Dbo::field(a, _passwordHash, "password_hash");
|
||||||
Wt::Dbo::field(a, _lastLogin, "last_login");
|
Wt::Dbo::field(a, _lastLogin, "last_login");
|
||||||
Wt::Dbo::field(a, _subsonicTranscodeEnable, "subsonic_transcode_enable");
|
Wt::Dbo::field(a, _subsonicDefaultTranscodeFormat, "subsonic_default_transcode_format");
|
||||||
Wt::Dbo::field(a, _subsonicTranscodeFormat, "subsonic_transcode_format");
|
Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate");
|
||||||
Wt::Dbo::field(a, _subsonicTranscodeBitrate, "subsonic_transcode_bitrate");
|
|
||||||
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
|
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
|
||||||
Wt::Dbo::field(a, _uiTheme, "ui_theme");
|
Wt::Dbo::field(a, _uiTheme, "ui_theme");
|
||||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||||
@@ -150,9 +146,8 @@ class User final : public Object<User, UserId>
|
|||||||
|
|
||||||
// User defined settings
|
// User defined settings
|
||||||
SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode };
|
SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode };
|
||||||
bool _subsonicTranscodeEnable {defaultSubsonicTranscodeEnable};
|
AudioFormat _subsonicDefaultTranscodeFormat{ defaultSubsonicTranscodeFormat };
|
||||||
AudioFormat _subsonicTranscodeFormat {defaultSubsonicTranscodeFormat};
|
int _subsonicDefaultTranscodeBitrate{ defaultSubsonicTranscodeBitrate };
|
||||||
int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate};
|
|
||||||
|
|
||||||
// User's dynamic data (UI)
|
// User's dynamic data (UI)
|
||||||
int _curPlayingTrackPos{}; // Current track position in queue
|
int _curPlayingTrackPos{}; // Current track position in queue
|
||||||
@@ -163,4 +158,3 @@ class User final : public Object<User, UserId>
|
|||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Databas'
|
} // namespace Databas'
|
||||||
|
|
||||||
|
|||||||
@@ -48,13 +48,17 @@ TEST_F(DatabaseFixture, Cluster)
|
|||||||
EXPECT_EQ(Cluster::getCount(session), 1);
|
EXPECT_EQ(Cluster::getCount(session), 1);
|
||||||
EXPECT_EQ(cluster->getType()->getId(), clusterType.getId());
|
EXPECT_EQ(cluster->getType()->getId(), clusterType.getId());
|
||||||
|
|
||||||
auto clusters{ Cluster::find(session, Cluster::FindParameters {}) };
|
{
|
||||||
|
const auto clusters{ Cluster::find(session, Cluster::FindParameters {}) };
|
||||||
ASSERT_EQ(clusters.results.size(), 1);
|
ASSERT_EQ(clusters.results.size(), 1);
|
||||||
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
EXPECT_EQ(std::get<ClusterId>(clusters.results.front()), cluster.getId());
|
||||||
|
}
|
||||||
|
|
||||||
clusters = Cluster::findOrphans(session, Range{});
|
{
|
||||||
|
const auto clusters{ Cluster::findOrphans(session, Range{}) };
|
||||||
ASSERT_EQ(clusters.results.size(), 1);
|
ASSERT_EQ(clusters.results.size(), 1);
|
||||||
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
||||||
|
}
|
||||||
|
|
||||||
auto clusterTypes{ ClusterType::find(session, Range {}) };
|
auto clusterTypes{ ClusterType::find(session, Range {}) };
|
||||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||||
@@ -114,7 +118,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
|
|||||||
auto transaction{ session.createSharedTransaction() };
|
auto transaction{ session.createSharedTransaction() };
|
||||||
auto clusters{ Cluster::find(session, Cluster::FindParameters {}.setTrack(track.getId())) };
|
auto clusters{ Cluster::find(session, Cluster::FindParameters {}.setTrack(track.getId())) };
|
||||||
ASSERT_EQ(clusters.results.size(), 1);
|
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 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);
|
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)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ DatabaseFixture::SetUpTestCase()
|
|||||||
{
|
{
|
||||||
Database::Session s {_tmpDb->getDb()};
|
Database::Session s {_tmpDb->getDb()};
|
||||||
s.prepareTables();
|
s.prepareTables();
|
||||||
s.optimize();
|
s.analyze();
|
||||||
|
|
||||||
// remove default created entries
|
// remove default created entries
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -684,6 +684,59 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
|
ScopedUser user{ session, "MyUser" };
|
||||||
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
track.get().modify()->setRelease(release.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) };
|
||||||
|
EXPECT_FALSE(listen);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime3 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
||||||
{
|
{
|
||||||
ScopedTrack track1{ session, "MyTrack1" };
|
ScopedTrack track1{ session, "MyTrack1" };
|
||||||
@@ -820,6 +873,52 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
|
ScopedUser user{ session, "MyUser" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) };
|
||||||
|
EXPECT_FALSE(listen);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime3 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
||||||
{
|
{
|
||||||
ScopedTrack track1{ session, "MyTrack1" };
|
ScopedTrack track1{ session, "MyTrack1" };
|
||||||
|
|||||||
@@ -36,25 +36,26 @@ std::unique_ptr<IEngine> createClustersEngine(Db& db)
|
|||||||
return std::make_unique<ClusterEngine>(db);
|
return std::make_unique<ClusterEngine>(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackContainer
|
TrackContainer ClusterEngine::findSimilarTracks(const std::vector<TrackId>& trackIds, std::size_t maxCount) const
|
||||||
ClusterEngine::findSimilarTracks(const std::vector<TrackId>& trackIds, std::size_t maxCount) const
|
|
||||||
{
|
{
|
||||||
Session& dbSession {_db.getTLSSession()};
|
if (maxCount == 0)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Session& dbSession{ _db.getTLSSession() };
|
||||||
auto transaction{ dbSession.createSharedTransaction() };
|
auto transaction{ dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
const auto similarTrackIds{ Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount}) };
|
const auto similarTrackIds{ Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount}) };
|
||||||
return std::move(similarTrackIds.results);
|
return std::move(similarTrackIds.results);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackContainer
|
TrackContainer ClusterEngine::findSimilarTracksFromTrackList(TrackListId tracklistId, std::size_t maxCount) const
|
||||||
ClusterEngine::findSimilarTracksFromTrackList(TrackListId tracklistId, std::size_t maxCount) const
|
{
|
||||||
|
TrackContainer res;
|
||||||
|
if (maxCount == 0)
|
||||||
|
return res;
|
||||||
|
|
||||||
{
|
{
|
||||||
Session& dbSession{ _db.getTLSSession() };
|
Session& dbSession{ _db.getTLSSession() };
|
||||||
|
|
||||||
TrackContainer res;
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ dbSession.createSharedTransaction() };
|
auto transaction{ dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
const TrackList::pointer trackList{ TrackList::find(dbSession, tracklistId) };
|
const TrackList::pointer trackList{ TrackList::find(dbSession, tracklistId) };
|
||||||
@@ -69,13 +70,14 @@ ClusterEngine::findSimilarTracksFromTrackList(TrackListId tracklistId, std::size
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReleaseContainer
|
ReleaseContainer ClusterEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
|
||||||
ClusterEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
|
{
|
||||||
|
ReleaseContainer res;
|
||||||
|
if (maxCount == 0)
|
||||||
|
return res;
|
||||||
|
|
||||||
{
|
{
|
||||||
Session& dbSession{ _db.getTLSSession() };
|
Session& dbSession{ _db.getTLSSession() };
|
||||||
|
|
||||||
ReleaseContainer res;
|
|
||||||
{
|
|
||||||
auto transaction{ dbSession.createSharedTransaction() };
|
auto transaction{ dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
auto release{ Release::find(dbSession, releaseId) };
|
auto release{ Release::find(dbSession, releaseId) };
|
||||||
@@ -90,11 +92,12 @@ ClusterEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) con
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistContainer
|
ArtistContainer ClusterEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> artistLinkTypes, std::size_t maxCount) const
|
||||||
ClusterEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> artistLinkTypes, std::size_t maxCount) const
|
|
||||||
{
|
{
|
||||||
Session& dbSession {_db.getTLSSession()};
|
if (maxCount == 0)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Session& dbSession{ _db.getTLSSession() };
|
||||||
auto transaction{ dbSession.createSharedTransaction() };
|
auto transaction{ dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
auto artist{ Artist::find(dbSession, artistId) };
|
auto artist{ Artist::find(dbSession, artistId) };
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ namespace
|
|||||||
const EnumSet<ReleaseTypeSecondary> secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) };
|
const EnumSet<ReleaseTypeSecondary> secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) };
|
||||||
if (release->getSecondaryTypes() != secondaryTypes)
|
if (release->getSecondaryTypes() != secondaryTypes)
|
||||||
release.modify()->setSecondaryTypes(secondaryTypes);
|
release.modify()->setSecondaryTypes(secondaryTypes);
|
||||||
|
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
|
||||||
|
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
Release::pointer
|
Release::pointer
|
||||||
@@ -525,5 +527,6 @@ namespace Scanner
|
|||||||
track.modify()->setCopyright(trackInfo->copyright);
|
track.modify()->setCopyright(trackInfo->copyright);
|
||||||
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
|
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
|
||||||
track.modify()->setTrackReplayGain(trackInfo->replayGain);
|
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();
|
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.analyze();
|
||||||
_dbSession.optimize();
|
|
||||||
|
|
||||||
if (!_abortScan)
|
if (!_abortScan)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -133,6 +133,32 @@ namespace Scrobbling
|
|||||||
return res;
|
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
|
// Top
|
||||||
ScrobblingService::ArtistContainer ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
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,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::Range range) override;
|
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,
|
ArtistContainer getTopArtists(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
std::optional<Database::TrackArtistLinkType> linkType,
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ namespace Scrobbling
|
|||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::Range range) = 0;
|
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
|
// Top
|
||||||
virtual ArtistContainer getTopArtists(Database::UserId userId,
|
virtual ArtistContainer getTopArtists(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace API::Subsonic
|
|||||||
};
|
};
|
||||||
|
|
||||||
static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
|
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
|
namespace StringUtils
|
||||||
|
|||||||
@@ -288,12 +288,12 @@ namespace API::Subsonic
|
|||||||
|
|
||||||
checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes);
|
checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes);
|
||||||
|
|
||||||
Response resp{ (itEntryPoint->second.func)(requestContext) };
|
const Response resp{ (itEntryPoint->second.func)(requestContext) };
|
||||||
|
|
||||||
resp.write(response.out(), format);
|
resp.write(response.out(), format);
|
||||||
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
|
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
|
||||||
|
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
|
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,8 @@
|
|||||||
#include "SubsonicResponse.hpp"
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <Wt/Json/Array.h>
|
#include <cmath>
|
||||||
#include <Wt/Json/Object.h>
|
#include <climits>
|
||||||
#include <Wt/Json/Value.h>
|
|
||||||
#include <Wt/Json/Serializer.h>
|
|
||||||
|
|
||||||
#include <boost/property_tree/json_parser.hpp>
|
|
||||||
#include <boost/property_tree/xml_parser.hpp>
|
#include <boost/property_tree/xml_parser.hpp>
|
||||||
|
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
@@ -57,59 +53,66 @@ namespace API::Subsonic
|
|||||||
_value = value;
|
_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);
|
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(!_value);
|
||||||
|
assert(_children.find(key) == std::cend(_children));
|
||||||
_childrenArrays.emplace(key, std::vector<Node>{});
|
_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);
|
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{});
|
_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);
|
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 });
|
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();}));
|
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);
|
assert(!_value);
|
||||||
auto& values {_childrenValues[std::string{ key }]};
|
auto& values{ _childrenValues[key] };
|
||||||
values.push_back(value);
|
values.push_back(value);
|
||||||
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
|
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();
|
assert(!_value);
|
||||||
return _children[key].back();
|
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();
|
_childrenArrays[key].emplace_back();
|
||||||
return _childrenArrays[key].back();
|
return _childrenArrays[key].back();
|
||||||
}
|
}
|
||||||
@@ -120,13 +123,30 @@ namespace API::Subsonic
|
|||||||
}
|
}
|
||||||
|
|
||||||
Response Response::createOkResponse(ProtocolVersion protocolVersion)
|
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;
|
Response response;
|
||||||
Node& responseNode{ response._root.createChild("subsonic-response") };
|
Node& responseNode{ response._root.createChild("subsonic-response") };
|
||||||
|
|
||||||
responseNode.setAttribute("status", "ok");
|
responseNode.setAttribute("status", error ? "failed" : "ok");
|
||||||
responseNode.setVersionAttribute(protocolVersion);
|
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
|
// OpenSubsonic mandatory fields
|
||||||
responseNode.setAttribute("type", "lms");
|
responseNode.setAttribute("type", "lms");
|
||||||
responseNode.setAttribute("serverVersion", serverVersion);
|
responseNode.setAttribute("serverVersion", serverVersion);
|
||||||
@@ -135,38 +155,22 @@ namespace API::Subsonic
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
|
void Response::addNode(Node::Key key, Node&& node)
|
||||||
{
|
{
|
||||||
Response response;
|
return _root._children["subsonic-response"].addChild(key, std::move(node));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
void Response::write(std::ostream& os, ResponseFormat format) const
|
||||||
{
|
|
||||||
return _root._children["subsonic-response"].front().createArrayChild(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Response::write(std::ostream& os, ResponseFormat format)
|
|
||||||
{
|
{
|
||||||
switch (format)
|
switch (format)
|
||||||
{
|
{
|
||||||
@@ -179,22 +183,22 @@ 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)
|
std::function<boost::property_tree::ptree(const Node&)> nodeToPropertyTree = [&](const Node& node)
|
||||||
{
|
{
|
||||||
boost::property_tree::ptree res;
|
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))
|
if (std::holds_alternative<std::string>(value))
|
||||||
res.put("<xmlattr>." + itAttribute.first, std::get<std::string>(itAttribute.second));
|
res.put("<xmlattr>." + std::string{ key.get() }, std::get<std::string>(value));
|
||||||
else if (std::holds_alternative<bool>(itAttribute.second))
|
else if (std::holds_alternative<bool>(value))
|
||||||
res.put("<xmlattr>." + itAttribute.first, std::get<bool>(itAttribute.second));
|
res.put("<xmlattr>." + std::string{ key.get() }, std::get<bool>(value));
|
||||||
else if (std::holds_alternative<float>(itAttribute.second))
|
else if (std::holds_alternative<float>(value))
|
||||||
res.put("<xmlattr>." + itAttribute.first, std::get<float>(itAttribute.second));
|
res.put("<xmlattr>." + std::string{ key.get() }, std::get<float>(value));
|
||||||
else if (std::holds_alternative<long long>(itAttribute.second))
|
else if (std::holds_alternative<long long>(value))
|
||||||
res.put("<xmlattr>." + itAttribute.first, std::get<long long>(itAttribute.second));
|
res.put("<xmlattr>." + std::string{ key.get() }, std::get<long long>(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto valueToPropertyTree = [](const Node::ValueType& value)
|
auto valueToPropertyTree = [](const Node::ValueType& value)
|
||||||
@@ -214,22 +218,21 @@ namespace API::Subsonic
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
for (const auto& [key, childNodes] : node._children)
|
for (const auto& [key, childNode] : node._children)
|
||||||
{
|
{
|
||||||
for (const Node& childNode : childNodes)
|
res.add_child(std::string{ key.get() }, nodeToPropertyTree(childNode));
|
||||||
res.add_child(key, nodeToPropertyTree(childNode));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
||||||
{
|
{
|
||||||
for (const Node& childNode : childArrayNodes)
|
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 auto& [key, childArrayValues] : node._childrenValues)
|
||||||
{
|
{
|
||||||
for (const Response::Node::ValueType& value : childArrayValues)
|
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);
|
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 };
|
||||||
{
|
|
||||||
Json::Object res;
|
|
||||||
|
|
||||||
auto valueToJsonValue{ [](const Node::ValueType& value) -> Json::Value
|
for (const auto& [key, value] : node._attributes)
|
||||||
{
|
{
|
||||||
Json::Value res;
|
if (!first)
|
||||||
std::visit([&](const auto& rawValue)
|
os << ',';
|
||||||
{
|
|
||||||
res = Json::Value{ rawValue };
|
|
||||||
}, value);
|
|
||||||
return res;
|
|
||||||
} };
|
|
||||||
|
|
||||||
for (auto itAttribute : node._attributes)
|
serializeEscapedString(os, key.get());
|
||||||
res[itAttribute.first] = valueToJsonValue(itAttribute.second);
|
os << ':';
|
||||||
|
serializeValue(os, value);
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (node._value)
|
if (node._value)
|
||||||
{
|
{
|
||||||
res["value"] = valueToJsonValue(*node._value);
|
if (!first)
|
||||||
|
os << ',';
|
||||||
|
|
||||||
|
os << "\"value\":";
|
||||||
|
serializeValue(os, *node._value);
|
||||||
|
|
||||||
|
first = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
for (const auto& [key, childNodes] : node._children)
|
for (const auto& [key, childNode] : node._children)
|
||||||
{
|
{
|
||||||
for (const Response::Node& childNode : childNodes)
|
if (!first)
|
||||||
res[key] = nodeToJsonObject(childNode);
|
os << ',';
|
||||||
|
|
||||||
|
serializeEscapedString(os, key.get());
|
||||||
|
os << ':';
|
||||||
|
serializeNode(os, childNode);
|
||||||
|
|
||||||
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
||||||
{
|
{
|
||||||
Json::Array array;
|
if (!first)
|
||||||
for (const Response::Node& childNode : childArrayNodes)
|
os << ',';
|
||||||
array.emplace_back(nodeToJsonObject(childNode));
|
|
||||||
|
|
||||||
res[key] = std::move(array);
|
serializeEscapedString(os, key.get());
|
||||||
|
os << ":[";
|
||||||
|
|
||||||
|
bool firstChild{ true };
|
||||||
|
for (const Response::Node& childNode : childArrayNodes)
|
||||||
|
{
|
||||||
|
if (!firstChild)
|
||||||
|
os << ",";
|
||||||
|
|
||||||
|
serializeNode(os, childNode);
|
||||||
|
firstChild = false;
|
||||||
|
}
|
||||||
|
os << ']';
|
||||||
|
|
||||||
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& [key, childValues] : node._childrenValues)
|
for (const auto& [key, childValues] : node._childrenValues)
|
||||||
{
|
{
|
||||||
Json::Array array;
|
if (!first)
|
||||||
|
os << ',';
|
||||||
|
|
||||||
|
serializeEscapedString(os, key.get());
|
||||||
|
os << ":[";
|
||||||
|
|
||||||
|
bool firstChild{ true };
|
||||||
for (const Node::ValueType& childValue : childValues)
|
for (const Node::ValueType& childValue : childValues)
|
||||||
array.emplace_back(valueToJsonValue(childValue));
|
{
|
||||||
|
if (!firstChild)
|
||||||
|
os << ",";
|
||||||
|
|
||||||
res[key] = std::move(array);
|
serializeValue(os, childValue);
|
||||||
|
|
||||||
|
firstChild = false;
|
||||||
|
}
|
||||||
|
os << ']';
|
||||||
|
|
||||||
|
first = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
os << '}';
|
||||||
};
|
}
|
||||||
|
|
||||||
Json::Object root{ nodeToJsonObject(_root) };
|
void Response::JsonSerializer::serializeValue(std::ostream& os, const Node::ValueType& value)
|
||||||
os << Json::serialize(root);
|
{
|
||||||
|
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
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -191,17 +191,30 @@ namespace API::Subsonic
|
|||||||
class Node
|
class Node
|
||||||
{
|
{
|
||||||
public:
|
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>
|
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)
|
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)
|
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)
|
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
|
else
|
||||||
static_assert("Unhandled type");
|
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
|
// A Node has either a single value or an array of values or some children
|
||||||
void setValue(std::string_view value);
|
void setValue(std::string_view value);
|
||||||
void setValue(long long value);
|
void setValue(long long value);
|
||||||
Node& createChild(const std::string& key);
|
Node& createChild(Key key);
|
||||||
Node& createArrayChild(const std::string& key);
|
Node& createArrayChild(Key key);
|
||||||
|
|
||||||
void addChild(const std::string& key, Node node);
|
void addChild(Key key, Node&& node);
|
||||||
void createEmptyArrayChild(std::string_view key);
|
void createEmptyArrayChild(Key key);
|
||||||
void addArrayChild(std::string_view key, Node node);
|
void addArrayChild(Key key, Node&& node);
|
||||||
void createEmptyArrayValue(std::string_view key);
|
void createEmptyArrayValue(Key key);
|
||||||
void addArrayValue(std::string_view key, std::string_view value);
|
void addArrayValue(Key key, std::string_view value);
|
||||||
void addArrayValue(std::string_view key, long long value);
|
void addArrayValue(Key key, long long value);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setVersionAttribute(ProtocolVersion version);
|
void setVersionAttribute(ProtocolVersion version);
|
||||||
|
|
||||||
friend class Response;
|
friend class Response;
|
||||||
using ValueType = std::variant<std::string, bool, float, long long>;
|
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::optional<ValueType> _value;
|
||||||
std::map<std::string, std::vector<Node>> _children;
|
std::map<Key, Node> _children;
|
||||||
std::map<std::string, std::vector<Node>> _childrenArrays;
|
std::map<Key, std::vector<Node>> _childrenArrays;
|
||||||
|
|
||||||
using ValuesType = std::vector<ValueType>;
|
using ValuesType = std::vector<ValueType>;
|
||||||
std::map<std::string, ValuesType> _childrenValues;
|
std::map<Key, ValuesType> _childrenValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
static Response createOkResponse(ProtocolVersion protocolVersion);
|
static Response createOkResponse(ProtocolVersion protocolVersion);
|
||||||
@@ -242,15 +255,25 @@ namespace API::Subsonic
|
|||||||
Response(Response&&) = default;
|
Response(Response&&) = default;
|
||||||
Response& operator=(Response&&) = default;
|
Response& operator=(Response&&) = default;
|
||||||
|
|
||||||
void addNode(const std::string& key, Node node);
|
void addNode(Node::Key key, Node&& node);
|
||||||
Node& createNode(const std::string& key);
|
Node& createNode(Node::Key key);
|
||||||
Node& createArrayNode(const std::string& key);
|
Node& createArrayNode(Node::Key key);
|
||||||
|
|
||||||
void write(std::ostream& os, ResponseFormat format);
|
void write(std::ostream& os, ResponseFormat format) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void writeJSON(std::ostream& os);
|
static Response createResponseCommon(ProtocolVersion protocolVersion, const Error* error = nullptr);
|
||||||
void writeXML(std::ostream& os);
|
|
||||||
|
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;
|
Response() = default;
|
||||||
Node _root;
|
Node _root;
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ namespace API::Subsonic
|
|||||||
throw NotImplementedGenericError{};
|
throw NotImplementedGenericError{};
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
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)
|
for (const ReleaseId releaseId : releases.results)
|
||||||
{
|
{
|
||||||
@@ -153,7 +153,7 @@ namespace API::Subsonic
|
|||||||
throw UserNotAuthorizedError{};
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
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() };
|
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) };
|
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(20) };
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
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() };
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
@@ -236,7 +236,7 @@ namespace API::Subsonic
|
|||||||
throw UserNotAuthorizedError{};
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
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)
|
for (const TrackId trackId : tracks)
|
||||||
{
|
{
|
||||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ namespace API::Subsonic::Scan
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
Response::Node
|
Response::Node createStatusResponseNode()
|
||||||
createStatusResponseNode()
|
|
||||||
{
|
{
|
||||||
Response::Node statusResponse;
|
Response::Node statusResponse;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "MediaRetrieval.hpp"
|
#include "MediaRetrieval.hpp"
|
||||||
|
|
||||||
|
#include "av/IAudioFile.hpp"
|
||||||
#include "av/TranscodeParameters.hpp"
|
#include "av/TranscodeParameters.hpp"
|
||||||
#include "av/TranscodeResourceHandlerCreator.hpp"
|
#include "av/TranscodeResourceHandlerCreator.hpp"
|
||||||
#include "av/Types.hpp"
|
#include "av/Types.hpp"
|
||||||
@@ -30,25 +31,40 @@
|
|||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/FileResourceHandlerCreator.hpp"
|
#include "utils/FileResourceHandlerCreator.hpp"
|
||||||
#include "utils/Utils.hpp"
|
#include "utils/Utils.hpp"
|
||||||
|
#include "utils/String.hpp"
|
||||||
#include "ParameterParsing.hpp"
|
#include "ParameterParsing.hpp"
|
||||||
#include "SubsonicId.hpp"
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
namespace {
|
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)
|
Av::Format userTranscodeFormatToAvFormat(AudioFormat format)
|
||||||
{
|
{
|
||||||
switch (format)
|
switch (format)
|
||||||
{
|
{
|
||||||
case AudioFormat::MP3: return Av::Format::MP3;
|
case Database::AudioFormat::MP3: return Av::Format::MP3;
|
||||||
case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
|
case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
|
||||||
case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
|
case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
|
||||||
case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
|
case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
|
||||||
case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
|
case Database::AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
|
||||||
default: return Av::Format::OGG_OPUS;
|
|
||||||
}
|
}
|
||||||
|
return Av::Format::OGG_OPUS;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StreamParameters
|
struct StreamParameters
|
||||||
@@ -64,9 +80,9 @@ namespace API::Subsonic
|
|||||||
const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||||
|
|
||||||
// Optional params
|
// Optional params
|
||||||
std::optional<std::size_t> maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate") };
|
std::size_t maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate").value_or(0) }; // "If set to zero, no limit is imposed"
|
||||||
const std::optional<std::string> format{ getParameterAs<std::string>(context.parameters, "format") };
|
const std::string format{ getParameterAs<std::string>(context.parameters, "format").value_or("") };
|
||||||
const std::optional<std::size_t> timeOffset{ getParameterAs<std::size_t>(context.parameters, "timeOffset") };
|
std::size_t timeOffset{ getParameterAs<std::size_t>(context.parameters, "timeOffset").value_or(0) };
|
||||||
bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) };
|
bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) };
|
||||||
|
|
||||||
StreamParameters parameters;
|
StreamParameters parameters;
|
||||||
@@ -84,31 +100,40 @@ namespace API::Subsonic
|
|||||||
parameters.inputFileParameters.duration = track->getDuration();
|
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() })
|
||||||
{
|
{
|
||||||
|
// assume reported codec is "mp3", "opus", "vorbis", etc.
|
||||||
|
if (StringUtils::stringCaseInsensitiveEqual(streamInfo->codec, format) && (maxBitRate == 0 || (streamInfo->bitrate / 1000) <= maxBitRate))
|
||||||
|
{
|
||||||
|
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) };
|
const User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
if (!user)
|
if (!user)
|
||||||
throw UserNotAuthorizedError{};
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
// format = "raw" => no transcode. Other format values will be ignored
|
Av::TranscodeParameters& transcodeParameters{ parameters.transcodeParameters.emplace() };
|
||||||
const bool transcode{ (!format || (*format != "raw")) && user->getSubsonicTranscodeEnable() };
|
|
||||||
if (transcode)
|
|
||||||
{
|
|
||||||
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.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
|
||||||
transcodeParameters.offset = std::chrono::seconds{ timeOffset ? *timeOffset : 0 };
|
transcodeParameters.offset = std::chrono::seconds{ timeOffset };
|
||||||
|
|
||||||
parameters.transcodeParameters = std::move(transcodeParameters);
|
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;
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ namespace API::Subsonic
|
|||||||
for (const TrackListEntry::pointer& entry : entries)
|
for (const TrackListEntry::pointer& entry : entries)
|
||||||
playlistNode.addArrayChild("entry", createSongNode(entry->getTrack(), context.dbSession, user));
|
playlistNode.addArrayChild("entry", createSongNode(entry->getTrack(), context.dbSession, user));
|
||||||
|
|
||||||
response.addNode("playlist", playlistNode);
|
response.addNode("playlist", std::move(playlistNode));
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,11 +107,14 @@ namespace API::Subsonic
|
|||||||
}
|
}
|
||||||
else if (!artists.empty())
|
else if (!artists.empty())
|
||||||
{
|
{
|
||||||
|
if (!release->getArtistDisplayName().empty())
|
||||||
|
albumNode.setAttribute("artist", release->getArtistDisplayName());
|
||||||
|
else
|
||||||
albumNode.setAttribute("artist", Utils::joinArtistNames(artists));
|
albumNode.setAttribute("artist", Utils::joinArtistNames(artists));
|
||||||
|
|
||||||
if (artists.size() == 1)
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -121,26 +124,32 @@ namespace API::Subsonic
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Report the first GENRE for this track
|
// 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())
|
if (!clusters.empty() && !clusters.front().empty())
|
||||||
albumNode.setAttribute("genre", clusters.front().front()->getName());
|
albumNode.setAttribute("genre", clusters.front().front()->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getStarredDateTime(user->getId(), release->getId()) }; dateTime.isValid())
|
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)
|
// OpenSubsonic specific fields (must always be set)
|
||||||
if (!id3)
|
if (!id3)
|
||||||
albumNode.setAttribute("mediaType", "album");
|
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() };
|
std::optional<UUID> mbid{ release->getMBID() };
|
||||||
albumNode.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : "");
|
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);
|
albumNode.createEmptyArrayValue(field);
|
||||||
|
|
||||||
@@ -151,41 +160,31 @@ namespace API::Subsonic
|
|||||||
params.setRelease(release->getId());
|
params.setRelease(release->getId());
|
||||||
params.setClusterType(clusterType->getId());
|
params.setClusterType(clusterType->getId());
|
||||||
|
|
||||||
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
|
for (const auto& cluster : Cluster::find(dbSession, params).results)
|
||||||
{
|
albumNode.addArrayValue(field, std::get<std::string>(cluster));
|
||||||
Cluster::pointer cluster{ Cluster::find(dbSession, clusterId) };
|
|
||||||
if (cluster)
|
|
||||||
albumNode.addArrayValue(field, cluster->getName());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} };
|
} };
|
||||||
|
|
||||||
addClusters("moods", "MOOD");
|
addClusters("moods", "MOOD");
|
||||||
|
|
||||||
// Genres
|
// Genres
|
||||||
{
|
|
||||||
albumNode.createEmptyArrayChild("genres");
|
albumNode.createEmptyArrayChild("genres");
|
||||||
|
if (genreClusterType)
|
||||||
ClusterType::pointer clusterType{ ClusterType::find(dbSession, "GENRE") };
|
|
||||||
if (clusterType)
|
|
||||||
{
|
{
|
||||||
Cluster::FindParameters params;
|
Cluster::FindParameters params;
|
||||||
params.setRelease(release->getId());
|
params.setRelease(release->getId());
|
||||||
params.setClusterType(clusterType->getId());
|
params.setClusterType(genreClusterType->getId());
|
||||||
|
|
||||||
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
|
for (const auto& cluster : Cluster::find(dbSession, params).results)
|
||||||
{
|
albumNode.addArrayChild("genres", createItemGenreNode(std::get<std::string>(cluster)));
|
||||||
Cluster::pointer cluster{ Cluster::find(dbSession, clusterId) };
|
|
||||||
if (cluster)
|
|
||||||
albumNode.addArrayChild("genres", createItemGenreNode(cluster));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
albumNode.createEmptyArrayChild("artists");
|
albumNode.createEmptyArrayChild("artists");
|
||||||
for (const Artist::pointer& artist : release->getReleaseArtists())
|
for (const Artist::pointer& artist : release->getReleaseArtists())
|
||||||
albumNode.addArrayChild("artists", createArtistNode(artist));
|
albumNode.addArrayChild("artists", createArtistNode(artist));
|
||||||
|
|
||||||
|
albumNode.setAttribute("displayArtist", release->getArtistDisplayName());
|
||||||
|
|
||||||
{
|
{
|
||||||
const Wt::WDate originalReleaseDate{ release->getOriginalReleaseDate() };
|
const Wt::WDate originalReleaseDate{ release->getOriginalReleaseDate() };
|
||||||
albumNode.setAttribute("originalReleaseDate", originalReleaseDate.isValid() ? StringUtils::toISO8601String(originalReleaseDate) : "");
|
albumNode.setAttribute("originalReleaseDate", originalReleaseDate.isValid() ? StringUtils::toISO8601String(originalReleaseDate) : "");
|
||||||
|
|||||||
@@ -23,11 +23,11 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
Response::Node createItemGenreNode(const Database::Cluster::pointer& cluster)
|
Response::Node createItemGenreNode(std::string_view name)
|
||||||
{
|
{
|
||||||
Response::Node genreNode;
|
Response::Node genreNode;
|
||||||
|
|
||||||
genreNode.setAttribute("name", cluster->getName());
|
genreNode.setAttribute("name", name);
|
||||||
|
|
||||||
return genreNode;
|
return genreNode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "services/database/Object.hpp"
|
#include <string_view>
|
||||||
#include "SubsonicResponse.hpp"
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
@@ -29,5 +29,5 @@ namespace Database
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
Response::Node createItemGenreNode(const Database::ObjectPtr<Database::Cluster>& cluster);
|
Response::Node createItemGenreNode(std::string_view name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,25 +120,28 @@ namespace API::Subsonic
|
|||||||
trackResponse.setAttribute("suffix", extension.string().substr(1));
|
trackResponse.setAttribute("suffix", extension.string().substr(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user->getSubsonicTranscodeEnable())
|
trackResponse.setAttribute("transcodedSuffix", formatToSuffix(user->getSubsonicDefaultTranscodeFormat()));
|
||||||
trackResponse.setAttribute("transcodedSuffix", formatToSuffix(user->getSubsonicTranscodeFormat()));
|
|
||||||
|
|
||||||
trackResponse.setAttribute("coverArt", idToString(track->getId()));
|
trackResponse.setAttribute("coverArt", idToString(track->getId()));
|
||||||
|
|
||||||
const std::vector<Artist::pointer>& artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
const std::vector<Artist::pointer>& artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||||
if (!artists.empty())
|
if (!artists.empty())
|
||||||
{
|
{
|
||||||
|
if (!track->getArtistDisplayName().empty())
|
||||||
|
trackResponse.setAttribute("artist", track->getArtistDisplayName());
|
||||||
|
else
|
||||||
trackResponse.setAttribute("artist", Utils::joinArtistNames(artists));
|
trackResponse.setAttribute("artist", Utils::joinArtistNames(artists));
|
||||||
|
|
||||||
if (artists.size() == 1)
|
if (artists.size() == 1)
|
||||||
trackResponse.setAttribute("artistId", idToString(artists.front()->getId()));
|
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("album", release->getName());
|
||||||
trackResponse.setAttribute("albumId", idToString(track->getRelease()->getId()));
|
trackResponse.setAttribute("albumId", idToString(release->getId()));
|
||||||
trackResponse.setAttribute("parent", idToString(track->getRelease()->getId()));
|
trackResponse.setAttribute("parent", idToString(release->getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count());
|
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));
|
trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime));
|
||||||
|
|
||||||
// Report the first GENRE for this track
|
// 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) };
|
auto clusters{ track->getClusterGroups({genreClusterType}, 1) };
|
||||||
if (!clusters.empty() && !clusters.front().empty())
|
if (!clusters.empty() && !clusters.front().empty())
|
||||||
@@ -159,6 +163,11 @@ namespace API::Subsonic
|
|||||||
// OpenSubsonic specific fields (must always be set)
|
// OpenSubsonic specific fields (must always be set)
|
||||||
trackResponse.setAttribute("mediaType", "song");
|
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() };
|
std::optional<UUID> mbid{ track->getRecordingMBID() };
|
||||||
trackResponse.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : "");
|
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);
|
trackResponse.createEmptyArrayChild(nodeName);
|
||||||
|
|
||||||
@@ -195,9 +204,13 @@ namespace API::Subsonic
|
|||||||
} };
|
} };
|
||||||
|
|
||||||
addArtistLinks("artists", TrackArtistLinkType::Artist);
|
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);
|
trackResponse.createEmptyArrayValue(field);
|
||||||
|
|
||||||
@@ -208,35 +221,23 @@ namespace API::Subsonic
|
|||||||
params.setTrack(track->getId());
|
params.setTrack(track->getId());
|
||||||
params.setClusterType(clusterType->getId());
|
params.setClusterType(clusterType->getId());
|
||||||
|
|
||||||
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
|
for (const auto& cluster : Cluster::find(dbSession, params).results)
|
||||||
{
|
trackResponse.addArrayValue(field, std::get<std::string>(cluster));
|
||||||
Cluster::pointer cluster {Cluster::find(dbSession, clusterId)};
|
|
||||||
if (cluster)
|
|
||||||
trackResponse.addArrayValue(field, cluster->getName());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} };
|
} };
|
||||||
|
|
||||||
addClusters("moods", "MOOD");
|
addClusters("moods", "MOOD");
|
||||||
|
|
||||||
// Genres
|
// Genres
|
||||||
{
|
|
||||||
trackResponse.createEmptyArrayChild("genres");
|
trackResponse.createEmptyArrayChild("genres");
|
||||||
|
if (genreClusterType)
|
||||||
ClusterType::pointer clusterType{ ClusterType::find(dbSession, "GENRE") };
|
|
||||||
if (clusterType)
|
|
||||||
{
|
{
|
||||||
Cluster::FindParameters params;
|
Cluster::FindParameters params;
|
||||||
params.setTrack(track->getId());
|
params.setTrack(track->getId());
|
||||||
params.setClusterType(clusterType->getId());
|
params.setClusterType(genreClusterType->getId());
|
||||||
|
|
||||||
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
|
for (const auto& cluster : Cluster::find(dbSession, params).results)
|
||||||
{
|
trackResponse.addArrayChild("genres", createItemGenreNode(std::get<std::string>(cluster)));
|
||||||
Cluster::pointer cluster{ Cluster::find(dbSession, clusterId) };
|
|
||||||
if (cluster)
|
|
||||||
trackResponse.addArrayChild("genres", createItemGenreNode(cluster));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trackResponse.addChild("replayGain", createReplayGainNode(track));
|
trackResponse.addChild("replayGain", createReplayGainNode(track));
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include "utils/StreamLogger.hpp"
|
#include "utils/StreamLogger.hpp"
|
||||||
|
|
||||||
StreamLogger::StreamLogger(std::ostream& os, EnumSet<Severity> severities)
|
StreamLogger::StreamLogger(std::ostream& os, EnumSet<Severity> severities)
|
||||||
@@ -25,10 +27,9 @@ StreamLogger::StreamLogger(std::ostream& os, EnumSet<Severity> severities)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void StreamLogger::processLog(const Log& log)
|
||||||
StreamLogger::processLog(const Log& log)
|
|
||||||
{
|
{
|
||||||
if (_severities.contains(log.getSeverity()))
|
if (_severities.contains(log.getSeverity()))
|
||||||
_os << "[" << getSeverityName(log.getSeverity()) << "] [" << getModuleName(log.getModule()) << "] " << log.getMessage() << std::endl;
|
_os << std::this_thread::get_id() << " [" << getSeverityName(log.getSeverity()) << "] [" << getModuleName(log.getModule()) << "] " << log.getMessage() << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,20 @@ namespace StringUtils
|
|||||||
return oss.str();
|
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)
|
void capitalize(std::string& str)
|
||||||
{
|
{
|
||||||
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
|
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
|
||||||
@@ -205,7 +219,7 @@ namespace StringUtils
|
|||||||
return res;
|
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
|
static const std::unordered_map<char, std::string_view> escapeMap
|
||||||
{
|
{
|
||||||
@@ -235,6 +249,28 @@ namespace StringUtils
|
|||||||
return escaped;
|
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 escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
||||||
{
|
{
|
||||||
std::string res;
|
std::string res;
|
||||||
|
|||||||
@@ -19,14 +19,25 @@
|
|||||||
|
|
||||||
#include "utils/WtLogger.hpp"
|
#include "utils/WtLogger.hpp"
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
#include <sstream>
|
||||||
#include <Wt/WApplication.h>
|
#include <Wt/WApplication.h>
|
||||||
#include <Wt/WLogger.h>
|
#include <Wt/WLogger.h>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
void
|
namespace
|
||||||
WtLogger::processLog(const Log& log)
|
|
||||||
{
|
{
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ class Log
|
|||||||
std::ostringstream& getOstream() { return _oss; }
|
std::ostringstream& getOstream() { return _oss; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
Log(const Log&) = delete;
|
||||||
|
Log& operator=(const Log&) = delete;
|
||||||
|
|
||||||
Module _module;
|
Module _module;
|
||||||
Severity _severity;
|
Severity _severity;
|
||||||
std::ostringstream _oss;
|
std::ostringstream _oss;
|
||||||
@@ -85,6 +88,5 @@ class Logger
|
|||||||
virtual void processLog(const Log& log) = 0;
|
virtual void processLog(const Log& log) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define LMS_LOG(module, severity) Log(Service<Logger>::get(), Module::module, Severity::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()
|
#define LMS_LOG_EX(module, severity) Log{Service<Logger>::get(), module, severity}.getOstream()
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ namespace StringUtils {
|
|||||||
|
|
||||||
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
|
[[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);
|
void capitalize(std::string& str);
|
||||||
|
|
||||||
template<typename T>
|
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 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);
|
[[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -238,7 +238,10 @@ int main(int argc, char* argv[])
|
|||||||
{
|
{
|
||||||
Database::Session session {database};
|
Database::Session session {database};
|
||||||
session.prepareTables();
|
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;
|
UserInterface::LmsApplicationManager appManager;
|
||||||
|
|||||||
@@ -535,8 +535,8 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
|||||||
if (!artists.empty())
|
if (!artists.empty())
|
||||||
{
|
{
|
||||||
entry->setCondition("if-has-artists", true);
|
entry->setCondition("if-has-artists", true);
|
||||||
entry->bindWidget("artists", Utils::createArtistContainer(artists));
|
entry->bindWidget("artists", Utils::createArtistAnchorList(artists));
|
||||||
entry->bindWidget("artists-md", Utils::createArtistContainer(artists));
|
entry->bindWidget("artists-md", Utils::createArtistAnchorList(artists));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto release {track->getRelease()};
|
const auto release {track->getRelease()};
|
||||||
|
|||||||
@@ -160,15 +160,13 @@ class SettingsModel : public Wt::WFormModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
user.modify()->setSubsonicTranscodeEnable(Wt::asNumber(value(SubsonicTranscodeEnableField)));
|
|
||||||
|
|
||||||
auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(SubsonicTranscodeBitrateField))};
|
auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(SubsonicTranscodeBitrateField))};
|
||||||
if (subsonicTranscodeBitrateRow)
|
if (subsonicTranscodeBitrateRow)
|
||||||
user.modify()->setSubsonicTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow));
|
user.modify()->setSubsonicDefaultTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow));
|
||||||
|
|
||||||
auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(SubsonicTranscodeFormatField))};
|
auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(SubsonicTranscodeFormatField))};
|
||||||
if (subsonicTranscodeFormatRow)
|
if (subsonicTranscodeFormatRow)
|
||||||
user.modify()->setSubsonicTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow));
|
user.modify()->setSubsonicDefaultTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow));
|
||||||
|
|
||||||
auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))};
|
auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))};
|
||||||
if (subsonicArtistListModeRow)
|
if (subsonicArtistListModeRow)
|
||||||
@@ -224,19 +222,12 @@ class SettingsModel : public Wt::WFormModel
|
|||||||
setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo);
|
setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue(SubsonicTranscodeEnableField, LmsApp->getUser()->getSubsonicTranscodeEnable());
|
|
||||||
{
|
{
|
||||||
const bool usesTranscode {LmsApp->getUser()->getSubsonicTranscodeEnable()};
|
auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodeBitrate())};
|
||||||
setReadOnly(SubsonicTranscodeFormatField, !usesTranscode);
|
|
||||||
setReadOnly(SubsonicTranscodeBitrateField, !usesTranscode);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromValue(user->getSubsonicTranscodeBitrate())};
|
|
||||||
if (subsonicTranscodeBitrateRow)
|
if (subsonicTranscodeBitrateRow)
|
||||||
setValue(SubsonicTranscodeBitrateField, _transcodeBitrateModel->getString(*subsonicTranscodeBitrateRow));
|
setValue(SubsonicTranscodeBitrateField, _transcodeBitrateModel->getString(*subsonicTranscodeBitrateRow));
|
||||||
|
|
||||||
auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromValue(user->getSubsonicTranscodeFormat())};
|
auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodeFormat())};
|
||||||
if (subsonicTranscodeFormatRow)
|
if (subsonicTranscodeFormatRow)
|
||||||
setValue(SubsonicTranscodeFormatField, _transcodeFormatModel->getString(*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));
|
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
|
// Format
|
||||||
auto transcodeFormat {std::make_unique<Wt::WComboBox>()};
|
auto transcodeFormat {std::make_unique<Wt::WComboBox>()};
|
||||||
transcodeFormat->setModel(model->getTranscodeFormatModel());
|
transcodeFormat->setModel(model->getTranscodeFormatModel());
|
||||||
@@ -501,15 +487,6 @@ SettingsView::refreshView()
|
|||||||
auto artistListMode {std::make_unique<Wt::WComboBox>()};
|
auto artistListMode {std::make_unique<Wt::WComboBox>()};
|
||||||
artistListMode->setModel(model->getSubsonicArtistListModeModel());
|
artistListMode->setModel(model->getSubsonicArtistListModeModel());
|
||||||
t->setFormWidget(SettingsModel::SubsonicArtistListModeField, std::move(artistListMode));
|
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
|
// Scrobbling
|
||||||
|
|||||||
+88
-23
@@ -38,8 +38,7 @@
|
|||||||
namespace UserInterface::Utils
|
namespace UserInterface::Utils
|
||||||
{
|
{
|
||||||
|
|
||||||
std::string
|
std::string durationToString(std::chrono::milliseconds msDuration)
|
||||||
durationToString(std::chrono::milliseconds msDuration)
|
|
||||||
{
|
{
|
||||||
const std::chrono::seconds duration{ std::chrono::duration_cast<std::chrono::seconds>(msDuration) };
|
const std::chrono::seconds duration{ std::chrono::duration_cast<std::chrono::seconds>(msDuration) };
|
||||||
|
|
||||||
@@ -64,8 +63,7 @@ namespace UserInterface::Utils
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::unique_ptr<Wt::WImage>
|
std::unique_ptr<Wt::WImage> createCover(Database::ReleaseId releaseId, CoverResource::Size size)
|
||||||
createCover(Database::ReleaseId releaseId, CoverResource::Size size)
|
|
||||||
{
|
{
|
||||||
auto cover{ std::make_unique<Wt::WImage>() };
|
auto cover{ std::make_unique<Wt::WImage>() };
|
||||||
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(releaseId, size));
|
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(releaseId, size));
|
||||||
@@ -74,8 +72,7 @@ namespace UserInterface::Utils
|
|||||||
return cover;
|
return cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WImage>
|
std::unique_ptr<Wt::WImage> createCover(Database::TrackId trackId, CoverResource::Size size)
|
||||||
createCover(Database::TrackId trackId, CoverResource::Size size)
|
|
||||||
{
|
{
|
||||||
auto cover{ std::make_unique<Wt::WImage>() };
|
auto cover{ std::make_unique<Wt::WImage>() };
|
||||||
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(trackId, size));
|
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(trackId, size));
|
||||||
@@ -84,8 +81,7 @@ namespace UserInterface::Utils
|
|||||||
return cover;
|
return cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WInteractWidget>
|
std::unique_ptr<Wt::WInteractWidget> createCluster(Database::ClusterId clusterId, bool canDelete)
|
||||||
createCluster(Database::ClusterId clusterId, bool canDelete)
|
|
||||||
{
|
{
|
||||||
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
|
|
||||||
@@ -120,8 +116,7 @@ namespace UserInterface::Utils
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WContainerWidget>
|
std::unique_ptr<Wt::WContainerWidget> createArtistAnchorList(const std::vector<Database::ArtistId>& artistIds, std::string_view cssAnchorClass)
|
||||||
createArtistContainer(const std::vector<Database::ArtistId>& artistIds)
|
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
@@ -140,7 +135,8 @@ namespace UserInterface::Utils
|
|||||||
artistContainer->addNew<Wt::WText>(" · ");
|
artistContainer->addNew<Wt::WText>(" · ");
|
||||||
|
|
||||||
auto anchor{ createArtistAnchor(artist) };
|
auto anchor{ createArtistAnchor(artist) };
|
||||||
anchor->addStyleClass("link-success text-decoration-none"); // hack
|
anchor->addStyleClass("text-decoration-none"); // hack
|
||||||
|
anchor->addStyleClass(std::string{ cssAnchorClass });
|
||||||
artistContainer->addWidget(std::move(anchor));
|
artistContainer->addWidget(std::move(anchor));
|
||||||
firstArtist = false;
|
firstArtist = false;
|
||||||
}
|
}
|
||||||
@@ -148,8 +144,82 @@ namespace UserInterface::Utils
|
|||||||
return artistContainer;
|
return artistContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WLink
|
std::unique_ptr<Wt::WContainerWidget> createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector<Database::ArtistId>& artistIds, std::string_view cssAnchorClass)
|
||||||
createArtistLink(Database::Artist::pointer artist)
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
std::size_t matchCount{};
|
||||||
|
std::string_view::size_type currentOffset{};
|
||||||
|
|
||||||
|
auto result{ std::make_unique<Wt::WContainerWidget>() };
|
||||||
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
const auto pos{ displayName.find(artist->getName(), currentOffset) };
|
||||||
|
if (pos == std::string_view::npos)
|
||||||
|
break;
|
||||||
|
|
||||||
|
assert(pos >= currentOffset);
|
||||||
|
if (pos != currentOffset)
|
||||||
|
result->addNew<Wt::WText>(std::string{ displayName.substr(currentOffset, pos - currentOffset) }, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchCount != artistIds.size())
|
||||||
|
return createArtistAnchorList(artistIds, cssAnchorClass);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchorsForRelease(Database::ObjectPtr<Database::Release> release, Database::ArtistId omitIfMatchThisArtist, std::string_view cssAnchorClass)
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
Artist::FindParameters params;
|
||||||
|
params.setRelease(release->getId());
|
||||||
|
params.setLinkType(TrackArtistLinkType::ReleaseArtist);
|
||||||
|
|
||||||
|
if (const auto releaseArtists{ Artist::find(LmsApp->getDbSession(), params) }; !releaseArtists.results.empty())
|
||||||
|
{
|
||||||
|
if (releaseArtists.results.size() == 1 && releaseArtists.results.front() == omitIfMatchThisArtist)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return Utils::createArtistDisplayNameWithAnchors(release->getArtistDisplayName(), releaseArtists.results, cssAnchorClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.setLinkType(TrackArtistLinkType::Artist);
|
||||||
|
const auto artists{ Artist::find(LmsApp->getDbSession(), params) };
|
||||||
|
if (artists.results.size() == 1)
|
||||||
|
{
|
||||||
|
if (artists.results.front() == omitIfMatchThisArtist)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return Utils::createArtistAnchorList({ artists.results.front() }, cssAnchorClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() })
|
if (const auto mbid{ artist->getMBID() })
|
||||||
return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()} };
|
return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()} };
|
||||||
@@ -157,8 +227,7 @@ namespace UserInterface::Utils
|
|||||||
return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString() };
|
return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString() };
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WAnchor>
|
std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::Artist::pointer artist, bool setText)
|
||||||
createArtistAnchor(Database::Artist::pointer artist, bool setText)
|
|
||||||
{
|
{
|
||||||
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist));
|
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist));
|
||||||
|
|
||||||
@@ -172,8 +241,7 @@ namespace UserInterface::Utils
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WLink
|
Wt::WLink createReleaseLink(Database::Release::pointer release)
|
||||||
createReleaseLink(Database::Release::pointer release)
|
|
||||||
{
|
{
|
||||||
if (const auto mbid{ release->getMBID() })
|
if (const auto mbid{ release->getMBID() })
|
||||||
return Wt::WLink{ Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()} };
|
return Wt::WLink{ Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()} };
|
||||||
@@ -181,8 +249,7 @@ namespace UserInterface::Utils
|
|||||||
return Wt::WLink{ Wt::LinkType::InternalPath, "/release/" + release->getId().toString() };
|
return Wt::WLink{ Wt::LinkType::InternalPath, "/release/" + release->getId().toString() };
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WAnchor>
|
std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Database::Release::pointer release, bool setText)
|
||||||
createReleaseAnchor(Database::Release::pointer release, bool setText)
|
|
||||||
{
|
{
|
||||||
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
|
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
|
||||||
|
|
||||||
@@ -196,8 +263,7 @@ namespace UserInterface::Utils
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WAnchor>
|
std::unique_ptr<Wt::WAnchor> createTrackListAnchor(Database::TrackList::pointer trackList, bool setText)
|
||||||
createTrackListAnchor(Database::TrackList::pointer trackList, bool setText)
|
|
||||||
{
|
{
|
||||||
Wt::WLink link{ Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString() };
|
Wt::WLink link{ Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString() };
|
||||||
auto res{ std::make_unique<Wt::WAnchor>(link) };
|
auto res{ std::make_unique<Wt::WAnchor>(link) };
|
||||||
@@ -213,8 +279,7 @@ namespace UserInterface::Utils
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WContainerWidget>
|
std::unique_ptr<Wt::WContainerWidget> createClustersForTrack(Database::Track::pointer track, Filters& filters)
|
||||||
createClustersForTrack(Database::Track::pointer track, Filters& filters)
|
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
#include <Wt/WContainerWidget.h>
|
#include <Wt/WContainerWidget.h>
|
||||||
#include <Wt/WImage.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::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);
|
Wt::WLink createArtistLink(Database::ObjectPtr<Database::Artist> artist);
|
||||||
std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool setText = true);
|
std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool setText = true);
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ using namespace Database;
|
|||||||
|
|
||||||
namespace UserInterface::ReleaseListHelpers
|
namespace UserInterface::ReleaseListHelpers
|
||||||
{
|
{
|
||||||
static
|
namespace
|
||||||
std::unique_ptr<Wt::WTemplate>
|
{
|
||||||
createEntryInternal(const Release::pointer& release, const std::string& templateKey, const Artist::pointer& artist, const bool showYear)
|
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)) };
|
auto entry{ std::make_unique<Wt::WTemplate>(Wt::WString::tr(templateKey)) };
|
||||||
|
|
||||||
@@ -48,20 +48,11 @@ namespace UserInterface::ReleaseListHelpers
|
|||||||
anchor->setImage(std::move(cover));
|
anchor->setImage(std::move(cover));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto artists {release->getReleaseArtists()};
|
auto artistAnchors{ Utils::createArtistsAnchorsForRelease(release, artist ? artist->getId() : ArtistId{}, "link-secondary") };
|
||||||
if (artists.empty())
|
if (artistAnchors)
|
||||||
artists = release->getArtists();
|
|
||||||
|
|
||||||
const bool isSameArtist {(std::find(std::cbegin(artists), std::cend(artists), artist) != artists.end())};
|
|
||||||
|
|
||||||
if (artists.size() > 1)
|
|
||||||
{
|
|
||||||
entry->setCondition("if-has-various-artists", true);
|
|
||||||
}
|
|
||||||
else if (artists.size() == 1 && !isSameArtist)
|
|
||||||
{
|
{
|
||||||
entry->setCondition("if-has-artist", true);
|
entry->setCondition("if-has-artist", true);
|
||||||
entry->bindWidget("artist-name", Utils::createArtistAnchor(artists.front()));
|
entry->bindWidget("artist-name", std::move(artistAnchors));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showYear)
|
if (showYear)
|
||||||
@@ -76,21 +67,19 @@ namespace UserInterface::ReleaseListHelpers
|
|||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WTemplate>
|
std::unique_ptr<Wt::WTemplate> createEntry(const Release::pointer& release, const Artist::pointer& artist, bool showYear)
|
||||||
createEntry(const Release::pointer& release, const Artist::pointer& artist, bool showYear)
|
|
||||||
{
|
{
|
||||||
return createEntryInternal(release, "Lms.Explore.Releases.template.entry-grid", artist, showYear);
|
return createEntryInternal(release, "Lms.Explore.Releases.template.entry-grid", artist, showYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WTemplate>
|
std::unique_ptr<Wt::WTemplate> createEntry(const Release::pointer& release)
|
||||||
createEntry(const Release::pointer& release)
|
|
||||||
{
|
{
|
||||||
return createEntry(release, Artist::pointer{}, false /*year*/);
|
return createEntry(release, Artist::pointer{}, false /*year*/);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WTemplate>
|
std::unique_ptr<Wt::WTemplate> createEntryForArtist(const Database::Release::pointer& release, const Database::Artist::pointer& artist)
|
||||||
createEntryForArtist(const Database::Release::pointer& release, const Database::Artist::pointer& artist)
|
|
||||||
{
|
{
|
||||||
return createEntry(release, artist, true);
|
return createEntry(release, artist, true);
|
||||||
}
|
}
|
||||||
@@ -98,8 +87,7 @@ namespace UserInterface::ReleaseListHelpers
|
|||||||
|
|
||||||
namespace UserInterface::ReleaseHelpers
|
namespace UserInterface::ReleaseHelpers
|
||||||
{
|
{
|
||||||
Wt::WString
|
Wt::WString buildReleaseTypeString(ReleaseTypePrimary primaryType, EnumSet<ReleaseTypeSecondary> secondaryTypes)
|
||||||
buildReleaseTypeString(ReleaseTypePrimary primaryType, EnumSet<ReleaseTypeSecondary> secondaryTypes)
|
|
||||||
{
|
{
|
||||||
Wt::WString res;
|
Wt::WString res;
|
||||||
|
|
||||||
@@ -143,11 +131,11 @@ namespace UserInterface::ReleaseHelpers
|
|||||||
if (!releaseDate.isValid())
|
if (!releaseDate.isValid())
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
if (originalReleaseDate.isValid() && originalReleaseDate != releaseDate)
|
if (originalReleaseDate.isValid() && originalReleaseDate.year() != releaseDate.year())
|
||||||
res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")";
|
res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")";
|
||||||
else
|
else
|
||||||
res = std::to_string(releaseDate.year());
|
res = std::to_string(releaseDate.year());
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
} // namespace UserInterface::ReleaseHelpers}
|
} // namespace UserInterface::ReleaseHelpers
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
|
|||||||
|
|
||||||
for (const auto& [role, artistIds] : artistMap)
|
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"))};
|
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
|
||||||
artistsEntry->bindString("type", role);
|
artistsEntry->bindString("type", role);
|
||||||
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
|
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
|
||||||
@@ -400,8 +400,8 @@ Release::refreshView()
|
|||||||
if (variousArtists && !artists.empty())
|
if (variousArtists && !artists.empty())
|
||||||
{
|
{
|
||||||
entry->setCondition("if-has-artists", true);
|
entry->setCondition("if-has-artists", true);
|
||||||
entry->bindWidget("artists", Utils::createArtistContainer(artists));
|
entry->bindWidget("artists", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
|
||||||
entry->bindWidget("artists-md", Utils::createArtistContainer(artists));
|
entry->bindWidget("artists-md", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(),artists));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto trackNumber {track->getTrackNumber()};
|
auto trackNumber {track->getTrackNumber()};
|
||||||
@@ -480,29 +480,11 @@ Release::refreshView()
|
|||||||
void
|
void
|
||||||
Release::refreshReleaseArtists(const Database::Release::pointer& release)
|
Release::refreshReleaseArtists(const Database::Release::pointer& release)
|
||||||
{
|
{
|
||||||
std::vector<ObjectPtr<Artist>> artists;
|
auto container{ Utils::createArtistsAnchorsForRelease(release) };
|
||||||
|
if (container)
|
||||||
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())
|
|
||||||
{
|
{
|
||||||
setCondition("if-has-release-artists", true);
|
setCondition("if-has-release-artists", true);
|
||||||
|
bindWidget("artists", std::move(container));
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ namespace UserInterface::TrackListHelpers
|
|||||||
|
|
||||||
for (const auto& [role, artistIds] : artistMap)
|
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"))};
|
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
|
||||||
artistsEntry->bindString("type", role);
|
artistsEntry->bindString("type", role);
|
||||||
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
|
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
|
||||||
@@ -171,8 +171,8 @@ namespace UserInterface::TrackListHelpers
|
|||||||
if (!artists.empty())
|
if (!artists.empty())
|
||||||
{
|
{
|
||||||
entry->setCondition("if-has-artists", true);
|
entry->setCondition("if-has-artists", true);
|
||||||
entry->bindWidget("artists", Utils::createArtistContainer(artists));
|
entry->bindWidget("artists", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
|
||||||
entry->bindWidget("artists-md", Utils::createArtistContainer(artists));
|
entry->bindWidget("artists-md", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track->getRelease())
|
if (track->getRelease())
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ operator<<(std::ostream& os, const MetaData::Release& release)
|
|||||||
if (release.mediumCount)
|
if (release.mediumCount)
|
||||||
std::cout << "\tMediumCount: " << *release.mediumCount << std::endl;
|
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)
|
for (const MetaData::Artist& artist : release.artists)
|
||||||
std::cout << "\tRelease artist: " << artist << std::endl;
|
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;
|
std::cout << "Parsed metadata:" << std::endl;
|
||||||
|
|
||||||
|
if (!track->artistDisplayName.empty())
|
||||||
|
std::cout << "Display artist: " << track->artistDisplayName << std::endl;
|
||||||
|
|
||||||
for (const Artist& artist : track->artists)
|
for (const Artist& artist : track->artists)
|
||||||
std::cout << "Artist: " << artist << std::endl;
|
std::cout << "Artist: " << artist << std::endl;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user