diff --git a/.clang-tidy b/.clang-tidy index 1eaf7039..aca1b6d3 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,7 +2,6 @@ Checks: '-*,bugprone-*,concurrency-*,cppcoreguidelines-*,misc-*,-misc-include-cleaner,-misc-no-recursion,performance*,portability-*,readability-*,-readability-braces-around-statements,-readability-identifier-length,-readability-implicit-bool-conversion' WarningsAsErrors: '' HeaderFilterRegex: '' -AnalyzeTemporaryDtors: false FormatStyle: none User: '' CheckOptions: diff --git a/Dockerfile-release b/Dockerfile-release index 4dc15e78..8dd54d03 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -2,7 +2,7 @@ FROM alpine:3.19 AS build WORKDIR /tmp/workdir -ARG PREFIX="/tmp/install" +ENV PREFIX="/tmp/install" ARG BUILD_PACKAGES=" \ ca-certificates \ @@ -118,7 +118,7 @@ COPY . /tmp/lms/ RUN \ DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \ PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_PREFIX_PATH=${PREFIX} && \ - LD_LIBRARY_PATH=${PREFIX}/lib VERBOSE=1 make -j$(nproc) && \ + LD_LIBRARY_PATH=${PREFIX}/lib make -j$(nproc) && \ LD_LIBRARY_PATH=${PREFIX}/lib make test && \ make install && \ mkdir -p ${PREFIX}/etc/ && \ diff --git a/SUBSONIC.md b/SUBSONIC.md index a4bcefb6..9948280e 100644 --- a/SUBSONIC.md +++ b/SUBSONIC.md @@ -1,16 +1,24 @@ # Subsonic API -The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_, _Symfonium_, and _DSub_. +The API version implemented is 1.16.0 and has been tested on _Android_ using _DSub_, _Subsonic Player_, _Symfonium_, _Tempo_ and _Ultrasonic_. Folder navigation commands are supported. However, since _LMS_ does not store information for each folder, it is not possible to star/unstar folders considered as artists. Given the API limitations of folder navigation commands, it is recommended to place all tracks of an album in the same folder and not to mix multiple albums in the same folder. The Subsonic API is enabled by default. -__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method. Since logins/passwords are passed in plain text through URLs, it is highly recommended to use a unique password when using the Subsonic API. Note that this may affect the use of authentication via PAM. In any case, ensure the web server logs (and proxy logs, if applicable) are properly secured. - # OpenSubsonic API OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'll find more details in the [official documentation](https://opensubsonic.netlify.app/) +## Authentication +_LMS_ supports the [API Key Authentication](https://opensubsonic.netlify.app/docs/extensions/apikeyauth/) method. Each user has to generate their own API key on the settings page to use the Subsonic API. + +By default, API keys can also be used as passwords, provided the `user` parameter matches the API key owner. To disable this fallback authentication method, set the following in `lms.conf`: +``` +api-subsonic-support-user-password-auth = false; +``` + +__Note__: the token+salt authentication method is not supported. + ## Extra fields The following extra fields are implemented: * `Album` response: @@ -50,5 +58,7 @@ The following extra fields are implemented: * `roles` ## Supported extensions +* [API Key Authentication](https://opensubsonic.netlify.app/docs/extensions/apikeyauth/) +* [HTTP form POST](https://opensubsonic.netlify.app/docs/extensions/formpost/) * [Transcode offset](https://opensubsonic.netlify.app/docs/extensions/transcodeoffset/) * [Song Lyrics](https://opensubsonic.netlify.app/docs/extensions/songlyrics/) diff --git a/approot/messages.xml b/approot/messages.xml index e34e36cf..ffee2275 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -289,7 +289,7 @@ Method to sort artist's albums Audio -These audio settings are local to your browser! +These audio settings are local to your browser Internal ListenBrainz ListenBrainz API token @@ -307,6 +307,7 @@ Original Release Date (Descending) Bad password Old password must be filled in +Generate ReplayGain mode No ReplayGain Auto @@ -317,13 +318,19 @@ Scrobbling Services Settings +New settings saved! Artist list mode All artists Album artists Track artists Subsonic API -User Interface +OpenSubsonic API key +Use this API key as the password for clients that do not support the 'API Key Authentication' extension Transcoding +Enable transcoding +Always +Only when format is not supported by the browser +Never Transcoding output bitrate Transcoding output format Matroska/Opus @@ -331,10 +338,6 @@ Ogg/Opus Ogg/Vorbis WebM/Vorbis -Enable transcoding -Always -Only when format is not supported by the browser -Never -New settings saved! +User Interface diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 99291866..4a7e576c 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -289,7 +289,7 @@ Méthode pour trier les albums de l'artiste Audio -Ces paramètres audio sont locaux à votre navigateur ! +Ces paramètres audio sont locaux à votre navigateur Interne ListenBrainz Jeton d'API ListenBrainz @@ -307,6 +307,7 @@ Date de sortie originale (Décroissant) Mauvais mot de passe L'ancien mot de passe doit être renseigné +Regénérer Mode ReplayGain Pas de ReplayGain Automatique @@ -317,13 +318,19 @@ Scrobbling Services Paramètres +Paramètres sauvegardés ! Mode de listage des artistes Tous les artistes Tous les artistes d'album Tous les artistes de piste API Subsonic -Interface utilisateur +Clef pour l'API OpenSubsonic +Utilisez cette clé d'API comme mot de passe pour les clients qui ne prennent pas en charge l'extension 'API Key Authentication' Transcodage +Appliquer le transcodage +Toujours +Seulement lorsque le format n'est pas pris en charge par votre navigateur +Jamais Bitrate du transcodage Format du transcodage Matroska/Opus @@ -331,11 +338,7 @@ Ogg/Opus Ogg/Vorbis WebM/Vorbis -Appliquer le transcodage -Toujours -Seulement lorsque le format n'est pas pris en charge par votre navigateur -Jamais -Paramètres sauvegardés ! +Interface utilisateur moins d'une seconde diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 749ca015..478c0dc4 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -46,7 +46,7 @@ C'è stato un errore! Ritorna alla home Album non trovato - +Playlist non trovata Non sei autorizzato ad eseguire questa operazione Utente non trovato @@ -289,7 +289,7 @@ Metodo per ordinare gli album dell'artista Audio -Queste impostazioni sono salvate localmente su questo dispositivo! +Queste impostazioni sono salvate localmente su questo dispositivo Interno ListenBrainz Token API ListenBrainz @@ -307,6 +307,7 @@ Data di uscita originale (Decrescente) Password errata Devi inserire la password attuale +Genera Modalità ReplayGain No ReplayGain Automatica @@ -317,13 +318,19 @@ Scrobbling Servizi Impostazioni +Nuove impostazioni salvate! Modalità di elencazione artisti Tutti gli artisti Artisti album Artisti tracce -Subsonic API -Interfaccia utente +API Subsonic +Chiave API OpenSubsonic +Utilizza questa chiave API come password per i client che non supportano l'estensione 'Autenticazione con chiave API' Transcodifica +Abilita transcodifica +Sempre +Quando il formato non è supportato +Mai Bitrate transcodifica Formato di transcodifica Matroska/Opus @@ -331,11 +338,7 @@ Ogg/Opus Ogg/Vorbis WebM/Vorbis -Abilita transcodifica -Sempre -Quando il formato non è supportato -Mai -Nuove impostazioni salvate! +Interfaccia utente meno di un secondo diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index ae9bf9c4..b0945b64 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -315,7 +315,7 @@ Metoda sortowania albumów artysty Dźwięk -Te ustawienia dotyczą wyłącznie tej przeglądarki! +Te ustawienia dotyczą wyłącznie tej przeglądarki Wewnętrzny ListenBrainz ListenBrainz API token @@ -333,6 +333,7 @@ Oryginalna data wydania (Malejąco) Błędne hasło Należy podać stare hasło +Generuj Tryb ReplayGain Bez ReplayGain Auto @@ -343,13 +344,19 @@ Scrobbling Usługi Ustawienia +Ustawienia zapisane! Tryb listy artystów Wszyscy artyści Artyści z albumów Artyści ze ścieżek -Subsonic API -Interfejs użytkownika +API Subsonic +Klucz API OpenSubsonic +Użyj tego klucza API jako hasła dla klientów, którzy nie obsługują rozszerzenia 'Uwierzytelnianie za pomocą klucza API' Transkodowanie +Aktywuj transkodowanie +Zawsze +Tylko gdy przeglądarka nie wspiera formatu +Nigdy Przepływność transkodowania Format wyjściowy transkodowania Matroska/Opus @@ -357,11 +364,7 @@ Ogg/Opus Ogg/Vorbis WebM/Vorbis -Aktywuj transkodowanie -Zawsze -Tylko gdy przeglądarka nie wspiera formatu -Nigdy -Ustawienia zapisane! +Interfejs użytkownika mniej niż sekunda diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 7b44aa1e..1a127517 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -289,7 +289,7 @@ 音频 -这些音频设置仅保存在您的本地浏览器! +这些音频设置仅保存在您的本地浏览器 @@ -307,6 +307,7 @@ 无效密码 必须填写旧密码 + ReplayGain 模式 没有 ReplayGain 自动模式 @@ -317,13 +318,19 @@ +新设置已保存! 歌手列表模式 所有歌手 专辑歌手 音轨歌手 Subsonic 应用程序接口 + 转码 +启动转码 +总是 +仅当格式不支持时使用 +从不 转码比特率 转码格式 Matroska/Opus @@ -331,11 +338,6 @@ Ogg/Opus Ogg/Vorbis WebM/Vorbis -启动转码 -总是 -仅当格式不支持时使用 -从不 -新设置已保存! diff --git a/approot/misc.xml b/approot/misc.xml index 528c6685..78ebf5b2 100644 --- a/approot/misc.xml +++ b/approot/misc.xml @@ -19,5 +19,6 @@ + diff --git a/approot/settings.xml b/approot/settings.xml index 1ee166e7..e839a8e1 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -85,6 +85,25 @@ ${} ${tr:Lms.Settings.subsonic-api} +
+ +
+ ${subsonic-token-regen-btn class="btn btn-outline-secondary"} + ${subsonic-token-del-btn class="btn btn-outline-warning"} + ${subsonic-token class="form-control"} + ${subsonic-token-visibility-btn class="btn btn-outline-secondary"} +
+ ${subsonic-token-info class="help-block"} +
+
+
${subsonic-enable-transcoding-by-default class="form-check-input"} @@ -150,9 +169,12 @@ - ${listenbrainz-token class="form-control"} -
- ${listenbrainz-token-info class="help-block"} +
+ ${listenbrainz-token class="form-control"} + ${listenbrainz-token-visibility-btn class="btn btn-outline-secondary"} +
+ ${listenbrainz-token-info class="help-block"} +
${} diff --git a/conf/lms.conf b/conf/lms.conf index d3d7675b..56822c3f 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -60,6 +60,9 @@ acousticbrainz-api-base-url = "https://acousticbrainz.org"; # Authentication # Available backends: "internal", "PAM", "http-headers" authentication-backend = "internal"; +# The number of bcrypt rounds to be used when backend is set to "internal". The higher the more secure +internal-password-bcrypt-round = 12; +# The header to be used to read the authentication user when backend is set to "http-headers" http-headers-login-field = "X-Forwarded-User"; # Max entries in the login throttler (1 entry per IP address. For IPv6, the whole /64 block is used) @@ -68,6 +71,10 @@ login-throttler-max-entries = 10000; # API api-subsonic = true; +# Enable or disable user/password authentication for the Subsonic API. +# Note: Since token/salt authentication is always disabled, setting this to 'false' means only API keys can be used to access the Subsonic API. +api-subsonic-support-user-password-auth = true; + # Use this list to make the reported server version to 1.12.0 depending on the client's name # Main usage is to make auto detections for the 'p' (password) parameter work api-subsonic-old-server-protocol-clients = ("DSub"); @@ -107,7 +114,7 @@ tracing-buffer-size = 16; # Set to true if you want to hide duplicate tracks scanner-skip-duplicate-mbid = false; -# Scanner read style for metadata, maybe be 'fast', 'average' or 'accurate' +# Scanner read style for metadata, may be 'fast', 'average' or 'accurate' scanner-parser-read-style = "average"; # Number of threads to use for scanning file metadata (0 means number of logical CPUs / 2) diff --git a/src/libs/core/include/core/Service.hpp b/src/libs/core/include/core/Service.hpp index b6603fc2..517665fb 100644 --- a/src/libs/core/include/core/Service.hpp +++ b/src/libs/core/include/core/Service.hpp @@ -24,7 +24,8 @@ namespace lms::core { - template + // Tag can be used if you have multiple services sharing the same interface + template class Service { public: @@ -46,12 +47,12 @@ namespace lms::core Class* operator->() const { - return Service::get(); + return Service::get(); } Class& operator*() const { - return *Service::get(); + return *Service::get(); } static Class* get() { return _service.get(); } diff --git a/src/libs/core/test/CMakeLists.txt b/src/libs/core/test/CMakeLists.txt index a063da46..7b6aced5 100644 --- a/src/libs/core/test/CMakeLists.txt +++ b/src/libs/core/test/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(test-core LiteralString.cpp Path.cpp RecursiveSharedMutex.cpp + Service.cpp String.cpp TraceLogger.cpp Utils.cpp diff --git a/src/libs/core/test/Path.cpp b/src/libs/core/test/Path.cpp index 56cf530e..1e525b06 100644 --- a/src/libs/core/test/Path.cpp +++ b/src/libs/core/test/Path.cpp @@ -93,6 +93,8 @@ namespace lms::core::pathUtils::tests { "/root/", "/root/", true }, { "/folder/file.txt", "/root", false }, { "/folder/file.txt", "/root/", false }, + { "/file.txt", "/root", false }, + { "/file.txt", "/root/", false }, { "", "/root", false }, }; diff --git a/src/libs/core/test/Service.cpp b/src/libs/core/test/Service.cpp new file mode 100644 index 00000000..0f648f5a --- /dev/null +++ b/src/libs/core/test/Service.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include + +#include "core/Service.hpp" + +namespace lms::core::tests +{ + class IMyService + { + }; + + class MyService : public IMyService + { + }; + + class MyOtherService : public IMyService + { + }; + + class MyServiceTag + { + }; + class MyOtherServiceTag + { + }; + + TEST(Service, ctr) + { + EXPECT_FALSE(Service().exists()); + EXPECT_EQ(Service().get(), nullptr); + + Service myService{ std::make_unique() }; + + EXPECT_TRUE(Service().exists()); + EXPECT_EQ(Service().get(), myService.get()); + } + + TEST(Service, tags) + { + Service myService{ std::make_unique() }; + Service myOtherService{ std::make_unique() }; + + EXPECT_FALSE(Service().exists()); + EXPECT_EQ(Service().get(), nullptr); + + EXPECT_TRUE((Service().exists())); + EXPECT_TRUE((Service().exists())); + EXPECT_EQ((Service().get()), myService.get()); + EXPECT_EQ((Service().get()), myOtherService.get()); + } +} // namespace lms::core::tests \ No newline at end of file diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index 3b2bf1d2..af4a6a1b 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(lmsdatabase SHARED impl/Listen.cpp impl/MediaLibrary.cpp impl/Migration.cpp + impl/PlayQueue.cpp impl/TrackArtistLink.cpp impl/TrackFeatures.cpp impl/TrackList.cpp diff --git a/src/libs/database/impl/AuthToken.cpp b/src/libs/database/impl/AuthToken.cpp index 5dea83c0..d963c653 100644 --- a/src/libs/database/impl/AuthToken.cpp +++ b/src/libs/database/impl/AuthToken.cpp @@ -30,29 +30,65 @@ namespace lms::db { - AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) - : _value{ value } + AuthToken::AuthToken(std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional maxUseCount, ObjectPtr user) + : _domain{ domain } + , _value{ value } , _expiry{ expiry } + , _maxUseCount{ maxUseCount } , _user{ getDboPtr(user) } { } - AuthToken::pointer AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) + AuthToken::pointer AuthToken::create(Session& session, std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional maxUseCount, ObjectPtr user) { - return session.getDboSession()->add(std::unique_ptr{ new AuthToken{ value, expiry, user } }); + return session.getDboSession()->add(std::unique_ptr{ new AuthToken{ domain, value, expiry, maxUseCount, user } }); } - void AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now) - { - session.checkWriteTransaction(); - - utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE expiry < ?", now); - } - - AuthToken::pointer AuthToken::find(Session& session, std::string_view value) + std::size_t AuthToken::getCount(Session& session) { session.checkReadTransaction(); - return utils::fetchQuerySingleResult(session.getDboSession()->find().where("value = ?").bind(value)); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM auth_token")); + } + + AuthToken::pointer AuthToken::find(Session& session, AuthTokenId id) + { + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a_t from auth_token a_t").where("a_t.id = ?").bind(id)); + } + + AuthToken::pointer AuthToken::find(Session& session, std::string_view domain, std::string_view value) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->find() }; + query.where("domain = ?").bind(domain); + query.where("value = ?").bind(value); + + return utils::fetchQuerySingleResult(query); + } + + void AuthToken::find(Session& session, std::string_view domain, UserId userId, std::function visitor) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->find() }; + query.where("domain = ?").bind(domain); + query.where("user_id = ?").bind(userId); + + utils::forEachQueryResult(query, visitor); + } + + void AuthToken::removeExpiredTokens(Session& session, std::string_view domain, const Wt::WDateTime& now) + { + session.checkWriteTransaction(); + + utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE expiry < ? AND domain = ?", now, domain); + } + + void AuthToken::clearUserTokens(Session& session, std::string_view domain, UserId user) + { + session.checkWriteTransaction(); + + utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE user_id = ? AND domain = ?", user, domain); } } // namespace lms::db diff --git a/src/libs/database/impl/MediaLibrary.cpp b/src/libs/database/impl/MediaLibrary.cpp index 59655bc1..a01b291c 100644 --- a/src/libs/database/impl/MediaLibrary.cpp +++ b/src/libs/database/impl/MediaLibrary.cpp @@ -30,15 +30,15 @@ namespace lms::db { - MediaLibrary::MediaLibrary(const std::filesystem::path& p, std::string_view name) - : _path{ p } - , _name{ std::string{ name, 0, maxNameLength } } + MediaLibrary::MediaLibrary(std::string_view name, const std::filesystem::path& p) + : _name{ std::string{ name, 0, maxNameLength } } { + setPath(p); } - MediaLibrary::pointer MediaLibrary::create(Session& session, const std::filesystem::path& p, std::string_view name) + MediaLibrary::pointer MediaLibrary::create(Session& session, std::string_view name, const std::filesystem::path& p) { - return session.getDboSession()->add(std::unique_ptr{ new MediaLibrary{ p, name } }); + return session.getDboSession()->add(std::unique_ptr{ new MediaLibrary{ name, p } }); } std::size_t MediaLibrary::getCount(Session& session) @@ -77,4 +77,13 @@ namespace lms::db func(mediaLibrary); }); } + + void MediaLibrary::setPath(const std::filesystem::path& p) + { + assert(p.is_absolute()); + if (!p.has_filename() && p.has_parent_path()) + _path = p.parent_path(); + else + _path = p; + } } // namespace lms::db diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index d1a0a1ce..44378136 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -35,7 +35,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 71 }; + static constexpr Version LMS_DATABASE_VERSION{ 76 }; } VersionInfo::VersionInfo() @@ -907,6 +907,67 @@ SELECT utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1"); } + void migrateFromV71(Session& session) + { + // Add a file name/stem in tracks + utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "playqueue" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "current_index" integer not null, + "current_position_in_track" integer, + "last_modified_date_time" text, + "user_id" bigint, + constraint "fk_playqueue_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred +))"); + + utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "playqueue_track" ( + "playqueue_id" bigint, + "track_id" bigint not null, + primary key ("playqueue_id", "track_id"), + constraint "fk_playqueue_track_key1" foreign key ("playqueue_id") references "playqueue" ("id") on delete cascade deferrable initially deferred, + constraint "fk_playqueue_track_key2" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred +))"); + utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "playqueue_track_playqueue" on "playqueue_track" ("playqueue_id"))"); + utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "playqueue_track_track" on "playqueue_track" ("track_id"))"); + } + + void migrateFromV72(Session& session) + { + // Add catalog number + utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD barcode TEXT NOT NULL DEFAULT ''"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1"); + } + + void migrateFromV73(Session& session) + { + // Remove any trailing '/' in library paths + utils::executeCommand(*session.getDboSession(), "UPDATE media_library SET path = rtrim(path, '/') WHERE path LIKE '%/'"); + } + + void migrateFromV74(Session& session) + { + // New auth token authentication for Subsonic API + // Previous tokens are not usable any more, no problem since they are just used for the ui's "remember me" feature + utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token"); + utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD domain TEXT NOT NULL"); + utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD use_count INTEGER NOT NULL"); + utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD last_used TEXT"); + utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD max_use_count INTEGER"); + + utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_user_idx"); + utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_expiry_idx"); + utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_value_idx"); + } + + void migrateFromV75(Session& session) + { + // Added a new option to set the bcrypt count to be use to hash user's passwords + utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD bcrypt_round_count INTEGER NOT NULL DEFAULT(7)"); + } + bool doDbMigration(Session& session) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -954,6 +1015,11 @@ SELECT { 68, migrateFromV68 }, { 69, migrateFromV69 }, { 70, migrateFromV70 }, + { 71, migrateFromV71 }, + { 72, migrateFromV72 }, + { 73, migrateFromV73 }, + { 74, migrateFromV74 }, + { 75, migrateFromV75 }, }; bool migrationPerformed{}; @@ -985,7 +1051,9 @@ SELECT LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "..."); auto itMigrationFunc{ migrationFunctions.find(version) }; - assert(itMigrationFunc != std::cend(migrationFunctions)); + if (itMigrationFunc == std::cend(migrationFunctions)) + throw core::LmsException{ "No code found to upgrade database!" }; + itMigrationFunc->second(session); VersionInfo::get(session).modify()->setVersion(++version); diff --git a/src/libs/database/impl/PlayQueue.cpp b/src/libs/database/impl/PlayQueue.cpp new file mode 100644 index 00000000..e08efbab --- /dev/null +++ b/src/libs/database/impl/PlayQueue.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "database/PlayQueue.hpp" + +#include + +#include "database/Directory.hpp" +#include "database/MediaLibrary.hpp" +#include "database/Release.hpp" +#include "database/Session.hpp" +#include "database/Track.hpp" +#include "database/User.hpp" + +#include "IdTypeTraits.hpp" +#include "StringViewTraits.hpp" +#include "Utils.hpp" + +namespace lms::db +{ + PlayQueue::PlayQueue(const ObjectPtr& user, std::string_view name) + : _name{ name } + , _user{ getDboPtr(user) } + { + } + + PlayQueue::pointer PlayQueue::create(Session& session, const ObjectPtr& user, std::string_view name) + { + return session.getDboSession()->add(std::unique_ptr{ new PlayQueue{ user, name } }); + } + + std::size_t PlayQueue::getCount(Session& session) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM playqueue")); + } + + PlayQueue::pointer PlayQueue::find(Session& session, PlayQueueId id) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT p from playqueue p").where("p.id = ?").bind(id)); + } + + PlayQueue::pointer PlayQueue::find(Session& session, UserId userId, std::string_view name) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->query>("SELECT p from playqueue p") }; + query.where("p.user_id = ?").bind(userId); + query.where("p.name = ?").bind(name); + + return utils::fetchQuerySingleResult(query); + } + + void PlayQueue::clear() + { + _tracks.clear(); + } + + void PlayQueue::addTrack(const ObjectPtr& track) + { + _tracks.insert(getDboPtr(track)); + } + + Track::pointer PlayQueue::getTrackAtCurrentIndex() const + { + auto query{ _tracks.find() }; + query.offset(_currentIndex); + query.limit(1); + + return utils::fetchQuerySingleResult(query); + } + + void PlayQueue::visitTracks(const std::function& track)>& visitor) const + { + utils::forEachQueryResult(_tracks.find(), visitor); + } + +} // namespace lms::db diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 23f6d637..d03389e2 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -30,6 +30,7 @@ #include "database/Image.hpp" #include "database/Listen.hpp" #include "database/MediaLibrary.hpp" +#include "database/PlayQueue.hpp" #include "database/RatedArtist.hpp" #include "database/RatedRelease.hpp" #include "database/RatedTrack.hpp" @@ -104,6 +105,7 @@ namespace lms::db _session.mapClass