diff --git a/approot/messages.xml b/approot/messages.xml index 1c97035d..28883d34 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -241,6 +241,8 @@ Audio These audio settings are local to your browser! Change password +Default transcode bitrate +Default transcode format Cannot save using a demo account! Settings Bad password @@ -265,7 +267,6 @@ Subsonic API Transcoding Transcode bitrate -Enable transcoding Transcode format Matroska/Opus MP3 diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index a7b59fa7..a5f2e255 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -241,6 +241,8 @@ Audio Ces paramètres audio sont locaux à votre navigateur ! Changement de mot de passe +Bitrate par defaut du transcodage +Format par defaut du transcodage Impossible de sauvegarder en utilisant un compte de démo ! Paramètres Mauvais mot de passe @@ -265,7 +267,6 @@ API Subsonic Transcodage Bitrate du transcodage -Activer le transcodage Format du transcodage Matroska/Opus MP3 diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 0e27ff55..7849ef42 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -241,6 +241,8 @@ Audio Queste impostazioni sono salvate localmente su questo dispositivo! Cambia password + + Non posso salvare le impostazioni utilizzando l'account demo! Impostazioni Password errata @@ -265,7 +267,6 @@ Subsonic API Transcodifica Bitrate transcodifica -Abilita transcodifica Formato di transcodifica Matroska/Opus MP3 diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 6bea76b1..c1eca28c 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -241,6 +241,8 @@ 音频 这些音频设置仅保存在您的本地浏览器! 更改密码 + + 使用演示账号时不能保存! 设置 无效密码 @@ -265,7 +267,6 @@ Subsonic 应用程序接口 转码 转码比特率 -开启转码 转码格式 Matroska/Opus MP3 diff --git a/approot/settings.xml b/approot/settings.xml index 537f4f76..c15db564 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -74,20 +74,9 @@ ${} ${tr:Lms.Settings.subsonic-api} - - - ${subsonic-transcode-enable class="form-check-input"} - - ${tr:Lms.Settings.transcode-enable class="form-control"} - - - ${subsonic-transcode-enable-info} - - - - ${tr:Lms.Settings.transcode-format} + ${tr:Lms.Settings.default-transcode-format} ${subsonic-transcode-format class="form-control"} @@ -95,8 +84,8 @@ - - ${tr:Lms.Settings.transcode-bitrate} + + ${tr:Lms.Settings.default-transcode-bitrate} ${subsonic-transcode-bitrate class="form-control"} diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp index 52bc4fb4..728caf4e 100644 --- a/src/libs/services/database/impl/Migration.cpp +++ b/src/libs/services/database/impl/Migration.cpp @@ -30,210 +30,174 @@ namespace Database { - VersionInfo::pointer - VersionInfo::getOrCreate(Session& session) - { - session.checkUniqueLocked(); + VersionInfo::pointer VersionInfo::getOrCreate(Session& session) + { + session.checkUniqueLocked(); - pointer versionInfo {session.getDboSession().find()}; - if (!versionInfo) - return session.getDboSession().add(std::make_unique()); + pointer versionInfo{ session.getDboSession().find() }; + if (!versionInfo) + return session.getDboSession().add(std::make_unique()); - return versionInfo; - } + return versionInfo; + } - VersionInfo::pointer - VersionInfo::get(Session& session) - { - session.checkSharedLocked(); - - return session.getDboSession().find(); - } + VersionInfo::pointer VersionInfo::get(Session& session) + { + session.checkSharedLocked(); + return session.getDboSession().find(); + } } namespace Database::Migration { - class ScopedNoForeignKeys - { - public: - ScopedNoForeignKeys(Db& db) : _db {db} - { - _db.executeSql("PRAGMA foreign_keys=OFF"); - } - ~ScopedNoForeignKeys() - { - _db.executeSql("PRAGMA foreign_keys=ON"); - } + class ScopedNoForeignKeys + { + public: + ScopedNoForeignKeys(Db& db) : _db{ db } + { + _db.executeSql("PRAGMA foreign_keys=OFF"); + } + ~ScopedNoForeignKeys() + { + _db.executeSql("PRAGMA foreign_keys=ON"); + } - ScopedNoForeignKeys(const ScopedNoForeignKeys&) = delete; - ScopedNoForeignKeys(ScopedNoForeignKeys&&) = delete; - ScopedNoForeignKeys& operator=(const ScopedNoForeignKeys&) = delete; - ScopedNoForeignKeys& operator=(ScopedNoForeignKeys&&) = delete; + ScopedNoForeignKeys(const ScopedNoForeignKeys&) = delete; + ScopedNoForeignKeys(ScopedNoForeignKeys&&) = delete; + ScopedNoForeignKeys& operator=(const ScopedNoForeignKeys&) = delete; + ScopedNoForeignKeys& operator=(ScopedNoForeignKeys&&) = delete; - private: - Db& _db; - }; + private: + Db& _db; + }; - static - std::string - dateTimeToDbFormat(const Wt::WDateTime& dateTime) - { - return dateTime.toString("yyyy'-'MM'-'dd'T'hh':'mm':'ss'.000'", false).toUTF8(); - } + static std::string dateTimeToDbFormat(const Wt::WDateTime& dateTime) + { + return dateTime.toString("yyyy'-'MM'-'dd'T'hh':'mm':'ss'.000'", false).toUTF8(); + } - static - void - migrateFromV5(Session& session) - { - session.getDboSession().execute("DELETE FROM auth_token"); // format has changed - } + static void migrateFromV5(Session& session) + { + session.getDboSession().execute("DELETE FROM auth_token"); // format has changed + } - static - void - migrateFromV6(Session& session) - { - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + static void migrateFromV6(Session& session) + { + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV7(Session& session) - { - session.getDboSession().execute("DROP TABLE similarity_settings"); - 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(ScanSettings::RecommendationEngineType::Clusters)) + ")"); - } + static void migrateFromV7(Session& session) + { + session.getDboSession().execute("DROP TABLE similarity_settings"); + 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(ScanSettings::RecommendationEngineType::Clusters)) + ")"); + } - static - void - migrateFromV8(Session& session) - { - // 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 - ScanSettings::get(session).modify()->incScanVersion(); - } + static void migrateFromV8(Session& session) + { + // 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 + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV9(Session& session) - { - session.getDboSession().execute(R"( + static void migrateFromV9(Session& session) + { + session.getDboSession().execute(R"( CREATE TABLE IF NOT EXISTS "track_bookmark" ( - "id" integer primary key autoincrement, - "version" integer not null, - "offset" integer, - "comment" text not null, - "track_id" bigint, - "user_id" bigint, - constraint "fk_track_bookmark_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, - constraint "fk_track_bookmark_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred + "id" integer primary key autoincrement, + "version" integer not null, + "offset" integer, + "comment" text not null, + "track_id" bigint, + "user_id" bigint, + constraint "fk_track_bookmark_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, + constraint "fk_track_bookmark_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred );)"); - } + } - static - void - migrateFromV10(Session& session) - { - ScanSettings::get(session).modify()->addAudioFileExtension(".m4b"); - ScanSettings::get(session).modify()->addAudioFileExtension(".alac"); - } + static void migrateFromV10(Session& session) + { + ScanSettings::get(session).modify()->addAudioFileExtension(".m4b"); + ScanSettings::get(session).modify()->addAudioFileExtension(".alac"); + } - static - void - migrateFromV11(Session& session) - { - // 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 - ScanSettings::get(session).modify()->incScanVersion(); - } + static void migrateFromV11(Session& session) + { + // 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 + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV12(Session& session) - { - // 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 - ScanSettings::get(session).modify()->incScanVersion(); - } + static void migrateFromV12(Session& session) + { + // 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 + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV13(Session& session) - { - // 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 - ScanSettings::get(session).modify()->incScanVersion(); - } + static void migrateFromV13(Session& session) + { + // 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 + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV14(Session& session) - { - // SortName now set from metadata - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + static void migrateFromV14(Session& session) + { + // SortName now set from metadata + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV15(Session& session) - { - session.getDboSession().execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultUITheme)) + ")"); - } + static void migrateFromV15(Session& session) + { + session.getDboSession().execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultUITheme)) + ")"); + } - static - 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_track INTEGER NOT NULL DEFAULT(0)"); + static 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_track INTEGER NOT NULL DEFAULT(0)"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV17(Session& session) - { - // Drop colums total_disc/total_track from release - session.getDboSession().execute(R"( + static void migrateFromV17(Session& session) + { + // Drop colums total_disc/total_track from release + session.getDboSession().execute(R"( CREATE TABLE "release_backup" ( "id" integer primary key autoincrement, "version" integer not null, "name" text not null, "mbid" text not null ))"); - session.getDboSession().execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release"); - session.getDboSession().execute("DROP TABLE release"); - session.getDboSession().execute("ALTER TABLE release_backup RENAME TO release"); + session.getDboSession().execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release"); + session.getDboSession().execute("DROP TABLE release"); + session.getDboSession().execute("ALTER TABLE release_backup RENAME TO release"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - - static - void - migrateFromV18(Session& session) - { - session.getDboSession().execute(R"( + static void migrateFromV18(Session& session) + { + session.getDboSession().execute(R"( CREATE TABLE IF NOT EXISTS "subsonic_settings" ( "id" integer primary key autoincrement, "version" integer not null, "api_enabled" boolean not null, "artist_list_mode" integer not null ))"); - } + } - static - void - migrateFromV19(Session& session) - { - session.getDboSession().execute(R"( + static void migrateFromV19(Session& session) + { + session.getDboSession().execute(R"( CREATE TABLE "user_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -251,96 +215,76 @@ CREATE TABLE "user_backup" ( "repeat_all" 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, "} - + (User::defaultSubsonicTranscodeEnable ? "1" : "0") - + ", " + std::to_string(static_cast(User::defaultSubsonicTranscodeFormat)) - + ", " + std::to_string(User::defaultSubsonicTranscodeBitrate) - + ", " + std::to_string(static_cast(User::defaultSubsonicArtistListMode)) - + ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); - session.getDboSession().execute("DROP TABLE user"); - session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user"); - } + session.getDboSession().execute(std::string{ "INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, " } + + "1" // default enable transcode + + ", " + std::to_string(static_cast(User::defaultSubsonicTranscodeFormat)) + + ", " + std::to_string(User::defaultSubsonicTranscodeBitrate) + + ", " + std::to_string(static_cast(User::defaultSubsonicArtistListMode)) + + ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); + session.getDboSession().execute("DROP TABLE user"); + session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user"); + } - static - void - migrateFromV20(Session& session) - { - session.getDboSession().execute("DROP TABLE subsonic_settings"); - } + static void migrateFromV20(Session& session) + { + session.getDboSession().execute("DROP TABLE subsonic_settings"); + } - static - void - migrateFromV21(Session& session) - { - session.getDboSession().execute("ALTER TABLE track ADD track_replay_gain REAL"); - session.getDboSession().execute("ALTER TABLE track ADD release_replay_gain REAL"); + static void migrateFromV21(Session& session) + { + session.getDboSession().execute("ALTER TABLE track ADD track_replay_gain REAL"); + session.getDboSession().execute("ALTER TABLE track ADD release_replay_gain REAL"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + static void migrateFromV22(Session& session) + { + session.getDboSession().execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''"); - static - void - migrateFromV22(Session& session) - { - session.getDboSession().execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''"); + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + static void migrateFromV23(Session& session) + { + // Better cover detection + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + static void migrateFromV24(Session& session) + { + // User's AuthMode + session.getDboSession().execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultAuthMode*/0)) + ")"); + } - static - void - migrateFromV23(Session& session) - { - // Better cover detection - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + static void migrateFromV25(Session& session) + { + // Better cover detection + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV24(Session& session) - { - // User's AuthMode - session.getDboSession().execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultAuthMode*/0)) + ")"); - } + static void migrateFromV26(Session& session) + { + // Composer, mixer, etc. support + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV25(Session& session) - { - // Better cover detection - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + static 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 + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV26(Session& session) - { - // Composer, mixer, etc. support - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static - 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 - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static - void - migrateFromV28(Session& session) - { - // Drop Auth mode - session.getDboSession().execute(R"( + static void migrateFromV28(Session& session) + { + // Drop Auth mode + session.getDboSession().execute(R"( CREATE TABLE "user_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -358,33 +302,29 @@ CREATE TABLE "user_backup" ( "repeat_all" boolean not null, "radio" boolean not null ))"); - session.getDboSession().execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); - session.getDboSession().execute("DROP TABLE user"); - session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user"); - } + session.getDboSession().execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); + session.getDboSession().execute("DROP TABLE user"); + session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user"); + } - static - void - migrateFromV29(Session& session) - { - 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 scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultScrobbler)) + ")"); - session.getDboSession().execute("ALTER TABLE track ADD recording_mbid TEXT"); + static void migrateFromV29(Session& session) + { + 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 scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultScrobbler)) + ")"); + session.getDboSession().execute("ALTER TABLE track ADD recording_mbid TEXT"); - session.getDboSession().execute("DELETE from tracklist WHERE name = ?").bind("__played_tracks__"); + session.getDboSession().execute("DELETE from tracklist WHERE name = ?").bind("__played_tracks__"); - // MBID changes - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + // MBID changes + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV30(Session& session) - { - // drop "year" and "original_year" (rescan needed to convert them into dates) - session.getDboSession().execute(R"( + static void migrateFromV30(Session& session) + { + // drop "year" and "original_year" (rescan needed to convert them into dates) + session.getDboSession().execute(R"( CREATE TABLE "track_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -405,20 +345,18 @@ CREATE TABLE "track_backup" ( "release_id" bigint, total_disc INTEGER NOT NULL DEFAULT(0), total_track INTEGER NOT NULL DEFAULT(0), track_replay_gain REAL, release_replay_gain REAL, disc_subtitle TEXT NOT NULL DEFAULT '', recording_mbid TEXT, constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred ))"); - session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, name, duration, \"1900-01-01\", \"1900-01-01\", file_path, file_last_write, file_added, has_cover, mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain, disc_subtitle, recording_mbid FROM track"); - session.getDboSession().execute("DROP TABLE track"); - session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); + session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, name, duration, \"1900-01-01\", \"1900-01-01\", file_path, file_last_write, file_added, has_cover, mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain, disc_subtitle, recording_mbid FROM track"); + session.getDboSession().execute("DROP TABLE track"); + session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV31(Session& session) - { - // new star system, using dedicated entries per scrobbler and date time - session.getDboSession().execute(R"( + static void migrateFromV31(Session& session) + { + // new star system, using dedicated entries per scrobbler and date time + session.getDboSession().execute(R"( CREATE TABLE "starred_artist" ( "id" integer primary key autoincrement, "version" integer not null, @@ -430,7 +368,7 @@ CREATE TABLE "starred_artist" ( constraint "fk_starred_artist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred ))"); - session.getDboSession().execute(R"( + session.getDboSession().execute(R"( CREATE TABLE "starred_release" ( "id" integer primary key autoincrement, "version" integer not null, @@ -442,7 +380,7 @@ CREATE TABLE "starred_release" ( constraint "fk_starred_release_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred ))"); - session.getDboSession().execute(R"( + session.getDboSession().execute(R"( CREATE TABLE "starred_track" ( "id" integer primary key autoincrement, "version" integer not null, @@ -454,53 +392,53 @@ CREATE TABLE "starred_track" ( constraint "fk_starred_track_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred ))"); - // Can't migrate using class mapping as mapping may evolve in the future + // Can't migrate using class mapping as mapping may evolve in the future - // use time_t to avoid rounding issues later - const std::string now {dateTimeToDbFormat(Wt::WDateTime::fromTime_t(Wt::WDateTime::currentDateTime().toTime_t()))}; + // use time_t to avoid rounding issues later + const std::string now{ dateTimeToDbFormat(Wt::WDateTime::fromTime_t(Wt::WDateTime::currentDateTime().toTime_t())) }; - std::map userScrobblers; - auto getScrobbler {[&](IdType::ValueType userId) - { - auto itScrobbler {userScrobblers.find(userId)}; - if (itScrobbler != std::cend(userScrobblers)) - return itScrobbler->second; + std::map userScrobblers; + auto getScrobbler{ [&](IdType::ValueType userId) + { + auto itScrobbler {userScrobblers.find(userId)}; + if (itScrobbler != std::cend(userScrobblers)) + return itScrobbler->second; - auto query {session.getDboSession().query("SELECT scrobbler FROM user WHERE id = ?").bind(userId)}; - [[maybe_unused]] auto [itInserted, inserted] {userScrobblers.emplace(userId, query.resultValue())}; - assert(inserted); - return itInserted->second; - }}; + auto query {session.getDboSession().query("SELECT scrobbler FROM user WHERE id = ?").bind(userId)}; + [[maybe_unused]] auto [itInserted, inserted] {userScrobblers.emplace(userId, query.resultValue())}; + assert(inserted); + return itInserted->second; + } }; - auto migrateStarEntries {[&session, &getScrobbler, &now](const std::string& colName, const std::string& oldTableName, const std::string& newTableName) - { - using UserIdObjectId = std::tuple; + auto migrateStarEntries{ [&session, &getScrobbler, &now](const std::string& colName, const std::string& oldTableName, const std::string& newTableName) + { + using UserIdObjectId = std::tuple; - std::vector starredEntries; - auto query {session.getDboSession().query("SELECT user_id, " + colName + " from " + oldTableName)}; - auto results {query.resultList()}; + std::vector starredEntries; + auto query {session.getDboSession().query("SELECT user_id, " + colName + " from " + oldTableName)}; + auto results {query.resultList()}; - LMS_LOG(DB, INFO) << "Found " << results.size() << " " << colName << " to migrate"; + LMS_LOG(DB, INFO) << "Found " << results.size() << " " << colName << " to migrate"; - for (const auto& [userId, entryId] : results) - { - session.getDboSession().execute("INSERT INTO " + newTableName + " ('version', 'scrobbler', 'date_time', '" + colName + "', 'user_id') VALUES (?, ?, ?, ?, ?)") - .bind(0) - .bind(getScrobbler(userId)) - .bind(now) - .bind(entryId) - .bind(userId); - } + for (const auto& [userId, entryId] : results) + { + session.getDboSession().execute("INSERT INTO " + newTableName + " ('version', 'scrobbler', 'date_time', '" + colName + "', 'user_id') VALUES (?, ?, ?, ?, ?)") + .bind(0) + .bind(getScrobbler(userId)) + .bind(now) + .bind(entryId) + .bind(userId); + } - session.getDboSession().execute("DROP TABLE " + oldTableName); - }}; + session.getDboSession().execute("DROP TABLE " + oldTableName); + } }; - migrateStarEntries("artist_id","user_artist_starred", "starred_artist"); - migrateStarEntries("release_id","user_release_starred", "starred_release"); - migrateStarEntries("track_id","user_track_starred", "starred_track"); + migrateStarEntries("artist_id", "user_artist_starred", "starred_artist"); + migrateStarEntries("release_id", "user_release_starred", "starred_release"); + migrateStarEntries("track_id", "user_track_starred", "starred_track"); - // new listen system, no longer using tracklists - session.getDboSession().execute(R"( + // new listen system, no longer using tracklists + session.getDboSession().execute(R"( CREATE TABLE "listen" ( "id" integer primary key autoincrement, "version" integer not null, @@ -513,49 +451,45 @@ CREATE TABLE "listen" ( constraint "fk_listen_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred ))"); - auto migrateListens {[&session](const std::string& trackListName, Scrobbler scrobbler) - { - using UserIdObjectId = std::tuple; + auto migrateListens{ [&session](const std::string& trackListName, Scrobbler scrobbler) + { + using UserIdObjectId = std::tuple; - std::vector listens; - auto query {session.getDboSession().query("SELECT t_l.user_id, t_l_e.track_id, t_l_e.date_time FROM tracklist t_l") - .join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id") - .where("t_l.name = ?").bind(trackListName)}; - auto results {query.resultList()}; - listens.reserve(results.size()); + std::vector listens; + auto query {session.getDboSession().query("SELECT t_l.user_id, t_l_e.track_id, t_l_e.date_time FROM tracklist t_l") + .join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id") + .where("t_l.name = ?").bind(trackListName)}; + auto results {query.resultList()}; + listens.reserve(results.size()); - LMS_LOG(DB, INFO) << "Found " << results.size() << " listens in " << trackListName; + LMS_LOG(DB, INFO) << "Found " << results.size() << " listens in " << trackListName; - for (const auto& [userId, trackId, dateTime] : results) - { - session.getDboSession().execute("INSERT INTO listen ('version', 'date_time', 'scrobbler', 'scrobbling_state', 'track_id', 'user_id') VALUES (?, ?, ?, ?, ?, ?)") - .bind(0) - .bind(dateTimeToDbFormat(dateTime)) - .bind(scrobbler) - .bind(ScrobblingState::Synchronized) // consider sync is done to avoid duplicate submissions - .bind(trackId) - .bind(userId); - } - }}; + for (const auto& [userId, trackId, dateTime] : results) + { + session.getDboSession().execute("INSERT INTO listen ('version', 'date_time', 'scrobbler', 'scrobbling_state', 'track_id', 'user_id') VALUES (?, ?, ?, ?, ?, ?)") + .bind(0) + .bind(dateTimeToDbFormat(dateTime)) + .bind(scrobbler) + .bind(ScrobblingState::Synchronized) // consider sync is done to avoid duplicate submissions + .bind(trackId) + .bind(userId); + } + } }; - migrateListens("__scrobbler_internal_history__", Scrobbler::Internal); - migrateListens("__scrobbler_listenbrainz_history__", Scrobbler::ListenBrainz); - } + migrateListens("__scrobbler_internal_history__", Scrobbler::Internal); + migrateListens("__scrobbler_listenbrainz_history__", Scrobbler::ListenBrainz); + } - static - void - migrateFromV32(Session& session) - { - ScanSettings::get(session).modify()->addAudioFileExtension(".wv"); - } + static void migrateFromV32(Session& session) + { + ScanSettings::get(session).modify()->addAudioFileExtension(".wv"); + } - static - void - migrateFromV33(Session& session) - { - // remove name from track_artist_link - // Drop Auth mode - session.getDboSession().execute(R"( + static void migrateFromV33(Session& session) + { + // remove name from track_artist_link + // Drop Auth mode + session.getDboSession().execute(R"( CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -566,59 +500,49 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( constraint "fk_track_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred ); ))"); - session.getDboSession().execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link"); - session.getDboSession().execute("DROP TABLE track_artist_link"); - session.getDboSession().execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link"); - } + session.getDboSession().execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link"); + session.getDboSession().execute("DROP TABLE track_artist_link"); + session.getDboSession().execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link"); + } - static - void - migrateFromV34(Session& session) - { - // Add scrobbling state - // By default, everything needs to be sent - session.getDboSession().execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); - session.getDboSession().execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); - session.getDboSession().execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); - } + static void migrateFromV34(Session& session) + { + // Add scrobbling state + // By default, everything needs to be sent + session.getDboSession().execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); + session.getDboSession().execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); + session.getDboSession().execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); + } - static - void - migrateFromV35(Session& session) - { - // 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 last_modified_date_time TEXT"); - } + static void migrateFromV35(Session& session) + { + // 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 last_modified_date_time TEXT"); + } - static - void - migrateFromV36(Session& session) - { - // 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 - ScanSettings::get(session).modify()->incScanVersion(); - } + static void migrateFromV36(Session& session) + { + // 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 + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV37(Session& session) - { - // Support Performer tags (via subtypes) - session.getDboSession().execute("ALTER TABLE track_artist_link ADD subtype TEXT"); + static void migrateFromV37(Session& session) + { + // Support Performer tags (via subtypes) + session.getDboSession().execute("ALTER TABLE track_artist_link ADD subtype TEXT"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV38(Session& session) - { - // migrate release-specific tags from Track to Release - session.getDboSession().execute("ALTER TABLE release ADD total_disc INTEGER"); + static void migrateFromV38(Session& session) + { + // migrate release-specific tags from Track to Release + session.getDboSession().execute("ALTER TABLE release ADD total_disc INTEGER"); - session.getDboSession().execute(R"( + session.getDboSession().execute(R"( CREATE TABLE IF NOT EXISTS "track_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -645,123 +569,127 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred ); ))"); - session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track"); - session.getDboSession().execute("DROP TABLE track"); - session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); + session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track"); + session.getDboSession().execute("DROP TABLE track"); + session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV39(Session& session) - { - // add release type - session.getDboSession().execute("ALTER TABLE release ADD primary_type INTEGER"); - session.getDboSession().execute("ALTER TABLE release ADD secondary_types INTEGER"); + static void migrateFromV39(Session& session) + { + // add release type + session.getDboSession().execute("ALTER TABLE release ADD primary_type INTEGER"); + session.getDboSession().execute("ALTER TABLE release ADD secondary_types INTEGER"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - static - void - migrateFromV40(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"); + static void migrateFromV40(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(); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - void - doDbMigration(Session& session) - { - static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"}; + 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"); + } - ScopedNoForeignKeys noPragmaKeys {session.getDb()}; + void doDbMigration(Session& session) + { + static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; - using MigrationFunction = std::function; + ScopedNoForeignKeys noPragmaKeys{ session.getDb() }; - const std::map migrationFunctions - { - {5, migrateFromV5}, - {6, migrateFromV6}, - {7, migrateFromV7}, - {8, migrateFromV8}, - {9, migrateFromV9}, - {10, migrateFromV10}, - {11, migrateFromV11}, - {12, migrateFromV12}, - {13, migrateFromV13}, - {14, migrateFromV14}, - {15, migrateFromV15}, - {16, migrateFromV16}, - {17, migrateFromV17}, - {18, migrateFromV18}, - {19, migrateFromV19}, - {20, migrateFromV20}, - {21, migrateFromV21}, - {22, migrateFromV22}, - {23, migrateFromV23}, - {24, migrateFromV24}, - {25, migrateFromV25}, - {26, migrateFromV26}, - {27, migrateFromV27}, - {28, migrateFromV28}, - {29, migrateFromV29}, - {30, migrateFromV30}, - {31, migrateFromV31}, - {32, migrateFromV32}, - {33, migrateFromV33}, - {34, migrateFromV34}, - {35, migrateFromV35}, - {36, migrateFromV36}, - {37, migrateFromV37}, - {38, migrateFromV38}, - {39, migrateFromV39}, - {40, migrateFromV40}, - }; + using MigrationFunction = std::function; - while (1) - { - auto uniqueTransaction {session.createUniqueTransaction()}; + const std::map migrationFunctions + { + {5, migrateFromV5}, + {6, migrateFromV6}, + {7, migrateFromV7}, + {8, migrateFromV8}, + {9, migrateFromV9}, + {10, migrateFromV10}, + {11, migrateFromV11}, + {12, migrateFromV12}, + {13, migrateFromV13}, + {14, migrateFromV14}, + {15, migrateFromV15}, + {16, migrateFromV16}, + {17, migrateFromV17}, + {18, migrateFromV18}, + {19, migrateFromV19}, + {20, migrateFromV20}, + {21, migrateFromV21}, + {22, migrateFromV22}, + {23, migrateFromV23}, + {24, migrateFromV24}, + {25, migrateFromV25}, + {26, migrateFromV26}, + {27, migrateFromV27}, + {28, migrateFromV28}, + {29, migrateFromV29}, + {30, migrateFromV30}, + {31, migrateFromV31}, + {32, migrateFromV32}, + {33, migrateFromV33}, + {34, migrateFromV34}, + {35, migrateFromV35}, + {36, migrateFromV36}, + {37, migrateFromV37}, + {38, migrateFromV38}, + {39, migrateFromV39}, + {40, migrateFromV40}, + {41, migrateFromV41}, + }; - Version version; - try - { - version = VersionInfo::getOrCreate(session)->getVersion(); - LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION; - } - catch (std::exception& e) - { - LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what(); - throw LmsException {outdatedMsg}; - } + while (1) + { + auto uniqueTransaction{ session.createUniqueTransaction() }; - 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"}; - } + Version version; + try + { + version = VersionInfo::getOrCreate(session)->getVersion(); + LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION; + } + catch (std::exception& e) + { + LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what(); + throw LmsException{ outdatedMsg }; + } - if (version < migrationFunctions.begin()->first) - throw LmsException {outdatedMsg}; + 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" }; + } - LMS_LOG(DB, INFO) << "Migrating database from version " << version << "..."; + if (version < migrationFunctions.begin()->first) + throw LmsException{ outdatedMsg }; - auto itMigrationFunc {migrationFunctions.find(version)}; - assert(itMigrationFunc != std::cend(migrationFunctions)); - itMigrationFunc->second(session); + LMS_LOG(DB, INFO) << "Migrating database from version " << version << "..."; - VersionInfo::get(session).modify()->setVersion(++version); - } - } + auto itMigrationFunc{ migrationFunctions.find(version) }; + assert(itMigrationFunc != std::cend(migrationFunctions)); + itMigrationFunc->second(session); + + VersionInfo::get(session).modify()->setVersion(++version); + } + } } diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp index b20c7680..68a70ade 100644 --- a/src/libs/services/database/impl/Migration.hpp +++ b/src/libs/services/database/impl/Migration.hpp @@ -23,33 +23,33 @@ namespace Database { - class Session; + class Session; - using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION {41}; - class VersionInfo - { - public: - using pointer = Wt::Dbo::ptr; + using Version = std::size_t; + static constexpr Version LMS_DATABASE_VERSION{ 42 }; + class VersionInfo + { + public: + using pointer = Wt::Dbo::ptr; - static VersionInfo::pointer getOrCreate(Session& session); - static VersionInfo::pointer get(Session& session); + static VersionInfo::pointer getOrCreate(Session& session); + static VersionInfo::pointer get(Session& session); - Version getVersion() const { return _version; } - void setVersion(Version version) { _version = static_cast(version); } + Version getVersion() const { return _version; } + void setVersion(Version version) { _version = static_cast(version); } - template - void persist(Action& a) - { - Wt::Dbo::field(a, _version, "db_version"); - } + template + void persist(Action& a) + { + Wt::Dbo::field(a, _version, "db_version"); + } - private: - int _version {LMS_DATABASE_VERSION}; - }; + private: + int _version{ LMS_DATABASE_VERSION }; + }; - namespace Migration - { - void doDbMigration(Session& session); - } + namespace Migration + { + void doDbMigration(Session& session); + } } diff --git a/src/libs/services/database/impl/User.cpp b/src/libs/services/database/impl/User.cpp index f7a4d4c0..0661ea69 100644 --- a/src/libs/services/database/impl/User.cpp +++ b/src/libs/services/database/impl/User.cpp @@ -30,73 +30,63 @@ namespace Database { -User::User(std::string_view loginName) -: _loginName {loginName} -{ -} + User::User(std::string_view loginName) + : _loginName{ loginName } + { + } -User::pointer -User::create(Session& session, std::string_view loginName) -{ - return session.getDboSession().add(std::unique_ptr {new User {loginName}}); -} + User::pointer User::create(Session& session, std::string_view loginName) + { + return session.getDboSession().add(std::unique_ptr {new User{ loginName }}); + } -std::size_t -User::getCount(Session& session) -{ - session.checkSharedLocked(); + std::size_t User::getCount(Session& session) + { + session.checkSharedLocked(); - return session.getDboSession().query("SELECT COUNT(*) FROM user"); -} + return session.getDboSession().query("SELECT COUNT(*) FROM user"); + } -RangeResults -User::find(Session& session, const FindParameters& params) -{ - session.checkSharedLocked(); + RangeResults User::find(Session& session, const FindParameters& params) + { + session.checkSharedLocked(); - auto query {session.getDboSession().query("SELECT id FROM user")}; + auto query{ session.getDboSession().query("SELECT id FROM user") }; - if (params.scrobbler) - query.where("scrobbler = ?").bind(*params.scrobbler); + if (params.scrobbler) + query.where("scrobbler = ?").bind(*params.scrobbler); - return Utils::execQuery(query, params.range); -} + return Utils::execQuery(query, params.range); + } -User::pointer -User::findDemoUser(Session& session) -{ - session.checkSharedLocked(); + User::pointer User::findDemoUser(Session& session) + { + session.checkSharedLocked(); - return session.getDboSession().find().where("type = ?").bind(UserType::DEMO).resultValue(); -} + return session.getDboSession().find().where("type = ?").bind(UserType::DEMO).resultValue(); + } -User::pointer -User::find(Session& session, UserId id) -{ - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); -} + User::pointer User::find(Session& session, UserId id) + { + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } -User::pointer -User::find(Session& session, std::string_view name) -{ - return session.getDboSession().find() - .where("login_name = ?").bind(name) - .resultValue(); -} + User::pointer User::find(Session& session, std::string_view name) + { + return session.getDboSession().find() + .where("login_name = ?").bind(name) + .resultValue(); + } -void -User::setSubsonicTranscodeBitrate(Bitrate bitrate) -{ - assert(isAudioBitrateAllowed(bitrate)); - _subsonicTranscodeBitrate = bitrate; -} + void User::setSubsonicDefaultTranscodeBitrate(Bitrate bitrate) + { + assert(isAudioBitrateAllowed(bitrate)); + _subsonicDefaultTranscodeBitrate = bitrate; + } -void -User::clearAuthTokens() -{ - _authTokens.clear(); -} + void User::clearAuthTokens() + { + _authTokens.clear(); + } } // namespace Database - - diff --git a/src/libs/services/database/include/services/database/User.hpp b/src/libs/services/database/include/services/database/User.hpp index eb238af1..5a3bd8ee 100644 --- a/src/libs/services/database/include/services/database/User.hpp +++ b/src/libs/services/database/include/services/database/User.hpp @@ -33,134 +33,128 @@ namespace Database { -class AuthToken; -class Session; + class AuthToken; + class Session; -class User final : public Object -{ - public: - struct PasswordHash - { - std::string salt; - std::string hash; - }; + class User final : public Object + { + public: + struct PasswordHash + { + std::string salt; + std::string hash; + }; - struct FindParameters - { - std::optional scrobbler; - Range range; + struct FindParameters + { + std::optional scrobbler; + Range range; - FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; } - FindParameters& setRange(Range _range) {range = _range; return *this; } - }; + FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; } + FindParameters& setRange(Range _range) { range = _range; return *this; } + }; - static inline const std::size_t MinNameLength {3}; - static inline const std::size_t MaxNameLength {15}; - static inline const bool defaultSubsonicTranscodeEnable {true}; - static inline const AudioFormat defaultSubsonicTranscodeFormat {AudioFormat::OGG_OPUS}; - static inline const Bitrate defaultSubsonicTranscodeBitrate {128000}; - static inline const UITheme defaultUITheme {UITheme::Dark}; - static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists}; - static inline const Scrobbler defaultScrobbler {Scrobbler::Internal}; + static inline const std::size_t MinNameLength{ 3 }; + static inline const std::size_t MaxNameLength{ 15 }; + static inline const AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS }; + static inline const Bitrate defaultSubsonicTranscodeBitrate{ 128000 }; + static inline const UITheme defaultUITheme{ UITheme::Dark }; + static inline const SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists }; + static inline const Scrobbler defaultScrobbler{ Scrobbler::Internal }; - User() = default; + User() = default; - static std::size_t getCount(Session& session); - static pointer find(Session& session, UserId id); - static pointer find(Session& session, std::string_view loginName); - static RangeResults find(Session& session, const FindParameters& params); - static pointer findDemoUser(Session& session); + static std::size_t getCount(Session& session); + static pointer find(Session& session, UserId id); + static pointer find(Session& session, std::string_view loginName); + static RangeResults find(Session& session, const FindParameters& params); + static pointer findDemoUser(Session& session); - // accessors - const std::string& getLoginName() const { return _loginName; } - PasswordHash getPasswordHash() const { return PasswordHash {_passwordSalt, _passwordHash}; } - const Wt::WDateTime& getLastLogin() const { return _lastLogin; } - std::size_t getAuthTokensCount() const { return _authTokens.size(); } + // accessors + const std::string& getLoginName() const { return _loginName; } + PasswordHash getPasswordHash() const { return PasswordHash{ _passwordSalt, _passwordHash }; } + const Wt::WDateTime& getLastLogin() const { return _lastLogin; } + std::size_t getAuthTokensCount() const { return _authTokens.size(); } - // write - void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; } - void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; } - void setType(UserType type) { _type = type; } - void setSubsonicTranscodeEnable(bool value) { _subsonicTranscodeEnable = value; } - void setSubsonicTranscodeFormat(AudioFormat encoding) { _subsonicTranscodeFormat = encoding; } - void setSubsonicTranscodeBitrate(Bitrate bitrate); - void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; } - void setRadio(bool val) { _radio = val; } - void setRepeatAll(bool val) { _repeatAll = val; } - void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; } - void clearAuthTokens(); - void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; } - void setScrobbler(Scrobbler scrobbler) { _scrobbler = scrobbler; } - void setListenBrainzToken(const std::optional& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; } + // write + void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; } + void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; } + void setType(UserType type) { _type = type; } + void setSubsonicDefaultTranscodeFormat(AudioFormat encoding) { _subsonicDefaultTranscodeFormat = encoding; } + void setSubsonicDefaultTranscodeBitrate(Bitrate bitrate); + void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; } + void setRadio(bool val) { _radio = val; } + void setRepeatAll(bool val) { _repeatAll = val; } + void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; } + void clearAuthTokens(); + void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; } + void setScrobbler(Scrobbler scrobbler) { _scrobbler = scrobbler; } + void setListenBrainzToken(const std::optional& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; } - // read - bool isAdmin() const { return _type == UserType::ADMIN; } - bool isDemo() const { return _type == UserType::DEMO; } - UserType getType() const { return _type; } - bool getSubsonicTranscodeEnable() const { return _subsonicTranscodeEnable; } - AudioFormat getSubsonicTranscodeFormat() const { return _subsonicTranscodeFormat; } - Bitrate getSubsonicTranscodeBitrate() const { return _subsonicTranscodeBitrate; } - std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; } - bool isRepeatAllSet() const { return _repeatAll; } - bool isRadioSet() const { return _radio; } - UITheme getUITheme() const { return _uiTheme; } - SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; } - Scrobbler getScrobbler() const { return _scrobbler; } - std::optional getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); } + // read + bool isAdmin() const { return _type == UserType::ADMIN; } + bool isDemo() const { return _type == UserType::DEMO; } + UserType getType() const { return _type; } + AudioFormat getSubsonicDefaultTranscodeFormat() const { return _subsonicDefaultTranscodeFormat; } + Bitrate getSubsonicDefaultTranscodeBitrate() const { return _subsonicDefaultTranscodeBitrate; } + std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; } + bool isRepeatAllSet() const { return _repeatAll; } + bool isRadioSet() const { return _radio; } + UITheme getUITheme() const { return _uiTheme; } + SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; } + Scrobbler getScrobbler() const { return _scrobbler; } + std::optional getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); } - template - void persist(Action& a) - { - Wt::Dbo::field(a, _type, "type"); - Wt::Dbo::field(a, _loginName, "login_name"); - Wt::Dbo::field(a, _passwordSalt, "password_salt"); - Wt::Dbo::field(a, _passwordHash, "password_hash"); - Wt::Dbo::field(a, _lastLogin, "last_login"); - Wt::Dbo::field(a, _subsonicTranscodeEnable, "subsonic_transcode_enable"); - Wt::Dbo::field(a, _subsonicTranscodeFormat, "subsonic_transcode_format"); - Wt::Dbo::field(a, _subsonicTranscodeBitrate, "subsonic_transcode_bitrate"); - Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode"); - Wt::Dbo::field(a, _uiTheme, "ui_theme"); - Wt::Dbo::field(a, _scrobbler, "scrobbler"); - Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token"); + template + void persist(Action& a) + { + Wt::Dbo::field(a, _type, "type"); + Wt::Dbo::field(a, _loginName, "login_name"); + Wt::Dbo::field(a, _passwordSalt, "password_salt"); + Wt::Dbo::field(a, _passwordHash, "password_hash"); + Wt::Dbo::field(a, _lastLogin, "last_login"); + Wt::Dbo::field(a, _subsonicDefaultTranscodeFormat, "subsonic_default_transcode_format"); + Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate"); + Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode"); + Wt::Dbo::field(a, _uiTheme, "ui_theme"); + Wt::Dbo::field(a, _scrobbler, "scrobbler"); + Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token"); - // UI player settings - Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos"); - Wt::Dbo::field(a, _repeatAll, "repeat_all"); - Wt::Dbo::field(a, _radio, "radio"); + // UI player settings + Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos"); + Wt::Dbo::field(a, _repeatAll, "repeat_all"); + Wt::Dbo::field(a, _radio, "radio"); - Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user"); - } + Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user"); + } - private: - friend class Session; - User(std::string_view loginName); - static pointer create(Session& session, std::string_view loginName); + private: + friend class Session; + User(std::string_view loginName); + static pointer create(Session& session, std::string_view loginName); - std::string _loginName; - std::string _passwordSalt; - std::string _passwordHash; - Wt::WDateTime _lastLogin; - UITheme _uiTheme {defaultUITheme}; - Scrobbler _scrobbler {defaultScrobbler}; - std::string _listenbrainzToken; // Musicbrainz Identifier + std::string _loginName; + std::string _passwordSalt; + std::string _passwordHash; + Wt::WDateTime _lastLogin; + UITheme _uiTheme{ defaultUITheme }; + Scrobbler _scrobbler{ defaultScrobbler }; + std::string _listenbrainzToken; // Musicbrainz Identifier - // Admin defined settings - UserType _type {UserType::REGULAR}; + // Admin defined settings + UserType _type{ UserType::REGULAR }; - // User defined settings - SubsonicArtistListMode _subsonicArtistListMode {defaultSubsonicArtistListMode}; - bool _subsonicTranscodeEnable {defaultSubsonicTranscodeEnable}; - AudioFormat _subsonicTranscodeFormat {defaultSubsonicTranscodeFormat}; - int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate}; + // User defined settings + SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode }; + AudioFormat _subsonicDefaultTranscodeFormat{ defaultSubsonicTranscodeFormat }; + int _subsonicDefaultTranscodeBitrate{ defaultSubsonicTranscodeBitrate }; - // User's dynamic data (UI) - int _curPlayingTrackPos {}; // Current track position in queue - bool _repeatAll {}; - bool _radio {}; + // User's dynamic data (UI) + int _curPlayingTrackPos{}; // Current track position in queue + bool _repeatAll{}; + bool _radio{}; - Wt::Dbo::collection> _authTokens; -}; + Wt::Dbo::collection> _authTokens; + }; } // namespace Databas' - diff --git a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp index d3343106..dcf50850 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp @@ -30,25 +30,40 @@ #include "utils/Logger.hpp" #include "utils/FileResourceHandlerCreator.hpp" #include "utils/Utils.hpp" +#include "utils/String.hpp" #include "ParameterParsing.hpp" #include "SubsonicId.hpp" -using namespace Database; - namespace API::Subsonic { + using namespace Database; + namespace { + std::optional subsonicStreamFormatToAvFormat(std::string_view format) + { + for (const auto& [str, avFormat] : std::initializer_list>{ + {"mp3", Av::Format::MP3}, + {"opus", Av::Format::OGG_OPUS}, + {"vorbis", Av::Format::OGG_VORBIS}, + }) + { + if (StringUtils::stringCaseInsensitiveEqual("str", format)) + return avFormat; + } + return std::nullopt; + } + Av::Format userTranscodeFormatToAvFormat(AudioFormat format) { switch (format) { - case AudioFormat::MP3: return Av::Format::MP3; - case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; - case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; - case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; - case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS; - default: return Av::Format::OGG_OPUS; + case Database::AudioFormat::MP3: return Av::Format::MP3; + case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; + case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; + case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; + case Database::AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS; } + return Av::Format::OGG_OPUS; } struct StreamParameters @@ -64,9 +79,9 @@ namespace API::Subsonic const TrackId id{ getMandatoryParameterAs(context.parameters, "id") }; // Optional params - std::optional maxBitRate{ getParameterAs(context.parameters, "maxBitRate") }; - const std::optional format{ getParameterAs(context.parameters, "format") }; - const std::optional timeOffset{ getParameterAs(context.parameters, "timeOffset") }; + std::size_t maxBitRate{ getParameterAs(context.parameters, "maxBitRate").value_or(0) }; // "If set to zero, no limit is imposed" + const std::string format{ getParameterAs(context.parameters, "format").value_or("") }; + std::size_t timeOffset{ getParameterAs(context.parameters, "timeOffset").value_or(0) }; bool estimateContentLength{ getParameterAs(context.parameters, "estimateContentLength").value_or(false) }; StreamParameters parameters; @@ -84,30 +99,25 @@ namespace API::Subsonic parameters.inputFileParameters.duration = track->getDuration(); } + if (format != "raw") // raw => no transcode { const User::pointer user{ User::find(context.dbSession, context.userId) }; if (!user) throw UserNotAuthorizedError{}; - // format = "raw" => no transcode. Other format values will be ignored - const bool transcode{ (!format || (*format != "raw")) && user->getSubsonicTranscodeEnable() }; - if (transcode) - { - std::size_t bitRate{ user->getSubsonicTranscodeBitrate() / 1000 }; + Av::TranscodeParameters& transcodeParameters{ parameters.transcodeParameters.emplace() }; - // "If set to zero, no limit is imposed" - if (maxBitRate && *maxBitRate != 0) - bitRate = Utils::clamp(*maxBitRate, std::size_t{ 48 }, bitRate); + transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) + transcodeParameters.offset = std::chrono::seconds{ timeOffset }; - Av::TranscodeParameters transcodeParameters; + if (std::optional requestedFormat{ subsonicStreamFormatToAvFormat(format) }) + transcodeParameters.format = *requestedFormat; + else + transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodeFormat()); - transcodeParameters.bitrate = bitRate * 1000; - transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat()); - transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) - transcodeParameters.offset = std::chrono::seconds{ timeOffset ? *timeOffset : 0 }; - - parameters.transcodeParameters = std::move(transcodeParameters); - } + transcodeParameters.bitrate = user->getSubsonicDefaultTranscodeBitrate(); + if (maxBitRate != 0) + transcodeParameters.bitrate = Utils::clamp(transcodeParameters.bitrate, std::size_t{ 48 }, maxBitRate); } return parameters; diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 9a7c6653..52705075 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -120,8 +120,7 @@ namespace API::Subsonic trackResponse.setAttribute("suffix", extension.string().substr(1)); } - if (user->getSubsonicTranscodeEnable()) - trackResponse.setAttribute("transcodedSuffix", formatToSuffix(user->getSubsonicTranscodeFormat())); + trackResponse.setAttribute("transcodedSuffix", formatToSuffix(user->getSubsonicDefaultTranscodeFormat())); trackResponse.setAttribute("coverArt", idToString(track->getId())); diff --git a/src/libs/utils/impl/String.cpp b/src/libs/utils/impl/String.cpp index ff7f64ff..cae5c406 100644 --- a/src/libs/utils/impl/String.cpp +++ b/src/libs/utils/impl/String.cpp @@ -177,6 +177,20 @@ namespace StringUtils return oss.str(); } + bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB) + { + if (strA.size() != strB.size()) + return false; + + for (std::size_t i{}; i < strA.size(); ++i) + { + if (std::tolower(strA[i]) != std::tolower(strB[i])) + return false; + } + + return true; + } + void capitalize(std::string& str) { for (auto it{ std::begin(str) }; it != std::end(str); ++it) diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index 670ea89a..70addee0 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -55,6 +55,8 @@ namespace StringUtils { [[nodiscard]] std::string bufferToString(const std::vector& data); + [[nodiscard]] bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB); + void capitalize(std::string& str); template diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 089e9367..cda71da2 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -160,15 +160,13 @@ class SettingsModel : public Wt::WFormModel } { - user.modify()->setSubsonicTranscodeEnable(Wt::asNumber(value(SubsonicTranscodeEnableField))); - auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(SubsonicTranscodeBitrateField))}; if (subsonicTranscodeBitrateRow) - user.modify()->setSubsonicTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow)); + user.modify()->setSubsonicDefaultTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow)); auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(SubsonicTranscodeFormatField))}; if (subsonicTranscodeFormatRow) - user.modify()->setSubsonicTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow)); + user.modify()->setSubsonicDefaultTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow)); auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))}; if (subsonicArtistListModeRow) @@ -224,19 +222,12 @@ class SettingsModel : public Wt::WFormModel setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo); } - setValue(SubsonicTranscodeEnableField, LmsApp->getUser()->getSubsonicTranscodeEnable()); { - const bool usesTranscode {LmsApp->getUser()->getSubsonicTranscodeEnable()}; - setReadOnly(SubsonicTranscodeFormatField, !usesTranscode); - setReadOnly(SubsonicTranscodeBitrateField, !usesTranscode); - } - - { - auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromValue(user->getSubsonicTranscodeBitrate())}; + auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodeBitrate())}; if (subsonicTranscodeBitrateRow) setValue(SubsonicTranscodeBitrateField, _transcodeBitrateModel->getString(*subsonicTranscodeBitrateRow)); - auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromValue(user->getSubsonicTranscodeFormat())}; + auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodeFormat())}; if (subsonicTranscodeFormatRow) setValue(SubsonicTranscodeFormatField, _transcodeFormatModel->getString(*subsonicTranscodeFormatRow)); @@ -482,11 +473,6 @@ SettingsView::refreshView() { t->setCondition("if-has-subsonic-api", Service::get()->getBool("api-subsonic", true)); - // Transcode - auto transcode {std::make_unique()}; - auto* transcodeRaw {transcode.get()}; - t->setFormWidget(SettingsModel::SubsonicTranscodeEnableField, std::move(transcode)); - // Format auto transcodeFormat {std::make_unique()}; transcodeFormat->setModel(model->getTranscodeFormatModel()); @@ -501,15 +487,6 @@ SettingsView::refreshView() auto artistListMode {std::make_unique()}; artistListMode->setModel(model->getSubsonicArtistListModeModel()); t->setFormWidget(SettingsModel::SubsonicArtistListModeField, std::move(artistListMode)); - - transcodeRaw->changed().connect([=] - { - const bool enable {transcodeRaw->checkState() == Wt::CheckState::Checked}; - model->setReadOnly(SettingsModel::SubsonicTranscodeFormatField, !enable); - model->setReadOnly(SettingsModel::SubsonicTranscodeBitrateField, !enable); - t->updateModel(model.get()); - t->updateView(model.get()); - }); } // Scrobbling