diff --git a/approot/mediaplayer.xml b/approot/mediaplayer.xml index f7b70da6..46f1078f 100644 --- a/approot/mediaplayer.xml +++ b/approot/mediaplayer.xml @@ -35,7 +35,7 @@ diff --git a/approot/messages.xml b/approot/messages.xml index 36d4bedd..751053d9 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -68,7 +68,7 @@ Scanned {1} files in {2} on {3} ({4} errors, {5} duplicates) No audio track Duplicated file hash -Duplicated MBID +Duplicated recording MBID Scan now Scanner Status diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index bf3ba440..808cebb4 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -68,7 +68,7 @@ {1} fichiers scannés en {2} le {3} ({4} erreurs, {5} duplicatas) Pas de piste audio Hash dupliqué -MBID dupliqué +Recording MBID dupliqué Lancer un scan Scanner Statut diff --git a/approot/messages_it.xml b/approot/messages_it.xml index f285e9dc..c3a3b895 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -67,7 +67,7 @@ Scansionati {1} files in {2} su {3} ({4} errori, {5} duplicati) Nessuna traccia audio Hash doppio -MBID doppio +Recording MBID doppio Scansiona ora Scanner Stato diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index e7f5dab7..cd3c18be 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -68,7 +68,7 @@ 已扫描 {1} 文件 在 {2} 和 {3} ({4} 错误, {5} 副本) 没有音轨 复制文件哈希值 -复制 MBID +复制 Recording MBID 立即扫描 扫描器 状态 diff --git a/approot/release.xml b/approot/release.xml index b440c6c6..724dc1c1 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -16,7 +16,7 @@

${name}

${}${artists}${} ${}
${tr:Lms.Explore.various-artists}
${
} -
${}${year}${}${} (${orig-year})${}
+
${}${year}${} (${orig-year})${} · ${}${duration}
${clusters} ${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"} diff --git a/conf/lms.conf b/conf/lms.conf index ab0ba1c9..b44af624 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -70,3 +70,6 @@ cover-max-cache-size = 30; # JPEG quality for covers (range is 1-100) cover-jpeg-quality = 75; + +# Set to true if you want to hide duplicate tracks +scanner-skip-duplicate-recording-mbid = false; diff --git a/docroot/css/lms.css b/docroot/css/lms.css index c2ff47ac..cf818f88 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -184,7 +184,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus { } .Lms-explore-release-entry { - min-height: 34px; + min-height: 36px; } .Lms-explore-release-entry:hover .Lms-explore-release-entry-controls { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c796f0e9..79236cc5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,8 +4,5 @@ add_subdirectory(libs) add_subdirectory(lms) add_subdirectory(tools) -if(BUILD_TESTING) - add_subdirectory(test) -endif() diff --git a/src/libs/CMakeLists.txt b/src/libs/CMakeLists.txt index e8c1413c..ca904a17 100644 --- a/src/libs/CMakeLists.txt +++ b/src/libs/CMakeLists.txt @@ -1,14 +1,7 @@ - -add_subdirectory(auth) add_subdirectory(av) -add_subdirectory(cover) -add_subdirectory(database) +add_subdirectory(image) add_subdirectory(metadata) -add_subdirectory(recommendation) -add_subdirectory(scanner) -add_subdirectory(scrobbling) +add_subdirectory(services) add_subdirectory(som) add_subdirectory(subsonic) add_subdirectory(utils) - - diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/av/impl/Transcoder.cpp index 7efa0442..55b00d1b 100644 --- a/src/libs/av/impl/Transcoder.cpp +++ b/src/libs/av/impl/Transcoder.cpp @@ -119,7 +119,6 @@ Transcoder::start() args.emplace_back("-b:a"); args.emplace_back(std::to_string(_parameters.bitrate)); - // Codecs and formats switch (_parameters.format) { diff --git a/src/libs/av/include/av/TranscodeParameters.hpp b/src/libs/av/include/av/TranscodeParameters.hpp index 7449e2d9..c155f6e7 100644 --- a/src/libs/av/include/av/TranscodeParameters.hpp +++ b/src/libs/av/include/av/TranscodeParameters.hpp @@ -24,16 +24,15 @@ #include "Types.hpp" -namespace Av { - -struct TranscodeParameters +namespace Av { - Format format; - std::size_t bitrate {128000}; - std::optional stream; // Id of the stream to be transcoded (auto detect by default) - std::chrono::milliseconds offset {0}; - bool stripMetadata {true}; -}; - + struct TranscodeParameters + { + Format format; + std::size_t bitrate {128000}; + std::optional stream; // Id of the stream to be transcoded (auto detect by default) + std::chrono::milliseconds offset {0}; + bool stripMetadata {true}; + }; } // namespace Av diff --git a/src/libs/av/include/av/Types.hpp b/src/libs/av/include/av/Types.hpp index d1cc4cb5..166a19da 100644 --- a/src/libs/av/include/av/Types.hpp +++ b/src/libs/av/include/av/Types.hpp @@ -33,12 +33,11 @@ namespace Av { enum class Format { - // Values are important and must not be changed (stored in the UI's localstorage) - MP3 = 0, - OGG_OPUS = 1, - MATROSKA_OPUS = 2, - OGG_VORBIS = 3, - WEBM_VORBIS = 4, + MP3, + OGG_OPUS, + MATROSKA_OPUS, + OGG_VORBIS, + WEBM_VORBIS, }; std::string_view formatToMimetype(Format format); diff --git a/src/libs/cover/CMakeLists.txt b/src/libs/cover/CMakeLists.txt deleted file mode 100644 index a675db76..00000000 --- a/src/libs/cover/CMakeLists.txt +++ /dev/null @@ -1,44 +0,0 @@ - -add_library(lmscover SHARED - impl/CoverArtGrabber.cpp - ) - -target_include_directories(lmscover INTERFACE - include - ) - -target_include_directories(lmscover PRIVATE - include - impl - ) - -target_link_libraries(lmscover PRIVATE - lmsav - ) - -target_link_libraries(lmscover PUBLIC - lmsdatabase - lmsutils - std::filesystem - ) - -if (IMAGE_LIBRARY STREQUAL STB) - target_sources(lmscover PRIVATE - impl/stb/JPEGImage.cpp - impl/stb/RawImage.cpp - ) - target_compile_options(lmscover PRIVATE "-DLMS_SUPPORT_IMAGE_STB") - target_include_directories(lmscover PRIVATE ${STB_INCLUDE_DIR}) -elseif (IMAGE_LIBRARY STREQUAL GraphicsMagick++) - target_sources(lmscover PRIVATE - impl/graphicsmagick/JPEGImage.cpp - impl/graphicsmagick/RawImage.cpp - ) - target_compile_options(lmscover PRIVATE "-DLMS_SUPPORT_IMAGE_GM") - target_link_libraries(lmscover PRIVATE PkgConfig::GraphicsMagick++) -else () - message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided") -endif() - -install(TARGETS lmscover DESTINATION lib) - diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp deleted file mode 100644 index 56ec77d1..00000000 --- a/src/libs/database/impl/Session.cpp +++ /dev/null @@ -1,508 +0,0 @@ -/* - * Copyright (C) 2020 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/Session.hpp" - -#include -#include -#include -#include - -#include "utils/Exception.hpp" -#include "utils/Logger.hpp" - -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/ScanSettings.hpp" -#include "database/Track.hpp" -#include "database/TrackBookmark.hpp" -#include "database/TrackArtistLink.hpp" -#include "database/TrackList.hpp" -#include "database/TrackFeatures.hpp" -#include "database/User.hpp" - -namespace Database -{ - - using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION {31}; - - class VersionInfo - { - public: - using pointer = Wt::Dbo::ptr; - - static VersionInfo::pointer getOrCreate(Session& session) - { - session.checkUniqueLocked(); - - pointer versionInfo {session.getDboSession().find()}; - if (!versionInfo) - return session.getDboSession().add(std::make_unique()); - - return versionInfo; - } - - static VersionInfo::pointer get(Session& session) - { - session.checkSharedLocked(); - - return session.getDboSession().find(); - } - - 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"); - } - - private: - int _version {LMS_DATABASE_VERSION}; - }; - -void -Session::doDatabaseMigrationIfNeeded() -{ - static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"}; - - Db::ScopedNoForeignKeys noPragmaKeys {_db}; - - while (1) - { - auto uniqueTransaction {createUniqueTransaction()}; - - Version version; - try - { - version = VersionInfo::getOrCreate(*this)->getVersion(); - LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION; - if (version == LMS_DATABASE_VERSION) - { - LMS_LOG(DB, DEBUG) << "Lms database version " << LMS_DATABASE_VERSION << ": up to date!"; - return; - } - } - catch (std::exception& e) - { - LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what(); - throw LmsException {outdatedMsg}; - } - - LMS_LOG(DB, INFO) << "Migrating database from version " << version << "..."; - - if (version == 5) - { - _session.execute("DELETE FROM auth_token"); // format has changed - } - else if (version == 6) - { - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(*this).modify()->incScanVersion(); - } - else if (version == 7) - { - _session.execute("DROP TABLE similarity_settings"); - _session.execute("DROP TABLE similarity_settings_feature"); - _session.execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(ScanSettings::RecommendationEngineType::Clusters)) + ")"); - } - else if (version == 8) - { - // 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(*this).modify()->incScanVersion(); - } - else if (version == 9) - { - _session.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 -);)"); - } - else if (version == 10) - { - ScanSettings::get(*this).modify()->addAudioFileExtension(".m4b"); - ScanSettings::get(*this).modify()->addAudioFileExtension(".alac"); - } - else if (version == 11) - { - // 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(*this).modify()->incScanVersion(); - } - else if (version == 12) - { - // 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(*this).modify()->incScanVersion(); - } - else if (version == 13) - { - // 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(*this).modify()->incScanVersion(); - } - else if (version == 14) - { - // SortName now set from metadata - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(*this).modify()->incScanVersion(); - } - else if (version == 15) - { - _session.execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultUITheme)) + ")"); - } - else if (version == 16) - { - _session.execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)"); - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 17) - { - // Drop colums total_disc/total_track from release - _session.execute(R"( -CREATE TABLE "release_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "name" text not null, - "mbid" text not null -))"); - _session.execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release"); - _session.execute("DROP TABLE release"); - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 18) - { - _session.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 -))"); - } - else if (version == 19) - { - _session.execute(R"( -CREATE TABLE "user_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "type" integer not null, - "login_name" text not null, - "password_salt" text not null, - "password_hash" text not null, - "last_login" text, - "subsonic_transcode_enable" boolean not null, - "subsonic_transcode_format" integer not null, - "subsonic_transcode_bitrate" integer not null, - "subsonic_artist_list_mode" integer not null, - "ui_theme" integer not null, - "cur_playing_track_pos" integer not null, - "repeat_all" boolean not null, - "radio" boolean not null -))"); - _session.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.execute("DROP TABLE user"); - _session.execute("ALTER TABLE user_backup RENAME TO user"); - } - else if (version == 20) - { - _session.execute("DROP TABLE subsonic_settings"); - } - else if (version == 21) - { - _session.execute("ALTER TABLE track ADD track_replay_gain REAL"); - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 22) - { - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 23) - { - // Better cover detection - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(*this).modify()->incScanVersion(); - } - else if (version == 24) - { - // User's AuthMode - _session.execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultAuthMode*/0)) + ")"); - } - else if (version == 25) - { - // Better cover detection - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(*this).modify()->incScanVersion(); - } - else if (version == 26) - { - // Composer, mixer, etc. support - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(*this).modify()->incScanVersion(); - } - else if (version == 27) - { - // 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(*this).modify()->incScanVersion(); - } - else if (version == 28) - { - // Drop Auth mode - _session.execute(R"( -CREATE TABLE "user_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "type" integer not null, - "login_name" text not null, - "password_salt" text not null, - "password_hash" text not null, - "last_login" text, - "subsonic_transcode_enable" boolean not null, - "subsonic_transcode_format" integer not null, - "subsonic_transcode_bitrate" integer not null, - "subsonic_artist_list_mode" integer not null, - "ui_theme" integer not null, - "cur_playing_track_pos" integer not null, - "repeat_all" boolean not null, - "radio" boolean not null -))"); - _session.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.execute("DROP TABLE user"); - _session.execute("ALTER TABLE user_backup RENAME TO user"); - } - else if (version == 29) - { - _session.execute("ALTER TABLE tracklist_entry ADD date_time TEXT"); - _session.execute("ALTER TABLE user ADD listenbrainz_token TEXT"); - _session.execute("ALTER TABLE user ADD scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultScrobbler)) + ")"); - _session.execute("ALTER TABLE track ADD recording_mbid TEXT"); - - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 30) - { - // drop "year" and "original_year" (rescan needed to convert them into dates) - _session.execute(R"( -CREATE TABLE "track_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "scan_version" integer not null, - "track_number" integer not null, - "disc_number" integer not null, - "name" text not null, - "duration" integer, - "date" integer text, - "original_date" integer text, - "file_path" text not null, - "file_last_write" text, - "file_added" text, - "has_cover" boolean not null, - "mbid" text not null, - "copyright" text not null, - "copyright_url" text not null, - "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.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.execute("DROP TABLE track"); - _session.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(*this).modify()->incScanVersion(); - } - else - { - LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; - throw LmsException { LMS_DATABASE_VERSION > version ? outdatedMsg : "Server binary outdated, please upgrade it to handle this database"}; - } - - VersionInfo::get(*this).modify()->setVersion(++version); - } -} - -Session::Session(Db& db) -: _db {db} -{ - _session.setConnectionPool(_db.getConnectionPool()); - - _session.mapClass("version_info"); - _session.mapClass("artist"); - _session.mapClass("auth_token"); - _session.mapClass("cluster"); - _session.mapClass("cluster_type"); - _session.mapClass("release"); - _session.mapClass("scan_settings"); - _session.mapClass("track"); - _session.mapClass("track_bookmark"); - _session.mapClass("track_artist_link"); - _session.mapClass("track_features"); - _session.mapClass("tracklist"); - _session.mapClass("tracklist_entry"); - _session.mapClass("user"); -} - - -enum class OwnedLock -{ - None, - Shared, - Unique, -}; - -UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session) -: _lock {mutex}, - _transaction {session} -{ -} - -SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session) -: _lock {mutex}, - _transaction {session} -{ -} - -void -Session::checkUniqueLocked() -{ -// assert(lockDebug[&_db.getMutex()] == OwnedLock::Unique); -} - -void -Session::checkSharedLocked() -{ -// assert(lockDebug[&_db.getMutex()] != OwnedLock::None); -} - -UniqueTransaction -Session::createUniqueTransaction() -{ - return UniqueTransaction {_db.getMutex(), _session}; -} - -SharedTransaction -Session::createSharedTransaction() -{ - return SharedTransaction {_db.getMutex(), _session}; -} - -void -Session::prepareTables() -{ - // Creation case - try { - _session.createTables(); - - LMS_LOG(DB, INFO) << "Tables created"; - } - catch (Wt::Dbo::Exception& e) - { - LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what(); - } - - doDatabaseMigrationIfNeeded(); - - // Indexes - { - auto uniqueTransaction {createUniqueTransaction()}; - _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); - _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); - _session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)"); - _session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)"); - _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); - _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_name_idx ON track_artist_link(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); - } - - // Initial settings tables - { - auto uniqueTransaction {createUniqueTransaction()}; - - ScanSettings::init(*this); - } -} - -void -Session::optimize() -{ - LMS_LOG(DB, DEBUG) << "Optimizing db..."; - { - auto uniqueTransaction {createUniqueTransaction()}; - _session.execute("ANALYZE"); - } - LMS_LOG(DB, DEBUG) << "Optimized db!"; -} - -} // namespace Database diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp deleted file mode 100644 index 44f8c2b1..00000000 --- a/src/libs/database/impl/User.cpp +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (C) 2013 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/User.hpp" - -#include "database/Artist.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackList.hpp" -#include "utils/Logger.hpp" -#include "StringViewTraits.hpp" -#include "Traits.hpp" - -namespace Database { - - -AuthToken::AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr user) -: _value {value} -, _expiry {expiry} -, _user {getDboPtr(user)} -{ - -} - -AuthToken::pointer -AuthToken::create(Session& session, const std::string& value, const Wt::WDateTime& expiry, ObjectPtr user) -{ - session.checkUniqueLocked(); - - AuthToken::pointer res {session.getDboSession().add(std::make_unique(value, expiry, user))}; - session.getDboSession().flush(); - - return res; -} - -void -AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now) -{ - session.checkUniqueLocked(); - - session.getDboSession().execute - ("DELETE FROM auth_token WHERE expiry < ?").bind(now); -} - -AuthToken::pointer -AuthToken::getByValue(Session& session, const std::string& value) -{ - session.checkSharedLocked(); - - return session.getDboSession().find() - .where("value = ?").bind(value) - .resultValue(); -} - -static const std::string queuedListName {"__queued_tracks__"}; - -User::User(std::string_view loginName) -: _loginName {loginName} -{ -} - -std::vector -User::getAll(Session& session) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().find().resultList()}; - return std::vector(res.begin(), res.end()); -} - -std::vector -User::getAllIds(Session& session) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().query("SELECT id FROM user").resultList()}; - return std::vector(res.begin(), res.end()); -} - -User::pointer -User::getDemo(Session& session) -{ - session.checkSharedLocked(); - - return session.getDboSession().find().where("type = ?").bind(UserType::DEMO).resultValue(); -} - -std::size_t -User::getCount(Session& session) -{ - session.checkSharedLocked(); - - return session.getDboSession().query("SELECT COUNT(*) FROM user"); -} - -User::pointer -User::create(Session& session, std::string_view loginName) -{ - session.checkUniqueLocked(); - - User::pointer user {session.getDboSession().add(std::make_unique(loginName))}; - - TrackList::create(session, queuedListName, TrackList::Type::Internal, false, user); - - session.getDboSession().flush(); - - return user; -} - -User::pointer -User::getById(Session& session, UserId id) -{ - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); -} - -User::pointer -User::getByLoginName(Session& session, std::string_view name) -{ - return session.getDboSession().find() - .where("login_name = ?").bind(name) - .resultValue(); -} - -void -User::setSubsonicTranscodeBitrate(Bitrate bitrate) -{ - assert(audioTranscodeAllowedBitrates.find(bitrate) != audioTranscodeAllowedBitrates.cend()); - _subsonicTranscodeBitrate = bitrate; -} - -void -User::clearAuthTokens() -{ - _authTokens.clear(); -} - -TrackList::pointer -User::getQueuedTrackList(Session& session) const -{ - assert(self()); - session.checkSharedLocked(); - - return TrackList::get(session, queuedListName, TrackList::Type::Internal, self()); -} - -void -User::starArtist(ObjectPtr artist) -{ - if (_starredArtists.count(getDboPtr(artist)) == 0) - _starredArtists.insert(getDboPtr(artist)); -} - -void -User::unstarArtist(ObjectPtr artist) -{ - if (_starredArtists.count(getDboPtr(artist)) != 0) - _starredArtists.erase(getDboPtr(artist)); -} - -bool -User::hasStarredArtist(ObjectPtr artist) const -{ - return _starredArtists.count(getDboPtr(artist)) != 0; -} - -void -User::starRelease(ObjectPtr release) -{ - if (_starredReleases.count(getDboPtr(release)) == 0) - _starredReleases.insert(getDboPtr(release)); -} - -void -User::unstarRelease(ObjectPtr release) -{ - if (_starredReleases.count(getDboPtr(release)) != 0) - _starredReleases.erase(getDboPtr(release)); -} - -bool -User::hasStarredRelease(ObjectPtr release) const -{ - return _starredReleases.count(getDboPtr(release)) != 0; -} - -void -User::starTrack(ObjectPtr track) -{ - if (_starredTracks.count(getDboPtr(track)) == 0) - _starredTracks.insert(getDboPtr(track)); -} - -void -User::unstarTrack(ObjectPtr track) -{ - if (_starredTracks.count(getDboPtr(track)) != 0) - _starredTracks.erase(getDboPtr(track)); -} - -bool -User::hasStarredTrack(ObjectPtr track) const -{ - return _starredTracks.count(getDboPtr(track)) != 0; -} - -} // namespace Database - - diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp deleted file mode 100644 index d5bab873..00000000 --- a/src/libs/database/include/database/Artist.hpp +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2015 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 . - */ - -#pragma once - -#include -#include -#include -#include - -#include -#include - -#include "database/Types.hpp" -#include "utils/EnumSet.hpp" -#include "utils/UUID.hpp" - -namespace Database -{ - -class Cluster; -class ClusterType; -class Release; -class Session; -class Track; -class TrackArtistLink; -class User; - -class Artist : public Object -{ - public: - enum class SortMethod - { - None, - ByName, - BySortName, - }; - - Artist() = default; - Artist(const std::string& name, const std::optional& MBID = {}); - - // Accessors - static pointer getByMBID(Session& session, const UUID& MBID); - static pointer getById(Session& session, ArtistId id); - static std::vector getByName(Session& session, const std::string& name); // exact match on name field - static std::vector getByClusters(Session& session, - const std::vector& clusters, // at least one track that belongs to these clusters - SortMethod sortMethod - ); - static std::vector getByFilter(Session& session, - const std::vector& clusters, // if non empty, at least one artist that belongs to these clusters - const std::vector& keywords, // if non empty, name must match all of these keywords (name + sort name fields) - std::optional linkType, // if set, only artists that have produced at least one track with this link type - SortMethod sortMethod, - std::optional range, - bool& moreExpected); - - static std::vector getAll(Session& session); - static std::vector getAll(Session& session, SortMethod sortMethod); - static std::vector getAll(Session& session, SortMethod sortMethod, std::optional range, bool& moreResults); - static std::vector getAllIds(Session& session); - static std::vector getAllIdsRandom(Session& session, const std::vector& clusters, std::optional linkType, std::optional size = {}); - static std::vector getAllOrphans(Session& session); // No track related - static std::vector getLastWritten(Session& session, - std::optional after, - const std::vector& clusters, - std::optional linkType, // if set, only artists that have produced at least one track with this link type - std::optional, - bool& moreResults); - static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); - static std::vector getStarred(Session& session, - ObjectPtr user, - const std::vector& clusters, - std::optional linkType, // if set, only artists that have produced at least one track with this link type - SortMethod sortMethod, - std::optional, bool& moreResults); - - // Accessors - const std::string& getName() const { return _name; } - const std::string& getSortName() const { return _sortName; } - std::optional getMBID() const { return UUID::fromString(_MBID); } - - std::vector> getReleases(const std::vector& clusterIds = {}) const; // if non empty, get the releases that match all these clusters - std::size_t getReleaseCount() const; - std::vector> getTracks(std::optional linkType = {}) const; - bool hasNonReleaseTracks(std::optional linkType = std::nullopt) const; - std::vector> getNonReleaseTracks(std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getRandomTracks(std::optional count) const; - - // No artistLinkTypes means get them all - std::vector getSimilarArtists(EnumSet artistLinkTypes = {}, std::optional range = std::nullopt) const; - - // Get the cluster of the tracks made by this artist - // Each clusters are grouped by cluster type, sorted by the number of occurence - // size is the max number of cluster per cluster type - std::vector>> getClusterGroups(std::vector> clusterTypes, std::size_t size) const; - - void setName(std::string_view name) { _name = name; } - void setMBID(const std::optional& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } - void setSortName(const std::string& sortName); - - // Create - static pointer create(Session& session, const std::string& name, const std::optional& UUID = {}); - - template - void persist(Action& a) - { - Wt::Dbo::field(a, _name, "name"); - Wt::Dbo::field(a, _sortName, "sort_name"); - Wt::Dbo::field(a, _MBID, "mbid"); - - Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist"); - Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade); - } - - private: - - static const std::size_t _maxNameLength = 128; - - std::string _name; - std::string _sortName; - std::string _MBID; // Musicbrainz Identifier - - Wt::Dbo::collection> _trackArtistLinks; // Tracks involving this artist - Wt::Dbo::collection> _starringUsers; // Users that starred this artist -}; - -} // namespace Database - diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp deleted file mode 100644 index c2fb4146..00000000 --- a/src/libs/database/include/database/Types.hpp +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2015 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 . - */ - -#pragma once - -#include -#include -#include -#include - -namespace Database -{ - class IdType - { - public: - using ValueType = Wt::Dbo::dbo_default_traits::IdType; - - IdType() = default; - IdType(ValueType id) : _id {id} { assert(isValid()); } - - bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); } - std::string toString() const { assert(isValid()); return std::to_string(_id); } - - ValueType getValue() const { return _id; } - - bool operator==(IdType other) const { return other._id == _id; } - bool operator!=(IdType other) const { return !(*this == other); } - bool operator<(IdType other) const { return other._id < _id; } - - private: - Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()}; - }; - - struct Range - { - std::size_t offset {}; - std::size_t limit {}; - }; - - enum class TrackArtistLinkType - { - Artist, // regular artist - Arranger, - Composer, - Conductor, - Lyricist, - Mixer, - Performer, - Producer, - ReleaseArtist, - Remixer, - Writer, - }; - - // User selectable audio file formats - // Do not change values - enum class AudioFormat - { - MP3 = 1, - OGG_OPUS = 2, - OGG_VORBIS = 3, - WEBM_VORBIS = 4, - MATROSKA_OPUS = 5, - }; - using Bitrate = std::uint32_t; - - // Do not change enum values! - enum class Scrobbler - { - Internal = 0, - ListenBrainz = 1, - }; - - // Do not change enum values! - enum class UserType - { - REGULAR = 0, - ADMIN = 1, - DEMO = 2, - }; - - template - class ObjectPtr - { - public: - ObjectPtr() = default; - ObjectPtr(Wt::Dbo::ptr obj) : _obj {obj} {} - - const T* operator->() const { return _obj.get(); } - operator bool() const { return _obj.get(); } - bool operator!() const { return !_obj.get(); } - - auto modify() { return _obj.modify(); } - void remove() { _obj.remove(); } - - private: - template friend class Object; - Wt::Dbo::ptr _obj; - }; - - template - class Object : public Wt::Dbo::Dbo - { - static_assert(std::is_base_of_v); - static_assert(!std::is_same_v); - - public: - using pointer = ObjectPtr; - using IdType = ObjectIdType; - - IdType getId() const { return Wt::Dbo::Dbo::self()->Wt::Dbo::template Dbo::id(); } - - // catch some misuses - typename Wt::Dbo::dbo_traits::IdType id() const = delete; - - protected: - // Can get raw dbo ptr only from Objects - template - static - Wt::Dbo::ptr getDboPtr(ObjectPtr ptr) { return ptr._obj; } - }; -} - -// TODO factorize hash with std::enable_if -#define LMS_DECLARE_IDTYPE(name) \ -namespace Database { \ - class name : public IdType \ - { \ - public: \ - using IdType::IdType; \ - };\ -} \ -namespace std \ -{ \ - template<> \ - class hash \ - { \ - public: \ - size_t operator()(Database::name id) const \ - { \ - return std::hash()(id.getValue()); \ - } \ - }; \ -} // ns std - -LMS_DECLARE_IDTYPE(ArtistId) -LMS_DECLARE_IDTYPE(AuthTokenId) -LMS_DECLARE_IDTYPE(ClusterId) -LMS_DECLARE_IDTYPE(ClusterTypeId) -LMS_DECLARE_IDTYPE(ReleaseId) -LMS_DECLARE_IDTYPE(ScanSettingsId) -LMS_DECLARE_IDTYPE(TrackArtistLinkId) -LMS_DECLARE_IDTYPE(TrackBookmarkId) -LMS_DECLARE_IDTYPE(TrackFeaturesId) -LMS_DECLARE_IDTYPE(TrackId) -LMS_DECLARE_IDTYPE(TrackListId) -LMS_DECLARE_IDTYPE(TrackListEntryId) -LMS_DECLARE_IDTYPE(UserId) - - diff --git a/src/libs/image/CMakeLists.txt b/src/libs/image/CMakeLists.txt new file mode 100644 index 00000000..cb855563 --- /dev/null +++ b/src/libs/image/CMakeLists.txt @@ -0,0 +1,38 @@ + +add_library(lmsimage SHARED + ) + +target_include_directories(lmsimage INTERFACE + include + ) + +target_include_directories(lmsimage PRIVATE + include + impl + ) + +target_link_libraries(lmsimage PUBLIC + lmsutils + std::filesystem + ) + +if (IMAGE_LIBRARY STREQUAL STB) + target_sources(lmsimage PRIVATE + impl/stb/JPEGImage.cpp + impl/stb/RawImage.cpp + ) + target_compile_options(lmsimage PRIVATE "-DLMS_SUPPORT_IMAGE_STB") + target_include_directories(lmsimage PRIVATE ${STB_INCLUDE_DIR}) +elseif (IMAGE_LIBRARY STREQUAL GraphicsMagick++) + target_sources(lmsimage PRIVATE + impl/graphicsmagick/JPEGImage.cpp + impl/graphicsmagick/RawImage.cpp + ) + target_compile_options(lmsimage PRIVATE "-DLMS_SUPPORT_IMAGE_GM") + target_link_libraries(lmsimage PRIVATE PkgConfig::GraphicsMagick++) +else () + message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided") +endif() + +install(TARGETS lmsimage DESTINATION lib) + diff --git a/src/libs/cover/impl/graphicsmagick/JPEGImage.cpp b/src/libs/image/impl/graphicsmagick/JPEGImage.cpp similarity index 95% rename from src/libs/cover/impl/graphicsmagick/JPEGImage.cpp rename to src/libs/image/impl/graphicsmagick/JPEGImage.cpp index 8cf7dfdc..85b2fac3 100644 --- a/src/libs/cover/impl/graphicsmagick/JPEGImage.cpp +++ b/src/libs/image/impl/graphicsmagick/JPEGImage.cpp @@ -19,11 +19,11 @@ #include "JPEGImage.hpp" -#include "Exception.hpp" #include "RawImage.hpp" +#include "image/Exception.hpp" #include "utils/Logger.hpp" -namespace CoverArt::GraphicsMagick +namespace Image::GraphicsMagick { JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality) { diff --git a/src/libs/cover/impl/graphicsmagick/JPEGImage.hpp b/src/libs/image/impl/graphicsmagick/JPEGImage.hpp similarity index 94% rename from src/libs/cover/impl/graphicsmagick/JPEGImage.hpp rename to src/libs/image/impl/graphicsmagick/JPEGImage.hpp index 37a5e163..9ead9f3c 100644 --- a/src/libs/cover/impl/graphicsmagick/JPEGImage.hpp +++ b/src/libs/image/impl/graphicsmagick/JPEGImage.hpp @@ -25,9 +25,9 @@ #include -#include "cover/IEncodedImage.hpp" +#include "image/IEncodedImage.hpp" -namespace CoverArt::GraphicsMagick +namespace Image::GraphicsMagick { class RawImage; class JPEGImage : public IEncodedImage diff --git a/src/libs/cover/impl/graphicsmagick/RawImage.cpp b/src/libs/image/impl/graphicsmagick/RawImage.cpp similarity index 66% rename from src/libs/cover/impl/graphicsmagick/RawImage.cpp rename to src/libs/image/impl/graphicsmagick/RawImage.cpp index 3beabc27..fc74ae37 100644 --- a/src/libs/cover/impl/graphicsmagick/RawImage.cpp +++ b/src/libs/image/impl/graphicsmagick/RawImage.cpp @@ -21,30 +21,44 @@ #include -#include "utils/Logger.hpp" #include "JPEGImage.hpp" -#include "Exception.hpp" +#include "image/Exception.hpp" +#include "utils/Logger.hpp" -namespace CoverArt::GraphicsMagick { - -void -init(const std::filesystem::path& path) +namespace Image { - Magick::InitializeMagick(path.string().c_str()); + std::unique_ptr decodeImage(const std::byte* encodedData, std::size_t encodedDataSize) + { + return std::make_unique(encodedData, encodedDataSize); + } - if (auto nbThreads {MagickLib::GetMagickResourceLimit(MagickLib::ThreadsResource)}; nbThreads != 1) - LMS_LOG(COVER, WARNING) << "Consider setting env var OMP_NUM_THREADS=1 to save resources"; + std::unique_ptr decodeImage(const std::filesystem::path& path) + { + return std::make_unique(path); + } - if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1)) - LMS_LOG(COVER, ERROR) << "Cannot set Magick thread resource limit to 1!"; + void + init(const std::filesystem::path& path) + { + Magick::InitializeMagick(path.string().c_str()); - if (!MagickLib::SetMagickResourceLimit(MagickLib::DiskResource, 0)) - LMS_LOG(COVER, ERROR) << "Cannot set Magick disk resource limit to 0!"; + if (auto nbThreads {MagickLib::GetMagickResourceLimit(MagickLib::ThreadsResource)}; nbThreads != 1) + LMS_LOG(COVER, WARNING) << "Consider setting env var OMP_NUM_THREADS=1 to save resources"; - LMS_LOG(COVER, INFO) << "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource); - LMS_LOG(COVER, INFO) << "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource); + if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1)) + LMS_LOG(COVER, ERROR) << "Cannot set Magick thread resource limit to 1!"; + + if (!MagickLib::SetMagickResourceLimit(MagickLib::DiskResource, 0)) + LMS_LOG(COVER, ERROR) << "Cannot set Magick disk resource limit to 0!"; + + LMS_LOG(COVER, INFO) << "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource); + LMS_LOG(COVER, INFO) << "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource); + } } +namespace Image::GraphicsMagick +{ + RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize) { try @@ -116,5 +130,5 @@ RawImage::getMagickImage() const return _image; } -} // namespace CoverArt::GraphicsMagick +} // namespace Image::GraphicsMagick diff --git a/src/libs/cover/impl/graphicsmagick/RawImage.hpp b/src/libs/image/impl/graphicsmagick/RawImage.hpp similarity index 87% rename from src/libs/cover/impl/graphicsmagick/RawImage.hpp rename to src/libs/image/impl/graphicsmagick/RawImage.hpp index 354c07c0..d1c180ed 100644 --- a/src/libs/cover/impl/graphicsmagick/RawImage.hpp +++ b/src/libs/image/impl/graphicsmagick/RawImage.hpp @@ -28,14 +28,12 @@ #include #include -#include "cover/IEncodedImage.hpp" -#include "IRawImage.hpp" +#include "image/IEncodedImage.hpp" +#include "image/IRawImage.hpp" -namespace CoverArt::GraphicsMagick +namespace Image::GraphicsMagick { - void init(const std::filesystem::path& path); - - class RawImage : IRawImage + class RawImage : public IRawImage { public: RawImage(const std::byte* encodedData, std::size_t encodedDataSize); diff --git a/src/libs/cover/impl/stb/JPEGImage.cpp b/src/libs/image/impl/stb/JPEGImage.cpp similarity index 97% rename from src/libs/cover/impl/stb/JPEGImage.cpp rename to src/libs/image/impl/stb/JPEGImage.cpp index 68eafe43..4bfcef4c 100644 --- a/src/libs/cover/impl/stb/JPEGImage.cpp +++ b/src/libs/image/impl/stb/JPEGImage.cpp @@ -22,10 +22,10 @@ #define STB_IMAGE_WRITE_IMPLEMENTATION #include +#include "image/Exception.hpp" #include "RawImage.hpp" -#include "Exception.hpp" -namespace CoverArt::STB +namespace Image::STB { JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality) { diff --git a/src/libs/cover/impl/stb/JPEGImage.hpp b/src/libs/image/impl/stb/JPEGImage.hpp similarity index 94% rename from src/libs/cover/impl/stb/JPEGImage.hpp rename to src/libs/image/impl/stb/JPEGImage.hpp index 3256cd10..ddce80f2 100644 --- a/src/libs/cover/impl/stb/JPEGImage.hpp +++ b/src/libs/image/impl/stb/JPEGImage.hpp @@ -21,9 +21,9 @@ #include -#include "cover/IEncodedImage.hpp" +#include "image/IEncodedImage.hpp" -namespace CoverArt::STB +namespace Image::STB { class RawImage; class JPEGImage : public IEncodedImage diff --git a/src/libs/cover/impl/stb/RawImage.cpp b/src/libs/image/impl/stb/RawImage.cpp similarity index 86% rename from src/libs/cover/impl/stb/RawImage.cpp rename to src/libs/image/impl/stb/RawImage.cpp index 13ff1d75..a68f36b9 100644 --- a/src/libs/cover/impl/stb/RawImage.cpp +++ b/src/libs/image/impl/stb/RawImage.cpp @@ -30,9 +30,27 @@ #include "JPEGImage.hpp" -#include "Exception.hpp" +#include "image/Exception.hpp" -namespace CoverArt::STB +namespace Image +{ + std::unique_ptr decodeImage(const std::byte* encodedData, std::size_t encodedDataSize) + { + return std::make_unique(encodedData, encodedDataSize); + } + + std::unique_ptr decodeImage(const std::filesystem::path& path) + { + return std::make_unique(path); + } + + void + init(const std::filesystem::path&) + { + } +} + +namespace Image::STB { RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize) { diff --git a/src/libs/cover/impl/stb/RawImage.hpp b/src/libs/image/impl/stb/RawImage.hpp similarity index 94% rename from src/libs/cover/impl/stb/RawImage.hpp rename to src/libs/image/impl/stb/RawImage.hpp index d4f36aa1..c7d7921a 100644 --- a/src/libs/cover/impl/stb/RawImage.hpp +++ b/src/libs/image/impl/stb/RawImage.hpp @@ -26,10 +26,10 @@ #include #include -#include "cover/IEncodedImage.hpp" -#include "IRawImage.hpp" +#include "image/IEncodedImage.hpp" +#include "image/IRawImage.hpp" -namespace CoverArt::STB +namespace Image::STB { class RawImage : public IRawImage { diff --git a/src/libs/cover/impl/Exception.hpp b/src/libs/image/include/image/Exception.hpp similarity index 95% rename from src/libs/cover/impl/Exception.hpp rename to src/libs/image/include/image/Exception.hpp index e6c8c342..53428934 100644 --- a/src/libs/cover/impl/Exception.hpp +++ b/src/libs/image/include/image/Exception.hpp @@ -21,7 +21,7 @@ #include "utils/Exception.hpp" -namespace CoverArt +namespace Image { // internal use only class ImageException : public LmsException @@ -30,5 +30,5 @@ namespace CoverArt using LmsException::LmsException; }; -} // namespace CoverArt +} // namespace Cover diff --git a/src/libs/cover/include/cover/IEncodedImage.hpp b/src/libs/image/include/image/IEncodedImage.hpp similarity index 95% rename from src/libs/cover/include/cover/IEncodedImage.hpp rename to src/libs/image/include/image/IEncodedImage.hpp index 0fc245be..96fc03a5 100644 --- a/src/libs/cover/include/cover/IEncodedImage.hpp +++ b/src/libs/image/include/image/IEncodedImage.hpp @@ -22,7 +22,7 @@ #include #include -namespace CoverArt +namespace Image { using ImageSize = std::size_t; @@ -36,5 +36,5 @@ namespace CoverArt virtual std::string_view getMimeType() const = 0; }; -} // namespace CoverArt +} // namespace Cover diff --git a/src/libs/cover/impl/IRawImage.hpp b/src/libs/image/include/image/IRawImage.hpp similarity index 75% rename from src/libs/cover/impl/IRawImage.hpp rename to src/libs/image/include/image/IRawImage.hpp index 6ff9e6d1..79d8587f 100644 --- a/src/libs/cover/impl/IRawImage.hpp +++ b/src/libs/image/include/image/IRawImage.hpp @@ -19,11 +19,12 @@ #pragma once +#include #include -#include "cover/IEncodedImage.hpp" +#include "image/IEncodedImage.hpp" -namespace CoverArt +namespace Image { class IRawImage { @@ -32,5 +33,9 @@ namespace CoverArt virtual void resize(ImageSize width) = 0; virtual std::unique_ptr encodeToJPEG(unsigned quality) const = 0; }; + + void init(const std::filesystem::path& path); + std::unique_ptr decodeImage(const std::byte* encodedData, std::size_t encodedDataSize); + std::unique_ptr decodeImage(const std::filesystem::path& path); } diff --git a/src/libs/metadata/test/CMakeLists.txt b/src/libs/metadata/test/CMakeLists.txt index acd4aec2..b1c57bee 100644 --- a/src/libs/metadata/test/CMakeLists.txt +++ b/src/libs/metadata/test/CMakeLists.txt @@ -14,5 +14,7 @@ target_link_libraries(test-metadata PRIVATE GTest::GTest ) -gtest_discover_tests(test-metadata) +if (NOT CMAKE_CROSSCOMPILING) + gtest_discover_tests(test-metadata) +endif() diff --git a/src/libs/recommendation/impl/Engine.cpp b/src/libs/recommendation/impl/Engine.cpp deleted file mode 100644 index 741a9dad..00000000 --- a/src/libs/recommendation/impl/Engine.cpp +++ /dev/null @@ -1,290 +0,0 @@ -/* - * 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 "Engine.hpp" - -#include -#include - -#include "ClustersClassifierCreator.hpp" -#include "FeaturesEngineCreator.hpp" - -#include "database/Db.hpp" -#include "database/Session.hpp" -#include "database/ScanSettings.hpp" -#include "utils/Exception.hpp" -#include "utils/Logger.hpp" - -namespace Recommendation { - - -static -std::unique_ptr -createClassifier(ClassifierType type) -{ - switch (type) - { - case ClassifierType::Clusters: - return createClustersClassifier(); - break; - - case ClassifierType::Features: - return createFeaturesEngine(); - break; - } - - return {}; -} - -std::unique_ptr -createEngine(Database::Db& db) -{ - return std::make_unique(db); -} - -Engine::Engine(Database::Db& db) -: _db {db} -{ -} - -Engine::TrackContainer -Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId trackListId, std::size_t maxCount) -{ - TrackContainer res; - - std::shared_lock lock {_classifiersMutex}; - for (const auto& classifierName : _classifierPriorities) - { - auto itClassifier {_classifiers.find(classifierName)}; - if (itClassifier == std::cend(_classifiers)) - continue; - - res = itClassifier->second->getSimilarTracksFromTrackList(session, trackListId, maxCount); - if (!res.empty()) - break; - } - - return res; -} - -Engine::TrackContainer -Engine::getSimilarTracks(Database::Session& dbSession, const std::vector& trackIds, std::size_t maxCount) -{ - TrackContainer res; - - std::shared_lock lock {_classifiersMutex}; - for (ClassifierType classifierType : _classifierPriorities) - { - auto itClassifier {_classifiers.find(classifierType)}; - if (itClassifier == std::cend(_classifiers)) - continue; - - const IClassifier& classifier {*itClassifier->second}; - res = classifier.getSimilarTracks(dbSession, trackIds, maxCount); - if (!res.empty()) - { - LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using classifier '" << classifier.getName() << "'"; - break; - } - } - - return res; -} - -Engine::ReleaseContainer -Engine::getSimilarReleases(Database::Session& dbSession, Database::ReleaseId releaseId, std::size_t maxCount) -{ - ReleaseContainer res; - - std::shared_lock lock {_classifiersMutex}; - for (ClassifierType classifierType : _classifierPriorities) - { - auto itClassifier {_classifiers.find(classifierType)}; - if (itClassifier == std::cend(_classifiers)) - continue; - - const IClassifier& classifier {*itClassifier->second}; - res = classifier.getSimilarReleases(dbSession, releaseId, maxCount); - if (!res.empty()) - { - LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using classifier '" << classifier.getName() << "'"; - break; - } - } - - return res; -} - -Engine::ArtistContainer -Engine::getSimilarArtists(Database::Session& dbSession, Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) -{ - ArtistContainer res; - - std::shared_lock lock {_classifiersMutex}; - for (ClassifierType classifierType : _classifierPriorities) - { - auto itClassifier {_classifiers.find(classifierType)}; - if (itClassifier == std::cend(_classifiers)) - continue; - - const IClassifier& classifier {*itClassifier->second}; - res = classifier.getSimilarArtists(dbSession, artistId, linkTypes, maxCount); - if (!res.empty()) - { - LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'"; - return res; - } - } - - return res; -} - -static -Database::ScanSettings::RecommendationEngineType -getRecommendationEngineType(Database::Session& session) -{ - auto transaction {session.createSharedTransaction()}; - - return Database::ScanSettings::get(session)->getRecommendationEngineType(); -} - -void -Engine::load(bool forceReload, const ProgressCallback& progressCallback) -{ - using namespace Database; - - LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines..."; - struct ClassifierWithType - { - ClassifierType type; - std::unique_ptr classifier; - }; - - std::vector classifiers; - auto addClassifier {[&](ClassifierType type) - { - classifiers.emplace_back(ClassifierWithType {type, createClassifier(type)}); - }}; - - switch (getRecommendationEngineType(_db.getTLSSession())) - { - case ScanSettings::RecommendationEngineType::Clusters: - setClassifierPriorities({ClassifierType::Clusters}); - addClassifier(ClassifierType::Clusters); - break; - case ScanSettings::RecommendationEngineType::Features: - setClassifierPriorities({ClassifierType::Features, ClassifierType::Clusters}); - // not same order since clusters is faster to load - addClassifier(ClassifierType::Clusters); - addClassifier(ClassifierType::Features); - break; - } - - assert(_pendingClassifiers.empty()); - clearClassifiers(); - - { - std::scoped_lock lock {_controlMutex}; - - std::transform(std::cbegin(classifiers), std::cend(classifiers), std::inserter(_pendingClassifiers, std::end(_pendingClassifiers)), - [](auto& classifier) { return classifier.classifier.get(); }); - } - - for (ClassifierWithType& classifier : classifiers) - loadClassifier(std::move(classifier.classifier), classifier.type, forceReload, progressCallback); - - LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines loaded!"; -} - -void -Engine::setClassifierPriorities(const std::vector& classifierPriorities) -{ - std::unique_lock lock {_classifiersMutex}; - - _classifierPriorities = classifierPriorities; -} - -void -Engine::clearClassifiers() -{ - std::unique_lock lock {_classifiersMutex}; - - _classifiers.clear(); -} - -void -Engine::loadClassifier(std::unique_ptr classifier, - ClassifierType classifierType, - bool forceReload, - const ProgressCallback& progressCallback) -{ - IClassifier* rawClassifier {classifier.get()}; - - bool res {}; - if (!_loadCancelled) - { - LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "'..."; - - auto progress {[&](IClassifier::Progress progress) - { - progressCallback(Progress {progress.processedElems, progress.totalElems}); - }}; - - res = classifier->load(_db.getTLSSession(), forceReload, progressCallback ? progress : IClassifier::ProgressCallback {}); - - LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "': " << (res ? "SUCCESS" : "FAILURE"); - } - - if (res) - { - std::unique_lock lock {_classifiersMutex}; - - _classifiers.emplace(classifierType, std::move(classifier)); - } - - { - std::scoped_lock lock {_controlMutex}; - - _pendingClassifiers.erase(rawClassifier); - } - - _pendingClassifiersCondvar.notify_one(); -} - -void -Engine::cancelLoad() -{ - LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading..."; - - std::unique_lock lock {_controlMutex}; - - LMS_LOG(RECOMMENDATION, DEBUG) << "Still " << _pendingClassifiers.size() << " pending classifiers!"; - - _loadCancelled = true; - - for (IClassifier* classifier : _pendingClassifiers) - classifier->requestCancelLoad(); - - _pendingClassifiersCondvar.wait(lock, [this] {return _pendingClassifiers.empty();}); - _loadCancelled = false; - - LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading DONE"; -} - -} // ns Similarity diff --git a/src/libs/recommendation/impl/Engine.hpp b/src/libs/recommendation/impl/Engine.hpp deleted file mode 100644 index 39338fbc..00000000 --- a/src/libs/recommendation/impl/Engine.hpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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 . - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include "recommendation/IEngine.hpp" -#include "IClassifier.hpp" - -namespace Database -{ - class Db; -} - -namespace Recommendation -{ - enum class ClassifierType - { - Clusters, - Features, - }; - - class Engine : public IEngine - { - public: - Engine(Database::Db& db); - ~Engine() = default; - - Engine(const Engine&) = delete; - Engine(Engine&&) = delete; - Engine& operator=(const Engine&) = delete; - Engine& operator=(Engine&&) = delete; - - private: - void load(bool forceReload, const ProgressCallback& progressCallback) override; - void cancelLoad() override; - void requestCancelLoad() override {}; - - ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) override; - ResultContainer getSimilarTracks(Database::Session& session, const std::vector& tracksId, std::size_t maxCount) override; - ResultContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) override; - ResultContainer getSimilarArtists(Database::Session& session, - Database::ArtistId artistId, - EnumSet linkTypes, - std::size_t maxCount) override; - - void setClassifierPriorities(const std::vector& classifierTypes); - void clearClassifiers(); - void loadClassifier(std::unique_ptr classifier, ClassifierType classifierType, bool forceReload, const ProgressCallback& progressCallback); - - Database::Db& _db; - - std::mutex _controlMutex; - bool _loadCancelled {}; - std::condition_variable _pendingClassifiersCondvar; - std::unordered_set _pendingClassifiers; - - std::shared_mutex _classifiersMutex; - using ClassifierContainer = std::unordered_map>; - ClassifierContainer _classifiers; - std::vector _classifierPriorities; // ordered by priority - }; - -} // ns Recommendation - diff --git a/src/libs/recommendation/impl/IClassifier.hpp b/src/libs/recommendation/impl/IClassifier.hpp deleted file mode 100644 index 62483d9e..00000000 --- a/src/libs/recommendation/impl/IClassifier.hpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2020 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 . - */ - -#pragma once - -#include -#include -#include - -#include "database/Types.hpp" -#include "recommendation/IRecommendation.hpp" -#include "utils/EnumSet.hpp" - -namespace Database -{ - class Session; -} - -namespace Recommendation -{ - - class IClassifier : public IRecommendation - { - public: - virtual ~IClassifier() = default; - - virtual std::string_view getName() const = 0; - - struct Progress - { - std::size_t totalElems {}; - std::size_t processedElems {}; - }; - using ProgressCallback = std::function; - virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0; - virtual void requestCancelLoad() = 0; - - template - using ResultContainer = std::vector; - - virtual ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const = 0; - virtual ResultContainer getSimilarTracks(Database::Session& session, const std::vector& tracksId, std::size_t maxCount) const = 0; - virtual ResultContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const = 0; - virtual ResultContainer getSimilarArtists(Database::Session& session, - Database::ArtistId artistId, - EnumSet linkTypes, std::size_t maxCount) const = 0; - }; - -} // ns Recommendation diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp deleted file mode 100644 index 44f39a76..00000000 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2018 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 "ClustersClassifier.hpp" - -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackList.hpp" - -namespace Recommendation { - -std::unique_ptr createClustersClassifier() -{ - return std::make_unique(); -} - -IClassifier::ResultContainer -ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::vector& trackIds, std::size_t maxCount) const -{ - ResultContainer res; - - auto transaction {dbSession.createSharedTransaction()}; - - const auto tracks {Database::Track::getSimilarTracks(dbSession, trackIds, 0, maxCount)}; - std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); }); - - return res; -} - -IClassifier::ResultContainer -ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const -{ - ResultContainer res; - - auto transaction {session.createSharedTransaction()}; - - const Database::TrackList::pointer trackList {Database::TrackList::getById(session, tracklistId)}; - if (!trackList) - return res; - - const auto tracks {trackList->getSimilarTracks(0, maxCount)}; - std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); }); - - return res; -} - -IClassifier::ResultContainer -ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::ReleaseId releaseId, std::size_t maxCount) const -{ - ResultContainer res; - - auto transaction {dbSession.createSharedTransaction()}; - - auto release {Database::Release::getById(dbSession, releaseId)}; - if (!release) - return res; - - const auto releases {release->getSimilarReleases(0, maxCount)}; - std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const auto& release) { return release->getId(); }); - - return res; -} - -IClassifier::ResultContainer -ClusterClassifier::getSimilarArtists(Database::Session& dbSession, - Database::ArtistId artistId, - EnumSet artistLinkTypes, - std::size_t maxCount) const -{ - ResultContainer res; - - auto transaction {dbSession.createSharedTransaction()}; - - auto artist {Database::Artist::getById(dbSession, artistId)}; - if (!artist) - return res; - - const auto artists {artist->getSimilarArtists(artistLinkTypes, Database::Range {0, maxCount})}; - std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(res), [](const auto& artist) { return artist->getId(); }); - - return res; -} - -} // namespace Recommendation diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp deleted file mode 100644 index 718d6872..00000000 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2018 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 . - */ - -#pragma once - -#include "IClassifier.hpp" - -namespace Recommendation -{ - - class ClusterClassifier : public IClassifier - { - public: - ClusterClassifier() = default; - ClusterClassifier(const ClusterClassifier&) = delete; - ClusterClassifier(ClusterClassifier&&) = delete; - ClusterClassifier& operator=(const ClusterClassifier&) = delete; - ClusterClassifier& operator=(ClusterClassifier&&) = delete; - - private: - - std::string_view getName() const override { return "Clusters"; } - - bool load(Database::Session&, bool, const ProgressCallback&) override { return true; } - void requestCancelLoad() override {} - - ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const override; - ResultContainer getSimilarTracks(Database::Session& session, const std::vector& tracksId, std::size_t maxCount) const override; - ResultContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const override; - ResultContainer getSimilarArtists(Database::Session& session, - Database::ArtistId artistId, - EnumSet linkTypes, - std::size_t maxCount) const override; -}; - -} // namespace Recommendation - diff --git a/src/libs/recommendation/include/recommendation/IEngine.hpp b/src/libs/recommendation/include/recommendation/IEngine.hpp deleted file mode 100644 index 6a15187b..00000000 --- a/src/libs/recommendation/include/recommendation/IEngine.hpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 . - */ - -#pragma once - -#include -#include -#include "database/Types.hpp" -#include "utils/EnumSet.hpp" - -namespace Database -{ - class Db; - class Session; -} - -namespace Recommendation -{ - class IEngine - { - public: - virtual ~IEngine() = default; - - struct Progress - { - std::size_t totalElems {}; - std::size_t processedElems {}; - }; - using ProgressCallback = std::function; - virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; - virtual void cancelLoad() = 0; - - template - using ResultContainer = std::vector; - - using ArtistContainer = ResultContainer; - using ReleaseContainer = ResultContainer; - using TrackContainer = ResultContainer; - - virtual TrackContainer getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) = 0; - virtual TrackContainer getSimilarTracks(Database::Session& session, const std::vector& tracksId, std::size_t maxCount) = 0; - virtual ReleaseContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) = 0; - virtual ArtistContainer getSimilarArtists(Database::Session& session, Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) = 0; - - protected: - virtual void requestCancelLoad() = 0; - }; - - std::unique_ptr createEngine(Database::Db& db); - -} // ns Recommendation - diff --git a/src/libs/scanner/impl/Scanner.hpp b/src/libs/scanner/impl/Scanner.hpp deleted file mode 100644 index 76aeb819..00000000 --- a/src/libs/scanner/impl/Scanner.hpp +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2013 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 . - */ - -#pragma once - -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include "database/Types.hpp" -#include "database/ScanSettings.hpp" -#include "database/Session.hpp" -#include "metadata/IParser.hpp" -#include "scanner/IScanner.hpp" -#include "utils/Path.hpp" - -class UUID; - -namespace Recommendation -{ - class IEngine; -} - -namespace Scanner { - -class Scanner : public IScanner -{ - public: - Scanner(Database::Db& db, Recommendation::IEngine& recommendationEngine); - ~Scanner(); - - Scanner(const Scanner&) = delete; - Scanner(Scanner&&) = delete; - Scanner& operator=(const Scanner&) = delete; - Scanner& operator=(Scanner&&) = delete; - - void requestReload() override; - void requestImmediateScan(bool force) override; - - Status getStatus() const override; - Events& getEvents() override { return _events; } - - private: - void start(); - void stop(); - - // Job handling - void scheduleNextScan(); - void scheduleScan(bool force, const Wt::WDateTime& dateTime = {}); - - void abortScan(); - - // Update database (scheduled callback) - void scan(bool force); - - void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats); - bool fetchTrackFeatures(Database::TrackId trackId, const UUID& MBID); - void fetchTrackFeatures(ScanStats& stats); - - // Helpers - void refreshScanSettings(); - - void countAllFiles(ScanStats& stats); - void removeMissingTracks(ScanStats& stats); - void removeOrphanEntries(); - void checkDuplicatedAudioFiles(ScanStats& stats); - void scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats); - void notifyInProgressIfNeeded(const ScanStepStats& stats); - void notifyInProgress(const ScanStepStats& stats); - void reloadSimilarityEngine(ScanStats& stats); - - Recommendation::IEngine& _recommendationEngine; - - std::mutex _controlMutex; - std::atomic _abortScan {}; - Wt::WIOService _ioService; - boost::asio::system_timer _scheduleTimer {_ioService}; - Events _events; - std::chrono::system_clock::time_point _lastScanInProgressEmit {}; - Database::Session _dbSession; - std::unique_ptr _metadataParser; - - mutable std::shared_mutex _statusMutex; - State _curState {State::NotScheduled}; - std::optional _lastCompleteScanStats; - std::optional _currentScanStepStats; - Wt::WDateTime _nextScheduledScan; - - // Current scan settings - std::size_t _scanVersion {}; - Wt::WTime _startTime; - Database::ScanSettings::UpdatePeriod _updatePeriod {Database::ScanSettings::UpdatePeriod::Never}; - std::unordered_set _fileExtensions; - std::filesystem::path _mediaDirectory; - Database::ScanSettings::RecommendationEngineType _recommendationEngineType; -}; - -} // Scanner - diff --git a/src/libs/scrobbling/impl/Scrobbling.cpp b/src/libs/scrobbling/impl/Scrobbling.cpp deleted file mode 100644 index add4cf2e..00000000 --- a/src/libs/scrobbling/impl/Scrobbling.cpp +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (C) 2021 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "Scrobbling.hpp" - -#include "database/Db.hpp" -#include "database/Session.hpp" -#include "database/TrackList.hpp" -#include "database/User.hpp" - -#include "internal/InternalScrobbler.hpp" -#include "listenbrainz/ListenBrainzScrobbler.hpp" - -namespace Scrobbling -{ - std::unique_ptr - createScrobbling(boost::asio::io_context& ioContext, Database::Db& db) - { - return std::make_unique(ioContext, db); - } - - Scrobbling::Scrobbling(boost::asio::io_context& ioContext, Database::Db& db) - : _db {db} - { - _scrobblers.emplace(Database::Scrobbler::Internal, std::make_unique(_db)); - _scrobblers.emplace(Database::Scrobbler::ListenBrainz, std::make_unique(ioContext, _db)); - } - - void - Scrobbling::listenStarted(const Listen& listen) - { - if (std::optional scrobbler {getUserScrobbler(listen.userId)}) - _scrobblers[*scrobbler]->listenStarted(listen); - } - - void - Scrobbling::listenFinished(const Listen& listen, std::optional duration) - { - if (std::optional scrobbler {getUserScrobbler(listen.userId)}) - _scrobblers[*scrobbler]->listenFinished(listen, duration); - } - - void - Scrobbling::addTimedListen(const TimedListen& listen) - { - if (std::optional scrobbler {getUserScrobbler(listen.userId)}) - _scrobblers[*scrobbler]->addTimedListen(listen); - } - - std::optional - Scrobbling::getUserScrobbler(Database::UserId userId) - { - std::optional scrobbler; - - Database::Session& session {_db.getTLSSession()}; - auto transaction {session.createSharedTransaction()}; - if (const Database::User::pointer user {Database::User::getById(session, userId)}) - scrobbler = user->getScrobbler(); - - return scrobbler; - } - - std::vector> - Scrobbling::getRecentArtists(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional linkType, - std::optional range, - bool& moreResults) - { - const Database::ObjectPtr history {getListensTrackList(session, user)}; - - std::vector> res; - if (history) - res = history->getArtistsReverse(clusterIds, linkType, range, moreResults); - - return res; - } - - std::vector> - Scrobbling::getRecentReleases(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) - { - const Database::ObjectPtr history {getListensTrackList(session, user)}; - - std::vector> res; - if (history) - res = history->getReleasesReverse(clusterIds, range, moreResults); - - return res; - } - - std::vector> - Scrobbling::getRecentTracks(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) - { - const Database::ObjectPtr history {getListensTrackList(session, user)}; - - std::vector> res; - if (history) - res = history->getTracksReverse(clusterIds, range, moreResults); - - return res; - } - - - // Top - std::vector> - Scrobbling::getTopArtists(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional linkType, - std::optional range, - bool& moreResults) - { - const Database::ObjectPtr history {getListensTrackList(session, user)}; - - std::vector> res; - if (history) - res = history->getTopArtists(clusterIds, linkType, range, moreResults); - - return res; - } - - std::vector> - Scrobbling::getTopReleases(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) - { - const Database::ObjectPtr history {getListensTrackList(session, user)}; - - std::vector> res; - if (history) - res = history->getTopReleases(clusterIds, range, moreResults); - - return res; - } - - std::vector> - Scrobbling::getTopTracks(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) - { - const Database::ObjectPtr history {getListensTrackList(session, user)}; - - std::vector> res; - if (history) - res = history->getTopTracks(clusterIds, range, moreResults); - - return res; - } - - Database::ObjectPtr - Scrobbling::getListensTrackList(Database::Session& session, Database::ObjectPtr user) - { - return _scrobblers[user->getScrobbler()]->getListensTrackList(session, user); - } - -} // ns Scrobbling - diff --git a/src/libs/scrobbling/impl/Scrobbling.hpp b/src/libs/scrobbling/impl/Scrobbling.hpp deleted file mode 100644 index 6aa2ed2f..00000000 --- a/src/libs/scrobbling/impl/Scrobbling.hpp +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2021 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include -#include -#include - -#include "scrobbling/IScrobbling.hpp" -#include "IScrobbler.hpp" - -namespace Scrobbling -{ - class Scrobbling : public IScrobbling - { - public: - Scrobbling(boost::asio::io_context& ioContext, Database::Db& db); - - private: - void listenStarted(const Listen& listen) override; - void listenFinished(const Listen& listen, std::optional duration) override; - void addTimedListen(const TimedListen& listen) override; - - std::vector> getRecentArtists(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional linkType, - std::optional range, - bool& moreResults) override; - - std::vector> getRecentReleases(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) override; - - std::vector> getRecentTracks(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) override; - - std::vector> getTopArtists(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional linkType, - std::optional range, - bool& moreResults) override; - - std::vector> getTopReleases(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) override; - - std::vector> getTopTracks(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) override; - - Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user); - - std::optional getUserScrobbler(Database::UserId userId); - - Database::Db& _db; - std::unordered_map> _scrobblers; - }; - -} // ns Scrobbling - diff --git a/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp b/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp deleted file mode 100644 index c17254da..00000000 --- a/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (C) 2021 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "ListenBrainzScrobbler.hpp" - -#include -#include -#include -#include - -#include "database/Artist.hpp" -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackList.hpp" -#include "database/User.hpp" -#include "utils/IConfig.hpp" -#include "utils/Logger.hpp" -#include "utils/Service.hpp" -#include "Utils.hpp" - -#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] - " - -namespace -{ - bool - canBeScrobbled(Database::Session& session, Database::TrackId trackId, std::chrono::seconds duration) - { - auto transaction {session.createSharedTransaction()}; - - const Database::Track::pointer track {Database::Track::getById(session, trackId)}; - if (!track) - return false; - - const bool res {duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2)}; - if (!res) - LOG(DEBUG) << "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast(track->getDuration()).count() << "s"; - - return res; - } - - std::optional - listenToJsonPayload(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint) - { - auto transaction {session.createSharedTransaction()}; - - const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)}; - if (!track) - return std::nullopt; - - auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; - if (artists.empty()) - artists = track->getArtists({Database::TrackArtistLinkType::ReleaseArtist}); - - if (artists.empty()) - { - LOG(DEBUG) << "Track cannot be scrobbled since it does not have any artist"; - return std::nullopt; - } - - Wt::Json::Object additionalInfo; - additionalInfo["listening_from"] = "LMS"; - if (track->getRelease()) - { - if (auto MBID {track->getRelease()->getMBID()}) - additionalInfo["release_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}}; - } - - { - Wt::Json::Array artistMBIDs; - for (const Database::Artist::pointer& artist : artists) - { - if (auto MBID {artist->getMBID()}) - artistMBIDs.push_back(Wt::Json::Value {std::string {MBID->getAsString()}}); - } - - if (!artistMBIDs.empty()) - additionalInfo["artist_mbids"] = std::move(artistMBIDs); - } - - if (auto MBID {track->getTrackMBID()}) - additionalInfo["track_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}}; - - if (auto MBID {track->getRecordingMBID()}) - additionalInfo["recording_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}}; - - if (const std::optional trackNumber {track->getTrackNumber()}) - additionalInfo["tracknumber"] = Wt::Json::Value {static_cast(*trackNumber)}; - - Wt::Json::Object trackMetadata; - trackMetadata["additional_info"] = std::move(additionalInfo); - trackMetadata["artist_name"] = Wt::Json::Value {artists.front()->getName()}; - trackMetadata["track_name"] = Wt::Json::Value {track->getName()}; - if (track->getRelease()) - trackMetadata["release_name"] = Wt::Json::Value {track->getRelease()->getName()}; - - Wt::Json::Object payload; - payload["track_metadata"] = std::move(trackMetadata); - if (timePoint.isValid()) - payload["listened_at"] = Wt::Json::Value {static_cast(timePoint.toTime_t())}; - - return payload; - } - - std::string - listenToJsonString(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint, std::string_view listenType) - { - std::string res; - - std::optional payload {listenToJsonPayload(session, listen, timePoint)}; - if (!payload) - return res; - - Wt::Json::Object root; - root["listen_type"] = Wt::Json::Value {std::string {listenType}}; - root["payload"] = Wt::Json::Array {std::move(*payload)}; - - res = Wt::Json::serialize(root); - return res; - } -} - -namespace Scrobbling::ListenBrainz -{ - Scrobbler::Scrobbler(boost::asio::io_context& ioContext, Database::Db& db) - : _ioContext {ioContext} - , _db {db} - , _sendQueue {_ioContext, Service::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org")} - , _listensSynchronizer {_ioContext, db, _sendQueue} - { - LOG(INFO) << "Starting ListenBrainz scrobbler... API endpoint = '" << _sendQueue.getAPIBaseURL(); - } - - Scrobbler::~Scrobbler() - { - LOG(INFO) << "Stopped ListenBrainz scrobbler!"; - } - - void - Scrobbler::listenStarted(const Listen& listen) - { - enqueListen(listen, Wt::WDateTime {}); - } - - void - Scrobbler::listenFinished(const Listen& listen, std::optional duration) - { - if (duration && !canBeScrobbled(_db.getTLSSession(), listen.trackId, *duration)) - return; - - const Listen timedListen {listen}; - const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; - - enqueListen(timedListen, now); - } - - void - Scrobbler::addTimedListen(const TimedListen& listen) - { - assert(listen.listenedAt.isValid()); - enqueListen(listen, listen.listenedAt); - } - - Database::TrackList::pointer - Scrobbler::getListensTrackList(Database::Session& session, Database::User::pointer user) - { - return Utils::getListensTrackList(session, user); - } - - void - Scrobbler::enqueListen(const Listen& listen, const Wt::WDateTime& timePoint) - { - std::optional requestData {createSubmitListenRequestData(listen, timePoint)}; - if (!requestData) - return; - - SendQueue::Request submitListen {std::move(*requestData)}; - if (timePoint.isValid()) - { - submitListen.setPriority(SendQueue::Request::Priority::Normal); - submitListen.setOnSuccessFunc([=](std::string_view) - { - _listensSynchronizer.saveListen(TimedListen {listen, timePoint}); - }); - } - else - { - // We want "listen now" to appear as soon as possible - submitListen.setPriority(SendQueue::Request::Priority::High); - } - - _sendQueue.enqueueRequest(std::move(submitListen)); - } - - std::optional - Scrobbler::createSubmitListenRequestData(const Listen& listen, const Wt::WDateTime& timePoint) - { - Database::Session& session {_db.getTLSSession()}; - - const std::optional listenBrainzToken {Utils::getListenBrainzToken(session, listen.userId)}; - if (!listenBrainzToken) - return std::nullopt; - - SendQueue::RequestData requestData; - requestData.endpoint = "/1/submit-listens"; - requestData.type = SendQueue::RequestData::Type::POST; - - std::string bodyText {listenToJsonString(session, listen, timePoint, timePoint.isValid() ? "single" : "playing_now")}; - if (bodyText.empty()) - { - LOG(DEBUG) << "Cannot convert listen to json: skipping"; - return std::nullopt; - } - - requestData.message.addBodyText(bodyText); - requestData.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()}); - requestData.message.addHeader("Content-Type", "application/json"); - - return requestData; - } -} // namespace Scrobbling::ListenBrainz - diff --git a/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp deleted file mode 100644 index 8a1809cc..00000000 --- a/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ /dev/null @@ -1,524 +0,0 @@ -/* - * Copyright (C) 2021 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "ListenBrainzScrobbler.hpp" - -#include -#include -#include -#include -#include - -#include "database/Artist.hpp" -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackList.hpp" -#include "database/User.hpp" -#include "scrobbling/Exception.hpp" -#include "utils/IConfig.hpp" -#include "utils/Logger.hpp" -#include "utils/Service.hpp" - -#include "Utils.hpp" - -#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz Synchronizer] - " - -namespace -{ - using namespace Scrobbling::ListenBrainz; - - SendQueue::RequestData - createValidateTokenRequestData(std::string_view authToken) - { - SendQueue::RequestData requestData; - requestData.type = SendQueue::RequestData::Type::GET; - requestData.endpoint = "/1/validate-token"; - requestData.headers = { {"Authorization", "Token " + std::string {authToken}} }; - - return requestData; - } - - std::string - parseValidateToken(std::string_view msgBody) - { - std::string listenBrainzUserName; - - Wt::Json::ParseError error; - Wt::Json::Object root; - if (!Wt::Json::parse(std::string {msgBody}, root, error)) - { - LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what(); - return listenBrainzUserName; - } - - if (!root.get("valid").orIfNull(false)) - { - LOG(INFO) << "Invalid listenbrainz user"; - return listenBrainzUserName; - } - - listenBrainzUserName = root.get("user_name").orIfNull(""); - return listenBrainzUserName; - } - - SendQueue::RequestData - createListenCountRequestData(std::string_view listenBrainzUserName) - { - LOG(DEBUG) << "Getting listen count for listenbrainz user '" << listenBrainzUserName << "'"; - - SendQueue::RequestData requestData; - requestData.type = SendQueue::RequestData::Type::GET; - requestData.endpoint = "/1/user/" + std::string {listenBrainzUserName} + "/listen-count"; - - return requestData; - } - - std::optional - parseListenCount(std::string_view msgBody) - { - try - { - Wt::Json::Object root; - Wt::Json::parse(std::string {msgBody}, root); - - const Wt::Json::Object& payload {static_cast(root.get("payload"))}; - return static_cast(payload.get("count")); - } - catch (const Wt::WException& e) - { - LOG(ERROR) << "Cannot parse listen count response: " << e.what(); - return std::nullopt; - } - } - - SendQueue::RequestData - createGetListensRequestData(std::string_view listenBrainzUserName, const Wt::WDateTime& maxDateTime) - { - LOG(DEBUG) << "Getting listens for listenbrainz user '" << listenBrainzUserName << "' with max_ts = " << maxDateTime.toString(); - - SendQueue::RequestData requestData; - requestData.type = SendQueue::RequestData::Type::GET; - requestData.endpoint = "/1/user/" + std::string {listenBrainzUserName} + "/listens?max_ts=" + std::to_string(maxDateTime.toTime_t()); - - return requestData; - } - - Database::Track::pointer - tryMatchListen(Database::Session& session, const Wt::Json::Object& metadata) - { - Database::Track::pointer track; - - // first try to get the associated track using MBIDs, and then fallback on names - if (metadata.type("additional_info") == Wt::Json::Type::Object) - { - const Wt::Json::Object& additionalInfo = metadata.get("additional_info"); - if (std::optional recordingMBID {UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""))}) - { - const auto tracks {Database::Track::getByRecordingMBID(session, *recordingMBID)}; - // if duplicated files, do not record it (let the user correct its database) - if (tracks.size() == 1) - track = tracks.front(); - } - } - - if (track) - return track; - - // these fields are mandatory - const std::string trackName {static_cast(metadata.get("track_name"))}; - const std::string releaseName {static_cast(metadata.get("release_name"))}; - - auto tracks {Database::Track::getByNameAndReleaseName(session, trackName, releaseName)}; - if (tracks.size() > 1) - { - tracks.erase(std::remove_if(std::begin(tracks), std::end(tracks), - [&](const Database::Track::pointer track) - { - if (std::string artistName {metadata.get("artist_name").orIfNull("")}; !artistName.empty()) - { - const auto& artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; - if (std::none_of(std::begin(artists), std::end(artists), [&](const Database::Artist::pointer& artist) { return artist->getName() == artistName; })) - return true; - } - if (metadata.type("additional_info") == Wt::Json::Type::Object) - { - const Wt::Json::Object& additionalInfo = metadata.get("additional_info"); - if (track->getTrackNumber()) - { - int otherTrackNumber {additionalInfo.get("tracknumber").orIfNull(-1)}; - if (otherTrackNumber > 0 && static_cast(otherTrackNumber) != *track->getTrackNumber()) - return true; - } - - if (auto releaseMBID {track->getRelease()->getMBID()}) - { - if (std::optional otherReleaseMBID {UUID::fromString(additionalInfo.get("release_mbid").orIfNull(""))}) - { - if (otherReleaseMBID->getAsString() != releaseMBID->getAsString()) - return true; - } - } - } - - return false; - }), std::end(tracks)); - } - - if (tracks.size() == 1) - track = tracks.front(); - - return track; - } - - struct ParseGetListensResult - { - Wt::WDateTime oldestEntry; - std::size_t listenCount{}; - std::vector matchedListens; - }; - ParseGetListensResult - parseGetListens(Database::Session& session, std::string_view msgBody, Database::UserId userId) - { - ParseGetListensResult result; - - try - { - Wt::Json::Object root; - Wt::Json::parse(std::string {msgBody}, root); - - const Wt::Json::Object& payload = root.get("payload"); - const Wt::Json::Array& listens = payload.get("listens"); - - LOG(DEBUG) << "Got " << listens.size() << " listens"; - - if (listens.empty()) - return result; - - auto transaction {session.createSharedTransaction()}; - - for (const Wt::Json::Value& value : listens) - { - const Wt::Json::Object& listen = value; - const Wt::WDateTime listenedAt {Wt::WDateTime::fromTime_t(static_cast(listen.get("listened_at")))}; - const Wt::Json::Object& metadata = listen.get("track_metadata"); - - if (!listenedAt.isValid()) - { - LOG(ERROR) << "bad listened_at field!"; - continue; - } - - result.listenCount++; - if (!result.oldestEntry.isValid()) - result.oldestEntry = listenedAt; - else if (listenedAt < result.oldestEntry) - result.oldestEntry = listenedAt; - - if (const Database::Track::pointer track {tryMatchListen(session, metadata)}) - result.matchedListens.emplace_back(Scrobbling::TimedListen {{userId, track->getId()}, listenedAt}); - } - } - catch (const Wt::WException& error) - { - LOG(ERROR) << "Cannot parse 'get-listens' result: " << error.what(); - } - - return result; - } -} - -namespace Scrobbling::ListenBrainz -{ - ListensSynchronizer::ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, SendQueue& sendQueue) - : _ioContext {ioContext} - , _db {db} - , _sendQueue {sendQueue} - , _maxSyncListenCount {Service::get()->getULong("listenbrainz-max-sync-listen-count", 1000)} - , _syncListensPeriod {Service::get()->getULong("listenbrainz-sync-listens-period-hours", 1)} - { - LOG(INFO) << "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours"; - - scheduleGetListens(std::chrono::seconds {30}); - } - - void - ListensSynchronizer::saveListen(const TimedListen& listen) - { - _strand.dispatch([=] - { - Database::Session& session {_db.getTLSSession()}; - - auto transaction {session.createUniqueTransaction()}; - - const Database::User::pointer user {Database::User::getById(session, listen.userId)}; - if (!user) - return; - - const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)}; - if (!track) - return; - - Database::TrackListEntry::create(session, track, Utils::getOrCreateListensTrackList(session, user), listen.listenedAt); - - UserContext& context {getUserContext(listen.userId)}; - if (context.listenCount) - (*context.listenCount)++; - }); - } - - ListensSynchronizer::UserContext& - ListensSynchronizer::getUserContext(Database::UserId userId) - { - auto itContext {_userContexts.find(userId)}; - if (itContext == std::cend(_userContexts)) - { - auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)}; - itContext = itNewContext; - } - - return itContext->second; - } - - bool - ListensSynchronizer::isFetching() const - { - return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry) - { - const auto& [userId, context] {contextEntry}; - return context.fetching; - }); - } - - void - ListensSynchronizer::scheduleGetListens(std::chrono::seconds fromNow) - { - if (_syncListensPeriod.count() == 0 || _maxSyncListenCount == 0) - return; - - LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds..."; - _getListensTimer.expires_after(fromNow); - _getListensTimer.async_wait(boost::asio::bind_executor(_strand, [this] (const boost::system::error_code& ec) - { - if (ec == boost::asio::error::operation_aborted) - { - LOG(DEBUG) << "getListens aborted"; - return; - } - else if (ec) - { - throw Exception {"GetListens timer failure: " + std::string {ec.message()} }; - } - - startGetListens(); - })); - } - - void - ListensSynchronizer::startGetListens() - { - LOG(DEBUG) << "GetListens started!!!"; - - assert(!isFetching()); - - std::vector userIds; - { - Database::Session& session {_db.getTLSSession()}; - auto transaction {session.createSharedTransaction()}; - userIds = Database::User::getAllIds(_db.getTLSSession()); - } - - for (const Database::UserId userId : userIds) - { - if (Utils::getListenBrainzToken(_db.getTLSSession(), userId)) - startGetListens(getUserContext(userId)); - } - - if (!isFetching()) - scheduleGetListens(_syncListensPeriod); - } - - void - ListensSynchronizer::startGetListens(UserContext& context) - { - context.fetching = true; - context.listenBrainzUserName = ""; - context.maxDateTime = {}; - context.fetchedListenCount = 0; - context.matchedListenCount = 0; - context.importedListenCount = 0; - - enqueValidateToken(context); - } - - void - ListensSynchronizer::onGetListensEnded(UserContext& context) - { - _strand.dispatch([this, &context] - { - LOG(DEBUG) << "Fetch done for user " << context.userId.getValue() << ", fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount; - context.fetching = false; - - if (!isFetching()) - scheduleGetListens(_syncListensPeriod); - }); - } - - void - ListensSynchronizer::enqueValidateToken(UserContext& context) - { - assert(context.listenBrainzUserName.empty()); - - std::optional requestData {createValidateTokenRequestData(context.userId)}; - if (!requestData) - { - onGetListensEnded(context); - return; - } - - SendQueue::Request validateTokenRequest {std::move(*requestData)}; - validateTokenRequest.setOnSuccessFunc([this, &context] (std::string_view msgBody) - { - context.listenBrainzUserName = parseValidateToken(msgBody); - if (context.listenBrainzUserName.empty()) - { - onGetListensEnded(context); - return; - } - enqueGetListenCount(context); - }); - validateTokenRequest.setOnFailureFunc([this, &context] - { - onGetListensEnded(context); - }); - - validateTokenRequest.setPriority(SendQueue::Request::Priority::Low); - _sendQueue.enqueueRequest(std::move(validateTokenRequest)); - } - - void - ListensSynchronizer::enqueGetListenCount(UserContext& context) - { - assert(!context.listenBrainzUserName.empty()); - - SendQueue::Request getListenCountRequest {createListenCountRequestData(context.listenBrainzUserName)}; - getListenCountRequest.setOnSuccessFunc([=, &context] (std::string_view msgBody) - { - const auto listenCount = parseListenCount(msgBody); - if (listenCount) - LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount; - - bool needSync {listenCount && (!context.listenCount || *context.listenCount != *listenCount)}; - context.listenCount = listenCount; - - if (!needSync) - { - onGetListensEnded(context); - return; - } - - context.maxDateTime = Wt::WDateTime::currentDateTime(); - enqueGetListens(context); - }); - getListenCountRequest.setOnFailureFunc([this, &context] - { - onGetListensEnded(context); - }); - - getListenCountRequest.setPriority(SendQueue::Request::Priority::Low); - _sendQueue.enqueueRequest(std::move(getListenCountRequest)); - } - - void - ListensSynchronizer::enqueGetListens(UserContext& context) - { - assert(!context.listenBrainzUserName.empty()); - - SendQueue::Request getListensRequest {::createGetListensRequestData(context.listenBrainzUserName, context.maxDateTime)}; - getListensRequest.setOnSuccessFunc([=, &context] (std::string_view msgBody) - { - processGetListensResponse(msgBody, context); - if (context.fetchedListenCount >= _maxSyncListenCount || !context.maxDateTime.isValid()) - { - onGetListensEnded(context); - return; - } - - enqueGetListens(context); - }); - getListensRequest.setOnFailureFunc([=, &context] - { - onGetListensEnded(context); - }); - - getListensRequest.setPriority(SendQueue::Request::Priority::Low); - _sendQueue.enqueueRequest(std::move(getListensRequest)); - } - - std::optional - ListensSynchronizer::createValidateTokenRequestData(Database::UserId userId) - { - Database::Session& session {_db.getTLSSession()}; - - const std::optional listenBrainzToken {Utils::getListenBrainzToken(session, userId)}; - if (!listenBrainzToken) - return std::nullopt; - - return ::createValidateTokenRequestData(listenBrainzToken->getAsString()); - } - - void - ListensSynchronizer::processGetListensResponse(std::string_view msgBody, UserContext& context) - { - Database::Session& session {_db.getTLSSession()}; - - const ParseGetListensResult parseResult {parseGetListens(session, msgBody, context.userId)}; - - context.fetchedListenCount += parseResult.listenCount; - context.matchedListenCount += parseResult.matchedListens.size(); - context.maxDateTime = parseResult.oldestEntry; - - if (parseResult.matchedListens.empty()) - return; - - auto transaction {session.createUniqueTransaction()}; - - Database::User::pointer user {Database::User::getById(session, context.userId)}; - if (!user) - return; - - Database::TrackList::pointer tracklist {Utils::getOrCreateListensTrackList(session, user)}; - - for (const TimedListen& listen : parseResult.matchedListens) - { - const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)}; - if (!track) - continue; - - if (!tracklist->getEntryByTrackAndDateTime(track, listen.listenedAt)) - { - context.importedListenCount++; - Database::TrackListEntry::create(session, track, tracklist, listen.listenedAt); - } - } - } - -} // namespace Scrobbling::ListenBrainz - diff --git a/src/libs/scrobbling/impl/listenbrainz/Utils.cpp b/src/libs/scrobbling/impl/listenbrainz/Utils.cpp deleted file mode 100644 index ab293954..00000000 --- a/src/libs/scrobbling/impl/listenbrainz/Utils.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2021 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "Utils.hpp" - -#include - -#include "database/Session.hpp" -#include "database/TrackList.hpp" -#include "database/User.hpp" - -static constexpr std::string_view historyTracklistName {"__scrobbler_listenbrainz_history__"}; - -namespace Scrobbling::ListenBrainz::Utils -{ - std::optional - getListenBrainzToken(Database::Session& session, Database::UserId userId) - { - auto transaction {session.createSharedTransaction()}; - - const Database::User::pointer user {Database::User::getById(session, userId)}; - if (!user) - return std::nullopt; - - if (user->getScrobbler() != Database::Scrobbler::ListenBrainz) - return std::nullopt; - - return user->getListenBrainzToken(); - } - - Database::TrackList::pointer - getListensTrackList(Database::Session& session, Database::User::pointer user) - { - return Database::TrackList::get(session, historyTracklistName, Database::TrackList::Type::Internal, user); - } - - Database::TrackList::pointer - getOrCreateListensTrackList(Database::Session& session, Database::User::pointer user) - { - Database::TrackList::pointer tracklist {getListensTrackList(session, user)}; - if (!tracklist) - tracklist = Database::TrackList::create(session, historyTracklistName, Database::TrackList::Type::Internal, false, user); - - return tracklist; - } - -} diff --git a/src/libs/scrobbling/impl/listenbrainz/Utils.hpp b/src/libs/scrobbling/impl/listenbrainz/Utils.hpp deleted file mode 100644 index 6b2d1271..00000000 --- a/src/libs/scrobbling/impl/listenbrainz/Utils.hpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2021 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include -#include "utils/UUID.hpp" - -#include "database/Types.hpp" - -namespace Database -{ - class Session; - class TrackList; - class User; -} - -namespace Scrobbling::ListenBrainz::Utils -{ - std::optional getListenBrainzToken(Database::Session& session, Database::UserId userId); - Database::ObjectPtr getOrCreateListensTrackList(Database::Session& session, Database::ObjectPtr user); - Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user); -} diff --git a/src/libs/scrobbling/include/scrobbling/IScrobbling.hpp b/src/libs/scrobbling/include/scrobbling/IScrobbling.hpp deleted file mode 100644 index 6f4436c7..00000000 --- a/src/libs/scrobbling/include/scrobbling/IScrobbling.hpp +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2021 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include - -#include -#include -#include -#include - -#include - -#include "scrobbling/Listen.hpp" -#include "database/Types.hpp" - -namespace Database -{ - class Artist; - class Db; - class Release; - class Session; - class Track; - class User; -} - -namespace Scrobbling -{ - - class IScrobbling - { - public: - virtual ~IScrobbling() = default; - - // Scrobbling - virtual void listenStarted(const Listen& listen) = 0; - virtual void listenFinished(const Listen& listen, std::optional playedDuration = std::nullopt) = 0; - - virtual void addTimedListen(const TimedListen& listen) = 0; - - // Stats - // From most recent to oldest - virtual std::vector> getRecentArtists(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional linkType, - std::optional range, - bool& moreResults) = 0; - - virtual std::vector> getRecentReleases(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) = 0; - - virtual std::vector> getRecentTracks(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) = 0; - - // Top - virtual std::vector> getTopArtists(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional linkType, - std::optional range, - bool& moreResults) = 0; - - virtual std::vector> getTopReleases(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) = 0; - - virtual std::vector> getTopTracks(Database::Session& session, - Database::ObjectPtr user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) = 0; - }; - - std::unique_ptr createScrobbling(boost::asio::io_service& ioService, Database::Db& db); - -} // ns Scrobbling - diff --git a/src/libs/services/CMakeLists.txt b/src/libs/services/CMakeLists.txt new file mode 100644 index 00000000..fd4d3160 --- /dev/null +++ b/src/libs/services/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(auth) +add_subdirectory(cover) +add_subdirectory(database) +add_subdirectory(recommendation) +add_subdirectory(scanner) +add_subdirectory(scrobbling) diff --git a/src/libs/auth/CMakeLists.txt b/src/libs/services/auth/CMakeLists.txt similarity index 100% rename from src/libs/auth/CMakeLists.txt rename to src/libs/services/auth/CMakeLists.txt diff --git a/src/libs/auth/impl/AuthServiceBase.cpp b/src/libs/services/auth/impl/AuthServiceBase.cpp similarity index 59% rename from src/libs/auth/impl/AuthServiceBase.cpp rename to src/libs/services/auth/impl/AuthServiceBase.cpp index b4ab450a..01b87a0a 100644 --- a/src/libs/auth/impl/AuthServiceBase.cpp +++ b/src/libs/services/auth/impl/AuthServiceBase.cpp @@ -19,25 +19,33 @@ #include "AuthServiceBase.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" +#include "services/database/Db.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" #include "utils/Logger.hpp" namespace Auth { - Database::UserId - AuthServiceBase::getOrCreateUser(Database::Session& session, std::string_view loginName) + using namespace Database; + + AuthServiceBase::AuthServiceBase(Db& db) + : _db {db} + {} + + UserId + AuthServiceBase::getOrCreateUser(std::string_view loginName) { + Session& session {getDbSession()}; auto transaction {session.createUniqueTransaction()}; - Database::User::pointer user {Database::User::getByLoginName(session, loginName)}; + User::pointer user {User::find(session, loginName)}; if (!user) { - const Database::UserType type {Database::User::getCount(session) == 0 ? Database::UserType::ADMIN : Database::UserType::REGULAR}; + const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR}; - LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == Database::UserType::ADMIN); + LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN); - user = Database::User::create(session, loginName); + user = User::create(session, loginName); user.modify()->setType(type); } @@ -45,11 +53,19 @@ namespace Auth } void - AuthServiceBase::onUserAuthenticated(Database::Session& session, Database::UserId userId) + AuthServiceBase::onUserAuthenticated(UserId userId) { + Session& session {getDbSession()}; auto transaction {session.createUniqueTransaction()}; - Database::User::pointer user {Database::User::getById(session, userId)}; + + User::pointer user {User::find(session, userId)}; if (user) user.modify()->setLastLogin(Wt::WDateTime::currentDateTime()); } + + Session& + AuthServiceBase::getDbSession() + { + return _db.getTLSSession(); + } } diff --git a/src/libs/auth/impl/AuthServiceBase.hpp b/src/libs/services/auth/impl/AuthServiceBase.hpp similarity index 74% rename from src/libs/auth/impl/AuthServiceBase.hpp rename to src/libs/services/auth/impl/AuthServiceBase.hpp index 032d8f84..50879896 100644 --- a/src/libs/auth/impl/AuthServiceBase.hpp +++ b/src/libs/services/auth/impl/AuthServiceBase.hpp @@ -20,10 +20,11 @@ #pragma once #include -#include "database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { + class Db; class Session; } @@ -32,7 +33,14 @@ namespace Auth class AuthServiceBase { protected: - Database::UserId getOrCreateUser(Database::Session& session, std::string_view loginName); - void onUserAuthenticated(Database::Session& session, Database::UserId userId); + AuthServiceBase(Database::Db& db); + + Database::UserId getOrCreateUser(std::string_view loginName); + void onUserAuthenticated(Database::UserId userId); + + Database::Session& getDbSession(); + + private: + Database::Db& _db; }; } diff --git a/src/libs/auth/impl/AuthTokenService.cpp b/src/libs/services/auth/impl/AuthTokenService.cpp similarity index 71% rename from src/libs/auth/impl/AuthTokenService.cpp rename to src/libs/services/auth/impl/AuthTokenService.cpp index 89ad8870..01ac9c87 100644 --- a/src/libs/auth/impl/AuthTokenService.cpp +++ b/src/libs/services/auth/impl/AuthTokenService.cpp @@ -23,36 +23,40 @@ #include #include -#include "auth/Types.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" +#include "services/auth/Types.hpp" +#include "services/database/AuthToken.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" namespace Auth { - std::unique_ptr createAuthTokenService(std::size_t maxThrottlerEntries) + std::unique_ptr createAuthTokenService(Database::Db& db, std::size_t maxThrottlerEntries) { - return std::make_unique(maxThrottlerEntries); + return std::make_unique(db, maxThrottlerEntries); } static const Wt::Auth::SHA1HashFunction sha1Function; - AuthTokenService::AuthTokenService(std::size_t maxThrottlerEntries) - : _loginThrottler {maxThrottlerEntries} + AuthTokenService::AuthTokenService(Database::Db& db, std::size_t maxThrottlerEntries) + : AuthServiceBase {db} + , _loginThrottler {maxThrottlerEntries} { } std::string - AuthTokenService::createAuthToken(Database::Session& session, Database::UserId userId, const Wt::WDateTime& expiry) + AuthTokenService::createAuthToken(Database::UserId userId, const Wt::WDateTime& expiry) { const std::string secret {Wt::WRandom::generateId(32)}; const std::string secretHash {sha1Function.compute(secret, {})}; + Database::Session& session {getDbSession()}; + auto transaction {session.createUniqueTransaction()}; - Database::User::pointer user {Database::User::getById(session, userId)}; + Database::User::pointer user {Database::User::find(session, userId)}; if (!user) throw Exception {"User deleted"}; @@ -66,15 +70,15 @@ namespace Auth return secret; } - static std::optional - processAuthToken(Database::Session& session, std::string_view secret) + AuthTokenService::processAuthToken(std::string_view secret) { const std::string secretHash {sha1Function.compute(std::string {secret}, {})}; + Database::Session& session {getDbSession()}; auto transaction {session.createUniqueTransaction()}; - Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(session, secretHash)}; + Database::AuthToken::pointer authToken {Database::AuthToken::find(session, secretHash)}; if (!authToken) return std::nullopt; @@ -93,7 +97,7 @@ namespace Auth } AuthTokenService::AuthTokenProcessResult - AuthTokenService::processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) + AuthTokenService::processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) { // Do not waste too much resource on brute force attacks (optim) { @@ -103,7 +107,7 @@ namespace Auth return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled}; } - auto res {Auth::processAuthToken(session, tokenValue)}; + auto res {processAuthToken(tokenValue)}; { std::unique_lock lock {_mutex}; @@ -117,17 +121,19 @@ namespace Auth } _loginThrottler.onGoodClientAttempt(clientAddress); - onUserAuthenticated(session, res->userId); + onUserAuthenticated(res->userId); return AuthTokenProcessResult {AuthTokenProcessResult::State::Granted, std::move(*res)}; } } void - AuthTokenService::clearAuthTokens(Database::Session& session, Database::UserId userId) + AuthTokenService::clearAuthTokens(Database::UserId userId) { + Database::Session& session {getDbSession()}; + auto transaction {session.createUniqueTransaction()}; - Database::User::pointer user {Database::User::getById(session, userId)}; + Database::User::pointer user {Database::User::find(session, userId)}; if (!user) throw Exception {"User deleted"}; diff --git a/src/libs/auth/impl/AuthTokenService.hpp b/src/libs/services/auth/impl/AuthTokenService.hpp similarity index 70% rename from src/libs/auth/impl/AuthTokenService.hpp rename to src/libs/services/auth/impl/AuthTokenService.hpp index 088f82ee..e95a8a79 100644 --- a/src/libs/auth/impl/AuthTokenService.hpp +++ b/src/libs/services/auth/impl/AuthTokenService.hpp @@ -21,7 +21,7 @@ #include -#include "auth/IAuthTokenService.hpp" +#include "services/auth/IAuthTokenService.hpp" #include "AuthServiceBase.hpp" #include "LoginThrottler.hpp" @@ -35,7 +35,7 @@ namespace Auth class AuthTokenService : public IAuthTokenService, public AuthServiceBase { public: - AuthTokenService(std::size_t maxThrottlerEntries); + AuthTokenService(Database::Db& db, std::size_t maxThrottlerEntries); AuthTokenService(const AuthTokenService&) = delete; AuthTokenService& operator=(const AuthTokenService&) = delete; @@ -43,9 +43,11 @@ namespace Auth AuthTokenService& operator=(AuthTokenService&&) = delete; private: - AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override; - std::string createAuthToken(Database::Session& session, Database::UserId userId, const Wt::WDateTime& expiry) override; - void clearAuthTokens(Database::Session& session, Database::UserId userId) override; + AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override; + std::string createAuthToken(Database::UserId userId, const Wt::WDateTime& expiry) override; + void clearAuthTokens(Database::UserId userId) override; + + std::optional processAuthToken(std::string_view secret); std::shared_mutex _mutex; LoginThrottler _loginThrottler; diff --git a/src/libs/auth/impl/EnvService.cpp b/src/libs/services/auth/impl/EnvService.cpp similarity index 82% rename from src/libs/auth/impl/EnvService.cpp rename to src/libs/services/auth/impl/EnvService.cpp index 5ca529da..26f972a6 100644 --- a/src/libs/auth/impl/EnvService.cpp +++ b/src/libs/services/auth/impl/EnvService.cpp @@ -17,18 +17,18 @@ * along with LMS. If not, see . */ -#include "auth/IEnvService.hpp" +#include "services/auth/IEnvService.hpp" -#include "auth/Types.hpp" +#include "services/auth/Types.hpp" #include "http-headers/HttpHeadersEnvService.hpp" namespace Auth { std::unique_ptr - createEnvService(std::string_view backendName) + createEnvService(std::string_view backendName, Database::Db& db) { if (backendName == "http-headers") - return std::make_unique(); + return std::make_unique(db); throw Exception {"Authentication backend '" + std::string {backendName} + "' is not supported!"}; } diff --git a/src/libs/auth/impl/LoginThrottler.cpp b/src/libs/services/auth/impl/LoginThrottler.cpp similarity index 100% rename from src/libs/auth/impl/LoginThrottler.cpp rename to src/libs/services/auth/impl/LoginThrottler.cpp diff --git a/src/libs/auth/impl/LoginThrottler.hpp b/src/libs/services/auth/impl/LoginThrottler.hpp similarity index 62% rename from src/libs/auth/impl/LoginThrottler.hpp rename to src/libs/services/auth/impl/LoginThrottler.hpp index b8390630..b60ca666 100644 --- a/src/libs/auth/impl/LoginThrottler.hpp +++ b/src/libs/services/auth/impl/LoginThrottler.hpp @@ -28,27 +28,24 @@ #include "utils/NetAddress.hpp" #include "utils/Exception.hpp" -namespace Auth { - -class LoginThrottler +namespace Auth { - public: - LoginThrottler(std::size_t maxEntries) : _maxEntries {maxEntries} {} + class LoginThrottler + { + public: + LoginThrottler(std::size_t maxEntries) : _maxEntries {maxEntries} {} - // user must lock these calls to avoid races - bool isClientThrottled(const boost::asio::ip::address& address) const; - void onBadClientAttempt(const boost::asio::ip::address& address); - void onGoodClientAttempt(const boost::asio::ip::address& address); + // user must lock these calls to avoid races + bool isClientThrottled(const boost::asio::ip::address& address) const; + void onBadClientAttempt(const boost::asio::ip::address& address); + void onGoodClientAttempt(const boost::asio::ip::address& address); - private: - - void removeOutdatedEntries(); - - const std::size_t _maxEntries; - - std::unordered_map _attemptsInfo; -}; + private: + void removeOutdatedEntries(); + const std::size_t _maxEntries; + std::unordered_map _attemptsInfo; + }; } // Auth diff --git a/src/libs/auth/impl/PasswordServiceBase.cpp b/src/libs/services/auth/impl/PasswordServiceBase.cpp similarity index 71% rename from src/libs/auth/impl/PasswordServiceBase.cpp rename to src/libs/services/auth/impl/PasswordServiceBase.cpp index 02a9c63d..79f57af1 100644 --- a/src/libs/auth/impl/PasswordServiceBase.cpp +++ b/src/libs/services/auth/impl/PasswordServiceBase.cpp @@ -27,9 +27,9 @@ #include "pam/PAMPasswordService.hpp" #endif // LMS_SUPPORT_PAM -#include "auth/Types.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" +#include "services/auth/Types.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" @@ -39,29 +39,27 @@ namespace Auth static const Wt::Auth::SHA1HashFunction sha1Function; std::unique_ptr - createPasswordService(std::string_view passwordAuthenticationBackend, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) + createPasswordService(std::string_view passwordAuthenticationBackend, Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) { if (passwordAuthenticationBackend == "internal") - return std::make_unique(maxThrottlerEntries, authTokenService); + return std::make_unique(db, maxThrottlerEntries, authTokenService); #ifdef LMS_SUPPORT_PAM else if (passwordAuthenticationBackend == "pam") - return std::make_unique(maxThrottlerEntries, authTokenService); + return std::make_unique(db, maxThrottlerEntries, authTokenService); #endif // LMS_SUPPORT_PAM throw Exception {"Authentication backend '" + std::string {passwordAuthenticationBackend} + "' is not supported!"}; } - PasswordServiceBase::PasswordServiceBase(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) - : _loginThrottler {maxThrottlerEntries} + PasswordServiceBase::PasswordServiceBase(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) + : AuthServiceBase {db} + , _loginThrottler {maxThrottlerEntries} , _authTokenService {authTokenService} { } PasswordServiceBase::CheckResult - PasswordServiceBase::checkUserPassword(Database::Session& session, - const boost::asio::ip::address& clientAddress, - std::string_view loginName, - std::string_view password) + PasswordServiceBase::checkUserPassword(const boost::asio::ip::address& clientAddress, std::string_view loginName, std::string_view password) { LMS_LOG(AUTH, DEBUG) << "Checking password for user '" << loginName << "'"; @@ -73,7 +71,7 @@ namespace Auth return {CheckResult::State::Throttled}; } - const bool match {checkUserPassword(session, loginName, password)}; + const bool match {checkUserPassword(loginName, password)}; { std::unique_lock lock {_mutex}; @@ -84,8 +82,8 @@ namespace Auth { _loginThrottler.onGoodClientAttempt(clientAddress); - const Database::UserId userId {getOrCreateUser(session, loginName)}; - onUserAuthenticated(session, userId); + const Database::UserId userId {getOrCreateUser(loginName)}; + onUserAuthenticated(userId); return {CheckResult::State::Granted, userId}; } else diff --git a/src/libs/auth/impl/PasswordServiceBase.hpp b/src/libs/services/auth/impl/PasswordServiceBase.hpp similarity index 74% rename from src/libs/auth/impl/PasswordServiceBase.hpp rename to src/libs/services/auth/impl/PasswordServiceBase.hpp index 8b5bc3dd..9f9575ec 100644 --- a/src/libs/auth/impl/PasswordServiceBase.hpp +++ b/src/libs/services/auth/impl/PasswordServiceBase.hpp @@ -21,12 +21,13 @@ #include -#include "auth/IPasswordService.hpp" +#include "services/auth/IPasswordService.hpp" #include "AuthServiceBase.hpp" #include "LoginThrottler.hpp" namespace Database { + class Db; class Session; } @@ -36,7 +37,7 @@ namespace Auth class PasswordServiceBase : public IPasswordService, public AuthServiceBase { public: - PasswordServiceBase(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService); + PasswordServiceBase(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService); PasswordServiceBase(const PasswordServiceBase&) = delete; PasswordServiceBase& operator=(const PasswordServiceBase&) = delete; @@ -47,14 +48,11 @@ namespace Auth IAuthTokenService& getAuthTokenService() { return _authTokenService; } private: - virtual bool checkUserPassword(Database::Session& session, - std::string_view loginName, - std::string_view password) = 0; + virtual bool checkUserPassword(std::string_view loginName, std::string_view password) = 0; - CheckResult checkUserPassword(Database::Session& session, - const boost::asio::ip::address& clientAddress, - std::string_view loginName, - std::string_view password) override; + CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress, + std::string_view loginName, + std::string_view password) override; std::shared_mutex _mutex; LoginThrottler _loginThrottler; diff --git a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp b/src/libs/services/auth/impl/http-headers/HttpHeadersEnvService.cpp similarity index 75% rename from src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp rename to src/libs/services/auth/impl/http-headers/HttpHeadersEnvService.cpp index 3db1fb4a..951925ec 100644 --- a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp +++ b/src/libs/services/auth/impl/http-headers/HttpHeadersEnvService.cpp @@ -28,14 +28,15 @@ namespace Auth { - HttpHeadersEnvService::HttpHeadersEnvService() - : _fieldName {Service::get()->getString("http-headers-login-field", "X-Forwarded-User")} + HttpHeadersEnvService::HttpHeadersEnvService(Database::Db& db) + : AuthServiceBase {db} + , _fieldName {Service::get()->getString("http-headers-login-field", "X-Forwarded-User")} { LMS_LOG(AUTH, INFO) << "Using http header field = '" << _fieldName << "'"; } HttpHeadersEnvService::CheckResult - HttpHeadersEnvService::processEnv(Database::Session& session, const Wt::WEnvironment& env) + HttpHeadersEnvService::processEnv(const Wt::WEnvironment& env) { const std::string loginName {env.headerValue(_fieldName)}; if (loginName.empty()) @@ -43,13 +44,13 @@ namespace Auth LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header"; - const Database::UserId userId {getOrCreateUser(session, loginName)}; - onUserAuthenticated(session, userId); + const Database::UserId userId {getOrCreateUser(loginName)}; + onUserAuthenticated(userId); return {CheckResult::State::Granted, userId}; } HttpHeadersEnvService::CheckResult - HttpHeadersEnvService::processRequest(Database::Session& session, const Wt::Http::Request& request) + HttpHeadersEnvService::processRequest(const Wt::Http::Request& request) { const std::string loginName {request.headerValue(_fieldName)}; if (loginName.empty()) @@ -57,8 +58,8 @@ namespace Auth LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header"; - const Database::UserId userId {getOrCreateUser(session, loginName)}; - onUserAuthenticated(session, userId); + const Database::UserId userId {getOrCreateUser(loginName)}; + onUserAuthenticated(userId); return {CheckResult::State::Granted, userId}; } diff --git a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp b/src/libs/services/auth/impl/http-headers/HttpHeadersEnvService.hpp similarity index 78% rename from src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp rename to src/libs/services/auth/impl/http-headers/HttpHeadersEnvService.hpp index 1295f182..02e4f94c 100644 --- a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp +++ b/src/libs/services/auth/impl/http-headers/HttpHeadersEnvService.hpp @@ -19,7 +19,7 @@ #pragma once -#include "auth/IEnvService.hpp" +#include "services/auth/IEnvService.hpp" #include "AuthServiceBase.hpp" namespace Auth @@ -27,11 +27,11 @@ namespace Auth class HttpHeadersEnvService : public IEnvService, public AuthServiceBase { public: - HttpHeadersEnvService(); + HttpHeadersEnvService(Database::Db& db); private: - CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) override; - CheckResult processRequest(Database::Session& session, const Wt::Http::Request& request) override; + CheckResult processEnv(const Wt::WEnvironment& env) override; + CheckResult processRequest(const Wt::Http::Request& request) override; std::string _fieldName; }; diff --git a/src/libs/auth/impl/internal/InternalPasswordService.cpp b/src/libs/services/auth/impl/internal/InternalPasswordService.cpp similarity index 81% rename from src/libs/auth/impl/internal/InternalPasswordService.cpp rename to src/libs/services/auth/impl/internal/InternalPasswordService.cpp index a021dd3c..ebc55158 100644 --- a/src/libs/auth/impl/internal/InternalPasswordService.cpp +++ b/src/libs/services/auth/impl/internal/InternalPasswordService.cpp @@ -20,17 +20,17 @@ #include "InternalPasswordService.hpp" #include -#include "auth/IAuthTokenService.hpp" -#include "auth/Types.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" +#include "services/auth/IAuthTokenService.hpp" +#include "services/auth/Types.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" namespace Auth { - InternalPasswordService::InternalPasswordService(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) - : PasswordServiceBase {maxThrottlerEntries, authTokenService} + InternalPasswordService::InternalPasswordService(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) + : PasswordServiceBase {db, maxThrottlerEntries, authTokenService} { _validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4); _validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4); @@ -42,17 +42,16 @@ namespace Auth } bool - InternalPasswordService::checkUserPassword(Database::Session& session, - std::string_view loginName, - std::string_view password) + InternalPasswordService::checkUserPassword(std::string_view loginName, std::string_view password) { LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'"; Database::User::PasswordHash passwordHash; { + Database::Session& session {getDbSession()}; auto transaction {session.createSharedTransaction()}; - const Database::User::pointer user {Database::User::getByLoginName(session, loginName)}; + const Database::User::pointer user {Database::User::find(session, loginName)}; if (!user) { LMS_LOG(AUTH, DEBUG) << "hashing random stuff"; @@ -96,13 +95,14 @@ namespace Auth } void - InternalPasswordService::setPassword(Database::Session& session, Database::UserId userId, std::string_view newPassword) + InternalPasswordService::setPassword(Database::UserId userId, std::string_view newPassword) { const Database::User::PasswordHash passwordHash {hashPassword(newPassword)}; + Database::Session& session {getDbSession()}; auto transaction {session.createUniqueTransaction()}; - Database::User::pointer user {Database::User::getById(session, userId)}; + Database::User::pointer user {Database::User::find(session, userId)}; if (!user) throw Exception {"User not found!"}; @@ -117,7 +117,7 @@ namespace Auth } user.modify()->setPasswordHash(passwordHash); - getAuthTokenService().clearAuthTokens(session, userId); + getAuthTokenService().clearAuthTokens(userId); } Database::User::PasswordHash diff --git a/src/libs/auth/impl/internal/InternalPasswordService.hpp b/src/libs/services/auth/impl/internal/InternalPasswordService.hpp similarity index 79% rename from src/libs/auth/impl/internal/InternalPasswordService.hpp rename to src/libs/services/auth/impl/internal/InternalPasswordService.hpp index 33cdb5cd..7754953a 100644 --- a/src/libs/auth/impl/internal/InternalPasswordService.hpp +++ b/src/libs/services/auth/impl/internal/InternalPasswordService.hpp @@ -22,7 +22,7 @@ #include #include -#include "database/User.hpp" +#include "services/database/User.hpp" #include "PasswordServiceBase.hpp" #include "LoginThrottler.hpp" @@ -33,16 +33,14 @@ namespace Auth class InternalPasswordService : public PasswordServiceBase { public: - InternalPasswordService(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService); + InternalPasswordService(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService); private: - bool checkUserPassword(Database::Session& session, - std::string_view loginName, - std::string_view password) override; + bool checkUserPassword(std::string_view loginName, std::string_view password) override; bool canSetPasswords() const override; PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override; - void setPassword(Database::Session& session, Database::UserId userId, std::string_view newPassword) override; + void setPassword(Database::UserId userId, std::string_view newPassword) override; Database::User::PasswordHash hashPassword(std::string_view password) const; void hashRandomPassword() const; diff --git a/src/libs/auth/impl/pam/PAMPasswordService.cpp b/src/libs/services/auth/impl/pam/PAMPasswordService.cpp similarity index 94% rename from src/libs/auth/impl/pam/PAMPasswordService.cpp rename to src/libs/services/auth/impl/pam/PAMPasswordService.cpp index 1b25f537..80c92644 100644 --- a/src/libs/auth/impl/pam/PAMPasswordService.cpp +++ b/src/libs/services/auth/impl/pam/PAMPasswordService.cpp @@ -26,8 +26,8 @@ #include #include -#include "auth/Types.hpp" -#include "database/Session.hpp" +#include "services/auth/Types.hpp" +#include "services/database/Session.hpp" #include "utils/Logger.hpp" namespace Auth @@ -161,7 +161,7 @@ namespace Auth }; bool - PAMPasswordService::checkUserPassword(Database::Session& /*session*/, std::string_view loginName, std::string_view password) + PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password) { try { @@ -193,7 +193,7 @@ namespace Auth } void - PAMPasswordService::setPassword(Database::Session&, Database::UserId, std::string_view) + PAMPasswordService::setPassword(Database::UserId, std::string_view) { throw NotImplementedException {}; } diff --git a/src/libs/auth/impl/pam/PAMPasswordService.hpp b/src/libs/services/auth/impl/pam/PAMPasswordService.hpp similarity index 81% rename from src/libs/auth/impl/pam/PAMPasswordService.hpp rename to src/libs/services/auth/impl/pam/PAMPasswordService.hpp index 4b661b4b..2de5bc82 100644 --- a/src/libs/auth/impl/pam/PAMPasswordService.hpp +++ b/src/libs/services/auth/impl/pam/PAMPasswordService.hpp @@ -31,14 +31,9 @@ namespace Auth using PasswordServiceBase::PasswordServiceBase; private: - bool checkUserPassword(Database::Session& session, - std::string_view loginName, - std::string_view password) override; - + bool checkUserPassword(std::string_view loginName,std::string_view password) override; bool canSetPasswords() const override; PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override; - void setPassword(Database::Session& session, - Database::UserId userId, - std::string_view newPassword) override; + void setPassword(Database::UserId userId, std::string_view newPassword) override; }; } diff --git a/src/libs/auth/include/auth/IAuthTokenService.hpp b/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp similarity index 73% rename from src/libs/auth/include/auth/IAuthTokenService.hpp rename to src/libs/services/auth/include/services/auth/IAuthTokenService.hpp index e57ee6a9..bd5754de 100644 --- a/src/libs/auth/include/auth/IAuthTokenService.hpp +++ b/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp @@ -27,11 +27,11 @@ #include #include -#include "database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { - class Session; + class Db; class User; } @@ -63,12 +63,12 @@ namespace Auth }; // Provided token is only accepted once - virtual AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0; + virtual AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0; // Returns a one time token - virtual std::string createAuthToken(Database::Session& session, Database::UserId userid, const Wt::WDateTime& expiry) = 0; - virtual void clearAuthTokens(Database::Session& session, Database::UserId userid) = 0; + virtual std::string createAuthToken(Database::UserId userid, const Wt::WDateTime& expiry) = 0; + virtual void clearAuthTokens(Database::UserId userid) = 0; }; - std::unique_ptr createAuthTokenService(std::size_t maxThrottlerEntryCount); + std::unique_ptr createAuthTokenService(Database::Db& db, std::size_t maxThrottlerEntryCount); } diff --git a/src/libs/auth/include/auth/IEnvService.hpp b/src/libs/services/auth/include/services/auth/IEnvService.hpp similarity index 83% rename from src/libs/auth/include/auth/IEnvService.hpp rename to src/libs/services/auth/include/services/auth/IEnvService.hpp index b001d602..ad9dda2c 100644 --- a/src/libs/auth/include/auth/IEnvService.hpp +++ b/src/libs/services/auth/include/services/auth/IEnvService.hpp @@ -22,10 +22,11 @@ #include #include -#include "database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { + class Db; class Session; } @@ -60,9 +61,9 @@ namespace Auth std::optional userId {}; }; - virtual CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) = 0; - virtual CheckResult processRequest(Database::Session& session, const Wt::Http::Request& request) = 0; + virtual CheckResult processEnv(const Wt::WEnvironment& env) = 0; + virtual CheckResult processRequest(const Wt::Http::Request& request) = 0; }; - std::unique_ptr createEnvService(std::string_view backendName); + std::unique_ptr createEnvService(std::string_view backendName, Database::Db& db); } // namespace Auth diff --git a/src/libs/auth/include/auth/IPasswordService.hpp b/src/libs/services/auth/include/services/auth/IPasswordService.hpp similarity index 75% rename from src/libs/auth/include/auth/IPasswordService.hpp rename to src/libs/services/auth/include/services/auth/IPasswordService.hpp index 69278fc5..18319106 100644 --- a/src/libs/auth/include/auth/IPasswordService.hpp +++ b/src/libs/services/auth/include/services/auth/IPasswordService.hpp @@ -21,16 +21,16 @@ #include +#include #include #include -#include -#include "auth/Types.hpp" -#include "database/Types.hpp" +#include "services/auth/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { - class Session; + class Db; class User; } @@ -56,10 +56,9 @@ namespace Auth std::optional userId {}; std::optional expiry {}; }; - virtual CheckResult checkUserPassword(Database::Session& session, - const boost::asio::ip::address& clientAddress, - std::string_view loginName, - std::string_view password) = 0; + virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress, + std::string_view loginName, + std::string_view password) = 0; virtual bool canSetPasswords() const = 0; @@ -70,9 +69,9 @@ namespace Auth MustMatchLoginName, }; virtual PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const = 0; - virtual void setPassword(Database::Session& session, Database::UserId userId, std::string_view newPassword) = 0; + virtual void setPassword(Database::UserId userId, std::string_view newPassword) = 0; }; - std::unique_ptr createPasswordService(std::string_view authPasswordBackend, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService); + std::unique_ptr createPasswordService(std::string_view authPasswordBackend, Database::Db& db, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService); } diff --git a/src/libs/auth/include/auth/Types.hpp b/src/libs/services/auth/include/services/auth/Types.hpp similarity index 97% rename from src/libs/auth/include/auth/Types.hpp rename to src/libs/services/auth/include/services/auth/Types.hpp index 8b97df6e..df470746 100644 --- a/src/libs/auth/include/auth/Types.hpp +++ b/src/libs/services/auth/include/services/auth/Types.hpp @@ -20,7 +20,7 @@ #pragma once #include -#include "database/Types.hpp" +#include "services/database/Types.hpp" #include "utils/Exception.hpp" namespace Auth diff --git a/src/libs/services/cover/CMakeLists.txt b/src/libs/services/cover/CMakeLists.txt new file mode 100644 index 00000000..41169c5d --- /dev/null +++ b/src/libs/services/cover/CMakeLists.txt @@ -0,0 +1,28 @@ + +add_library(lmsservice-cover SHARED + impl/CoverService.cpp + ) + +target_include_directories(lmsservice-cover INTERFACE + include + ) + +target_include_directories(lmsservice-cover PRIVATE + include + impl + ) + +target_link_libraries(lmsservice-cover PRIVATE + lmsav + lmsimage + ) + +target_link_libraries(lmsservice-cover PUBLIC + lmsdatabase + lmsimage + lmsutils + std::filesystem + ) + +install(TARGETS lmsservice-cover DESTINATION lib) + diff --git a/src/libs/cover/impl/CoverArtGrabber.cpp b/src/libs/services/cover/impl/CoverService.cpp similarity index 74% rename from src/libs/cover/impl/CoverArtGrabber.cpp rename to src/libs/services/cover/impl/CoverService.cpp index 226d28a1..a9c2b8e4 100644 --- a/src/libs/cover/impl/CoverArtGrabber.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -17,26 +17,21 @@ * along with LMS. If not, see . */ -#include "CoverArtGrabber.hpp" +#include "CoverService.hpp" #include "av/IAudioFile.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" - -#if LMS_SUPPORT_IMAGE_STB -#include "stb/RawImage.hpp" -using RawImage = CoverArt::STB::RawImage; -#elif LMS_SUPPORT_IMAGE_GM -#include "graphicsmagick/RawImage.hpp" -using RawImage = CoverArt::GraphicsMagick::RawImage; -#endif +#include "services/database/Db.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "image/Exception.hpp" +#include "image/IRawImage.hpp" +#include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Random.hpp" #include "utils/Utils.hpp" -#include "Exception.hpp" namespace { @@ -55,7 +50,7 @@ namespace auto transaction {dbSession.createSharedTransaction()}; - const Database::Track::pointer track {Database::Track::getById(dbSession, trackId)}; + const Database::Track::pointer track {Database::Track::find(dbSession, trackId)}; if (!track) return res; @@ -75,7 +70,9 @@ namespace } } -namespace CoverArt { +namespace Cover { + +using namespace Image; static bool @@ -84,28 +81,26 @@ isFileSupported(const std::filesystem::path& file, const std::vector -createGrabber(const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath, - std::size_t maxCacheSize, std::size_t maxFileSize, unsigned jpegQuality) +std::unique_ptr +createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath) { - return std::make_unique(execPath, defaultCoverPath, maxCacheSize, maxFileSize, jpegQuality); + return std::make_unique(db, execPath, defaultCoverPath); } -Grabber::Grabber(const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath, - std::size_t maxCacheSize, - std::size_t maxFileSize, - unsigned jpegQuality) - : _defaultCoverPath {defaultCoverPath} - , _maxCacheSize {maxCacheSize} - , _maxFileSize {maxFileSize} - , _jpegQuality {Utils::clamp(jpegQuality, 1, 100)} +CoverService::CoverService(Database::Db& db, + const std::filesystem::path& execPath, + const std::filesystem::path& defaultCoverPath) + : _db {db} + , _defaultCoverPath {defaultCoverPath} + , _maxCacheSize {Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000} + , _maxFileSize {Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000} + { + setJpegQuality(Service::get()->getULong("cover-jpeg-quality", 75)); + LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'"; LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize; LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize; - LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality; #if LMS_SUPPORT_IMAGE_GM GraphicsMagick::init(execPath); @@ -117,14 +112,14 @@ Grabber::Grabber(const std::filesystem::path& execPath, { getDefault(512); } - catch (const ImageException& e) + catch (const Image::ImageException& e) { throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what()); } } std::unique_ptr -Grabber::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const +CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const { std::unique_ptr image; @@ -135,11 +130,11 @@ Grabber::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const try { - RawImage rawImage {picture.data, picture.dataSize}; - rawImage.resize(width); - image = rawImage.encodeToJPEG(_jpegQuality); + std::unique_ptr rawImage {decodeImage(picture.data, picture.dataSize)}; + rawImage->resize(width); + image = rawImage->encodeToJPEG(_jpegQuality); } - catch (const ImageException& e) + catch (const Image::ImageException& e) { LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what(); } @@ -149,15 +144,15 @@ Grabber::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const } std::unique_ptr -Grabber::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const +CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const { std::unique_ptr image; try { - RawImage rawImage {p}; - rawImage.resize(width); - image = rawImage.encodeToJPEG(_jpegQuality); + std::unique_ptr rawImage {decodeImage(p)}; + rawImage->resize(width); + image = rawImage->encodeToJPEG(_jpegQuality); } catch (const ImageException& e) { @@ -168,7 +163,7 @@ Grabber::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const } std::shared_ptr -Grabber::getDefault(ImageSize width) +CoverService::getDefault(ImageSize width) { { std::shared_lock lock {_cacheMutex}; @@ -192,7 +187,7 @@ Grabber::getDefault(ImageSize width) } std::unique_ptr -Grabber::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const +CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const { const std::multimap coverPaths {getCoverPaths(directory)}; @@ -231,7 +226,7 @@ Grabber::getFromDirectory(const std::filesystem::path& directory, ImageSize widt } std::unique_ptr -Grabber::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const +CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const { std::unique_ptr res; @@ -252,7 +247,7 @@ Grabber::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize w } bool -Grabber::checkCoverFile(const std::filesystem::path& filePath) const +CoverService::checkCoverFile(const std::filesystem::path& filePath) const { std::error_code ec; @@ -275,7 +270,7 @@ Grabber::checkCoverFile(const std::filesystem::path& filePath) const } std::multimap -Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const +CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const { std::multimap res; std::error_code ec; @@ -296,7 +291,7 @@ Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const } std::unique_ptr -Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const +CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const { std::unique_ptr image; @@ -313,13 +308,13 @@ Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const } std::shared_ptr -Grabber::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width) +CoverService::getFromTrack(Database::TrackId trackId, ImageSize width) { - return getFromTrack(dbSession, trackId, width, true /* allow release fallback*/); + return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/); } std::shared_ptr -Grabber::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback) +CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback) { using namespace Database; @@ -338,7 +333,7 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, I cover = getFromSameNamedFile(trackInfo->trackPath, width); if (!cover && trackInfo->releaseId && allowReleaseFallback) - cover = getFromRelease(dbSession, *trackInfo->releaseId, width); + cover = getFromRelease(*trackInfo->releaseId, width); if (!cover && trackInfo->isMultiDisc) { @@ -357,7 +352,7 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, I } std::shared_ptr -Grabber::getFromRelease(Database::Session& session, Database::ReleaseId releaseId, ImageSize width) +CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width) { const CacheEntryDesc cacheEntryDesc {releaseId, width}; @@ -371,13 +366,15 @@ Grabber::getFromRelease(Database::Session& session, Database::ReleaseId releaseI std::filesystem::path releaseDirectory; }; + Database::Session& session {_db.getTLSSession()}; + auto getReleaseInfo {[&] { std::optional res; auto transaction {session.createSharedTransaction()}; - if (const Database::Release::pointer release {Database::Release::getById(session, releaseId)}) + if (const Database::Release::pointer release {Database::Release::find(session, releaseId)}) { if (const auto firstTrack {release->getFirstTrack()}) { @@ -407,7 +404,7 @@ Grabber::getFromRelease(Database::Session& session, Database::ReleaseId releaseI } void -Grabber::flushCache() +CoverService::flushCache() { std::unique_lock lock {_cacheMutex}; @@ -419,7 +416,15 @@ Grabber::flushCache() } void -Grabber::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image) +CoverService::setJpegQuality(unsigned quality) +{ + _jpegQuality = Utils::clamp(quality, 1, 100); + + LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality; +} + +void +CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image) { std::unique_lock lock {_cacheMutex}; @@ -435,7 +440,7 @@ Grabber::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr -Grabber::loadFromCache(const CacheEntryDesc& entryDesc) +CoverService::loadFromCache(const CacheEntryDesc& entryDesc) { std::shared_lock lock {_cacheMutex}; @@ -450,5 +455,5 @@ Grabber::loadFromCache(const CacheEntryDesc& entryDesc) return it->second; } -} // namespace CoverArt +} // namespace Cover diff --git a/src/libs/cover/impl/CoverArtGrabber.hpp b/src/libs/services/cover/impl/CoverService.hpp similarity index 51% rename from src/libs/cover/impl/CoverArtGrabber.hpp rename to src/libs/services/cover/impl/CoverService.hpp index e5c33fb8..bf6e9d6c 100644 --- a/src/libs/cover/impl/CoverArtGrabber.hpp +++ b/src/libs/services/cover/impl/CoverService.hpp @@ -29,9 +29,9 @@ #include #include -#include "cover/ICoverArtGrabber.hpp" -#include "cover/IEncodedImage.hpp" -#include "database/Types.hpp" +#include "services/cover/ICoverService.hpp" +#include "image/IEncodedImage.hpp" +#include "services/database/Types.hpp" namespace Database { @@ -43,7 +43,7 @@ namespace Av class IAudioFile; } -namespace CoverArt +namespace Cover { struct CacheEntryDesc { @@ -57,16 +57,16 @@ namespace CoverArt } }; -} // ns CoverArt +} // ns Cover namespace std { template<> - class hash + class hash { public: - size_t operator()(const CoverArt::CacheEntryDesc& e) const + size_t operator()(const Cover::CacheEntryDesc& e) const { size_t h {}; std::visit([&](auto id) @@ -81,56 +81,57 @@ namespace std } // ns std -namespace CoverArt +namespace Cover { - class Grabber : public IGrabber + class CoverService : public ICoverService { public: - Grabber(const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath, - std::size_t maxCacheEntries, - std::size_t maxFileSize, - unsigned jpegQuality); + CoverService(Database::Db& db, + const std::filesystem::path& execPath, + const std::filesystem::path& defaultCoverPath); - Grabber(const Grabber&) = delete; - Grabber& operator=(const Grabber&) = delete; - Grabber(Grabber&&) = delete; - Grabber& operator=(Grabber&&) = delete; + CoverService(const CoverService&) = delete; + CoverService& operator=(const CoverService&) = delete; + CoverService(CoverService&&) = delete; + CoverService& operator=(CoverService&&) = delete; private: - std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width) override; - std::shared_ptr getFromRelease(Database::Session& dbSession, Database::ReleaseId releaseId, ImageSize width) override; + std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) override; + std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) override; void flushCache() override; + void setJpegQuality(unsigned quality) override; - std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback); - std::unique_ptr getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const; - std::unique_ptr getFromCoverFile(const std::filesystem::path& p, ImageSize width) const; + std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, Image::ImageSize width, bool allowReleaseFallback); + std::unique_ptr getFromAvMediaFile(const Av::IAudioFile& input, Image::ImageSize width) const; + std::unique_ptr getFromCoverFile(const std::filesystem::path& p, Image::ImageSize width) const; - std::unique_ptr getFromTrack(const std::filesystem::path& path, ImageSize width) const; + std::unique_ptr getFromTrack(const std::filesystem::path& path, Image::ImageSize width) const; std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; - std::unique_ptr getFromDirectory(const std::filesystem::path& directory, ImageSize width) const; - std::unique_ptr getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const; - std::shared_ptr getDefault(ImageSize width); + std::unique_ptr getFromDirectory(const std::filesystem::path& directory, Image::ImageSize width) const; + std::unique_ptr getFromSameNamedFile(const std::filesystem::path& filePath, Image::ImageSize width) const; + std::shared_ptr getDefault(Image::ImageSize width); bool checkCoverFile(const std::filesystem::path& directoryPath) const; + Database::Db& _db; + std::shared_mutex _cacheMutex; - std::unordered_map> _cache; - std::unordered_map> _defaultCoverCache; + std::unordered_map> _cache; + std::unordered_map> _defaultCoverCache; std::atomic _cacheMisses {}; std::atomic _cacheHits {}; std::size_t _cacheSize {}; - void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); - std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); + void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); + std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); const std::filesystem::path _defaultCoverPath; const std::size_t _maxCacheSize; static inline const std::vector _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize const std::size_t _maxFileSize; static inline const std::vector _preferredFileNames {"cover", "front"}; // TODO parametrize - const unsigned _jpegQuality; + unsigned _jpegQuality; }; -} // namespace CoverArt +} // namespace Cover diff --git a/src/libs/cover/include/cover/ICoverArtGrabber.hpp b/src/libs/services/cover/include/services/cover/ICoverService.hpp similarity index 55% rename from src/libs/cover/include/cover/ICoverArtGrabber.hpp rename to src/libs/services/cover/include/services/cover/ICoverService.hpp index 544cee7a..0421f589 100644 --- a/src/libs/cover/include/cover/ICoverArtGrabber.hpp +++ b/src/libs/services/cover/include/services/cover/ICoverService.hpp @@ -22,32 +22,33 @@ #include #include -#include "database/Types.hpp" -#include "cover/IEncodedImage.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" +#include "image/IEncodedImage.hpp" namespace Database { - class Session; + class Db; } -namespace CoverArt +namespace Cover { - class IGrabber + class ICoverService { public: - virtual ~IGrabber() = default; + virtual ~ICoverService() = default; - virtual std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width) = 0; - virtual std::shared_ptr getFromRelease(Database::Session& dbSession, Database::ReleaseId releaseId, ImageSize width) = 0; + virtual std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) = 0; + virtual std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) = 0; virtual void flushCache() = 0; + + virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 }; - std::unique_ptr createGrabber(const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath, - std::size_t maxCacheEntries, - std::size_t maxFileSize, - unsigned jpegQuality); + std::unique_ptr createCoverService(Database::Db& db, + const std::filesystem::path& execPath, + const std::filesystem::path& defaultCoverPath); } // namespace CoverArt diff --git a/src/libs/database/CMakeLists.txt b/src/libs/services/database/CMakeLists.txt similarity index 75% rename from src/libs/database/CMakeLists.txt rename to src/libs/services/database/CMakeLists.txt index cdf1288f..dc1a11f5 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/services/database/CMakeLists.txt @@ -1,17 +1,23 @@ - add_library(lmsdatabase SHARED impl/Artist.cpp + impl/AuthToken.cpp impl/Cluster.cpp impl/Db.cpp + impl/Listen.cpp + impl/Migration.cpp impl/TrackArtistLink.cpp impl/TrackFeatures.cpp impl/TrackList.cpp impl/Release.cpp impl/ScanSettings.cpp impl/Session.cpp + impl/StarredArtist.cpp + impl/StarredRelease.cpp + impl/StarredTrack.cpp impl/SqlQuery.cpp impl/Track.cpp impl/TrackBookmark.cpp + impl/Types.cpp impl/User.cpp impl/Utils.cpp ) @@ -36,3 +42,6 @@ target_link_libraries(lmsdatabase PUBLIC install(TARGETS lmsdatabase DESTINATION lib) +if(BUILD_TESTING) + add_subdirectory(test) +endif() diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp similarity index 51% rename from src/libs/database/impl/Artist.cpp rename to src/libs/services/database/impl/Artist.cpp index a519f4c2..6a7428a0 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/services/database/impl/Artist.cpp @@ -16,19 +16,19 @@ * You should have received a copy of the GNU General Public License * along with LMS. If not, see . */ -#include "database/Artist.hpp" +#include "services/database/Artist.hpp" #include -#include "database/Cluster.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/User.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" #include "utils/Logger.hpp" #include "SqlQuery.hpp" #include "Utils.hpp" -#include "Traits.hpp" +#include "IdTypeTraits.hpp" namespace Database { @@ -40,8 +40,16 @@ _MBID {MBID ? MBID->getAsString() : ""} { } +std::size_t +Artist::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM artist"); +} + std::vector -Artist::getByName(Session& session, const std::string& name) +Artist::find(Session& session, const std::string& name) { session.checkSharedLocked(); @@ -53,19 +61,26 @@ Artist::getByName(Session& session, const std::string& name) } Artist::pointer -Artist::getByMBID(Session& session, const UUID& mbid) +Artist::find(Session& session, const UUID& mbid) { session.checkSharedLocked(); return session.getDboSession().find().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue(); } Artist::pointer -Artist::getById(Session& session, ArtistId id) +Artist::find(Session& session, ArtistId id) { session.checkSharedLocked(); return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } +bool +Artist::exists(Session& session, ArtistId id) +{ + session.checkSharedLocked(); + return session.getDboSession().query("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1; +} + Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional& MBID) { @@ -77,36 +92,37 @@ Artist::create(Session& session, const std::string& name, const std::optional static -Wt::Dbo::Query -createQuery(Session& session, - const std::string& queryStr, - const std::vector& clusterIds, - const std::vector& keywords, - std::optional linkType) +Wt::Dbo::Query +createQuery(Session& session, const Artist::FindParameters& params) { session.checkSharedLocked(); - auto query {session.getDboSession().query(queryStr)}; - query.join("track t ON t.id = t_a_l.track_id"); - query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); + auto query {session.getDboSession().query("SELECT DISTINCT a.id FROM artist a")}; + if (params.sortMethod == ArtistSortMethod::LastWritten || params.writtenAfter.isValid() || params.linkType) + { + query.join("track t ON t.id = t_a_l.track_id"); + query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); + } - if (linkType) - query.where("t_a_l.type = ?").bind(*linkType); + if (params.linkType) + query.where("t_a_l.type = ?").bind(*params.linkType); - if (!keywords.empty()) + if (params.writtenAfter.isValid()) + query.where("t.file_last_write > ?").bind(params.writtenAfter); + + if (!params.keywords.empty()) { std::vector clauses; std::vector sortClauses; - for (std::string_view keyword : keywords) + for (std::string_view keyword : params.keywords) { clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); query.bind("%" + escapeLikeKeyword(keyword) + "%"); } - for (std::string_view keyword : keywords) + for (std::string_view keyword : params.keywords) { sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); query.bind("%" + escapeLikeKeyword(keyword) + "%"); @@ -115,7 +131,15 @@ createQuery(Session& session, query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")"); } - if (!clusterIds.empty()) + if (params.starringUser.isValid()) + { + assert(params.scrobbler); + query.join("starred_artist s_a ON s_a.artist_id = a.id") + .where("s_a.user_id = ?").bind(params.starringUser) + .where("s_a.scrobbler = ?").bind(*params.scrobbler); + } + + if (!params.clusters.empty()) { std::ostringstream oss; oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" @@ -125,268 +149,59 @@ createQuery(Session& session, " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; WhereClause clusterClause; - for (const ClusterId clusterId : clusterIds) + for (const ClusterId clusterId : params.clusters) { clusterClause.Or(WhereClause("c.id = ?")); query.bind(clusterId); } oss << " " << clusterClause.get(); - oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; + oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")"; query.where(oss.str()); } + switch (params.sortMethod) + { + case ArtistSortMethod::None: + break; + case ArtistSortMethod::ByName: + query.orderBy("a.name COLLATE NOCASE"); + break; + case ArtistSortMethod::BySortName: + query.orderBy("a.sort_name COLLATE NOCASE"); + break; + case ArtistSortMethod::Random: + query.orderBy("RANDOM()"); + break; + case ArtistSortMethod::LastWritten: + query.orderBy("t.file_last_write DESC"); + break; + case ArtistSortMethod::StarredDateDesc: + assert(params.starringUser.isValid()); + query.orderBy("s_a.date_time DESC"); + break; + } + return query; } -std::vector -Artist::getAll(Session& session) +RangeResults +Artist::findAllOrphans(Session& session, Range range) { session.checkSharedLocked(); + auto query {session.getDboSession().query("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")}; - Wt::Dbo::collection> res = session.getDboSession().find(); - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } -std::vector -Artist::getAll(Session& session, SortMethod sortMethod) +RangeResults +Artist::find(Session& session, const FindParameters& params) { session.checkSharedLocked(); - auto query {session.getDboSession().find()}; - switch (sortMethod) - { - case Artist::SortMethod::None: - break; - case Artist::SortMethod::ByName: - query.orderBy("name COLLATE NOCASE"); - break; - case Artist::SortMethod::BySortName: - query.orderBy("sort_name COLLATE NOCASE"); - break; - } - - Wt::Dbo::collection> res = query; - return std::vector(res.begin(), res.end()); -} - - -std::vector -Artist::getAll(Session& session, SortMethod sortMethod, std::optional range, bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT a FROM Artist a", {}, {}, std::nullopt)}; - - switch (sortMethod) - { - case Artist::SortMethod::None: - break; - case Artist::SortMethod::ByName: - query.orderBy("a.name COLLATE NOCASE"); - break; - case Artist::SortMethod::BySortName: - query.orderBy("a.sort_name COLLATE NOCASE"); - break; - } - - Wt::Dbo::collection> collection = query - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - std::vector res (collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -Artist::getAllIds(Session& session) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM artist"); - return std::vector(res.begin(), res.end()); -} - -std::vector -Artist::getAllIdsRandom(Session& session, const std::vector& clusters, std::optional linkType, std::optional size) -{ - session.checkSharedLocked(); - - auto query {createQuery(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)}; - - Wt::Dbo::collection res = query - .orderBy("RANDOM()") - .limit(size ? static_cast(*size) : -1); - - return std::vector(res.begin(), res.end()); - -} - -std::vector -Artist::getAllOrphans(Session& session) -{ - session.checkSharedLocked(); - Wt::Dbo::collection> res {session.getDboSession().query>("SELECT DISTINCT a FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -Artist::getAllIdsWithClusters(Session& session, std::optional limit) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query - ("SELECT DISTINCT a.id FROM artist a" - " INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .limit(limit ? static_cast(*limit) : -1); - - return std::vector(res.begin(), res.end()); -} - -std::vector -Artist::getByClusters(Session& session, const std::vector& clusters, SortMethod sortMethod) -{ - assert(!clusters.empty()); - - session.checkSharedLocked(); - bool more{}; - return getByFilter(session, clusters, {}, std::nullopt, sortMethod, std::nullopt, more); -} - -std::vector -Artist::getByFilter(Session& session, - const std::vector& clusters, - const std::vector& keywords, - std::optional linkType, - SortMethod sortMethod, - std::optional range, - bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT DISTINCT a from artist a", clusters, keywords, linkType)}; - switch (sortMethod) - { - case Artist::SortMethod::None: - break; - case Artist::SortMethod::ByName: - query.orderBy("a.name COLLATE NOCASE"); - break; - case Artist::SortMethod::BySortName: - query.orderBy("a.sort_name COLLATE NOCASE"); - break; - } - - Wt::Dbo::collection> collection = query - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - std::vector res (collection.begin(), collection.end()); - - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -Artist::getLastWritten(Session& session, - std::optional after, - const std::vector& clusters, - std::optional linkType, - std::optional range, bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)}; - - if (after) - query.where("t.file_last_write > ?").bind(*after); - - Wt::Dbo::collection> collection = query - .orderBy("t.file_last_write DESC") - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - std::vector res (collection.begin(), collection.end()); - - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return std::vector(res.begin(), res.end()); -} - -std::vector -Artist::getStarred(Session& session, - User::pointer user, - const std::vector& clusters, - std::optional linkType, - SortMethod sortMethod, - std::optional range, bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)}; - - { - std::ostringstream oss; - oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" - " INNER JOIN user_artist_starred uas ON uas.artist_id = a.id" - " INNER JOIN user u ON u.id = uas.user_id WHERE u.id = ?)"; - - query.bind(user->getId()); - query.where(oss.str()); - } - - switch (sortMethod) - { - case Artist::SortMethod::None: - break; - case Artist::SortMethod::ByName: - query.orderBy("name COLLATE NOCASE"); - break; - case Artist::SortMethod::BySortName: - query.orderBy("sort_name COLLATE NOCASE"); - break; - } - - Wt::Dbo::collection> collection = query - .groupBy("a.id") - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - std::vector res (collection.begin(), collection.end()); - - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return std::vector(res.begin(), res.end()); + auto query {createQuery(session, params)}; + return execQuery(query, params.range); } std::vector @@ -456,32 +271,19 @@ Artist::getTracks(std::optional linkType) const return std::vector(tracks.begin(), tracks.end()); } -std::vector -Artist::getNonReleaseTracks(std::optional linkType, std::optional range, bool& moreResults) const +RangeResults +Artist::getNonReleaseTracks(std::optional linkType, Range range) const { assert(session()); auto query {session()->query>("SELECT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id") .where("a.id = ?").bind(getId()) .where("t.release_id is NULL") - .orderBy("t.name") - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1)}; - + .orderBy("t.name")}; if (linkType) query.where("t_a_l.type = ?").bind(*linkType); - Wt::Dbo::collection> tracks {query.resultList()}; - std::vector res(tracks.begin(), tracks.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; + return execQuery(query, range); } bool @@ -511,14 +313,14 @@ Artist::getRandomTracks(std::optional count) const return std::vector(tracks.begin(), tracks.end()); } -std::vector -Artist::getSimilarArtists(EnumSet artistLinkTypes, std::optional range) const +RangeResults +Artist::findSimilarArtists(EnumSet artistLinkTypes, Range range) const { assert(session()); std::ostringstream oss; oss << - "SELECT a FROM artist a" + "SELECT a.id FROM artist a" " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" " INNER JOIN track t ON t.id = t_a_l.track_id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" @@ -547,19 +349,15 @@ Artist::getSimilarArtists(EnumSet artistLinkTypes, std::opt oss << ")"; } - Wt::Dbo::Query> query {session()->query>(oss.str()) + auto query {session()->query(oss.str()) .bind(getId()) .bind(getId()) .groupBy("a.id") - .orderBy("COUNT(*) DESC, RANDOM()") - .limit(range ? static_cast(range->limit) : -1) - .offset(range ? static_cast(range->offset) : -1)}; - + .orderBy("COUNT(*) DESC, RANDOM()")}; for (TrackArtistLinkType type : artistLinkTypes) query.bind(type); - Wt::Dbo::collection> res {query.resultList()}; - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } std::vector> diff --git a/src/libs/services/database/impl/AuthToken.cpp b/src/libs/services/database/impl/AuthToken.cpp new file mode 100644 index 00000000..85058a01 --- /dev/null +++ b/src/libs/services/database/impl/AuthToken.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2013 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 "services/database/AuthToken.hpp" + +#include +#include "services/database/Session.hpp" +#include "services/database/User.hpp" +#include "StringViewTraits.hpp" +#include "IdTypeTraits.hpp" + +namespace Database +{ + + AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) + : _value {value} + , _expiry {expiry} + , _user {getDboPtr(user)} + { + } + + AuthToken::pointer + AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) + { + session.checkUniqueLocked(); + + AuthToken::pointer res {session.getDboSession().add(std::make_unique(value, expiry, user))}; + session.getDboSession().flush(); + + return res; + } + + void + AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now) + { + session.checkUniqueLocked(); + + session.getDboSession().execute("DELETE FROM auth_token WHERE expiry < ?").bind(now); + } + + AuthToken::pointer + AuthToken::find(Session& session, std::string_view value) + { + session.checkSharedLocked(); + + return session.getDboSession().find() + .where("value = ?").bind(value) + .resultValue(); + } +} diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/services/database/impl/Cluster.cpp similarity index 55% rename from src/libs/database/impl/Cluster.cpp rename to src/libs/services/database/impl/Cluster.cpp index 184a03a1..87ec28b1 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/services/database/impl/Cluster.cpp @@ -17,15 +17,16 @@ * along with LMS. If not, see . */ -#include "database/Cluster.hpp" +#include "services/database/Cluster.hpp" -#include "database/Artist.hpp" -#include "database/Release.hpp" -#include "database/ScanSettings.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Release.hpp" +#include "services/database/ScanSettings.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "IdTypeTraits.hpp" #include "SqlQuery.hpp" -#include "Traits.hpp" +#include "Utils.hpp" namespace Database { @@ -35,6 +36,14 @@ Cluster::Cluster(ObjectPtr type, std::string_view name) { } +std::size_t +Cluster::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM cluster"); +} + Cluster::pointer Cluster::create(Session& session, ObjectPtr type, std::string_view name) { @@ -46,25 +55,26 @@ Cluster::create(Session& session, ObjectPtr type, std::string_view return res; } -std::vector -Cluster::getAll(Session& session) +RangeResults +Cluster::find(Session& session, Range range) { session.checkSharedLocked(); + auto query {session.getDboSession().query("SELECT id FROM cluster")}; - Wt::Dbo::collection> res {session.getDboSession().find()}; - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } -std::vector -Cluster::getAllOrphans(Session& session) +RangeResults +Cluster::findOrphans(Session& session, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().query>("SELECT DISTINCT c FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)").resultList()}; - return std::vector(res.begin(), res.end()); + auto query {session.getDboSession().query("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)")}; + + return execQuery(query, range); } Cluster::pointer -Cluster::getById(Session& session, ClusterId id) +Cluster::find(Session& session, ClusterId id) { session.checkSharedLocked(); @@ -77,29 +87,15 @@ Cluster::addTrack(ObjectPtr track) _tracks.insert(getDboPtr(track)); } -std::vector -Cluster::getTracks(std::optional offset, std::optional limit) const +RangeResults +Cluster::getTracks(Range range) const { assert(session()); - auto res {session()->query>("SELECT t FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .where("c.id = ?").bind(getId()) - .offset(offset ? static_cast(*offset) : -1) - .limit(limit ? static_cast(*limit) : -1) - .resultList()}; + auto query {session()->query("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") + .where("c.id = ?").bind(getId())}; - return std::vector(res.begin(), res.end()); -} - -std::vector -Cluster::getTrackIds() const -{ - assert(session()); - - Wt::Dbo::collection res = session()->query("SELECT t_c.track_id FROM track_cluster t_c INNER JOIN cluster c ON c.id = t_c.cluster_id") - .where("c.id = ?").bind(getId()); - - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } std::size_t @@ -117,33 +113,42 @@ ClusterType::ClusterType(std::string_view name) { } -std::vector -ClusterType::getAllOrphans(Session& session) +std::size_t +ClusterType::getCount(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection> res = session.getDboSession().query>( - "SELECT c_t from cluster_type c_t" - " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") - .where("c.id IS NULL"); - - return std::vector(res.begin(), res.end()); + return session.getDboSession().query("SELECT COUNT(*) FROM cluster_type"); } -std::vector -ClusterType::getAllUsed(Session& session) + +RangeResults +ClusterType::findOrphans(Session& session, Range range) { session.checkSharedLocked(); - Wt::Dbo::collection> res = session.getDboSession().query>( - "SELECT DISTINCT c_t from cluster_type c_t") - .join("cluster c ON c_t.id = c.cluster_type_id"); + auto query {session.getDboSession().query( + "SELECT c_t.id from cluster_type c_t" + " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") + .where("c.id IS NULL")}; - return std::vector(res.begin(), res.end()); + return execQuery(query, range); +} + +RangeResults +ClusterType::findUsed(Session& session, Range range) +{ + session.checkSharedLocked(); + + auto query {session.getDboSession().query( + "SELECT DISTINCT c_t.id from cluster_type c_t") + .join("cluster c ON c_t.id = c.cluster_type_id")}; + + return execQuery(query, range); } ClusterType::pointer -ClusterType::getByName(Session& session, const std::string& name) +ClusterType::find(Session& session, const std::string& name) { session.checkSharedLocked(); @@ -151,20 +156,21 @@ ClusterType::getByName(Session& session, const std::string& name) } ClusterType::pointer -ClusterType::getById(Session& session, ClusterTypeId id) +ClusterType::find(Session& session, ClusterTypeId id) { session.checkSharedLocked(); return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } -std::vector -ClusterType::getAll(Session& session) +RangeResults +ClusterType::find(Session& session, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().find().resultList()}; - return std::vector(res.begin(), res.end()); + auto query {session.getDboSession().query("SELECT id from cluster_type")}; + + return execQuery(query, range); } ClusterType::pointer diff --git a/src/libs/database/impl/Db.cpp b/src/libs/services/database/impl/Db.cpp similarity index 90% rename from src/libs/database/impl/Db.cpp rename to src/libs/services/database/impl/Db.cpp index 9854facb..7eb7bf7a 100644 --- a/src/libs/database/impl/Db.cpp +++ b/src/libs/services/database/impl/Db.cpp @@ -17,19 +17,19 @@ * along with LMS. If not, see . */ -#include "database/Db.hpp" +#include "services/database/Db.hpp" #include #include -#include "database/Session.hpp" -#include "database/User.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" #include "utils/Logger.hpp" namespace Database { // Session living class handling the database and the login -Db::Db(const std::filesystem::path& dbPath) +Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount) { LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string(); @@ -38,7 +38,7 @@ Db::Db(const std::filesystem::path& dbPath) connection->executeSql("pragma journal_mode=WAL"); connection->executeSql("pragma synchronous=normal"); - auto connectionPool = std::make_unique(std::move(connection), 10); + auto connectionPool = std::make_unique(std::move(connection), connectionCount); connectionPool->setTimeout(std::chrono::seconds(10)); _connectionPool = std::move(connectionPool); diff --git a/src/libs/database/impl/Traits.hpp b/src/libs/services/database/impl/IdTypeTraits.hpp similarity index 97% rename from src/libs/database/impl/Traits.hpp rename to src/libs/services/database/impl/IdTypeTraits.hpp index bdfa0045..54b3e8d7 100644 --- a/src/libs/database/impl/Traits.hpp +++ b/src/libs/services/database/impl/IdTypeTraits.hpp @@ -22,7 +22,7 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Types.hpp" namespace Wt::Dbo { diff --git a/src/libs/services/database/impl/Listen.cpp b/src/libs/services/database/impl/Listen.cpp new file mode 100644 index 00000000..c9a2cb45 --- /dev/null +++ b/src/libs/services/database/impl/Listen.cpp @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "services/database/Listen.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "IdTypeTraits.hpp" +#include "SqlQuery.hpp" +#include "Utils.hpp" + +namespace +{ + using namespace Database; + + Wt::Dbo::Query + createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, std::optional linkType) + { + auto query {session.query("SELECT a.id from artist a") + .join("track t ON t.id = t_a_l.track_id") + .join("track_artist_link t_a_l ON t_a_l.artist_id = a.id") + .join("listen l ON l.track_id = t.id") + .where("l.user_id = ?").bind(userId) + .where("l.scrobbler = ?").bind(scrobbler)}; + + if (linkType) + query.where("t_a_l.type = ?").bind(*linkType); + + if (!clusterIds.empty()) + { + std::ostringstream oss; + oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" + " INNER JOIN track t ON t.id = t_a_l.track_id" + " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" + " INNER JOIN cluster c ON c.id = t_c.cluster_id" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; + + WhereClause clusterClause; + for (auto id : clusterIds) + { + clusterClause.Or(WhereClause("c.id = ?")); + query.bind(id); + } + + oss << " " << clusterClause.get(); + oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; + + query.where(oss.str()); + } + + return query; + } + + Wt::Dbo::Query + createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds) + { + auto query {session.query("SELECT r.id from release r") + .join("track t ON t.release_id = r.id") + .join("listen l ON l.track_id = t.id") + .where("l.user_id = ?").bind(userId) + .where("l.scrobbler = ?").bind(scrobbler)}; + + if (!clusterIds.empty()) + { + std::ostringstream oss; + oss << "r.id IN (SELECT DISTINCT r.id FROM release r" + " INNER JOIN track t ON t.release_id = r.id" + " INNER JOIN cluster c ON c.id = t_c.cluster_id" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; + + WhereClause clusterClause; + for (ClusterId id : clusterIds) + { + clusterClause.Or(WhereClause("c.id = ?")); + query.bind(id); + } + + oss << " " << clusterClause.get(); + oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; + + query.where(oss.str()); + } + + return query; + } + + Wt::Dbo::Query + createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds) + { + auto query {session.query("SELECT t.id from track t") + .join("listen l ON l.track_id = t.id") + .where("l.user_id = ?").bind(userId) + .where("l.scrobbler = ?").bind(scrobbler)}; + + if (!clusterIds.empty()) + { + std::ostringstream oss; + oss << "t.id IN (SELECT DISTINCT t.id FROM track t" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" + " INNER JOIN cluster c ON c.id = t_c.cluster_id"; + + WhereClause clusterClause; + for (auto id : clusterIds) + { + clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString()); + query.bind(id); + } + + oss << " " << clusterClause.get(); + oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")"; + + query.where(oss.str()); + } + + return query; + } +} + +namespace Database +{ + Listen::Listen(ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime) + : _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())} + , _scrobbler {scrobbler} + , _user {getDboPtr(user)} + , _track {getDboPtr(track)} + {} + + std::size_t + Listen::getCount(Session& session) + { + session.checkSharedLocked(); + return session.getDboSession().query("SELECT COUNT(*) FROM listen"); + } + + Listen::pointer + Listen::find(Session& session, ListenId id) + { + session.checkSharedLocked(); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } + + RangeResults + Listen::find(Session& session, const FindParameters& parameters) + { + session.checkSharedLocked(); + + auto query {session.getDboSession().query("SELECT id FROM listen") + .orderBy("date_time")}; + + if (parameters.user.isValid()) + query.where("user_id = ?").bind(parameters.user); + + if (parameters.scrobbler) + query.where("scrobbler = ?").bind(*parameters.scrobbler); + + if (parameters.scrobblingState) + query.where("scrobbling_state = ?").bind(*parameters.scrobblingState); + + return execQuery(query, parameters.range); + } + + Listen::pointer + Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime) + { + session.checkSharedLocked(); + + return session.getDboSession().find() + .where("user_id = ?").bind(userId) + .where("track_id = ?").bind(trackId) + .where("scrobbler = ?").bind(scrobbler) + .where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t())) + .resultValue(); + } + + Listen::pointer + Listen::create(Session& session, ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime) + { + session.checkUniqueLocked(); + + Listen::pointer res {session.getDboSession().add(std::make_unique(user, track, scrobbler, dateTime))}; + session.getDboSession().flush(); + + return res; + } + + RangeResults + Listen::getTopArtists(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + std::optional linkType, + Range range) + { + auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)}; + + auto collection {query + .orderBy("COUNT(a.id) DESC") + .groupBy("a.id")}; + + return execQuery(query, range); + } + + RangeResults + Listen::getTopReleases(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range) + { + auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds) + .orderBy("COUNT(r.id) DESC") + .groupBy("r.id")}; + + return execQuery(query, range); + } + + RangeResults + Listen::getTopTracks(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range) + { + auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds) + .orderBy("COUNT(t.id) DESC") + .groupBy("t.id")}; + + return execQuery(query, range); + } + + RangeResults + Listen::getRecentArtists(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + std::optional linkType, + Range range) + { + auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) + .groupBy("a.id").having("l.date_time = MAX(l.date_time)") + .orderBy("l.date_time DESC")}; + + return execQuery(query, range); + } + + RangeResults + Listen::getRecentReleases(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range) + { + auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds) + .groupBy("r.id").having("l.date_time = MAX(l.date_time)") + .orderBy("l.date_time DESC")}; + + return execQuery(query, range); + } + + RangeResults + Listen::getRecentTracks(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range) + { + auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds) + .groupBy("t.id").having("l.date_time = MAX(l.date_time)") + .orderBy("l.date_time DESC")}; + + return execQuery(query, range); + } + + +} // namespace Database + diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp new file mode 100644 index 00000000..b83ebee2 --- /dev/null +++ b/src/libs/services/database/impl/Migration.cpp @@ -0,0 +1,631 @@ +/* + * Copyright (C) 2020 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 "Migration.hpp" + +#include + +#include "services/database/Db.hpp" +#include "services/database/ScanSettings.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" +#include "utils/Exception.hpp" +#include "utils/Logger.hpp" + +namespace Database +{ + VersionInfo::pointer + VersionInfo::getOrCreate(Session& session) + { + session.checkUniqueLocked(); + + pointer versionInfo {session.getDboSession().find()}; + if (!versionInfo) + return session.getDboSession().add(std::make_unique()); + + return versionInfo; + } + + 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"); + } + + ScopedNoForeignKeys(const ScopedNoForeignKeys&) = delete; + ScopedNoForeignKeys(ScopedNoForeignKeys&&) = delete; + ScopedNoForeignKeys& operator=(const ScopedNoForeignKeys&) = delete; + ScopedNoForeignKeys& operator=(ScopedNoForeignKeys&&) = delete; + + 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 + 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 + 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 + 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 +);)"); + } + + 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 + 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 + 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 + 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(); + } + + 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"); + + // 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"( +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"( +CREATE TABLE "user_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "type" integer not null, + "login_name" text not null, + "password_salt" text not null, + "password_hash" text not null, + "last_login" text, + "subsonic_transcode_enable" boolean not null, + "subsonic_transcode_format" integer not null, + "subsonic_transcode_bitrate" integer not null, + "subsonic_artist_list_mode" integer not null, + "ui_theme" integer not null, + "cur_playing_track_pos" integer not null, + "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"); + } + + 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"); + + // 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 ''"); + + // 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 + 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 + 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"( +CREATE TABLE "user_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "type" integer not null, + "login_name" text not null, + "password_salt" text not null, + "password_hash" text not null, + "last_login" text, + "subsonic_transcode_enable" boolean not null, + "subsonic_transcode_format" integer not null, + "subsonic_transcode_bitrate" integer not null, + "subsonic_artist_list_mode" integer not null, + "ui_theme" integer not null, + "cur_playing_track_pos" integer not null, + "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"); + } + + 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__"); + + // 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"( +CREATE TABLE "track_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scan_version" integer not null, + "track_number" integer not null, + "disc_number" integer not null, + "name" text not null, + "duration" integer, + "date" integer text, + "original_date" integer text, + "file_path" text not null, + "file_last_write" text, + "file_added" text, + "has_cover" boolean not null, + "mbid" text not null, + "copyright" text not null, + "copyright_url" text not null, + "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"); + + // 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"( +CREATE TABLE "starred_artist" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scrobbler" integer not null, + "date_time" text, + "artist_id" bigint, + "user_id" bigint, + constraint "fk_starred_artist_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred, + constraint "fk_starred_artist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred +))"); + + session.getDboSession().execute(R"( +CREATE TABLE "starred_release" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scrobbler" integer not null, + "date_time" text, + "release_id" bigint, + "user_id" bigint, + constraint "fk_starred_release_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred, + constraint "fk_starred_release_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred +))"); + + session.getDboSession().execute(R"( +CREATE TABLE "starred_track" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scrobbler" integer not null, + "date_time" text, + "track_id" bigint, + "user_id" bigint, + constraint "fk_starred_track_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, + 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 + + // 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; + + auto query {session.getDboSession().query("SELECT scrobbler FROM user WHERE id = ?").bind(userId)}; + 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; + + 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"; + + 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); + }}; + + 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"( +CREATE TABLE "listen" ( + "id" integer primary key autoincrement, + "version" integer not null, + "date_time" text, + "scrobbler" integer not null, + "scrobbling_state" integer not null, + "track_id" bigint, + "user_id" bigint, + constraint "fk_listen_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, + 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; + + 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; + + 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); + } + + static + void + migrateFromV32(Session& session) + { + ScanSettings::get(session).modify()->addAudioFileExtension(".wv"); + } + + void + doDbMigration(Session& session) + { + static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"}; + + ScopedNoForeignKeys noPragmaKeys {session.getDb()}; + + using MigrationFunction = std::function; + + 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}, + }; + + while (1) + { + auto uniqueTransaction {session.createUniqueTransaction()}; + + 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 == 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"}; + } + + if (version < migrationFunctions.begin()->first) + throw LmsException {outdatedMsg}; + + LMS_LOG(DB, INFO) << "Migrating database from version " << 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 new file mode 100644 index 00000000..9b4f1eed --- /dev/null +++ b/src/libs/services/database/impl/Migration.hpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 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 . + */ + +#pragma once + +#include + +namespace Database +{ + class Session; + + using Version = std::size_t; + static constexpr Version LMS_DATABASE_VERSION {33}; + class VersionInfo + { + public: + using pointer = Wt::Dbo::ptr; + + 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); } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _version, "db_version"); + } + + private: + int _version {LMS_DATABASE_VERSION}; + }; + + namespace Migration + { + void doDbMigration(Session& session); + } +} diff --git a/src/libs/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp similarity index 58% rename from src/libs/database/impl/Release.cpp rename to src/libs/services/database/impl/Release.cpp index a42d7c91..c268ef7c 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/services/database/impl/Release.cpp @@ -17,39 +17,56 @@ * along with LMS. If not, see . */ -#include "database/Release.hpp" +#include "services/database/Release.hpp" #include -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/User.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" #include "utils/Logger.hpp" #include "SqlQuery.hpp" -#include "Traits.hpp" +#include "IdTypeTraits.hpp" #include "Utils.hpp" namespace Database { -template -static -Wt::Dbo::Query -createQuery(Session& session, - const std::string& queryStr, - const std::vector& clusterIds, - const std::vector& keywords) +Wt::Dbo::Query +createQuery(Session& session, const Release::FindParameters& params) { + auto query {session.getDboSession().query("SELECT DISTINCT r.id from release r")}; - auto query {session.getDboSession().query(queryStr)}; - query.join("track t ON t.release_id = r.id"); + if (params.sortMethod == ReleaseSortMethod::LastWritten + || params.writtenAfter.isValid() + || params.dateRange) + { + query.join("track t ON t.release_id = r.id"); + } - for (std::string_view keyword : keywords) + if (params.writtenAfter.isValid()) + query.where("t.file_last_write > ?").bind(params.writtenAfter); + + if (params.dateRange) + { + query.where("t.date >= ?").bind(params.dateRange->begin); + query.where("t.date <= ?").bind(params.dateRange->end); + } + + for (std::string_view keyword : params.keywords) query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%"); - if (!clusterIds.empty()) + if (params.starringUser.isValid()) + { + assert(params.scrobbler); + query.join("starred_release s_r ON s_r.release_id = r.id") + .where("s_r.user_id = ?").bind(params.starringUser) + .where("s_r.scrobbler = ?").bind(*params.scrobbler); + } + + if (!params.clusters.empty()) { std::ostringstream oss; oss << "r.id IN (SELECT DISTINCT r.id FROM release r" @@ -58,18 +75,40 @@ createQuery(Session& session, " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; WhereClause clusterClause; - for (const ClusterId clusterId : clusterIds) + for (const ClusterId clusterId : params.clusters) { clusterClause.Or(WhereClause("c.id = ?")); query.bind(clusterId); } oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")"; + oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; query.where(oss.str()); } + switch (params.sortMethod) + { + case ReleaseSortMethod::None: + break; + case ReleaseSortMethod::Name: + query.orderBy("r.name COLLATE NOCASE"); + break; + case ReleaseSortMethod::Random: + query.orderBy("RANDOM()"); + break; + case ReleaseSortMethod::LastWritten: + query.orderBy("t.file_last_write DESC"); + break; + case ReleaseSortMethod::Date: + query.orderBy("t.date, r.name COLLATE NOCASE"); + break; + case ReleaseSortMethod::StarredDateDesc: + assert(params.starringUser.isValid()); + query.orderBy("s_r.date_time DESC"); + break; + } + return query; } @@ -80,7 +119,7 @@ _MBID {MBID ? MBID->getAsString() : ""} } std::vector -Release::getByName(Session& session, const std::string& name) +Release::find(Session& session, const std::string& name) { session.checkUniqueLocked(); @@ -93,7 +132,7 @@ Release::getByName(Session& session, const std::string& name) } Release::pointer -Release::getByMBID(Session& session, const UUID& mbid) +Release::find(Session& session, const UUID& mbid) { session.checkSharedLocked(); @@ -104,7 +143,7 @@ Release::getByMBID(Session& session, const UUID& mbid) } Release::pointer -Release::getById(Session& session, ReleaseId id) +Release::find(Session& session, ReleaseId id) { session.checkSharedLocked(); @@ -114,6 +153,13 @@ Release::getById(Session& session, ReleaseId id) .resultValue(); } +bool +Release::exists(Session& session, ReleaseId id) +{ + session.checkSharedLocked(); + return session.getDboSession().query("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1; +} + Release::pointer Release::create(Session& session, const std::string& name, const std::optional& MBID) { @@ -130,230 +176,44 @@ Release::getCount(Session& session) { session.checkSharedLocked(); - return session.getDboSession().find().resultList().size(); + return session.getDboSession().query("SELECT COUNT(*) FROM release"); } -std::vector -Release::getAll(Session& session, std::optional range) +RangeResults +Release::findOrderedByArtist(Session& session, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().find() - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) : -1) - .orderBy("name COLLATE NOCASE") - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -Release::getAllIds(Session& session) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM release"); - return std::vector(res.begin(), res.end()); -} - -std::vector -Release::getAllOrderedByArtist(Session& session, std::optional offset, std::optional size) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().query>( - "SELECT DISTINCT r FROM release r" + // TODO merge with execQuery + auto query {session.getDboSession().query( + "SELECT DISTINCT r.id FROM release r" " INNER JOIN track t ON r.id = t.release_id" " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" " INNER JOIN artist a ON t_a_l.artist_id = a.id") - .offset(offset ? static_cast(*offset) : -1) - .limit(size ? static_cast(*size) : -1) - .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") - .resultList()}; + .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")}; - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } -std::vector -Release::getAllRandom(Session& session, const std::vector& clusterIds, std::optional size) +RangeResults +Release::findOrphans(Session& session, Range range) { session.checkSharedLocked(); - auto query {createQuery>(session, "SELECT DISTINCT r from release r", clusterIds, {})}; - auto res {query - .orderBy("RANDOM()") - .limit(size ? static_cast(*size) : -1) - .resultList()}; - - return std::vector(res.begin(), res.end()); + auto query {session.getDboSession().query("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL")}; + return execQuery(query, range); } -std::vector -Release::getAllIdsRandom(Session& session, const std::vector& clusterIds, std::optional size) +RangeResults +Release::find(Session& session, const FindParameters& params) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT DISTINCT r.id from release r", clusterIds, {})}; + auto query {createQuery(session, params)}; - Wt::Dbo::collection res = query - .orderBy("RANDOM()") - .limit(size ? static_cast(*size) : -1); - - return std::vector(res.begin(), res.end()); + return execQuery(query, params.range); } - -std::vector -Release::getAllOrphans(Session& session) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().query>("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL").resultList()}; - return std::vector(res.begin(), res.end()); -} - -std::vector -Release::getLastWritten(Session& session, - std::optional after, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT r from release r", clusterIds, {})}; - if (after) - query.where("t.file_last_write > ?").bind(after); - - auto collection {query - .orderBy("t.file_last_write DESC") - .groupBy("r.id") - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional range) -{ - auto res {session.getDboSession().query> - ("SELECT DISTINCT r from release r INNER JOIN track t ON r.id = t.release_id") - .where("t.date >= ?").bind(Wt::WDate {yearFrom, 1, 1}) - .where("t.date <= ?").bind(Wt::WDate {yearTo, 12, 31}) - .orderBy("t.date, r.name COLLATE NOCASE") - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) : -1) - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -Release::getStarred(Session& session, - User::pointer user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT r from release r", clusterIds, {})}; - { - std::ostringstream oss; - oss << "r.id IN (SELECT DISTINCT r.id FROM release r" - " INNER JOIN user_release_starred urs ON urs.release_id = r.id" - " INNER JOIN user u ON u.id = urs.user_id WHERE u.id = ?)"; - - query.bind(user->getId()); - query.where(oss.str()); - } - - auto collection {query - .groupBy("r.id") - .orderBy("r.name COLLATE NOCASE") - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; - - -} - -std::vector -Release::getByClusters(Session& session, const std::vector& clusters) -{ - assert(!clusters.empty()); - - session.checkSharedLocked(); - - bool moreResults; - return getByFilter(session, clusters, {}, std::nullopt, moreResults); -} - -std::vector -Release::getByFilter(Session& session, - const std::vector& clusterIds, - const std::vector& keywords, - std::optional range, - bool& moreResults) -{ - session.checkSharedLocked(); - - auto collection {createQuery>(session, "SELECT r from release r", clusterIds, keywords) - .groupBy("r.id") - .orderBy("r.name COLLATE NOCASE") - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -Release::getAllIdsWithClusters(Session& session, std::optional limit) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query - ("SELECT DISTINCT r.id FROM release r" - " INNER JOIN track t ON t.release_id = r.id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .limit(limit ? static_cast(*limit) : -1); - - return std::vector(res.begin(), res.end()); -} - - std::optional Release::getTotalTrack(void) const { diff --git a/src/libs/database/impl/ScanSettings.cpp b/src/libs/services/database/impl/ScanSettings.cpp similarity index 94% rename from src/libs/database/impl/ScanSettings.cpp rename to src/libs/services/database/impl/ScanSettings.cpp index 9aafed53..70f08b72 100644 --- a/src/libs/database/impl/ScanSettings.cpp +++ b/src/libs/services/database/impl/ScanSettings.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "database/ScanSettings.hpp" +#include "services/database/ScanSettings.hpp" #include @@ -25,8 +25,8 @@ #include "utils/Logger.hpp" #include "utils/String.hpp" -#include "database/Cluster.hpp" -#include "database/Session.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Session.hpp" namespace { @@ -111,7 +111,7 @@ ScanSettings::setClusterTypes(Session& session, const std::set& clu // Create any missing cluster type for (const std::string& clusterTypeName : clusterTypeNames) { - ClusterType::pointer clusterType {ClusterType::getByName(session, clusterTypeName)}; + ClusterType::pointer clusterType {ClusterType::find(session, clusterTypeName)}; if (!clusterType) { LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName; diff --git a/src/libs/services/database/impl/Session.cpp b/src/libs/services/database/impl/Session.cpp new file mode 100644 index 00000000..660e7082 --- /dev/null +++ b/src/libs/services/database/impl/Session.cpp @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2020 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 "services/database/Session.hpp" + +#include + +#include "utils/Exception.hpp" +#include "utils/Logger.hpp" + +#include "services/database/Artist.hpp" +#include "services/database/AuthToken.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Db.hpp" +#include "services/database/Listen.hpp" +#include "services/database/Release.hpp" +#include "services/database/ScanSettings.hpp" +#include "services/database/StarredArtist.hpp" +#include "services/database/StarredRelease.hpp" +#include "services/database/StarredTrack.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackBookmark.hpp" +#include "services/database/TrackArtistLink.hpp" +#include "services/database/TrackList.hpp" +#include "services/database/TrackFeatures.hpp" +#include "services/database/User.hpp" +#include "Migration.hpp" + +namespace Database +{ + +Session::Session(Db& db) +: _db {db} +{ + _session.setConnectionPool(_db.getConnectionPool()); + + _session.mapClass("version_info"); + _session.mapClass("artist"); + _session.mapClass("auth_token"); + _session.mapClass("cluster"); + _session.mapClass("cluster_type"); + _session.mapClass("listen"); + _session.mapClass("release"); + _session.mapClass("scan_settings"); + _session.mapClass("starred_artist"); + _session.mapClass("starred_release"); + _session.mapClass("starred_track"); + _session.mapClass("track"); + _session.mapClass("track_bookmark"); + _session.mapClass("track_artist_link"); + _session.mapClass("track_features"); + _session.mapClass("tracklist"); + _session.mapClass("tracklist_entry"); + _session.mapClass("user"); +} + +UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session) +: _lock {mutex}, + _transaction {session} +{ +} + +SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session) +: _lock {mutex}, + _transaction {session} +{ +} + +void +Session::checkUniqueLocked() +{ + assert(_db.getMutex().isUniqueLocked()); +} + +void +Session::checkSharedLocked() +{ + assert(_db.getMutex().isSharedLocked()); +} + +UniqueTransaction +Session::createUniqueTransaction() +{ + return UniqueTransaction {_db.getMutex(), _session}; +} + +SharedTransaction +Session::createSharedTransaction() +{ + return SharedTransaction {_db.getMutex(), _session}; +} + +void +Session::prepareTables() +{ + // Creation case + try + { + _session.createTables(); + + LMS_LOG(DB, INFO) << "Tables created"; + } + catch (Wt::Dbo::Exception& e) + { + LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what(); + } + + Migration::doDbMigration(*this); + + // Indexes + { + auto uniqueTransaction {createUniqueTransaction()}; + _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); + _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)"); + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)"); + _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); + _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_name_idx ON track_artist_link(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_scrobbler_idx ON listen(scrobbler)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_scrobbler_idx ON listen(user_id,scrobbler)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_scrobbler_idx ON starred_artist(user_id,scrobbler)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_scrobbler_idx ON starred_release(user_id,scrobbler)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler)"); + } + + // Initial settings tables + { + auto uniqueTransaction {createUniqueTransaction()}; + + ScanSettings::init(*this); + } +} + +void +Session::optimize() +{ + LMS_LOG(DB, DEBUG) << "Optimizing db..."; + { + auto uniqueTransaction {createUniqueTransaction()}; + _session.execute("ANALYZE"); + } + LMS_LOG(DB, DEBUG) << "Optimized db!"; +} + +} // namespace Database diff --git a/src/libs/database/impl/SqlQuery.cpp b/src/libs/services/database/impl/SqlQuery.cpp similarity index 100% rename from src/libs/database/impl/SqlQuery.cpp rename to src/libs/services/database/impl/SqlQuery.cpp diff --git a/src/libs/database/impl/SqlQuery.hpp b/src/libs/services/database/impl/SqlQuery.hpp similarity index 98% rename from src/libs/database/impl/SqlQuery.hpp rename to src/libs/services/database/impl/SqlQuery.hpp index 76e6487e..bb15408a 100644 --- a/src/libs/database/impl/SqlQuery.hpp +++ b/src/libs/services/database/impl/SqlQuery.hpp @@ -26,7 +26,6 @@ class WhereClause { public: - WhereClause() {} WhereClause(const std::string& clause) { _clause = clause; } @@ -40,16 +39,13 @@ class WhereClause const std::list& getBindArgs(void) const {return _bindArgs;} private: - std::string _clause; // WHERE clause std::list _bindArgs; - }; class InnerJoinClause { public: - InnerJoinClause() {} InnerJoinClause(const std::string& clause); @@ -57,7 +53,6 @@ class InnerJoinClause std::string get() const { return _clause;} private: - std::string _clause; }; @@ -73,7 +68,6 @@ class GroupByStatement private: std::string _statement; // SELECT statement - }; class SelectStatement @@ -87,14 +81,12 @@ class SelectStatement std::string get() const; private: - std::list _statement; }; class FromClause { public: - FromClause() {} FromClause(const std::string& clause); @@ -103,15 +95,12 @@ class FromClause std::string get() const; private: - std::list _clause; - }; class SqlQuery { public: - SelectStatement& select(void) { return _selectStatement;} SelectStatement& select(const std::string& statement) { _selectStatement = SelectStatement(statement); return _selectStatement; } FromClause& from(void) { return _fromClause; } @@ -122,10 +111,9 @@ class SqlQuery GroupByStatement& groupBy(void) { return _groupByStatement; } const GroupByStatement& groupBy(void) const { return _groupByStatement; } - std::string get(void) const; + std::string get() const; private: - SelectStatement _selectStatement; // SELECT statement InnerJoinClause _innerJoinClause; // INNER JOIN FromClause _fromClause; // FROM tables diff --git a/src/libs/services/database/impl/StarredArtist.cpp b/src/libs/services/database/impl/StarredArtist.cpp new file mode 100644 index 00000000..ed617665 --- /dev/null +++ b/src/libs/services/database/impl/StarredArtist.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "services/database/StarredArtist.hpp" + +#include + +#include "services/database/Artist.hpp" +#include "services/database/User.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" + +namespace Database +{ + StarredArtist::StarredArtist(ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler) + : _scrobbler {scrobbler} + , _artist {getDboPtr(artist)} + , _user {getDboPtr(user)} + { + } + + std::size_t + StarredArtist::getCount(Session& session) + { + session.checkSharedLocked(); + return session.getDboSession().query("SELECT COUNT(*) FROM starred_artist"); + } + + StarredArtist::pointer + StarredArtist::find(Session& session, StarredArtistId id) + { + session.checkSharedLocked(); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } + + StarredArtist::pointer + StarredArtist::find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler) + { + session.checkSharedLocked(); + return session.getDboSession().find() + .where("artist_id = ?").bind(artistId) + .where("user_id = ?").bind(userId) + .where("scrobbler = ?").bind(scrobbler) + .resultValue(); + } + + StarredArtist::pointer + StarredArtist::create(Session& session, ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler) + { + session.checkUniqueLocked(); + + StarredArtist::pointer res {session.getDboSession().add(std::make_unique(artist, user, scrobbler))}; + session.getDboSession().flush(); + + return res; + } + + void + StarredArtist::setDateTime(const Wt::WDateTime& dateTime) + { + _dateTime = normalizeDateTime(dateTime); + } +} diff --git a/src/libs/services/database/impl/StarredRelease.cpp b/src/libs/services/database/impl/StarredRelease.cpp new file mode 100644 index 00000000..c72331d5 --- /dev/null +++ b/src/libs/services/database/impl/StarredRelease.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "services/database/StarredRelease.hpp" + +#include + +#include "services/database/Release.hpp" +#include "services/database/User.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" + +namespace Database +{ + StarredRelease::StarredRelease(ObjectPtr release, ObjectPtr user, Scrobbler scrobbler) + : _scrobbler {scrobbler} + , _release {getDboPtr(release)} + , _user {getDboPtr(user)} + { + } + + std::size_t + StarredRelease::getCount(Session& session) + { + session.checkSharedLocked(); + return session.getDboSession().query("SELECT COUNT(*) FROM starred_release"); + } + + StarredRelease::pointer + StarredRelease::find(Session& session, StarredReleaseId id) + { + session.checkSharedLocked(); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } + + StarredRelease::pointer + StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler) + { + session.checkSharedLocked(); + return session.getDboSession().find() + .where("release_id = ?").bind(releaseId) + .where("user_id = ?").bind(userId) + .where("scrobbler = ?").bind(scrobbler) + .resultValue(); + } + + StarredRelease::pointer + StarredRelease::create(Session& session, ObjectPtr release, ObjectPtr user, Scrobbler scrobbler) + { + session.checkUniqueLocked(); + + StarredRelease::pointer res {session.getDboSession().add(std::make_unique(release, user, scrobbler))}; + session.getDboSession().flush(); + + return res; + } + + void + StarredRelease::setDateTime(const Wt::WDateTime& dateTime) + { + _dateTime = normalizeDateTime(dateTime); + } +} diff --git a/src/libs/services/database/impl/StarredTrack.cpp b/src/libs/services/database/impl/StarredTrack.cpp new file mode 100644 index 00000000..6543654f --- /dev/null +++ b/src/libs/services/database/impl/StarredTrack.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "services/database/StarredTrack.hpp" + +#include + +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" + +namespace Database +{ + StarredTrack::StarredTrack(ObjectPtr track, ObjectPtr user, Scrobbler scrobbler) + : _scrobbler {scrobbler} + , _track {getDboPtr(track)} + , _user {getDboPtr(user)} + { + } + + std::size_t + StarredTrack::getCount(Session& session) + { + session.checkSharedLocked(); + return session.getDboSession().query("SELECT COUNT(*) FROM starred_track"); + } + + StarredTrack::pointer + StarredTrack::find(Session& session, StarredTrackId id) + { + session.checkSharedLocked(); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } + + StarredTrack::pointer + StarredTrack::find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler) + { + session.checkSharedLocked(); + return session.getDboSession().find() + .where("track_id = ?").bind(trackId) + .where("user_id = ?").bind(userId) + .where("scrobbler = ?").bind(scrobbler) + .resultValue(); + } + + StarredTrack::pointer + StarredTrack::create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler) + { + session.checkUniqueLocked(); + + StarredTrack::pointer res {session.getDboSession().add(std::make_unique(track, user, scrobbler))}; + session.getDboSession().flush(); + + return res; + } + + void + StarredTrack::setDateTime(const Wt::WDateTime& dateTime) + { + _dateTime = normalizeDateTime(dateTime); + } +} diff --git a/src/libs/database/impl/StringViewTraits.hpp b/src/libs/services/database/impl/StringViewTraits.hpp similarity index 100% rename from src/libs/database/impl/StringViewTraits.hpp rename to src/libs/services/database/impl/StringViewTraits.hpp diff --git a/src/libs/database/impl/Track.cpp b/src/libs/services/database/impl/Track.cpp similarity index 51% rename from src/libs/database/impl/Track.cpp rename to src/libs/services/database/impl/Track.cpp index e8f3436b..6c1c2a94 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/services/database/impl/Track.cpp @@ -17,42 +17,49 @@ * along with LMS. If not, see . */ -#include "database/Track.hpp" +#include "services/database/Track.hpp" #include -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Release.hpp" -#include "database/TrackArtistLink.hpp" -#include "database/TrackFeatures.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Release.hpp" +#include "services/database/TrackArtistLink.hpp" +#include "services/database/TrackFeatures.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" #include "utils/Logger.hpp" +#include "IdTypeTraits.hpp" #include "SqlQuery.hpp" #include "StringViewTraits.hpp" -#include "Traits.hpp" #include "Utils.hpp" namespace Database { -template static -Wt::Dbo::Query -createQuery(Session& session, - const std::string& queryStr, - const std::vector& clusterIds, - const std::vector& keywords) +Wt::Dbo::Query +createQuery(Session& session, const Track::FindParameters& params) { session.checkSharedLocked(); - auto query {session.getDboSession().query(queryStr)}; + auto query {session.getDboSession().query("SELECT t.id from track t")}; - for (std::string_view keyword : keywords) + for (std::string_view keyword : params.keywords) query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%"); - if (!clusterIds.empty()) + if (params.writtenAfter.isValid()) + query.where("t.file_last_write > ?").bind(params.writtenAfter); + + if (params.starringUser.isValid()) + { + assert(params.scrobbler); + query.join("starred_track s_t ON s_t.track_id = t.id") + .where("s_t.user_id = ?").bind(params.starringUser) + .where("s_t.scrobbler = ?").bind(*params.scrobbler); + } + + if (!params.clusters.empty()) { std::ostringstream oss; oss << "t.id IN (SELECT DISTINCT t.id FROM track t" @@ -60,18 +67,34 @@ createQuery(Session& session, " INNER JOIN cluster c ON c.id = t_c.cluster_id"; WhereClause clusterClause; - for (const ClusterId clusterId : clusterIds) + for (const ClusterId clusterId : params.clusters) { clusterClause.Or(WhereClause("c.id = ?")); query.bind(clusterId); } oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")"; + oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; query.where(oss.str()); } + switch (params.sortMethod) + { + case TrackSortMethod::None: + break; + case TrackSortMethod::LastWritten: + query.orderBy("t.file_last_write DESC"); + break; + case TrackSortMethod::Random: + query.orderBy("RANDOM()"); + break; + case TrackSortMethod::StarredDateDesc: + assert(params.starringUser.isValid()); + query.orderBy("s_t.date_time DESC"); + break; + } + return query; } @@ -80,94 +103,6 @@ Track::Track(const std::filesystem::path& p) { } -std::size_t -Track::getCount(Session& session) -{ - session.checkSharedLocked(); - - return session.getDboSession().query("SELECT COUNT(*) FROM track"); -} - -std::vector -Track::getAll(Session& session, std::optional limit) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().find() - .limit(limit ? static_cast(*limit) : -1) - .resultList()}; - - return std::vector(std::cbegin(res), std::cend(res)); -} - -std::vector -Track::getAllRandom(Session& session, const std::vector& clusterIds, std::optional limit) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT t from track t", clusterIds, {})}; - auto collection {query - .orderBy("RANDOM()") - .limit(limit ? static_cast(*limit) + 1: -1) - .resultList()}; - - return std::vector(collection.begin(), collection.end()); -} - -std::vector -Track::getAllIdsRandom(Session& session, const std::vector& clusterIds, std::optional limit) -{ - session.checkSharedLocked(); - - auto query {createQuery(session, "SELECT t.id from track t", clusterIds, {})}; - - Wt::Dbo::collection collection = query - .orderBy("RANDOM()") - .limit(limit ? static_cast(*limit) + 1: -1); - - return std::vector(collection.begin(), collection.end()); -} - - -std::vector -Track::getAllIds(Session& session) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM track"); - return std::vector(res.begin(), res.end()); -} - -Track::pointer -Track::getByPath(Session& session, const std::filesystem::path& p) -{ - session.checkSharedLocked(); - - return session.getDboSession().find().where("file_path = ?").bind(p.string()).resultValue(); -} - -Track::pointer -Track::getById(Session& session, TrackId id) -{ - session.checkSharedLocked(); - - return session.getDboSession().find() - .where("id = ?").bind(id) - .resultValue(); -} - -std::vector -Track::getByRecordingMBID(Session& session, const UUID& mbid) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().find() - .where("recording_mbid = ?").bind(std::string {mbid.getAsString()}) - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - Track::pointer Track::create(Session& session, const std::filesystem::path& p) { @@ -179,140 +114,98 @@ Track::create(Session& session, const std::filesystem::path& p) return res; } -std::vector> -Track::getAllPaths(Session& session, std::optional offset, std::optional size) +std::size_t +Track::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM track"); +} + +Track::pointer +Track::findByPath(Session& session, const std::filesystem::path& p) +{ + session.checkSharedLocked(); + + return session.getDboSession().find().where("file_path = ?").bind(p.string()).resultValue(); +} + +Track::pointer +Track::find(Session& session, TrackId id) +{ + session.checkSharedLocked(); + + return session.getDboSession().find() + .where("id = ?").bind(id) + .resultValue(); +} + +bool +Track::exists(Session& session, TrackId id) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT 1 from track").where("id = ?").bind(id).resultValue() == 1; +} + +std::vector +Track::findByRecordingMBID(Session& session, const UUID& mbid) +{ + session.checkSharedLocked(); + + auto res {session.getDboSession().find() + .where("recording_mbid = ?").bind(std::string {mbid.getAsString()}) + .resultList()}; + + return std::vector(res.begin(), res.end()); +} + +RangeResults +Track::findPaths(Session& session, Range range) { using QueryResultType = std::tuple; session.checkSharedLocked(); - Wt::Dbo::collection queryRes = session.getDboSession().query("SELECT id,file_path FROM track") - .limit(size ? static_cast(*size) + 1 : -1) - .offset(offset ? static_cast(*offset) : -1); + // TODO Dbo traits on filesystem + auto query {session.getDboSession().query("SELECT id, file_path FROM track")}; - std::vector> result; - result.reserve(queryRes.size()); + RangeResults queryResults {execQuery(query, range)}; - std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result), + RangeResults res; + res.range = queryResults.range; + res.moreResults = queryResults.moreResults; + res.results.reserve(queryResults.results.size()); + + std::transform(std::cbegin(queryResults.results), std::cend(queryResults.results), std::back_inserter(res.results), [](const QueryResultType& queryResult) { - return std::make_pair(std::get<0>(queryResult), std::get<1>(queryResult)); + return PathResult {std::get<0>(queryResult), std::get<1>(queryResult)}; }); - return result; -} - -std::vector -Track::getMBIDDuplicates(Session& session) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().query>( "SELECT track FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)") - .orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") - .resultList()}; - return std::vector(res.begin(), res.end()); -} - -std::vector -Track::getLastWritten(Session& session, std::optional after, const std::vector& clusterIds, std::optional range, bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT t from track t", clusterIds, {})}; - if (after) - query.where("t.file_last_write > ?").bind(after); - - auto collection {query - .orderBy("t.file_last_write DESC") - .groupBy("t.id") - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - return res; } -std::vector -Track::getAllWithRecordingMBIDAndMissingFeatures(Session& session) +RangeResults +Track::findRecordingMBIDDuplicates(Session& session, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().query> - ("SELECT t FROM track t") + auto query {session.getDboSession().query( "SELECT track.id FROM track WHERE recording_mbid in (SELECT recording_mbid FROM track WHERE recording_mbid <> '' GROUP BY recording_mbid HAVING COUNT (*) > 1)") + .orderBy("track.release_id,track.disc_number,track.track_number,track.recording_mbid")}; + + return execQuery(query, range); +} + +RangeResults +Track::findWithRecordingMBIDAndMissingFeatures(Session& session, Range range) +{ + session.checkSharedLocked(); + + auto query {session.getDboSession().query("SELECT t.id FROM track t") .where("LENGTH(t.recording_mbid) > 0") - .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") - .resultList()}; - return std::vector(res.begin(), res.end()); -} + .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)")}; -std::vector -Track::getAllIdsWithFeatures(Session& session, std::optional limit) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query - ("SELECT t.id FROM track t") - .where("EXISTS (SELECT * from track_features t_f WHERE t_f.track_id = t.id)") - .limit(limit ? static_cast(*limit) : -1); - - return std::vector(res.begin(), res.end()); -} - -std::vector -Track::getAllIdsWithClusters(Session& session, std::optional limit) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query - ("SELECT DISTINCT t.id FROM track t" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .limit(limit ? static_cast(*limit) : -1); - - return std::vector(res.begin(), res.end()); -} - -std::vector -Track::getStarred(Session& session, - ObjectPtr user, - const std::vector& clusterIds, - std::optional range, bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT t from track t", clusterIds, {})}; - { - std::ostringstream oss; - oss << "t.id IN (SELECT DISTINCT t.id FROM track t" - " INNER JOIN user_track_starred uts ON uts.track_id = t.id" - " INNER JOIN user u ON u.id = uts.user_id WHERE u.id = ?)"; - - query.bind(user->getId().toString()); - query.where(oss.str()); - } - - auto collection {query - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; + return execQuery(query, range); } std::vector @@ -334,56 +227,31 @@ Track::getClusterIds() const return std::vector(res.begin(), res.end()); } -bool -Track::hasTrackFeatures() const -{ - return (_trackFeatures.lock() != Wt::Dbo::ptr {}); -} - -std::vector -Track::getByFilter(Session& session, - const std::vector& clusterIds, - const std::vector& keywords, - std::optional range, - bool& moreResults) +RangeResults +Track::find(Session& session, const FindParameters& parameters) { session.checkSharedLocked(); - auto collection {createQuery>(session, "SELECT t from track t", clusterIds, keywords) - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; + auto query {createQuery(session, parameters)}; - std::vector res(collection.begin(), collection.end()); - if (range && (res.size() == static_cast(range->limit) + 1)) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; + return execQuery(query, parameters.range); } -std::vector -Track::getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName) +RangeResults +Track::findByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName) { session.checkSharedLocked(); - auto res {session.getDboSession().query>("SELECT t from track t") + auto query {session.getDboSession().query("SELECT t.id from track t") .join("release r ON t.release_id = r.id") .where("t.name = ?").bind(trackName) - .where("r.name = ?").bind(releaseName) - .resultList()}; - return std::vector(res.begin(), res.end()); + .where("r.name = ?").bind(releaseName)}; + + return execQuery(query, Range {}); } -std::vector -Track::getSimilarTracks(Session& session, - const std::vector& tracks, - std::optional offset, - std::optional size) +RangeResults +Track::findSimilarTracks(Session& session, const std::vector& tracks, Range range) { assert(!tracks.empty()); session.checkSharedLocked(); @@ -396,15 +264,13 @@ Track::getSimilarTracks(Session& session, oss << "?"; } - auto query {session.getDboSession().query>( - "SELECT t FROM track t" + auto query {session.getDboSession().query( + "SELECT t.id FROM track t" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " AND t_c.cluster_id IN (SELECT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN (" + oss.str() + "))" " AND t.id NOT IN (" + oss.str() + ")") .groupBy("t.id") - .orderBy("COUNT(*) DESC, RANDOM()") - .limit(size ? static_cast(*size) : -1) - .offset(offset ? static_cast(*offset) : -1)}; + .orderBy("COUNT(*) DESC, RANDOM()")}; for (TrackId trackId : tracks) query.bind(trackId); @@ -412,22 +278,7 @@ Track::getSimilarTracks(Session& session, for (TrackId trackId : tracks) query.bind(trackId); - auto res {query.resultList()}; - return std::vector(res.begin(), res.end()); -} - -std::vector -Track::getByClusters(Session& session, const std::vector& clusters) -{ - assert(!clusters.empty()); - session.checkSharedLocked(); - - bool moreResults; - return getByFilter(session, - clusters, - {}, // keywords - std::nullopt, // range - moreResults); + return execQuery(query, range); } void @@ -450,12 +301,6 @@ Track::setClusters(const std::vector>& clusters) _clusters.insert(getDboPtr(cluster)); } -void -Track::setFeatures(const ObjectPtr& features) -{ - _trackFeatures = getDboPtr(features); -} - std::optional Track::getTrackNumber() const { @@ -585,12 +430,6 @@ Track::getArtistLinks() const return std::vector(_trackArtistLinks.begin(), _trackArtistLinks.end()); } -ObjectPtr -Track::getTrackFeatures() const -{ - return _trackFeatures.lock(); -} - std::vector> Track::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const { diff --git a/src/libs/database/impl/TrackArtistLink.cpp b/src/libs/services/database/impl/TrackArtistLink.cpp similarity index 86% rename from src/libs/database/impl/TrackArtistLink.cpp rename to src/libs/services/database/impl/TrackArtistLink.cpp index f8791dee..d54339e8 100644 --- a/src/libs/database/impl/TrackArtistLink.cpp +++ b/src/libs/services/database/impl/TrackArtistLink.cpp @@ -17,13 +17,13 @@ * along with LMS. If not, see . */ -#include "database/TrackArtistLink.hpp" +#include "services/database/TrackArtistLink.hpp" -#include "database/Artist.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" -#include "Traits.hpp" +#include "IdTypeTraits.hpp" namespace Database { @@ -46,7 +46,7 @@ TrackArtistLink::create(Session& session, ObjectPtr track, ObjectPtr -TrackArtistLink::getUsedTypes(Session& session) +TrackArtistLink::findUsedTypes(Session& session) { session.checkSharedLocked(); diff --git a/src/libs/database/impl/TrackBookmark.cpp b/src/libs/services/database/impl/TrackBookmark.cpp similarity index 62% rename from src/libs/database/impl/TrackBookmark.cpp rename to src/libs/services/database/impl/TrackBookmark.cpp index 91f457bd..28e169ca 100644 --- a/src/libs/database/impl/TrackBookmark.cpp +++ b/src/libs/services/database/impl/TrackBookmark.cpp @@ -17,12 +17,13 @@ * along with LMS. If not, see . */ -#include "database/TrackBookmark.hpp" +#include "services/database/TrackBookmark.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/User.hpp" -#include "Traits.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" namespace Database { @@ -32,6 +33,15 @@ _track {getDboPtr(track)} { } +std::size_t +TrackBookmark::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM track_bookmark"); +} + + TrackBookmark::pointer TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr track) { @@ -43,40 +53,30 @@ TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr t return res; } -std::vector -TrackBookmark::getAll(Session& session) +RangeResults +TrackBookmark::find(Session& session, UserId userId, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().find().resultList()}; - return std::vector(std::cbegin(res), std::cend(res)); -} + auto query {session.getDboSession().query("SELECT id from track_bookmark") + .where("user_id = ?").bind(userId)}; -std::vector -TrackBookmark::getByUser(Session& session, User::pointer user) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().find() - .where("user_id = ?").bind(user->getId()) - .resultList()}; - - return std::vector(std::cbegin(res), std::cend(res)); + return execQuery(query, range); } TrackBookmark::pointer -TrackBookmark::getByUser(Session& session, ObjectPtr user, ObjectPtr track) +TrackBookmark::find(Session& session, UserId userId, TrackId trackId) { session.checkSharedLocked(); return session.getDboSession().find() - .where("user_id = ?").bind(user->getId()) - .where("track_id = ?").bind(track->getId()) + .where("user_id = ?").bind(userId) + .where("track_id = ?").bind(trackId) .resultValue(); } TrackBookmark::pointer -TrackBookmark::getById(Session& session, TrackBookmarkId id) +TrackBookmark::find(Session& session, TrackBookmarkId id) { session.checkSharedLocked(); @@ -85,6 +85,5 @@ TrackBookmark::getById(Session& session, TrackBookmarkId id) .resultValue(); } - } // namespace Database diff --git a/src/libs/database/impl/TrackFeatures.cpp b/src/libs/services/database/impl/TrackFeatures.cpp similarity index 63% rename from src/libs/database/impl/TrackFeatures.cpp rename to src/libs/services/database/impl/TrackFeatures.cpp index 5770d0b1..628c10c7 100644 --- a/src/libs/database/impl/TrackFeatures.cpp +++ b/src/libs/services/database/impl/TrackFeatures.cpp @@ -17,14 +17,16 @@ * along with LMS. If not, see . */ -#include "database/TrackFeatures.hpp" +#include "services/database/TrackFeatures.hpp" #include #include -#include "database/Session.hpp" -#include "database/Track.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" #include "utils/Logger.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" namespace Database { @@ -34,11 +36,53 @@ _track {getDboPtr(track)} { } +std::size_t +TrackFeatures::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM track_features"); +} + +TrackFeatures::pointer +TrackFeatures::find(Session& session, TrackFeaturesId id) +{ + session.checkSharedLocked(); + + return session.getDboSession().find() + .where("id = ?").bind(id) + .resultValue(); +} + +TrackFeatures::pointer +TrackFeatures::find(Session& session, TrackId trackId) +{ + session.checkSharedLocked(); + + return session.getDboSession().find() + .where("track_id = ?").bind(trackId) + .resultValue(); +} + +RangeResults +TrackFeatures::find(Session& session, Range range) +{ + session.checkSharedLocked(); + + auto query {session.getDboSession().query("SELECT id from track_features")}; + + return execQuery(query, range); +} + TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures) { session.checkUniqueLocked(); - return session.getDboSession().add(std::make_unique(track, jsonEncodedFeatures)); + + TrackFeatures::pointer res {session.getDboSession().add(std::make_unique(track, jsonEncodedFeatures))}; + session.getDboSession().flush(); + + return res; } FeatureValues @@ -51,6 +95,8 @@ TrackFeatures::getFeatureValues(const FeatureName& featureNode) const FeatureValuesMap TrackFeatures::getFeatureValuesMap(const std::unordered_set& featureNames) const { + FeatureValuesMap res; + try { std::istringstream iss {_data}; @@ -58,7 +104,6 @@ TrackFeatures::getFeatureValuesMap(const std::unordered_set& featur boost::property_tree::read_json(iss, root); - FeatureValuesMap res; for (const FeatureName& featureName : featureNames) { FeatureValues& featureValues {res[featureName]}; @@ -75,14 +120,14 @@ TrackFeatures::getFeatureValuesMap(const std::unordered_set& featur if (!hasChildren) featureValues.push_back(node.get_value()); } - - return res; } catch (boost::property_tree::ptree_error& error) { LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what(); - return {}; + res.clear(); } + + return res; } } // namespace Database diff --git a/src/libs/database/impl/TrackList.cpp b/src/libs/services/database/impl/TrackList.cpp similarity index 70% rename from src/libs/database/impl/TrackList.cpp rename to src/libs/services/database/impl/TrackList.cpp index a9863d52..58ea58b2 100644 --- a/src/libs/database/impl/TrackList.cpp +++ b/src/libs/services/database/impl/TrackList.cpp @@ -16,21 +16,22 @@ * You should have received a copy of the GNU General Public License * along with LMS. If not, see . */ -#include "database/TrackList.hpp" +#include "services/database/TrackList.hpp" #include #include "utils/Logger.hpp" -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" -#include "database/Track.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" +#include "services/database/Track.hpp" #include "SqlQuery.hpp" #include "StringViewTraits.hpp" -#include "Traits.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" namespace Database { @@ -40,7 +41,6 @@ TrackList::TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr< _isPublic {isPublic}, _user {getDboPtr(user)} { - } TrackList::pointer @@ -55,56 +55,54 @@ TrackList::create(Session& session, std::string_view name, Type type, bool isPub return res; } -TrackList::pointer -TrackList::get(Session& session, std::string_view name, Type type, ObjectPtr user) +std::size_t +TrackList::getCount(Session& session) { session.checkSharedLocked(); - assert(user); + + return session.getDboSession().query("SELECT COUNT(*) FROM tracklist"); +} + + +TrackList::pointer +TrackList::find(Session& session, std::string_view name, Type type, UserId userId) +{ + session.checkSharedLocked(); + assert(userId.isValid()); return session.getDboSession().find() .where("name = ?").bind(name) .where("type = ?").bind(type) - .where("user_id = ?").bind(user->getId()).resultValue(); + .where("user_id = ?").bind(userId).resultValue(); } -std::vector -TrackList::getAll(Session& session) +RangeResults +TrackList::find(Session& session, UserId userId, Range range) { session.checkSharedLocked(); - auto res = session.getDboSession().find().resultList(); - return std::vector(res.begin(), res.end()); + auto query {session.getDboSession().query("SELECT id FROM tracklist") + .where("user_id = ?").bind(userId) + .orderBy("name COLLATE NOCASE")}; + + return execQuery(query, range); } -std::vector -TrackList::getAll(Session& session, ObjectPtr user) +RangeResults +TrackList::find(Session& session, UserId userId, Type type, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().find() - .where("user_id = ?").bind(user->getId()) - .orderBy("name COLLATE NOCASE") - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -TrackList::getAll(Session& session, ObjectPtr user, Type type) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().find() - .where("user_id = ?").bind(user->getId()) + auto query {session.getDboSession().query("SELECT id FROM tracklist") + .where("user_id = ?").bind(userId) .where("type = ?").bind(type) - .orderBy("name COLLATE NOCASE") - .resultList()}; + .orderBy("name COLLATE NOCASE")}; - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } TrackList::pointer -TrackList::getById(Session& session, TrackListId id) +TrackList::find(Session& session, TrackListId id) { session.checkSharedLocked(); @@ -159,7 +157,7 @@ TrackList::getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTim return session()->find() .where("tracklist_id = ?").bind(getId()) .where("track_id = ?").bind(track->getId()) - .where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t())) + .where("date_time = ?").bind(normalizeDateTime(dateTime)) .resultValue(); } @@ -270,19 +268,109 @@ createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std: } std::vector -TrackList::getArtistsReverse(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const +TrackList::getArtists(const std::vector& clusterIds, std::optional linkType, ArtistSortMethod sortMethod, std::optional range, bool& moreResults) const +{ + assert(session()); + + auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType) + .groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)") + .limit(range ? static_cast(range->size) + 1 : -1) + .offset(range ? static_cast(range->offset) : -1)}; + + switch (sortMethod) + { + case ArtistSortMethod::None: + break; + case ArtistSortMethod::ByName: + query.orderBy("a.name COLLATE NOCASE"); + break; + case ArtistSortMethod::BySortName: + query.orderBy("a.sort_name COLLATE NOCASE"); + break; + case ArtistSortMethod::Random: + query.orderBy("RANDOM()"); + break; + case ArtistSortMethod::LastWritten: + case ArtistSortMethod::StarredDateDesc: + assert(false); // Not implemented! + break; + } + + Wt::Dbo::collection> collection {query.resultList()}; + + auto res {std::vector(collection.begin(), collection.end())}; + if (range && res.size() == static_cast(range->size) + 1) + { + moreResults = true; + res.pop_back(); + } + else + moreResults = false; + + return res; +} + + +std::vector> +TrackList::getReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const +{ + assert(session()); + + auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds) + .groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)") + .limit(range ? static_cast(range->size) + 1 : -1) + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; + + std::vector res(collection.begin(), collection.end()); + if (range && res.size() == static_cast(range->size) + 1) + { + moreResults = true; + res.pop_back(); + } + else + moreResults = false; + + return res; +} + +std::vector> +TrackList::getTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const +{ + assert(session()); + + auto collection {createTracksQuery(*session(), getId(), clusterIds) + .groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)") + .limit(range ? static_cast(range->size) + 1 : -1) + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; + + std::vector res(collection.begin(), collection.end()); + if (range && res.size() == static_cast(range->size) + 1) + { + moreResults = true; + res.pop_back(); + } + else + moreResults = false; + + return res; +} + +std::vector +TrackList::getArtistsOrderedByRecentFirst(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const { assert(session()); auto collection {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType) .groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC") - .limit(range ? static_cast(range->limit) + 1 : -1) + .orderBy("p_e.date_time DESC, p_e.id DESC") + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; auto res {std::vector(collection.begin(), collection.end())}; - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -294,19 +382,19 @@ TrackList::getArtistsReverse(const std::vector& clusterIds, std::opti } std::vector -TrackList::getReleasesReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const +TrackList::getReleasesOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const { assert(session()); auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds) .groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC") - .limit(range ? static_cast(range->limit) + 1 : -1) + .orderBy("p_e.date_time DESC, p_e.id DESC") + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -318,19 +406,19 @@ TrackList::getReleasesReverse(const std::vector& clusterIds, std::opt } std::vector -TrackList::getTracksReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const +TrackList::getTracksOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const { assert(session()); auto collection {createTracksQuery(*session(), getId(), clusterIds) .groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC") - .limit(range ? static_cast(range->limit) + 1 : -1) + .orderBy("p_e.date_time DESC, p_e.id DESC") + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -424,13 +512,13 @@ TrackList::getTopArtists(const std::vector& clusterIds, std::optional auto collection {query .orderBy("COUNT(a.id) DESC") .groupBy("a.id") - .limit(range ? static_cast(range->limit) + 1 : -1) + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -450,12 +538,12 @@ TrackList::getTopReleases(const std::vector& clusterIds, std::optiona auto collection {query .orderBy("COUNT(r.id) DESC") .groupBy("r.id") - .limit(range ? static_cast(range->limit) + 1 : -1) + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -475,12 +563,12 @@ TrackList::getTopTracks(const std::vector& clusterIds, std::optional< auto collection {query .orderBy("COUNT(t.id) DESC") .groupBy("t.id") - .limit(range ? static_cast(range->limit) + 1 : -1) + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -492,11 +580,10 @@ TrackList::getTopTracks(const std::vector& clusterIds, std::optional< } TrackListEntry::TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) -: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())} // force second resolution +: _dateTime {normalizeDateTime(dateTime)} , _track {getDboPtr(track)} , _tracklist {getDboPtr(tracklist)} { - assert(_dateTime.isValid()); } TrackListEntry::pointer diff --git a/src/libs/services/database/impl/Types.cpp b/src/libs/services/database/impl/Types.cpp new file mode 100644 index 00000000..ef02556d --- /dev/null +++ b/src/libs/services/database/impl/Types.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 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 "services/database/Types.hpp" + +#include + +namespace Database +{ + static const std::set allowedAudioBitrates + { + 64000, + 96000, + 128000, + 192000, + 320000, + }; + + void visitAllowedAudioBitrates(std::function func) + { + for (Bitrate bitrate : allowedAudioBitrates) + func(bitrate); + } + + bool isAudioBitrateAllowed(Bitrate bitrate) + { + return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates); + } + + DateRange + DateRange::fromYearRange(int from, int to) + { + return DateRange {{from, 1, 1}, {to, 12, 31}}; + } +} + diff --git a/src/libs/services/database/impl/User.cpp b/src/libs/services/database/impl/User.cpp new file mode 100644 index 00000000..31728e06 --- /dev/null +++ b/src/libs/services/database/impl/User.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2013 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 "services/database/User.hpp" + +#include "services/database/Artist.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "utils/Logger.hpp" +#include "IdTypeTraits.hpp" +#include "StringViewTraits.hpp" +#include "Utils.hpp" + +namespace Database { + +User::User(std::string_view loginName) +: _loginName {loginName} +{ +} + +std::size_t +User::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM user"); +} + +RangeResults +User::find(Session& session, const FindParameters& params) +{ + session.checkSharedLocked(); + + auto query {session.getDboSession().query("SELECT id FROM user")}; + + if (params.scrobbler) + query.where("scrobbler = ?").bind(*params.scrobbler); + + return execQuery(query, params.range); +} + +User::pointer +User::findDemoUser(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().find().where("type = ?").bind(UserType::DEMO).resultValue(); +} + +User::pointer +User::create(Session& session, std::string_view loginName) +{ + session.checkUniqueLocked(); + + User::pointer user {session.getDboSession().add(std::make_unique(loginName))}; + session.getDboSession().flush(); + + return user; +} + +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(); +} + +void +User::setSubsonicTranscodeBitrate(Bitrate bitrate) +{ + assert(isAudioBitrateAllowed(bitrate)); + _subsonicTranscodeBitrate = bitrate; +} + +void +User::clearAuthTokens() +{ + _authTokens.clear(); +} + +} // namespace Database + + diff --git a/src/libs/database/impl/Utils.cpp b/src/libs/services/database/impl/Utils.cpp similarity index 85% rename from src/libs/database/impl/Utils.cpp rename to src/libs/services/database/impl/Utils.cpp index 39849db1..e0994090 100644 --- a/src/libs/database/impl/Utils.cpp +++ b/src/libs/services/database/impl/Utils.cpp @@ -29,5 +29,12 @@ namespace Database return StringUtils::escapeString(keyword, "%_", escapeChar); } + Wt::WDateTime + normalizeDateTime(const Wt::WDateTime& dateTime) + { + // force second resolution + return Wt::WDateTime::fromTime_t(dateTime.toTime_t()); + } + } // namespace Database diff --git a/src/libs/services/database/impl/Utils.hpp b/src/libs/services/database/impl/Utils.hpp new file mode 100644 index 00000000..b92b12c3 --- /dev/null +++ b/src/libs/services/database/impl/Utils.hpp @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include +#include + +#include "services/database/Types.hpp" + +namespace Database +{ +#define ESCAPE_CHAR_STR "\\" + static inline constexpr char escapeChar {'\\'}; + std::string escapeLikeKeyword(std::string_view keywords); + + template + RangeResults + execQuery(Wt::Dbo::Query& query, Range range) + { + RangeResults res; + + auto collection {query.limit(range.size ? static_cast(range.size) + 1 : -1) + .offset(range.offset ? static_cast(range.offset) : -1) + .resultList()}; + + res.results.assign(collection.begin(), collection.end()); + if (range.size && res.results.size() == static_cast(range.size) + 1) + { + res.moreResults = true; + res.results.pop_back(); + } + else + res.moreResults = false; + + res.range.offset = range.offset; + res.range.size = res.results.size(); + return res; + } + + template + RangeResults + execQuery(Wt::Dbo::Query>& query, Range range) + { + RangeResults res; + + auto collection {query.limit(range.size ? static_cast(range.size) + 1 : -1) + .offset(range.offset ? static_cast(range.offset) : -1) + .resultList()}; + + res.results.assign(collection.begin(), collection.end()); + if (range.size && res.results.size() == static_cast(range.size) + 1) + { + res.moreResults = true; + res.results.pop_back(); + } + else + res.moreResults = false; + + res.range.offset = range.offset; + res.range.size = res.results.size(); + return res; + } + + Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); +} // namespace Database + diff --git a/src/libs/services/database/include/services/database/Artist.hpp b/src/libs/services/database/include/services/database/Artist.hpp new file mode 100644 index 00000000..ac94ab3d --- /dev/null +++ b/src/libs/services/database/include/services/database/Artist.hpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 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 . + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "services/database/ArtistId.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/Object.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" +#include "utils/EnumSet.hpp" +#include "utils/UUID.hpp" + +namespace Database +{ + +class Cluster; +class ClusterType; +class Release; +class Session; +class StarredArtist; +class Track; +class TrackArtistLink; +class User; + +class Artist : public Object +{ + public: + struct FindParameters + { + std::vector clusters; // if non empty, at least one artist that belongs to these clusters + std::vector keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field) + std::optional linkType; // if set, only artists that have produced at least one track with this link type + ArtistSortMethod sortMethod {ArtistSortMethod::None}; + Range range; + Wt::WDateTime writtenAfter; + UserId starringUser; // only artists starred by this user + std::optional scrobbler; // and for this scrobbler + + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } + FindParameters& setLinkType(std::optional _linkType) { linkType = _linkType; return *this; } + FindParameters& setSortMethod(ArtistSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; } + FindParameters& setRange(Range _range) {range = _range; return *this; } + FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } + FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } + }; + + Artist() = default; + Artist(const std::string& name, const std::optional& MBID = {}); + + // Accessors + static std::size_t getCount(Session& session); + static pointer find(Session& session, const UUID& MBID); + static pointer find(Session& session, ArtistId id); + static std::vector find(Session& session, const std::string& name); // exact match on name field + static RangeResults find(Session& session, const FindParameters& parameters); + static RangeResults findAllOrphans(Session& session, Range range); // No track related + static bool exists(Session& session, ArtistId id); + + + // Accessors + const std::string& getName() const { return _name; } + const std::string& getSortName() const { return _sortName; } + std::optional getMBID() const { return UUID::fromString(_MBID); } + + std::vector> getReleases(const std::vector& clusterIds = {}) const; // if non empty, get the releases that match all these clusters + std::size_t getReleaseCount() const; + std::vector> getTracks(std::optional linkType = {}) const; + bool hasNonReleaseTracks(std::optional linkType = std::nullopt) const; + RangeResults> getNonReleaseTracks(std::optional linkType, Range range) const; + std::vector> getRandomTracks(std::optional count) const; + + // No artistLinkTypes means get them all + RangeResults findSimilarArtists(EnumSet artistLinkTypes = {}, Range range = {}) const; + + // Get the cluster of the tracks made by this artist + // Each clusters are grouped by cluster type, sorted by the number of occurence + // size is the max number of cluster per cluster type + std::vector>> getClusterGroups(std::vector> clusterTypes, std::size_t size) const; + + void setName(std::string_view name) { _name = name; } + void setMBID(const std::optional& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } + void setSortName(const std::string& sortName); + + // Create + static pointer create(Session& session, const std::string& name, const std::optional& UUID = {}); + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _name, "name"); + Wt::Dbo::field(a, _sortName, "sort_name"); + Wt::Dbo::field(a, _MBID, "mbid"); + + Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist"); + Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade); + } + + private: + static const std::size_t _maxNameLength = 128; + + std::string _name; + std::string _sortName; + std::string _MBID; // Musicbrainz Identifier + + Wt::Dbo::collection> _trackArtistLinks; // Tracks involving this artist + Wt::Dbo::collection> _starredArtists; // starred entries for this artist +}; + +} // namespace Database + diff --git a/src/libs/recommendation/include/recommendation/IRecommendation.hpp b/src/libs/services/database/include/services/database/ArtistId.hpp similarity index 78% rename from src/libs/recommendation/include/recommendation/IRecommendation.hpp rename to src/libs/services/database/include/services/database/ArtistId.hpp index 834a983e..01461d90 100644 --- a/src/libs/recommendation/include/recommendation/IRecommendation.hpp +++ b/src/libs/services/database/include/services/database/ArtistId.hpp @@ -19,20 +19,7 @@ #pragma once -#include "database/Types.hpp" -#include "utils/EnumSet.hpp" +#include "services/database/IdType.hpp" -namespace Database -{ - class Db; - class Session; -} - -namespace Recommendation -{ - class IRecommendation - { - }; - -} // ns Recommendation +LMS_DECLARE_IDTYPE(ArtistId) diff --git a/src/libs/services/database/include/services/database/AuthToken.hpp b/src/libs/services/database/include/services/database/AuthToken.hpp new file mode 100644 index 00000000..646a5d9f --- /dev/null +++ b/src/libs/services/database/include/services/database/AuthToken.hpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 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 . + */ + +#pragma once + +#include + +#include +#include + +#include "services/database/AuthTokenId.hpp" +#include "services/database/Object.hpp" + +namespace Database +{ + class Session; + + class User; + class AuthToken : public Object + { + public: + AuthToken() = default; + AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user); + + // Utility + static pointer create(Session& session, std::string_view value, const Wt::WDateTime&expiry, ObjectPtr user); + static void removeExpiredTokens(Session& session, const Wt::WDateTime& now); + static pointer find(Session& session, std::string_view value); + + // Accessors + const Wt::WDateTime& getExpiry() const { return _expiry; } + ObjectPtr getUser() const { return _user; } + const std::string& getValue() const { return _value; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _value, "value"); + Wt::Dbo::field(a, _expiry, "expiry"); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + std::string _value; + Wt::WDateTime _expiry; + Wt::Dbo::ptr _user; + }; +} // namespace Databas' + diff --git a/src/libs/services/database/include/services/database/AuthTokenId.hpp b/src/libs/services/database/include/services/database/AuthTokenId.hpp new file mode 100644 index 00000000..86de8770 --- /dev/null +++ b/src/libs/services/database/include/services/database/AuthTokenId.hpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(AuthTokenId) diff --git a/src/libs/database/include/database/Cluster.hpp b/src/libs/services/database/include/services/database/Cluster.hpp similarity index 65% rename from src/libs/database/include/database/Cluster.hpp rename to src/libs/services/database/include/services/database/Cluster.hpp index 42a11e77..a1cac3f7 100644 --- a/src/libs/database/include/database/Cluster.hpp +++ b/src/libs/services/database/include/services/database/Cluster.hpp @@ -26,7 +26,10 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Object.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/Types.hpp" namespace Database { @@ -42,20 +45,20 @@ class Cluster : public Object Cluster(ObjectPtr type, std::string_view name); // Find utility - static std::vector getAll(Session& session); - static std::vector getAllOrphans(Session& session); - static pointer getById(Session& session, ClusterId id); + static std::size_t getCount(Session& session); + static RangeResults find(Session& session, Range range); + static pointer find(Session& session, ClusterId id); + static RangeResults findOrphans(Session& session, Range range); // Create utility static pointer create(Session& session, ObjectPtr type, std::string_view name); // Accessors - const std::string& getName() const { return _name; } - ObjectPtr getType() const { return _clusterType; } - std::size_t getTracksCount() const { return _tracks.size(); } - std::vector> getTracks(std::optional offset = {}, std::optional limit = {}) const; - std::vector getTrackIds() const; - std::size_t getReleasesCount() const; + const std::string& getName() const { return _name; } + ObjectPtr getType() const { return _clusterType; } + std::size_t getTracksCount() const { return _tracks.size(); } + RangeResults getTracks(Range range) const; + std::size_t getReleasesCount() const; void addTrack(ObjectPtr track); @@ -85,19 +88,20 @@ class ClusterType : public Object ClusterType(std::string_view name); // Getters - static std::vector getAllOrphans(Session& session); - static std::vector getAllUsed(Session& session); - static pointer getByName(Session& session, const std::string& name); - static pointer getById(Session& session, ClusterTypeId id); - static std::vector getAll(Session& session); + static std::size_t getCount(Session& session); + static RangeResults find(Session& session, Range range); + static pointer find(Session& session, const std::string& name); + static pointer find(Session& session, ClusterTypeId id); + static RangeResults findOrphans(Session& session, Range range); + static RangeResults findUsed(Session& session, Range range); static pointer create(Session& session, const std::string& name); static void remove(Session& session, const std::string& name); // Accessors - const std::string& getName(void) const { return _name; } - std::vector getClusters() const; - Cluster::pointer getCluster(const std::string& name) const; + const std::string& getName() const { return _name; } + std::vector getClusters() const; + Cluster::pointer getCluster(const std::string& name) const; template void persist(Action& a) @@ -108,7 +112,6 @@ class ClusterType : public Object } private: - static const std::size_t _maxNameLength = 128; std::string _name; diff --git a/src/libs/database/impl/Utils.hpp b/src/libs/services/database/include/services/database/ClusterId.hpp similarity index 75% rename from src/libs/database/impl/Utils.hpp rename to src/libs/services/database/include/services/database/ClusterId.hpp index 547c7995..d14a88c0 100644 --- a/src/libs/database/impl/Utils.hpp +++ b/src/libs/services/database/include/services/database/ClusterId.hpp @@ -19,15 +19,8 @@ #pragma once -#include -#include -#include +#include "services/database/IdType.hpp" -namespace Database -{ -#define ESCAPE_CHAR_STR "\\" - static constexpr char escapeChar {'\\'}; - std::string escapeLikeKeyword(std::string_view keywords); - -} // namespace Database +LMS_DECLARE_IDTYPE(ClusterId) +LMS_DECLARE_IDTYPE(ClusterTypeId) diff --git a/src/libs/database/include/database/Db.hpp b/src/libs/services/database/include/services/database/Db.hpp similarity index 77% rename from src/libs/database/include/database/Db.hpp rename to src/libs/services/database/include/services/database/Db.hpp index bdcd20b3..b58077e8 100644 --- a/src/libs/database/include/database/Db.hpp +++ b/src/libs/services/database/include/services/database/Db.hpp @@ -31,8 +31,7 @@ class Session; class Db { public: - - Db(const std::filesystem::path& dbPath); + Db(const std::filesystem::path& dbPath, std::size_t connectionCount = 10); ~Db(); Db(const Db&) = delete; @@ -42,6 +41,8 @@ class Db Session& getTLSSession(); + void executeSql(const std::string& sql); + private: friend class Session; @@ -66,29 +67,6 @@ class Db std::unique_ptr _connection; }; - 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; - - private: - Db& _db; - }; - - void executeSql(const std::string& sql); - RecursiveSharedMutex _sharedMutex; std::unique_ptr _connectionPool; diff --git a/src/libs/services/database/include/services/database/IdType.hpp b/src/libs/services/database/include/services/database/IdType.hpp new file mode 100644 index 00000000..04aaa2e4 --- /dev/null +++ b/src/libs/services/database/include/services/database/IdType.hpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + + +#include +#include +#include + +namespace Database +{ + class IdType + { + public: + using ValueType = Wt::Dbo::dbo_default_traits::IdType; + + IdType() = default; + IdType(ValueType id) : _id {id} { assert(isValid()); } + + bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); } + std::string toString() const { assert(isValid()); return std::to_string(_id); } + + ValueType getValue() const { return _id; } + + bool operator==(IdType other) const { return other._id == _id; } + bool operator!=(IdType other) const { return !(*this == other); } + bool operator<(IdType other) const { return _id < other._id; } + bool operator>(IdType other) const { return _id > other._id; } + + private: + Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()}; + }; + +#define LMS_DECLARE_IDTYPE(name) \ + namespace Database { \ + class name : public IdType \ + { \ + public: \ + using IdType::IdType; \ + };\ + } \ + namespace std \ + { \ + template<> \ + class hash \ + { \ + public: \ + size_t operator()(Database::name id) const \ + { \ + return std::hash()(id.getValue()); \ + } \ + }; \ + } // ns std +} // namespace Database + diff --git a/src/libs/services/database/include/services/database/Listen.hpp b/src/libs/services/database/include/services/database/Listen.hpp new file mode 100644 index 00000000..edbb218b --- /dev/null +++ b/src/libs/services/database/include/services/database/Listen.hpp @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "services/database/ArtistId.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/ListenId.hpp" +#include "services/database/Object.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +namespace Database +{ + +class Session; +class Track; +class User; + +class Listen : public Object +{ + public: + Listen() = default; + Listen(ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); + + struct FindParameters + { + UserId user; + std::optional scrobbler; + std::optional scrobblingState; + Range range; + + FindParameters& setUser(UserId _user) { user = _user; return *this; } + FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; } + FindParameters& setScrobblingState(ScrobblingState _scrobblingState) { scrobblingState = _scrobblingState; return *this; } + FindParameters& setRange(Range _range) {range = _range; return *this; } + }; + + // Accessors + static std::size_t getCount(Session& session); + static pointer find(Session& session, ListenId id); + static pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime); + static RangeResults find(Session& session, const FindParameters& parameters); + + // Create + static pointer create(Session& session, ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); + + // Stats + static RangeResults getTopArtists(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + std::optional linkType, + Range range = {}); + static RangeResults getTopReleases(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range = {}); + static RangeResults getTopTracks(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range = {}); + + static RangeResults getRecentArtists(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + std::optional linkType, + Range range = {}); + static RangeResults getRecentReleases(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range = {}); + static RangeResults getRecentTracks(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range = {}); + + ScrobblingState getScrobblingState() const { return _scrobblingState; } + ObjectPtr getUser() const { return _user; } + ObjectPtr getTrack() const { return _track; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } + + void setScrobblingState(ScrobblingState state) { _scrobblingState = state; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _dateTime, "date_time"); + Wt::Dbo::field(a, _scrobbler, "scrobbler"); + Wt::Dbo::field(a, _scrobblingState, "scrobbling_state"); + + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + Wt::WDateTime _dateTime; + Scrobbler _scrobbler; + ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; + + Wt::Dbo::ptr _user; + Wt::Dbo::ptr _track; +}; + +} // namespace Database + diff --git a/src/libs/services/database/include/services/database/ListenId.hpp b/src/libs/services/database/include/services/database/ListenId.hpp new file mode 100644 index 00000000..e19019ee --- /dev/null +++ b/src/libs/services/database/include/services/database/ListenId.hpp @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 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 . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(ListenId) + diff --git a/src/libs/services/database/include/services/database/Object.hpp b/src/libs/services/database/include/services/database/Object.hpp new file mode 100644 index 00000000..79ccf4be --- /dev/null +++ b/src/libs/services/database/include/services/database/Object.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include "services/database/IdType.hpp" + +namespace Database +{ + template + class ObjectPtr + { + public: + ObjectPtr() = default; + ObjectPtr(Wt::Dbo::ptr obj) : _obj {obj} {} + + const T* operator->() const { return _obj.get(); } + operator bool() const { return _obj.get(); } + bool operator!() const { return !_obj.get(); } + bool operator==(const ObjectPtr& other) const { return _obj == other._obj; } + bool operator!=(const ObjectPtr& other) const { return other._obj != _obj; } + + auto modify() { return _obj.modify(); } + void remove() { _obj.remove(); } + + private: + template friend class Object; + Wt::Dbo::ptr _obj; + }; + + template + class Object : public Wt::Dbo::Dbo + { + static_assert(std::is_base_of_v); + static_assert(!std::is_same_v); + + public: + using pointer = ObjectPtr; + using IdType = ObjectIdType; + + IdType getId() const { return Wt::Dbo::Dbo::self()->Wt::Dbo::template Dbo::id(); } + + // catch some misuses + typename Wt::Dbo::dbo_traits::IdType id() const = delete; + + protected: + // Can get raw dbo ptr only from Objects + template + static + Wt::Dbo::ptr getDboPtr(ObjectPtr ptr) { return ptr._obj; } + }; +} diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp similarity index 57% rename from src/libs/database/include/database/Release.hpp rename to src/libs/services/database/include/services/database/Release.hpp index 80c16eeb..999899a6 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -25,7 +25,11 @@ #include #include -#include "database/Types.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/Object.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" #include "utils/UUID.hpp" namespace Database @@ -42,35 +46,42 @@ class User; class Release : public Object { public: + struct FindParameters + { + std::vector clusters; // if non empty, releases that belong to these clusters + std::vector keywords; // if non empty, name must match all of these keywords + ReleaseSortMethod sortMethod {ReleaseSortMethod::None}; + Range range; + Wt::WDateTime writtenAfter; + std::optional dateRange; + UserId starringUser; // only releases starred by this user + std::optional scrobbler; // and for this scrobbler + + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } + FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; } + FindParameters& setRange(Range _range) {range = _range; return *this; } + FindParameters& setWrittenAfter(const Wt::WDateTime& _after) {writtenAfter = _after; return *this; } + FindParameters& setDateRange(const std::optional& _dateRange) {dateRange = _dateRange; return *this; } + FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } + }; + Release() = default; Release(const std::string& name, const std::optional& MBID = {}); // Accessors - static std::size_t getCount(Session& session); - static pointer getByMBID(Session& session, const UUID& MBID); - static std::vector getByName(Session& session, const std::string& name); - static pointer getById(Session& session, ReleaseId id); - static std::vector getAllOrphans(Session& session); // no track related - static std::vector getAll(Session& session, std::optional range = std::nullopt); - static std::vector getAllIds(Session& session); - static std::vector getAllOrderedByArtist(Session& session, std::optional offset = {}, std::optional size = {}); - static std::vector getAllRandom(Session& session, const std::vector& clusters, std::optional size = {}); - static std::vector getAllIdsRandom(Session& session, const std::vector& clusters, std::optional size = {}); - static std::vector getLastWritten(Session& session, std::optional after, const std::vector& clusters, std::optional range, bool& moreResults); - static std::vector getByYear(Session& session, int yearFrom, int yearTo, std::optional range = std::nullopt); - static std::vector getStarred(Session& session, ObjectPtr user, const std::vector& clusters, std::optional range, bool& moreResults); + static std::size_t getCount(Session& session); + static bool exists(Session& session, ReleaseId id); + static pointer find(Session& session, const UUID& MBID); + static std::vector find(Session& session, const std::string& name); + static pointer find(Session& session, ReleaseId id); + static RangeResults find(Session& session, const FindParameters& parameters); + static RangeResults findOrphans(Session& session, Range range); // no track related + static RangeResults findOrderedByArtist(Session& session, Range range); - static std::vector getByClusters(Session& session, const std::vector& clusters); - static std::vector getByFilter(Session& session, - const std::vector& clusters, // if non empty, at least one release that belongs to these clusters - const std::vector& keywords, // if non empty, name must match all of these keywords - std::optional range, - bool& moreExpected); - static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); - - std::vector> getTracks(const std::vector& clusters = {}) const; - std::size_t getTracksCount() const; - ObjectPtr getFirstTrack() const; + std::vector> getTracks(const std::vector& clusters = {}) const; + std::size_t getTracksCount() const; + ObjectPtr getFirstTrack() const; // Get the cluster of the tracks that belong to this release // Each clusters are grouped by cluster type, sorted by the number of occurence (max to min) @@ -109,7 +120,6 @@ class Release : public Object Wt::Dbo::field(a, _MBID, "mbid"); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); - Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade); } private: @@ -119,7 +129,6 @@ class Release : public Object std::string _MBID; Wt::Dbo::collection> _tracks; // Tracks in the release - Wt::Dbo::collection> _starringUsers; // Users that starred this release }; } // namespace Database diff --git a/src/libs/services/database/include/services/database/ReleaseId.hpp b/src/libs/services/database/include/services/database/ReleaseId.hpp new file mode 100644 index 00000000..c9c07c7a --- /dev/null +++ b/src/libs/services/database/include/services/database/ReleaseId.hpp @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(ReleaseId) + diff --git a/src/libs/database/include/database/ScanSettings.hpp b/src/libs/services/database/include/services/database/ScanSettings.hpp similarity index 94% rename from src/libs/database/include/database/ScanSettings.hpp rename to src/libs/services/database/include/services/database/ScanSettings.hpp index 9cb8cb5a..19def107 100644 --- a/src/libs/database/include/database/ScanSettings.hpp +++ b/src/libs/services/database/include/services/database/ScanSettings.hpp @@ -25,7 +25,10 @@ #include #include -#include "database/Types.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" + +LMS_DECLARE_IDTYPE(ScanSettingsId) namespace Database { @@ -86,13 +89,12 @@ class ScanSettings : public Object } private: - int _scanVersion {}; std::string _mediaDirectory; Wt::WTime _startTime = Wt::WTime {0,0,0}; UpdatePeriod _updatePeriod {UpdatePeriod::Never}; RecommendationEngineType _recommendationEngineType {RecommendationEngineType::Clusters}; - std::string _audioFileExtensions {".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus"}; + std::string _audioFileExtensions {".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv"}; Wt::Dbo::collection> _clusterTypes; }; diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/services/database/include/services/database/Session.hpp similarity index 93% rename from src/libs/database/include/database/Session.hpp rename to src/libs/services/database/include/services/database/Session.hpp index 6378c0a3..1aab2d66 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/services/database/include/services/database/Session.hpp @@ -19,7 +19,6 @@ #pragma once -#include #include #include @@ -72,12 +71,9 @@ namespace Database void prepareTables(); // need to run only once at startup Wt::Dbo::Session& getDboSession() { return _session; } + Db& getDb() { return _db; } private: - Session(std::shared_mutex& mutex, Wt::Dbo::SqlConnectionPool& connectionPool); - - void doDatabaseMigrationIfNeeded(); - Db& _db; Wt::Dbo::Session _session; }; diff --git a/src/libs/services/database/include/services/database/StarredArtist.hpp b/src/libs/services/database/include/services/database/StarredArtist.hpp new file mode 100644 index 00000000..0abb51e0 --- /dev/null +++ b/src/libs/services/database/include/services/database/StarredArtist.hpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "services/database/ArtistId.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/Session.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +LMS_DECLARE_IDTYPE(StarredArtistId) + +namespace Database +{ + class Artist; + class Session; + class User; + + class StarredArtist : public Object + { + public: + StarredArtist() = default; + StarredArtist(ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler); + + // Search utility + static std::size_t getCount(Session& session); + static pointer find(Session& session, StarredArtistId id); + static pointer find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler); + + // Create utility + static pointer create(Session& session, ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler); + + // Accessors + ObjectPtr getArtist() const { return _artist; } + ObjectPtr getUser() const { return _user; } + Scrobbler getScrobbler() const { return _scrobbler; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } + + // Setters + void setDateTime(const Wt::WDateTime& dateTime); + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _scrobbler, "scrobbler"); + Wt::Dbo::field(a, _dateTime, "date_time"); + + Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + Scrobbler _scrobbler; // for which scrobbler + Wt::WDateTime _dateTime; // when it was starred + + Wt::Dbo::ptr _artist; + Wt::Dbo::ptr _user; + }; +} // namespace Database + diff --git a/src/libs/services/database/include/services/database/StarredRelease.hpp b/src/libs/services/database/include/services/database/StarredRelease.hpp new file mode 100644 index 00000000..e7a1ad1e --- /dev/null +++ b/src/libs/services/database/include/services/database/StarredRelease.hpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "services/database/ReleaseId.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/Session.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +LMS_DECLARE_IDTYPE(StarredReleaseId) + +namespace Database +{ + class Release; + class Session; + class User; + + class StarredRelease : public Object + { + public: + StarredRelease() = default; + StarredRelease(ObjectPtr release, ObjectPtr user, Scrobbler scrobbler); + + // Search utility + static std::size_t getCount(Session& session); + static pointer find(Session& session, StarredReleaseId id); + static pointer find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler); + + // Create utility + static pointer create(Session& session, ObjectPtr release, ObjectPtr user, Scrobbler scrobbler); + + // Accessors + ObjectPtr getRelease() const { return _release; } + ObjectPtr getUser() const { return _user; } + Scrobbler getScrobbler() const { return _scrobbler; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } + + // Setters + void setDateTime(const Wt::WDateTime& dateTime); + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _scrobbler, "scrobbler"); + Wt::Dbo::field(a, _dateTime, "date_time"); + + Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + Scrobbler _scrobbler; // for which scrobbler + Wt::WDateTime _dateTime; // when it was starred + + Wt::Dbo::ptr _release; + Wt::Dbo::ptr _user; + }; +} // namespace Database + diff --git a/src/libs/services/database/include/services/database/StarredTrack.hpp b/src/libs/services/database/include/services/database/StarredTrack.hpp new file mode 100644 index 00000000..ab4cf1d1 --- /dev/null +++ b/src/libs/services/database/include/services/database/StarredTrack.hpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "services/database/TrackId.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/Session.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +LMS_DECLARE_IDTYPE(StarredTrackId) + +namespace Database +{ + class Track; + class Session; + class User; + + class StarredTrack : public Object + { + public: + StarredTrack() = default; + StarredTrack(ObjectPtr track, ObjectPtr user, Scrobbler scrobbler); + + // Search utility + static std::size_t getCount(Session& session); + static pointer find(Session& session, StarredTrackId id); + static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler); + + // Create utility + static pointer create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler); + + // Accessors + ObjectPtr getTrack() const { return _track; } + ObjectPtr getUser() const { return _user; } + Scrobbler getScrobbler() const { return _scrobbler; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } + + // Setters + void setDateTime(const Wt::WDateTime& dateTime); + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _scrobbler, "scrobbler"); + Wt::Dbo::field(a, _dateTime, "date_time"); + + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + Scrobbler _scrobbler; // for which scrobbler + Wt::WDateTime _dateTime; // when it was starred + + Wt::Dbo::ptr _track; + Wt::Dbo::ptr _user; + }; +} // namespace Database + diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp similarity index 69% rename from src/libs/database/include/database/Track.hpp rename to src/libs/services/database/include/services/database/Track.hpp index 1e193405..cc530921 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -34,7 +34,12 @@ #include "utils/EnumSet.hpp" #include "utils/UUID.hpp" -#include "database/Types.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { @@ -44,49 +49,51 @@ class ClusterType; class Release; class Session; class TrackArtistLink; -class TrackFeatures; -class TrackListEntry; class TrackStats; class User; class Track : public Object { public: + struct FindParameters + { + std::vector clusters; // if non empty, tracks that belong to these clusters + std::vector keywords; // if non empty, name must match all of these keywords + TrackSortMethod sortMethod {TrackSortMethod::None}; + Range range; + Wt::WDateTime writtenAfter; + UserId starringUser; // only tracks starred by this user + std::optional scrobbler; // and for this scrobbler + + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } + FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; } + FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } + FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } + }; + struct PathResult + { + TrackId trackId; + std::filesystem::path path; + }; + Track() = default; Track(const std::filesystem::path& p); // Find utility functions - static std::size_t getCount(Session& session); - static pointer getByPath(Session& session, const std::filesystem::path& p); - static pointer getById(Session& session, TrackId id); - static std::vector getByRecordingMBID(Session& session, const UUID& MBID); - static std::vector getSimilarTracks(Session& session, - const std::vector& trackIds, - std::optional offset = {}, - std::optional size = {}); - static std::vector getByClusters(Session& session, - const std::vector& clusters); // tracks that belong to these clusters - static std::vector getByFilter(Session& session, - const std::vector& clusters, // if non empty, tracks that belong to these clusters - const std::vector& keywords, // if non empty, name must match all of these keywords - std::optional range, - bool& moreExpected); - static std::vector getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName); + static std::size_t getCount(Session& session); + static pointer findByPath(Session& session, const std::filesystem::path& p); + static pointer find(Session& session, TrackId id); + static bool exists(Session& session, TrackId id); + static std::vector findByRecordingMBID(Session& session, const UUID& MBID); + static RangeResults findSimilarTracks(Session& session, const std::vector& trackIds, Range range); - static std::vector getAll(Session& session, std::optional limit = std::nullopt); - static std::vector getAllRandom(Session& session, const std::vector& clusters, std::optional limit = std::nullopt); - static std::vector getAllIdsRandom(Session& session, const std::vector& clusters, std::optional limit = std::nullopt); - static std::vector getAllIds(Session& session); - static std::vector> getAllPaths(Session& session, std::optional offset = std::nullopt, std::optional size = std::nullopt); - static std::vector getMBIDDuplicates(Session& session); - static std::vector getLastWritten(Session& session, std::optional after, const std::vector& clusters, std::optional range, bool& moreResults); - static std::vector getAllWithRecordingMBIDAndMissingFeatures(Session& session); - static std::vector getAllIdsWithFeatures(Session& session, std::optional limit = {}); - static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); - static std::vector getStarred(Session& session, - ObjectPtr user, - const std::vector& clusters, - std::optional range, bool& hasMore); + static RangeResults find(Session& session, const FindParameters& parameters); + static RangeResults findByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName); + static RangeResults findPaths(Session& session, Range range); + static RangeResults findRecordingMBIDDuplicates(Session& session, Range range); + static RangeResults findWithRecordingMBIDAndMissingFeatures(Session& session, Range range); // Create utility static pointer create(Session& session, const std::filesystem::path& p); @@ -109,13 +116,12 @@ class Track : public Object void setRecordingMBID(const std::optional& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); } void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); } - void setTrackReplayGain(float replayGain) { _trackReplayGain = replayGain; } - void setReleaseReplayGain(float replayGain) { _releaseReplayGain = replayGain; } + void setTrackReplayGain(std::optional replayGain) { _trackReplayGain = replayGain; } + void setReleaseReplayGain(std::optional replayGain) { _releaseReplayGain = replayGain; } void clearArtistLinks(); void addArtistLink(const ObjectPtr& artistLink); void setRelease(ObjectPtr release) { _release = getDboPtr(release); } void setClusters(const std::vector>& clusters ); - void setFeatures(const ObjectPtr& features); std::size_t getScanVersion() const { return _scanVersion; } std::optional getTrackNumber() const; @@ -146,8 +152,6 @@ class Track : public Object ObjectPtr getRelease() const { return _release; } std::vector> getClusters() const; std::vector getClusterIds() const; - bool hasTrackFeatures() const; - ObjectPtr getTrackFeatures() const; std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; @@ -177,13 +181,9 @@ class Track : public Object Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _playlistEntries, Wt::Dbo::ManyToOne, "track"); - Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasOne(a, _trackFeatures); } private: - static const std::size_t _maxNameLength = 128; static const std::size_t _maxCopyrightLength = 128; static const std::size_t _maxCopyrightURLLength = 128; @@ -214,10 +214,6 @@ class Track : public Object Wt::Dbo::ptr _release; Wt::Dbo::collection> _trackArtistLinks; Wt::Dbo::collection> _clusters; - Wt::Dbo::collection> _playlistEntries; - Wt::Dbo::collection> _starringUsers; - Wt::Dbo::weak_ptr _trackFeatures; - }; } // namespace database diff --git a/src/libs/database/include/database/TrackArtistLink.hpp b/src/libs/services/database/include/services/database/TrackArtistLink.hpp similarity index 85% rename from src/libs/database/include/database/TrackArtistLink.hpp rename to src/libs/services/database/include/services/database/TrackArtistLink.hpp index 55d3b0c5..8a1ba0b2 100644 --- a/src/libs/database/include/database/TrackArtistLink.hpp +++ b/src/libs/services/database/include/services/database/TrackArtistLink.hpp @@ -23,9 +23,13 @@ #include -#include "database/Types.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/Types.hpp" #include "utils/EnumSet.hpp" +LMS_DECLARE_IDTYPE(TrackArtistLinkId) + namespace Database { @@ -40,11 +44,10 @@ namespace Database TrackArtistLink(ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type); static pointer create(Session& session, ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type); - - static EnumSet getUsedTypes(Session& session); + static EnumSet findUsedTypes(Session& session); ObjectPtr getTrack() const { return _track; } - ObjectPtr getArtist() const { return _artist; } + ObjectPtr getArtist() const { return _artist; } TrackArtistLinkType getType() const { return _type; } template diff --git a/src/libs/database/include/database/TrackBookmark.hpp b/src/libs/services/database/include/services/database/TrackBookmark.hpp similarity index 72% rename from src/libs/database/include/database/TrackBookmark.hpp rename to src/libs/services/database/include/services/database/TrackBookmark.hpp index 2c4af0db..d1d31768 100644 --- a/src/libs/database/include/database/TrackBookmark.hpp +++ b/src/libs/services/database/include/services/database/TrackBookmark.hpp @@ -23,7 +23,13 @@ #include -#include "database/Types.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +LMS_DECLARE_IDTYPE(TrackBookmarkId) namespace Database { @@ -41,10 +47,10 @@ class TrackBookmark : public Object static pointer create(Session& session, ObjectPtr user, ObjectPtr track); // Find utility functions - static std::vector getAll(Session& session); - static std::vector getByUser(Session& session, ObjectPtr user); - static pointer getByUser(Session& session, ObjectPtr user, ObjectPtr track); - static pointer getById(Session& session, TrackBookmarkId id); + static std::size_t getCount(Session& session); + static pointer find(Session& session, TrackBookmarkId id); + static RangeResults find(Session& session, UserId userId, Range range); + static pointer find(Session& session, UserId userId, TrackId trackId); // Setters void setOffset(std::chrono::milliseconds offset) { _offset = offset; } @@ -52,9 +58,9 @@ class TrackBookmark : public Object // Getters std::chrono::milliseconds getOffset() const { return _offset; } - std::string_view getComment() const { return _comment; } - ObjectPtr getTrack() const { return _track; } - ObjectPtr getUser() const { return _user; } + std::string_view getComment() const { return _comment; } + ObjectPtr getTrack() const { return _track; } + ObjectPtr getUser() const { return _user; } template void persist(Action& a) @@ -65,7 +71,6 @@ class TrackBookmark : public Object Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); } private: - static const std::size_t _maxCommentLength = 128; std::chrono::duration _offset; diff --git a/src/libs/database/include/database/TrackFeatures.hpp b/src/libs/services/database/include/services/database/TrackFeatures.hpp similarity index 71% rename from src/libs/database/include/database/TrackFeatures.hpp rename to src/libs/services/database/include/services/database/TrackFeatures.hpp index 99c36d6d..1c1251fa 100644 --- a/src/libs/database/include/database/TrackFeatures.hpp +++ b/src/libs/services/database/include/services/database/TrackFeatures.hpp @@ -26,7 +26,12 @@ #include -#include "database/Types.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/Types.hpp" + +LMS_DECLARE_IDTYPE(TrackFeaturesId) namespace Database { @@ -43,12 +48,21 @@ class TrackFeatures : public Object TrackFeatures() = default; TrackFeatures(ObjectPtr track, const std::string& jsonEncodedFeatures); + // Find utilities + static std::size_t getCount(Session& session); + static pointer find(Session& session, TrackFeaturesId id); + static pointer find(Session& session, TrackId trackId); + static RangeResults find(Session& session, Range range); + // Create utility - static pointer create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures); + static pointer create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures); FeatureValues getFeatureValues(const FeatureName& feature) const; FeatureValuesMap getFeatureValuesMap(const std::unordered_set& featureNames) const; + // Accessors + Wt::Dbo::ptr getTrack() const { return _track; } + template void persist(Action& a) { @@ -57,7 +71,6 @@ class TrackFeatures : public Object } private: - std::string _data; Wt::Dbo::ptr _track; }; diff --git a/src/libs/services/database/include/services/database/TrackId.hpp b/src/libs/services/database/include/services/database/TrackId.hpp new file mode 100644 index 00000000..40684e3e --- /dev/null +++ b/src/libs/services/database/include/services/database/TrackId.hpp @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(TrackId) + diff --git a/src/libs/database/include/database/TrackList.hpp b/src/libs/services/database/include/services/database/TrackList.hpp similarity index 61% rename from src/libs/database/include/database/TrackList.hpp rename to src/libs/services/database/include/services/database/TrackList.hpp index 03aaca0f..fe44db40 100644 --- a/src/libs/database/include/database/TrackList.hpp +++ b/src/libs/services/database/include/services/database/TrackList.hpp @@ -21,13 +21,18 @@ #include #include -#include +#include #include #include #include -#include "database/Types.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/TrackListId.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { @@ -52,25 +57,25 @@ class TrackList : public Object TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr user); // Stats utility - std::vector> getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getTopReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector> getTopTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + std::vector> getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; + std::vector> getTopReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + std::vector> getTopTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const; // Search utility - static pointer get(Session& session, std::string_view name, Type type, ObjectPtr user); - static pointer getById(Session& session, TrackListId tracklistId); - static std::vector getAll(Session& session); - static std::vector getAll(Session& session, ObjectPtr user); - static std::vector getAll(Session& session, ObjectPtr user, Type type); + static std::size_t getCount(Session& session); + static pointer find(Session& session, std::string_view name, Type type, UserId userId); + static pointer find(Session& session, TrackListId tracklistId); + static RangeResults find(Session& session, UserId userId, Range range); + static RangeResults find(Session& session, UserId userId, Type type, Range range); // Create utility static pointer create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr user); // Accessors - std::string getName() const { return _name; } - bool isPublic() const { return _isPublic; } - Type getType() const { return _type; } - ObjectPtr getUser() const { return _user; } + std::string_view getName() const { return _name; } + bool isPublic() const { return _isPublic; } + Type getType() const { return _type; } + ObjectPtr getUser() const { return _user; } // Modifiers void setName(const std::string& name) { _name = name; } @@ -81,18 +86,20 @@ class TrackList : public Object bool isEmpty() const; std::size_t getCount() const; ObjectPtr getEntry(std::size_t pos) const; - std::vector> getEntries(std::optional offset = {}, std::optional size = {}) const; + std::vector> getEntries(std::optional offset = {}, std::optional size = {}) const; ObjectPtr getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const; - // Get track bya + std::vector> getArtists(const std::vector& clusters, std::optional linkType, ArtistSortMethod sortMethod, std::optional range, bool& moreResults) const; + std::vector> getReleases(const std::vector& clusters, std::optional range, bool& moreResults) const; + std::vector> getTracks(const std::vector& clusters, std::optional range, bool& moreResults) const; - std::vector> getArtistsReverse(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getReleasesReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector> getTracksReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + // Sorted by date time + std::vector> getArtistsOrderedByRecentFirst(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; + std::vector> getReleasesOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + std::vector> getTracksOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector getTrackIds() const; - - std::chrono::milliseconds getDuration() const; + std::vector getTrackIds() const; + std::chrono::milliseconds getDuration() const; // Get clusters, order by occurence std::vector> getClusters() const; @@ -114,14 +121,12 @@ class TrackList : public Object } private: - std::string _name; Type _type {Type::Playlist}; bool _isPublic {false}; Wt::Dbo::ptr _user; Wt::Dbo::collection> _entries; - }; class TrackListEntry : public Object @@ -129,12 +134,13 @@ class TrackListEntry : public Object public: TrackListEntry() = default; TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime); + TrackListEntry(ObjectPtr track, ObjectPtr tracklist); // find utility static pointer getById(Session& session, TrackListEntryId id); // Create utility - static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = Wt::WDateTime::currentDateTime()); + static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = {}); // Accessors ObjectPtr getTrack() const { return _track; } @@ -150,8 +156,7 @@ class TrackListEntry : public Object } private: - - Wt::WDateTime _dateTime; + Wt::WDateTime _dateTime; // optional date time Wt::Dbo::ptr _track; Wt::Dbo::ptr _tracklist; }; diff --git a/src/libs/services/database/include/services/database/TrackListId.hpp b/src/libs/services/database/include/services/database/TrackListId.hpp new file mode 100644 index 00000000..3dc35d07 --- /dev/null +++ b/src/libs/services/database/include/services/database/TrackListId.hpp @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2014 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 . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(TrackListId) +LMS_DECLARE_IDTYPE(TrackListEntryId) + diff --git a/src/libs/services/database/include/services/database/Types.hpp b/src/libs/services/database/include/services/database/Types.hpp new file mode 100644 index 00000000..4333be45 --- /dev/null +++ b/src/libs/services/database/include/services/database/Types.hpp @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2015 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 . + */ + +#pragma once + +#include +#include +#include +#include + +namespace Database +{ + // Caution: do not change enum values if they are set! + + // Request: + // size = 0 => no size limit! + // Response (via RangeResults) + // size => results size + struct Range + { + std::size_t offset {}; + std::size_t size {}; + + // TODO remove this + operator bool() const { return size != 0; } + }; + + template + struct RangeResults + { + Range range; + std::vector results; + bool moreResults; + + RangeResults getSubRange(Range subRange) + { + assert(subRange.offset >= range.offset); + + if (!subRange.size) + subRange.size = range.size - (subRange.offset - range.offset); + + subRange.offset = std::min(subRange.offset, range.offset + range.size); + subRange.size = std::min(subRange.size, range.offset + range.size - subRange.offset); + + RangeResults subResults; + + auto itBegin {std::cbegin(results) + subRange.offset - range.offset}; + auto itEnd {itBegin + subRange.size}; + subResults.results.reserve(std::distance(itBegin, itEnd)); + std::copy(itBegin, itEnd, std::back_inserter(subResults.results)); + + subResults.range = subRange; + if (subRange.offset + subRange.size == range.offset + range.size) + subResults.moreResults = moreResults; + else + subResults.moreResults = true; + + return subResults; + } + }; + + struct DateRange + { + Wt::WDate begin; + Wt::WDate end; + + static DateRange fromYearRange(int from, int to); + }; + + enum class ArtistSortMethod + { + None, + ByName, + BySortName, + Random, + LastWritten, + StarredDateDesc, + }; + + enum class ReleaseSortMethod + { + None, + Name, + Date, + Random, + LastWritten, + StarredDateDesc, + }; + + enum class TrackSortMethod + { + None, + Random, + LastWritten, + StarredDateDesc, + }; + + enum class TrackArtistLinkType + { + Artist = 0, // regular track artist + Arranger = 1, + Composer = 2, + Conductor = 3, + Lyricist = 4, + Mixer = 5, + Performer = 6, + Producer = 7, + ReleaseArtist = 8, + Remixer = 9, + Writer = 10, + }; + + // User selectable audio file formats + enum class AudioFormat + { + MP3 = 1, + OGG_OPUS = 2, + OGG_VORBIS = 3, + WEBM_VORBIS = 4, + MATROSKA_OPUS = 5, + }; + + using Bitrate = std::uint32_t; + // Do not remove values! + void visitAllowedAudioBitrates(std::function); + bool isAudioBitrateAllowed(Bitrate bitrate); + + enum class Scrobbler + { + Internal = 0, + ListenBrainz = 1, + }; + + enum class ScrobblingState + { + PendingAdd = 0, + Synchronized = 1, + PendingRemove = 2, + }; + + enum class UserType + { + REGULAR = 0, + ADMIN = 1, + DEMO = 2, + }; + + enum class UITheme + { + Light = 0, + Dark = 1, + }; + + enum class SubsonicArtistListMode + { + AllArtists = 0, + ReleaseArtists = 1, + TrackArtists = 2, + }; +} + diff --git a/src/libs/database/include/database/User.hpp b/src/libs/services/database/include/services/database/User.hpp similarity index 60% rename from src/libs/database/include/database/User.hpp rename to src/libs/services/database/include/services/database/User.hpp index b94d41af..4a446296 100644 --- a/src/libs/database/include/database/User.hpp +++ b/src/libs/services/database/include/services/database/User.hpp @@ -26,51 +26,15 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Object.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" #include "utils/UUID.hpp" namespace Database { - -class Artist; -class Release; +class AuthToken; class Session; -class TrackList; -class Track; - -class User; -class AuthToken : public Object -{ - public: - AuthToken() = default; - AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr user); - - // Utility - static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, ObjectPtr user); - static void removeExpiredTokens(Session& session, const Wt::WDateTime& now); - static pointer getByValue(Session& session, const std::string& value); - static pointer getById(Session& session, AuthTokenId tokenId); - - // Accessors - const Wt::WDateTime& getExpiry() const { return _expiry; } - ObjectPtr getUser() const { return _user; } - const std::string& getValue() const { return _value; } - - template - void persist(Action& a) - { - Wt::Dbo::field(a, _value, "value"); - Wt::Dbo::field(a, _expiry, "expiry"); - Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); - } - - private: - - std::string _value; - Wt::WDateTime _expiry; - - Wt::Dbo::ptr _user; -}; class User : public Object { @@ -81,35 +45,19 @@ class User : public Object std::string hash; }; - // Do not change enum values! - enum class UITheme + struct FindParameters { - Light = 0, - Dark = 1, - }; + std::optional scrobbler; + Range range; - // Do not remove values! - static inline const std::set audioTranscodeAllowedBitrates - { - 64000, - 96000, - 128000, - 192000, - 320000, - }; - - // Do not change enum values! - enum class SubsonicArtistListMode - { - AllArtists = 0, - ReleaseArtists = 1, - TrackArtists = 2, + 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 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}; @@ -121,18 +69,17 @@ class User : public Object // utility static pointer create(Session& session, std::string_view loginName); - static pointer getById(Session& session, UserId id); - static pointer getByLoginName(Session& session, std::string_view loginName); - static std::vector getAll(Session& session); - static std::vector getAllIds(Session& session); - static pointer getDemo(Session& session); - static std::size_t getCount(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}; } - Wt::WDateTime getLastLogin() const { return _lastLogin; } - std::size_t getAuthTokensCount() const { return _authTokens.size(); } + 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; } @@ -165,21 +112,6 @@ class User : public Object Scrobbler getScrobbler() const { return _scrobbler; } std::optional getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); } - ObjectPtr getQueuedTrackList(Session& session) const; - - void starArtist(ObjectPtr artist); - void unstarArtist(ObjectPtr artist); - bool hasStarredArtist(ObjectPtr artist) const; - - void starRelease(ObjectPtr release); - void unstarRelease(ObjectPtr release); - bool hasStarredRelease(ObjectPtr release) const; - - // Stars - void starTrack(ObjectPtr track); - void unstarTrack(ObjectPtr track); - bool hasStarredTrack(ObjectPtr track) const; - template void persist(Action& a) { @@ -196,20 +128,15 @@ class User : public Object Wt::Dbo::field(a, _scrobbler, "scrobbler"); Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token"); - // UI settings + // 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, _tracklists, Wt::Dbo::ManyToOne, "user"); - Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _starredTracks, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user"); } private: - std::string _loginName; std::string _passwordSalt; std::string _passwordHash; @@ -228,16 +155,11 @@ class User : public Object int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate}; // User's dynamic data (UI) - int _curPlayingTrackPos {}; // Current track position in queue + int _curPlayingTrackPos {}; // Current track position in queue bool _repeatAll {}; bool _radio {}; - Wt::Dbo::collection> _tracklists; - Wt::Dbo::collection> _starredArtists; - Wt::Dbo::collection> _starredReleases; - Wt::Dbo::collection> _starredTracks; Wt::Dbo::collection> _authTokens; - }; } // namespace Databas' diff --git a/src/libs/services/database/include/services/database/UserId.hpp b/src/libs/services/database/include/services/database/UserId.hpp new file mode 100644 index 00000000..c337f8fa --- /dev/null +++ b/src/libs/services/database/include/services/database/UserId.hpp @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(UserId) + diff --git a/src/test/database/Artist.cpp b/src/libs/services/database/test/Artist.cpp similarity index 57% rename from src/test/database/Artist.cpp rename to src/libs/services/database/test/Artist.cpp index 7193b164..81dd3771 100644 --- a/src/test/database/Artist.cpp +++ b/src/libs/services/database/test/Artist.cpp @@ -21,8 +21,16 @@ using namespace Database; -TEST_F(DatabaseFixture, SingleArtist) +TEST_F(DatabaseFixture, Artist) { + { + auto transaction {session.createSharedTransaction()}; + EXPECT_FALSE(Artist::exists(session, 35)); + EXPECT_FALSE(Artist::exists(session, 0)); + EXPECT_FALSE(Artist::exists(session, 1)); + EXPECT_EQ(Artist::getCount(session), 0); + } + ScopedArtist artist {session, "MyArtist"}; { @@ -31,22 +39,25 @@ TEST_F(DatabaseFixture, SingleArtist) EXPECT_TRUE(artist.get()); EXPECT_FALSE(!artist.get()); EXPECT_EQ(artist.get()->getId(), artist.getId()); + + EXPECT_TRUE(Artist::exists(session, artist.getId())); + EXPECT_EQ(Artist::getCount(session), 1); } { auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getAll(session, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {})}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::getAllOrphans(session); - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + artists = Artist::findAllOrphans(session, Range {}); + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); } } -TEST_F(DatabaseFixture, SingleTrackSingleArtist) +TEST_F(DatabaseFixture, Artist_singleTrack) { ScopedTrack track {session, "MyTrack"}; ScopedArtist artist {session, "MyArtist"}; @@ -59,7 +70,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtist) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { @@ -93,7 +104,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtist) } } -TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles) +TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles) { ScopedTrack track {session, "MyTrack"}; ScopedArtist artist {session, "MyArtist"}; @@ -107,17 +118,16 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { auto transaction {session.createSharedTransaction()}; - bool hasMore{}; - EXPECT_EQ(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1); - EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Artist, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1); - EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::ReleaseArtist, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1); - EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Writer, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1); - EXPECT_TRUE(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty()); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0); } { @@ -140,7 +150,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles) } } -TEST_F(DatabaseFixture,SingleTrackMultiArtists) +TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) { ScopedTrack track {session, "track"}; ScopedArtist artist1 {session, "artist1"}; @@ -156,7 +166,7 @@ TEST_F(DatabaseFixture,SingleTrackMultiArtists) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { @@ -170,8 +180,8 @@ TEST_F(DatabaseFixture,SingleTrackMultiArtists) EXPECT_EQ(track->getArtists({}).size(), 2); EXPECT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 2); EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); - EXPECT_EQ(Artist::getAll(session, Artist::SortMethod::ByName).size(), 2); - EXPECT_EQ(Artist::getAllIds(session).size(), 2); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 2); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2); } { @@ -187,7 +197,7 @@ TEST_F(DatabaseFixture,SingleTrackMultiArtists) } } -TEST_F(DatabaseFixture, SingleArtistSearchByName) +TEST_F(DatabaseFixture, Artist_findByName) { ScopedArtist artist {session, "AAA"}; ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans @@ -201,22 +211,22 @@ TEST_F(DatabaseFixture, SingleArtistSearchByName) { auto transaction {session.createSharedTransaction()}; - bool more {}; - EXPECT_TRUE(Artist::getByFilter(session, {}, {"N"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more).empty()); + EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setKeywords({"N"})).results.empty()); - const auto artistsByAAA {Artist::Artist::getByFilter(session, {}, {"A"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - ASSERT_EQ(artistsByAAA.size(), 1); - EXPECT_EQ(artistsByAAA.front()->getId(), artist.getId()); + const auto artistsByAAA {Artist::find(session, Artist::FindParameters {}.setKeywords({"A"}))}; + ASSERT_EQ(artistsByAAA.results.size(), 1); + EXPECT_EQ(artistsByAAA.results.front(), artist.getId()); - const auto artistsByZZZ {Artist::Artist::getByFilter(session, {}, {"Z"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - ASSERT_EQ(artistsByZZZ.size(), 1); - EXPECT_EQ(artistsByZZZ.front()->getId(), artist.getId()); + const auto artistsByZZZ {Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"}))}; + ASSERT_EQ(artistsByZZZ.results.size(), 1); + EXPECT_EQ(artistsByZZZ.results.front(), artist.getId()); - EXPECT_TRUE(Artist::getByName(session, "NNN").empty()); + EXPECT_TRUE(Artist::find(session, "NNN").empty()); + EXPECT_EQ(Artist::find(session, "AAA").size(), 1); } } -TEST_F(DatabaseFixture, MultipleArtistsSearchByNameEscaped) +TEST_F(DatabaseFixture, Artist_findByNameEscaped) { ScopedArtist artist1 {session, "MyArtist%"}; ScopedArtist artist2 {session, "%MyArtist"}; @@ -229,70 +239,55 @@ TEST_F(DatabaseFixture, MultipleArtistsSearchByNameEscaped) { auto transaction {session.createSharedTransaction()}; { - const auto artists {Artist::getByName(session, "MyArtist%")}; + const auto artists {Artist::find(session, "MyArtist%")}; ASSERT_TRUE(artists.size() == 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - EXPECT_TRUE(Artist::getByName(session, "MyArtistFoo").empty()); + EXPECT_TRUE(Artist::find(session, "MyArtistFoo").empty()); } { - const auto artists {Artist::getByName(session, "%MyArtist")}; + const auto artists {Artist::find(session, "%MyArtist")}; ASSERT_TRUE(artists.size() == 1); EXPECT_EQ(artists.front()->getId(), artist2.getId()); - EXPECT_TRUE(Artist::getByName(session, "FooMyArtist").empty()); + EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty()); } { - const auto artists {Artist::getByName(session, "%_MyArtist")}; + const auto artists {Artist::find(session, "%_MyArtist")}; ASSERT_TRUE(artists.size() == 1); ASSERT_EQ(artists.front()->getId(), artist3.getId()); - EXPECT_TRUE(Artist::getByName(session, "%CMyArtist").empty()); + EXPECT_TRUE(Artist::find(session, "%CMyArtist").empty()); } } - // get by filter only works with tracks links... - ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans - - { - auto transaction {session.createUniqueTransaction()}; - - TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist3.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist4.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist5.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist6.get(), TrackArtistLinkType::Artist); - } - { auto transaction {session.createSharedTransaction()}; - bool more; { - const auto artists {Artist::getByFilter(session, {}, {"MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - EXPECT_EQ(artists.size(), 6); + const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"}))}; + EXPECT_EQ(artists.results.size(), 6); } { - const auto artists {Artist::getByFilter(session, {}, {"MyArtist%"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist4.getId()); + const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName))}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist1.getId()); + EXPECT_EQ(artists.results[1], artist4.getId()); } { - const auto artists {Artist::getByFilter(session, {}, {"%MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist2.getId()); - EXPECT_EQ(artists[1]->getId(), artist5.getId()); + const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName))}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist2.getId()); + EXPECT_EQ(artists.results[1], artist5.getId()); } { - const auto artists {Artist::getByFilter(session, {}, {"_MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists[0]->getId(), artist3.getId()); + const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist3.getId()); } } } -TEST_F(DatabaseFixture, MultiArtistsSortMethod) +TEST_F(DatabaseFixture, Artist_sortMethod) { ScopedArtist artistA {session, "artistA"}; ScopedArtist artistB {session, "artistB"}; @@ -307,20 +302,20 @@ TEST_F(DatabaseFixture, MultiArtistsSortMethod) { auto transaction {session.createSharedTransaction()}; - auto allArtistsByName {Artist::getAll(session, Artist::SortMethod::ByName)}; - auto allArtistsBySortName {Artist::getAll(session, Artist::SortMethod::BySortName)}; + auto allArtistsByName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName))}; + auto allArtistsBySortName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))}; - ASSERT_EQ(allArtistsByName.size(), 2); - EXPECT_EQ(allArtistsByName.front()->getId(), artistA.getId()); - EXPECT_EQ(allArtistsByName.back()->getId(), artistB.getId()); + ASSERT_EQ(allArtistsByName.results.size(), 2); + EXPECT_EQ(allArtistsByName.results.front(), artistA.getId()); + EXPECT_EQ(allArtistsByName.results.back(), artistB.getId()); - ASSERT_EQ(allArtistsBySortName.size(), 2); - EXPECT_EQ(allArtistsBySortName.front()->getId(), artistB.getId()); - EXPECT_EQ(allArtistsBySortName.back()->getId(), artistA.getId()); + ASSERT_EQ(allArtistsBySortName.results.size(), 2); + EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId()); + EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId()); } } -TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) +TEST_F(DatabaseFixture, Artist_nonReleaseTracks) { ScopedArtist artist {session, "artist"}; ScopedTrack track1 {session, "MyTrack1"}; @@ -331,9 +326,8 @@ TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) auto transaction {session.createSharedTransaction()}; EXPECT_FALSE(artist->hasNonReleaseTracks(std::nullopt)); - bool moreResults; - const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )}; - EXPECT_EQ(tracks.size(), 0); + const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {})}; + EXPECT_EQ(tracks.results.size(), 0); } { @@ -349,11 +343,11 @@ TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) { auto transaction {session.createSharedTransaction()}; - bool moreResults; - const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )}; + const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {})}; EXPECT_TRUE(artist->hasNonReleaseTracks(std::nullopt)); - EXPECT_FALSE(moreResults); - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track2.getId()); + EXPECT_FALSE(tracks.moreResults); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front()->getId(), track2.getId()); } } + diff --git a/src/libs/services/database/test/CMakeLists.txt b/src/libs/services/database/test/CMakeLists.txt new file mode 100644 index 00000000..99bbf7b7 --- /dev/null +++ b/src/libs/services/database/test/CMakeLists.txt @@ -0,0 +1,26 @@ + +add_executable(test-database + Artist.cpp + Cluster.cpp + Common.cpp + DatabaseTest.cpp + Listen.cpp + Release.cpp + StarredArtist.cpp + StarredRelease.cpp + StarredTrack.cpp + Track.cpp + TrackBookmark.cpp + TrackFeatures.cpp + TrackList.cpp + ) + +target_link_libraries(test-database PRIVATE + lmsdatabase + GTest::GTest + ) + +if (NOT CMAKE_CROSSCOMPILING) + gtest_discover_tests(test-database) +endif() + diff --git a/src/test/database/Cluster.cpp b/src/libs/services/database/test/Cluster.cpp similarity index 66% rename from src/test/database/Cluster.cpp rename to src/libs/services/database/test/Cluster.cpp index aba28a41..df6c677d 100644 --- a/src/test/database/Cluster.cpp +++ b/src/libs/services/database/test/Cluster.cpp @@ -24,60 +24,73 @@ using namespace Database; -TEST_F(DatabaseFixture, SingleCluster) +TEST_F(DatabaseFixture, Cluster) { + { + auto transaction {session.createUniqueTransaction()}; + EXPECT_EQ(Cluster::getCount(session), 0); + EXPECT_EQ(ClusterType::getCount(session), 0); + } + ScopedClusterType clusterType {session, "MyType"}; + { + auto transaction {session.createUniqueTransaction()}; + EXPECT_EQ(ClusterType::getCount(session), 1); + } + { ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; { auto transaction {session.createUniqueTransaction()}; - auto clusters {Cluster::getAll(session)}; - ASSERT_EQ(clusters.size(), 1); - EXPECT_EQ(clusters.front()->getId(), cluster.getId()); - EXPECT_EQ(clusters.front()->getType()->getId(), clusterType.getId()); + EXPECT_EQ(Cluster::getCount(session), 1); + EXPECT_EQ(cluster->getType()->getId(), clusterType.getId()); - clusters = Cluster::getAllOrphans(session); - ASSERT_EQ(clusters.size(), 1); - EXPECT_EQ(clusters.front()->getId(), cluster.getId()); + auto clusters {Cluster::find(session, Range {})}; + ASSERT_EQ(clusters.results.size(), 1); + EXPECT_EQ(clusters.results.front(), cluster.getId()); - auto clusterTypes {ClusterType::getAll(session)}; - ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); + clusters = Cluster::findOrphans(session, Range {}); + ASSERT_EQ(clusters.results.size(), 1); + EXPECT_EQ(clusters.results.front(), cluster.getId()); - clusterTypes = ClusterType::getAllUsed(session); - ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); + auto clusterTypes {ClusterType::find(session, Range {})}; + ASSERT_EQ(clusterTypes.results.size(), 1); + EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); - clusterTypes = ClusterType::getAllOrphans(session); - EXPECT_TRUE(clusterTypes.empty()); + clusterTypes = ClusterType::findUsed(session, Range {}); + ASSERT_EQ(clusterTypes.results.size(), 1); + EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); + + clusterTypes = ClusterType::findOrphans(session, Range {}); + EXPECT_TRUE(clusterTypes.results.empty()); } } { auto transaction {session.createUniqueTransaction()}; - auto clusterTypes {ClusterType::getAllOrphans(session)}; - ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); + auto clusterTypes {ClusterType::findOrphans(session, Range {})}; + ASSERT_EQ(clusterTypes.results.size(), 1); + EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); - ASSERT_TRUE(ClusterType::getAllUsed(session).empty()); + ASSERT_TRUE(ClusterType::findUsed(session, Range {}).results.empty()); } } -TEST_F(DatabaseFixture, SingleTrackSingleCluster) +TEST_F(DatabaseFixture, Cluster_singleTrack) { ScopedTrack track {session, "MyTrack"}; ScopedClusterType clusterType {session, "MyClusterType"}; { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); - auto clusterTypes {ClusterType::getAllOrphans(session)}; - ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); + auto clusterTypes {ClusterType::findOrphans(session, Range {})}; + ASSERT_EQ(clusterTypes.results.size(), 1); + EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); } ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"}; @@ -85,17 +98,12 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster) { auto transaction {session.createSharedTransaction()}; - auto clusters {Cluster::getAllOrphans(session)}; - EXPECT_EQ(clusters.size(), 2); + auto clusters {Cluster::findOrphans(session, Range {})}; + EXPECT_EQ(clusters.results.size(), 2); EXPECT_TRUE(track->getClusters().empty()); EXPECT_TRUE(track->getClusterIds().empty()); } - { - auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Track::getAllIdsWithClusters(session).empty()); - } - { auto transaction {session.createUniqueTransaction()}; @@ -104,29 +112,22 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster) { auto transaction {session.createSharedTransaction()}; - auto tracks {Track::getAllIdsWithClusters(session)}; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front(), track.getId()); - } + auto clusters {Cluster::findOrphans(session, Range {})}; + ASSERT_EQ(clusters.results.size(), 1); + EXPECT_EQ(clusters.results.front(), cluster2.getId()); - { - auto transaction {session.createSharedTransaction()}; - auto clusters {Cluster::getAllOrphans(session)}; - ASSERT_EQ(clusters.size(), 1); - EXPECT_EQ(clusters.front()->getId(), cluster2.getId()); - - EXPECT_TRUE(ClusterType::getAllOrphans(session).empty()); + EXPECT_TRUE(ClusterType::findOrphans(session, Range {}).results.empty()); } { auto transaction {session.createSharedTransaction()}; - auto tracks {Track::getByClusters(session, {cluster1.getId()})}; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track.getId()); + auto tracks {Track::find(session, Track::FindParameters {}.setClusters({cluster1.getId()}))}; + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front(), track.getId()); - tracks = Track::getByClusters(session, {cluster2.getId()}); - EXPECT_TRUE(tracks.empty()); + tracks = Track::find(session, Track::FindParameters {}.setClusters({cluster2.getId()})); + EXPECT_TRUE(tracks.results.empty()); } { @@ -142,7 +143,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster) } } -TEST_F(DatabaseFixture, MultipleTracksSingleCluster) +TEST_F(DatabaseFixture, Cluster_multiTracks) { std::list tracks; ScopedClusterType clusterType {session, "MyClusterType"}; @@ -160,19 +161,19 @@ TEST_F(DatabaseFixture, MultipleTracksSingleCluster) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); EXPECT_EQ(cluster->getTracksCount(), tracks.size()); - for (auto trackCluster : cluster->getTracks()) + for (TrackId trackId : cluster->getTracks(Range {}).results) { - auto it {std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackCluster->getId() == track.getId(); })}; + auto it {std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackId == track.getId(); })}; EXPECT_TRUE(it != std::cend(tracks)); } } } -TEST_F(DatabaseFixture, MultipleTracksMultipleClustersTopRelease) +TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease) { ScopedClusterType clusterType {session, "ClusterType"}; ScopedCluster cluster1 {session, clusterType.lockAndGet(), "Cluster1"}; @@ -268,16 +269,25 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersTopRelease) } } -TEST_F(DatabaseFixture,SingleTrackSingleReleaseSingleCluster) +TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { ScopedTrack track {session, "MyTrackFile"}; ScopedRelease release {session, "MyRelease"}; - ScopedClusterType clusterType {session, "MyClusterType"}; - ScopedCluster cluster {session, clusterType .lockAndGet(), "MyCluster"}; { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Release::getAllIdsWithClusters(session).empty()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); + } + + ScopedClusterType clusterType {session, "MyClusterType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + ScopedCluster unusedCluster {session, clusterType.lockAndGet(), "MyClusterUnused"}; + + { + auto transaction {session.createSharedTransaction()}; + ASSERT_EQ(Cluster::findOrphans(session, Range {}).results.size(), 2); + EXPECT_TRUE(Release::find(session, Release::FindParameters {}.setClusters({unusedCluster.getId()})).results.empty()); + EXPECT_EQ(Release::find(session, Release::FindParameters {}).results.size(), 1); } { @@ -289,24 +299,27 @@ TEST_F(DatabaseFixture,SingleTrackSingleReleaseSingleCluster) { auto transaction {session.createSharedTransaction()}; - auto releases {Release::getAllIdsWithClusters(session)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front(), release.getId()); + + { + auto clusters {Cluster::findOrphans(session, Range {})}; + ASSERT_EQ(clusters.results.size(), 1); + EXPECT_EQ(clusters.results.front(), unusedCluster.getId()); + } } { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); - EXPECT_TRUE(Release::getAllOrphans(session).empty()); + auto releases {Release::find(session, Release::FindParameters {}.setClusters({cluster.getId()}))}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); } { auto transaction {session.createSharedTransaction()}; - auto releases {Release::getByClusters(session, {cluster.getId()})}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); + auto releases {Release::find(session, Release::FindParameters {}.setClusters({unusedCluster.getId()}))}; + EXPECT_EQ(releases.results.size(), 0); } { @@ -314,6 +327,8 @@ TEST_F(DatabaseFixture,SingleTrackSingleReleaseSingleCluster) EXPECT_EQ(cluster->getReleasesCount(), 1); EXPECT_EQ(cluster->getTracksCount(), 1); + EXPECT_EQ(unusedCluster->getReleasesCount(), 0); + EXPECT_EQ(unusedCluster->getTracksCount(), 0); } } @@ -334,10 +349,10 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(ClusterType::getAllOrphans(session).empty()); - EXPECT_EQ(Cluster::getAllOrphans(session).size(), 2); - EXPECT_TRUE(Release::getAllOrphans(session).empty()); - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(ClusterType::findOrphans(session, Range {}).results.empty()); + EXPECT_EQ(Cluster::findOrphans(session, Range {}).results.size(), 2); + EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { @@ -349,12 +364,12 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) { auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getByClusters(session, {cluster1.getId()}, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()}))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - EXPECT_TRUE(Artist::getByClusters(session, {cluster2.getId()}, Artist::SortMethod::ByName).empty()); - EXPECT_TRUE(Artist::getByClusters(session, {cluster3.getId()}, Artist::SortMethod::ByName).empty()); + EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setClusters({cluster2.getId()})).results.empty()); + EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setClusters({cluster3.getId()})).results.empty()); cluster2.get().modify()->addTrack(track.get()); } @@ -362,19 +377,19 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) { auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getByClusters(session, {cluster1.getId()}, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()}))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::getByClusters(session, {cluster2.getId()}, Artist::SortMethod::ByName); - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + artists = Artist::find(session, Artist::FindParameters {}.setClusters({cluster2.getId()})); + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::getByClusters(session, {cluster1.getId(), cluster2.getId()}, Artist::SortMethod::ByName); - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + artists = Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()})); + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - EXPECT_TRUE(Artist::getByClusters(session, {cluster3.getId()}, Artist::SortMethod::ByName).empty()); + EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setClusters({cluster3.getId()})).results.empty()); } } @@ -395,17 +410,17 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); - EXPECT_TRUE(Release::getAllOrphans(session).empty()); - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getByClusters(session, {cluster.getId()}, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster.getId()}))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); } } @@ -435,8 +450,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { @@ -445,9 +460,9 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters) std::vector clusterIds; std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); }); - auto artists {Artist::getByClusters(session, clusterIds, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {}.setClusters(clusterIds))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); } } @@ -471,15 +486,11 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity) { auto transaction {session.createSharedTransaction()}; - const auto similarTracks {Track::getSimilarTracks(session, {tracks.front().getId()})}; - EXPECT_EQ(similarTracks.size(), tracks.size() - 1); - for (auto similarTrack : similarTracks) + const auto similarTracks {Track::findSimilarTracks(session, {tracks.front().getId()}, Range {})}; + EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); + for (const TrackId similarTrackId : similarTracks.results) { - EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), - [&](const ScopedTrack& track) - { - return similarTrack->getId() == track.getId(); - }) != std::cend(tracks)); + EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); } } } @@ -516,17 +527,17 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity) auto transaction {session.createSharedTransaction()}; { - auto similarTracks {Track::getSimilarTracks(session, {tracks.back().getId()}, 0, 4)}; - EXPECT_EQ(similarTracks.size(), 4); - for (auto similarTrack : similarTracks) - EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const ScopedTrack& track) { return similarTrack->getId() == track.getId(); }) != std::cend(tracks)); + auto similarTracks {Track::findSimilarTracks(session, {tracks.back().getId()}, Range {0, 4})}; + EXPECT_EQ(similarTracks.results.size(), 4); + for (const TrackId similarTrackId : similarTracks.results) + EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); } { - auto similarTracks {Track::getSimilarTracks(session, {tracks.front().getId()})}; - EXPECT_EQ(similarTracks.size(), tracks.size() - 1); - for (auto similarTrack : similarTracks) - EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const ScopedTrack& track) { return similarTrack->getId() == track.getId(); }) != std::cend(tracks)); + auto similarTracks {Track::findSimilarTracks(session, {tracks.front().getId()}, Range {})}; + EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); + for (const TrackId similarTrackId : similarTracks.results) + EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); } } } @@ -539,11 +550,6 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster) ScopedClusterType clusterType {session, "MyType"}; ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; - { - auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::getAllIdsWithClusters(session).empty()); - } - { auto transaction {session.createUniqueTransaction()}; @@ -555,25 +561,18 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); - EXPECT_TRUE(ClusterType::getAllOrphans(session).empty()); - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); - EXPECT_TRUE(Release::getAllOrphans(session).empty()); - } - - { - auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getAllIdsWithClusters(session)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front(), artist.getId()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(ClusterType::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); } { auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getByClusters(session, {cluster.getId()}, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster.getId()}))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); auto releases {artist->getReleases()}; ASSERT_EQ(releases.size(), 1); @@ -735,9 +734,9 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - EXPECT_TRUE(trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesReverse({}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksReverse({}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); } { @@ -750,15 +749,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size() , 1); EXPECT_EQ(releases.front()->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front()->getId(), track1.getId()); } @@ -767,15 +766,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); EXPECT_EQ(releases.front()->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front()->getId(), track1.getId()); } @@ -784,15 +783,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); EXPECT_EQ(releases.front()->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front()->getId(), track1.getId()); } @@ -801,7 +800,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); } @@ -810,7 +809,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); } @@ -819,11 +818,11 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - EXPECT_TRUE(trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({cluster2.getId()}, std::nullopt, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getArtistsReverse({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty()); } { @@ -836,17 +835,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); EXPECT_EQ(artists[0]->getId(), artist2.getId()); EXPECT_EQ(artists[1]->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); EXPECT_EQ(releases[0]->getId(), release2.getId()); EXPECT_EQ(releases[1]->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); EXPECT_EQ(tracks[0]->getId(), track2.getId()); EXPECT_EQ(tracks[1]->getId(),track1.getId()); @@ -856,17 +855,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); EXPECT_EQ(artists[0]->getId(), artist2.getId()); EXPECT_EQ(artists[1]->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); EXPECT_EQ(releases[0]->getId(), release2.getId()); EXPECT_EQ(releases[1]->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); EXPECT_EQ(tracks[0]->getId(), track2.getId()); EXPECT_EQ(tracks[1]->getId(), track1.getId()); @@ -876,15 +875,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); EXPECT_EQ(releases.front()->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front()->getId(), track1.getId()); } @@ -893,15 +892,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster2.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist2.getId()); - const auto releases {trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); EXPECT_EQ(releases.front()->getId(), release2.getId()); - const auto tracks {trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front()->getId(), track2.getId()); } @@ -916,17 +915,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); EXPECT_EQ(artists[0]->getId(), artist1.getId()); EXPECT_EQ(artists[1]->getId(), artist2.getId()); - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); EXPECT_EQ(releases[0]->getId(), release1.getId()); EXPECT_EQ(releases[1]->getId(), release2.getId()); - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); EXPECT_EQ(tracks[0]->getId(), track1.getId()); EXPECT_EQ(tracks[1]->getId(), track2.getId()); @@ -936,17 +935,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); EXPECT_EQ(artists[0]->getId(), artist1.getId()); EXPECT_EQ(artists[1]->getId(), artist2.getId()); - const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); EXPECT_EQ(releases[0]->getId(), release1.getId()); EXPECT_EQ(releases[1]->getId(), release2.getId()); - const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); EXPECT_EQ(tracks[0]->getId(), track1.getId()); EXPECT_EQ(tracks[1]->getId(), track2.getId()); @@ -964,9 +963,9 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(artist1->getSimilarArtists().empty()); - EXPECT_TRUE(artist2->getSimilarArtists().empty()); - EXPECT_TRUE(artist3->getSimilarArtists().empty()); + EXPECT_TRUE(artist1->findSimilarArtists().results.empty()); + EXPECT_TRUE(artist2->findSimilarArtists().results.empty()); + EXPECT_TRUE(artist3->findSimilarArtists().results.empty()); } std::list tracks; @@ -998,38 +997,38 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters) auto transaction {session.createSharedTransaction()}; { - auto artists {artist1->getSimilarArtists()}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); + auto artists {artist1->findSimilarArtists()}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist2.getId()); } { - auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist})}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); + auto artists {artist1->findSimilarArtists({TrackArtistLinkType::Artist})}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist2.getId()); } { - auto artists {artist1->getSimilarArtists({TrackArtistLinkType::ReleaseArtist})}; - EXPECT_EQ(artists.empty(), 1); + auto artists {artist1->findSimilarArtists({TrackArtistLinkType::ReleaseArtist})}; + EXPECT_EQ(artists.results.empty(), 1); } { - auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist})}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); + auto artists {artist1->findSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist})}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist2.getId()); } { - auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Composer})}; - EXPECT_TRUE(artists.empty()); + auto artists {artist1->findSimilarArtists({TrackArtistLinkType::Composer})}; + EXPECT_TRUE(artists.results.empty()); } { - auto artists {artist2->getSimilarArtists()}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist3.getId()); + auto artists {artist2->findSimilarArtists()}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist1.getId()); + EXPECT_EQ(artists.results[1], artist3.getId()); } } } diff --git a/src/libs/services/database/test/Common.cpp b/src/libs/services/database/test/Common.cpp new file mode 100644 index 00000000..033aa754 --- /dev/null +++ b/src/libs/services/database/test/Common.cpp @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Common.hpp" + +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Db.hpp" +#include "services/database/Listen.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/StarredArtist.hpp" +#include "services/database/StarredRelease.hpp" +#include "services/database/StarredTrack.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackArtistLink.hpp" +#include "services/database/TrackBookmark.hpp" +#include "services/database/TrackList.hpp" +#include "services/database/Types.hpp" +#include "services/database/User.hpp" + +TmpDatabase::TmpDatabase() +: _tmpFile {std::tmpnam(nullptr)} +, _fileDeleter {_tmpFile} +, _db {_tmpFile} +{ +} + +Database::Db& +TmpDatabase::getDb() +{ + return _db; +} + +DatabaseFixture::~DatabaseFixture() +{ + testDatabaseEmpty(); +} + +void +DatabaseFixture::SetUpTestCase() +{ + _tmpDb = std::make_unique(); + { + Database::Session s {_tmpDb->getDb()}; + s.prepareTables(); + s.optimize(); + + // remove default created entries + { + auto transaction {s.createUniqueTransaction()}; + auto clusterTypes {Database::ClusterType::find(s, Database::Range {})}; + for (auto clusterTypeId : clusterTypes.results) + { + auto clusterType {Database::ClusterType::find(s, clusterTypeId)}; + clusterType.remove(); + } + } + } +} + +void +DatabaseFixture::TearDownTestCase() +{ + _tmpDb.reset(); +} + +void +DatabaseFixture::testDatabaseEmpty() +{ + using namespace Database; + + auto uniqueTransaction {session.createUniqueTransaction()}; + + EXPECT_EQ(Artist::getCount(session), 0); + EXPECT_EQ(Cluster::getCount(session), 0); + EXPECT_EQ(ClusterType::getCount(session), 0); + EXPECT_EQ(Listen::getCount(session), 0); + EXPECT_EQ(Release::getCount(session), 0); + EXPECT_EQ(StarredArtist::getCount(session), 0); + EXPECT_EQ(StarredRelease::getCount(session), 0); + EXPECT_EQ(StarredTrack::getCount(session), 0); + EXPECT_EQ(Track::getCount(session), 0); + EXPECT_EQ(TrackBookmark::getCount(session), 0); + EXPECT_EQ(TrackList::getCount(session), 0); + EXPECT_EQ(User::getCount(session), 0); +} + +TEST_F(DatabaseFixture, Common_subRangeEmpty) +{ + using namespace Database; + + RangeResults results; + results.range = Range {0, 0}; + results.results = {}; + results.moreResults = false; + + { + auto subRange {results.getSubRange(Range {0, 0})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + EXPECT_EQ(subRange.range, Range {}); + } + { + auto subRange {results.getSubRange(Range {0, 1})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Common_IdType) +{ + using namespace Database; + + { + const IdType id{}; + EXPECT_FALSE(id.isValid()); + } + + { + const IdType id{0}; + EXPECT_TRUE(id.isValid()); + } + + { + const IdType id1{0}; + const IdType id2{0}; + EXPECT_EQ(id1, id2); + } + + { + const IdType id1{0}; + const IdType id2{1}; + EXPECT_NE(id1, id2); + EXPECT_LT(id1, id2); + EXPECT_GT(id2, id1); + } +} + +TEST_F(DatabaseFixture, Common_subRange) +{ + using namespace Database; + + RangeResults results; + results.range = Range {0, 2}; + results.results = {5, 6}; + results.moreResults = false; + + { + auto subRange {results.getSubRange(Range {0, 1})}; + EXPECT_TRUE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 5); + } + { + auto subRange {results.getSubRange(Range {1, 1})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 6); + } + { + auto subRange {results.getSubRange(Range {0, 2})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 2); + EXPECT_EQ(subRange.results.front(), 5); + EXPECT_EQ(subRange.results.back(), 6); + } + { + auto subRange {results.getSubRange(Range {})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 2); + EXPECT_EQ(subRange.results.front(), 5); + EXPECT_EQ(subRange.results.back(), 6); + EXPECT_EQ(subRange.range, results.range); + } + + { + auto subRange {results.getSubRange(Range {1, 0})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 6); + const Range expectedRange {1, 1}; + EXPECT_EQ(subRange.range, expectedRange); + } + { + auto subRange {results.getSubRange(Range {3, 2})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + const Range expectedRange {2, 0}; + EXPECT_EQ(subRange.range, expectedRange); + } +} + + + diff --git a/src/test/database/Common.hpp b/src/libs/services/database/test/Common.hpp similarity index 61% rename from src/test/database/Common.hpp rename to src/libs/services/database/test/Common.hpp index 8864e681..25c47de6 100644 --- a/src/test/database/Common.hpp +++ b/src/libs/services/database/test/Common.hpp @@ -24,17 +24,19 @@ #include -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackArtistLink.hpp" -#include "database/TrackBookmark.hpp" -#include "database/TrackList.hpp" -#include "database/Types.hpp" -#include "database/User.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Db.hpp" +#include "services/database/Listen.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackArtistLink.hpp" +#include "services/database/TrackBookmark.hpp" +#include "services/database/TrackFeatures.hpp" +#include "services/database/TrackList.hpp" +#include "services/database/Types.hpp" +#include "services/database/User.hpp" template class ScopedEntity @@ -57,7 +59,7 @@ class ScopedEntity { auto transaction {_session.createUniqueTransaction()}; - auto entity {T::getById(_session, _id)}; + auto entity {T::find(_session, _id)}; entity.remove(); } @@ -76,7 +78,7 @@ class ScopedEntity { _session.checkSharedLocked(); - auto entity {T::getById(_session, _id)}; + auto entity {T::find(_session, _id)}; EXPECT_TRUE(entity); return entity; } @@ -98,7 +100,6 @@ using ScopedCluster = ScopedEntity; using ScopedClusterType = ScopedEntity; using ScopedRelease = ScopedEntity; using ScopedTrack = ScopedEntity; -using ScopedTrackBookmark = ScopedEntity; using ScopedTrackList = ScopedEntity; using ScopedUser = ScopedEntity; @@ -120,60 +121,27 @@ class ScopedFileDeleter final class TmpDatabase final { public: - Database::Db& getDb() { return _db; } + TmpDatabase (); + + Database::Db& getDb(); private: - const std::filesystem::path _tmpFile {std::tmpnam(nullptr)}; - ScopedFileDeleter fileDeleter {_tmpFile}; - Database::Db _db {_tmpFile}; + const std::filesystem::path _tmpFile; + ScopedFileDeleter _fileDeleter; + Database::Db _db; }; class DatabaseFixture : public ::testing::Test { public: - ~DatabaseFixture() - { - testDatabaseEmpty(); - } + ~DatabaseFixture(); public: - static void SetUpTestCase() - { - _tmpDb = std::make_unique(); - { - Database::Session s {_tmpDb->getDb()}; - s.prepareTables(); - s.optimize(); - - // remove default created entries - { - auto transaction {s.createUniqueTransaction()}; - auto clusterTypes {Database::ClusterType::getAll(s)}; - for (auto& clusterType : clusterTypes) - clusterType.remove(); - } - } - } - - static void TearDownTestCase() - { - _tmpDb.reset(); - } + static void SetUpTestCase(); + static void TearDownTestCase(); private: - void testDatabaseEmpty() - { - auto uniqueTransaction {session.createUniqueTransaction()}; - - EXPECT_TRUE(Database::Artist::getAll(session, Database::Artist::SortMethod::ByName).empty()); - EXPECT_TRUE(Database::Cluster::getAll(session).empty()); - EXPECT_TRUE(Database::ClusterType::getAll(session).empty()); - EXPECT_TRUE(Database::Release::getAll(session).empty()); - EXPECT_TRUE(Database::Track::getAll(session).empty()); - EXPECT_TRUE(Database::TrackBookmark::getAll(session).empty()); - EXPECT_TRUE(Database::TrackList::getAll(session).empty()); - EXPECT_TRUE(Database::User::getAll(session).empty()); - } + void testDatabaseEmpty(); static inline std::unique_ptr _tmpDb {}; diff --git a/src/libs/services/database/test/DatabaseTest.cpp b/src/libs/services/database/test/DatabaseTest.cpp new file mode 100644 index 00000000..918d519a --- /dev/null +++ b/src/libs/services/database/test/DatabaseTest.cpp @@ -0,0 +1,110 @@ +/* + * 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 "Common.hpp" + +using namespace Database; + +TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease) +{ + constexpr std::size_t nbTracks {10}; + std::list tracks; + ScopedArtist artist {session, "MyArtst"}; + ScopedRelease release {session, "MyRelease"}; + + for (std::size_t i {}; i < nbTracks; ++i) + { + tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + + auto transaction {session.createUniqueTransaction()}; + + TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); + tracks.back().get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); + } + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(artist->getReleaseCount(), 1); + ASSERT_EQ(artist->getReleases().size(), 1); + EXPECT_EQ(artist->getReleases().front()->getId(), release.getId()); + + EXPECT_EQ(release->getTracks().size(), nbTracks); + } +} + +TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedRelease release {session, "MyRelease"}; + ScopedArtist artist {session, "MyArtist"}; + + { + auto transaction {session.createUniqueTransaction()}; + + auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)}; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createUniqueTransaction()}; + + auto releases {artist->getReleases()}; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front()->getId(), release.getId()); + + EXPECT_EQ(artist->getReleaseCount(), 1); + + auto artists {release->getArtists()}; + ASSERT_EQ(artists.size(), 1); + ASSERT_EQ(artists.front()->getId(), artist.getId()); + } +} + +TEST_F(DatabaseFixture, SingleUser) +{ + { + auto transaction {session.createSharedTransaction()}; + EXPECT_TRUE(User::find(session, User::FindParameters {}).results.empty()); + EXPECT_EQ(User::getCount(session), 0); + } + + ScopedUser user {session, "MyUser"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(User::find(session, User::FindParameters {}).results.size(), 1); + EXPECT_EQ(User::getCount(session), 1); + } +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/libs/services/database/test/Listen.cpp b/src/libs/services/database/test/Listen.cpp new file mode 100644 index 00000000..d43f7ad0 --- /dev/null +++ b/src/libs/services/database/test/Listen.cpp @@ -0,0 +1,903 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Common.hpp" +#include "services/database/Listen.hpp" + +using namespace Database; + +using ScopedListen = ScopedEntity; + +TEST_F(DatabaseFixture, Listen_getAll) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(Listen::getCount(session), 0); + } + + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}}}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(Listen::getCount(session), 1); + } + + { + auto transaction {session.createUniqueTransaction()}; + ScopedListen listen2 {session, user.get(), track.get(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{13, 0, 1}}}; + + EXPECT_EQ(Listen::getCount(session), 2); + } + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(Listen::getCount(session), 1); + } +} + +TEST_F(DatabaseFixture, Listen_get) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto listens {Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::ListenBrainz))}; + EXPECT_EQ(listens.results.size(), 0); + } + + { + auto transaction {session.createSharedTransaction()}; + + { + auto listens {Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal))}; + EXPECT_EQ(listens.moreResults, false); + ASSERT_EQ(listens.results.size(), 1); + EXPECT_EQ(listens.results.front(), listen->getId()); + } + + { + auto listens {Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal).setScrobblingState(ScrobblingState::PendingAdd))}; + EXPECT_EQ(listens.results.size(), 1); + } + { + auto listens {Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal).setScrobblingState(ScrobblingState::Synchronized))}; + EXPECT_EQ(listens.results.size(), 0); + } + } +} + +TEST_F(DatabaseFixture, Listen_get_multi) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedListen listen3 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 3}}}; + ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}}}; + ScopedListen listen2 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 2}}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto listens {Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal))}; + ASSERT_EQ(listens.results.size(), 3); + EXPECT_EQ(listens.results[0], listen1.getId()); + EXPECT_EQ(listens.results[1], listen2.getId()); + EXPECT_EQ(listens.results[2], listen3.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_get_byDateTime) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime1 {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + const Wt::WDateTime dateTime2 {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 2}}; + ASSERT_GT(dateTime2, dateTime1); + + ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1}; + ScopedListen listen2 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2}; + + { + auto transaction {session.createSharedTransaction()}; + + { + Listen::pointer listen {Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime1)}; + ASSERT_TRUE(listen); + EXPECT_EQ(listen->getId(), listen1.getId()); + } + + { + Listen::pointer listen {Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime2)}; + ASSERT_TRUE(listen); + EXPECT_EQ(listen->getId(), listen2.getId()); + } + + { + Listen::pointer listen {Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime2.addSecs(56))}; + EXPECT_FALSE(listen); + } + } +} + +TEST_F(DatabaseFixture, Listen_getTopArtists) +{ + ScopedTrack track1 {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime1 {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime1}; + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.moreResults, false); + } + + ScopedTrack track2 {session, "MyTrack2"}; + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime1.addSecs(1)}; + + { + auto transaction {session.createUniqueTransaction()}; + + TrackArtistLink::create(session, track2.get(), artist1.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist1->getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::ListenBrainz, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 0); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, TrackArtistLinkType::Producer)}; + EXPECT_EQ(artists.results.size(), 0); + } + { + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster->getId()}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + } + } +} + +TEST_F(DatabaseFixture, Listen_getTopArtists_multi) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedArtist artist2 {session, "MyArtist2"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + } + + ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist1->getId()); + } + ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)}; + ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(3)}; + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist2->getId()); + EXPECT_EQ(artists.results[1], artist1->getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt, Range {0, 1})}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.moreResults, true); + EXPECT_EQ(artists.results[0], artist2->getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getTopArtists_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedArtist artist {session, "MyArtist"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getTopReleases) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedRelease release {session, "MyRelease"}; + { + auto transaction {session.createSharedTransaction()}; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + EXPECT_EQ(releases.results.size(), 0); + } + + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release.getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::ListenBrainz, {})}; + EXPECT_EQ(releases.moreResults, false); + EXPECT_EQ(releases.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Listen_getTopReleases_multi) +{ + ScopedTrack track1 {session, "MyTrack"}; + ScopedTrack track2 {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedRelease release1 {session, "MyRelease1"}; + ScopedRelease release2 {session, "MyRelease2"}; + + { + auto transaction {session.createSharedTransaction()}; + track1.get().modify()->setRelease(release1.get()); + track2.get().modify()->setRelease(release2.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release1.getId()); + } + ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release2.getId()); + EXPECT_EQ(releases.results[1], release1.getId()); + } + ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedListen listen5 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release1.getId()); + EXPECT_EQ(releases.results[1], release2.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getTopReleases_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + ScopedRelease release {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(releases.results.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getTopTracks) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 0); + } + + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track.getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::ListenBrainz, {})}; + EXPECT_EQ(tracks.moreResults, false); + EXPECT_EQ(tracks.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Listen_getTopTrack_multi) +{ + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track1.getId()); + } + ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track2.getId()); + EXPECT_EQ(tracks.results[1], track1.getId()); + } + ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedListen listen5 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track1.getId()); + EXPECT_EQ(tracks.results[1], track2.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getTopTracks_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(tracks.results.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentArtists) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedArtist artist {session, "MyArtist"}; + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.moreResults, false); + } + + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist->getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::ListenBrainz, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 0); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, TrackArtistLinkType::Producer)}; + EXPECT_EQ(artists.results.size(), 0); + } + { + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster->getId()}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + } + } +} + +TEST_F(DatabaseFixture, Listen_getRecentArtists_multi) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedArtist artist2 {session, "MyArtist2"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + } + + ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist1->getId()); + } + ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)}; + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist2->getId()); + EXPECT_EQ(artists.results[1], artist1->getId()); + } + ScopedListen listen3 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt, Range {0, 1})}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.moreResults, true); + EXPECT_EQ(artists.results[0], artist2->getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedArtist artist {session, "MyArtist"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentReleases) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedRelease release {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 0); + } + + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release.getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::ListenBrainz, {})}; + EXPECT_EQ(releases.moreResults, false); + EXPECT_EQ(releases.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentReleases_multi) +{ + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedUser user {session, "MyUser"}; + ScopedRelease release1 {session, "MyRelease1"}; + ScopedRelease release2 {session, "MyRelease2"}; + + { + auto transaction {session.createSharedTransaction()}; + track1.get().modify()->setRelease(release1.get()); + track2.get().modify()->setRelease(release2.get()); + } + + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release2.getId()); + } + + ScopedListen listen2 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release1.getId()); + EXPECT_EQ(releases.results[1], release2.getId()); + } + + ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)}; + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release2.getId()); + EXPECT_EQ(releases.results[1], release1.getId()); + } + + ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release2.getId()); + EXPECT_EQ(releases.results[1], release1.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + ScopedRelease release {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + track.get().modify()->setRelease(release.get()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(releases.results.size(), 0); + } + + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(releases.results.size(), 0); + } + + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentTracks) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 0); + } + + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track.getId()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::ListenBrainz, {})}; + EXPECT_EQ(tracks.moreResults, false); + EXPECT_EQ(tracks.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentTracks_multi) +{ + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedUser user {session, "MyUser"}; + + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track2.getId()); + } + + ScopedListen listen2 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track1.getId()); + EXPECT_EQ(tracks.results[1], track2.getId()); + } + + ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)}; + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track2.getId()); + EXPECT_EQ(tracks.results[1], track1.getId()); + } + + ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track2.getId()); + EXPECT_EQ(tracks.results[1], track1.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(tracks.results.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track.getId()); + } +} + diff --git a/src/test/database/Release.cpp b/src/libs/services/database/test/Release.cpp similarity index 63% rename from src/test/database/Release.cpp rename to src/libs/services/database/test/Release.cpp index a65db032..9ff1db24 100644 --- a/src/test/database/Release.cpp +++ b/src/libs/services/database/test/Release.cpp @@ -21,25 +21,38 @@ using namespace Database; -TEST_F(DatabaseFixture, SingleRelease) +TEST_F(DatabaseFixture, Release) { + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(Release::getCount(session), 0); + EXPECT_FALSE(Release::exists(session, 0)); + EXPECT_FALSE(Release::exists(session, 1)); + } + ScopedRelease release {session, "MyRelease"}; { auto transaction {session.createSharedTransaction()}; - auto releases {Release::getAllOrphans(session)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); + EXPECT_EQ(Release::getCount(session), 1); + EXPECT_TRUE(Release::exists(session, release.getId())); - releases = Release::getAll(session); - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); - EXPECT_EQ(release->getDuration(), std::chrono::seconds {0}); + auto releases {Release::findOrphans(session, Range {})}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); + + { + auto releases {Release::find(session, Release::FindParameters {})}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); + EXPECT_EQ(release->getDuration(), std::chrono::seconds {0}); + } } } -TEST_F(DatabaseFixture, SingleTrackSingleRelease) +TEST_F(DatabaseFixture, Release_singleTrack) { ScopedRelease release {session, "MyRelease"}; @@ -55,7 +68,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Release::getAllOrphans(session).empty()); + EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); EXPECT_EQ(release->getTracksCount(), 1); ASSERT_EQ(release->getTracks().size(), 1); @@ -71,19 +84,19 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease) { auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseName")}; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track.getId()); + auto tracks {Track::findByNameAndReleaseName(session, "MyTrackName", "MyReleaseName")}; + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front(), track.getId()); } { auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseFoo")}; - EXPECT_EQ(tracks.size(), 0); + auto tracks {Track::findByNameAndReleaseName(session, "MyTrackName", "MyReleaseFoo")}; + EXPECT_EQ(tracks.results.size(), 0); } { auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::getByNameAndReleaseName(session, "MyTrackFoo", "MyReleaseName")}; - EXPECT_EQ(tracks.size(), 0); + auto tracks {Track::findByNameAndReleaseName(session, "MyTrackFoo", "MyReleaseName")}; + EXPECT_EQ(tracks.results.size(), 0); } } @@ -92,9 +105,9 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease) EXPECT_TRUE(release->getTracks().empty()); - auto releases {Release::getAllOrphans(session)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); + auto releases {Release::findOrphans(session, Range {})}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); } } @@ -129,38 +142,37 @@ TEST_F(DatabaseFixture, MulitpleReleaseSearchByName) { auto transaction {session.createSharedTransaction()}; - bool more; { - const auto releases {Release::getByFilter(session, {}, {"Release"}, std::nullopt, more)}; - EXPECT_EQ(releases.size(), 6); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"Release"}))}; + EXPECT_EQ(releases.results.size(), 6); } { - const auto releases {Release::getByFilter(session, {}, {"MyRelease"}, std::nullopt, more)}; - EXPECT_EQ(releases.size(), 5); - EXPECT_TRUE(std::none_of(std::cbegin(releases), std::cend(releases), [&](const Release::pointer& release) { return release->getId() == release6.getId(); })); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease"}))}; + ASSERT_EQ(releases.results.size(), 5); + EXPECT_TRUE(std::none_of(std::cbegin(releases.results), std::cend(releases.results), [&](const ReleaseId releaseId) { return releaseId == release6.getId(); })); } { - const auto releases {Release::getByFilter(session, {}, {"MyRelease%"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release2.getId()); - EXPECT_EQ(releases[1]->getId(), release4.getId()); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease%"}))}; + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release2.getId()); + EXPECT_EQ(releases.results[1], release4.getId()); } { - const auto releases {Release::getByFilter(session, {}, {"%MyRelease"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release3.getId()); - EXPECT_EQ(releases[1]->getId(), release5.getId()); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"%MyRelease"}))}; + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release3.getId()); + EXPECT_EQ(releases.results[1], release5.getId()); } { - const auto releases {Release::getByFilter(session, {}, {"Foo%MyRelease"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), release5.getId()); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"Foo%MyRelease"}))}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release5.getId()); } { - const auto releases {Release::getByFilter(session, {}, {"MyRelease%Foo"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), release4.getId()); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease%Foo"}))}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release4.getId()); } } } @@ -314,8 +326,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate) { auto transaction {session.createSharedTransaction()}; - const auto releases {Release::getByYear(session, 0, 3000)}; - EXPECT_EQ(releases.size(), 0); + const auto releases {Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(0, 3000)))}; + EXPECT_EQ(releases.results.size(), 0); } { @@ -339,16 +351,49 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate) { auto transaction {session.createSharedTransaction()}; - auto releases {Release::getByYear(session, 1950, 2000)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); + auto releases {Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1950, 2000)))}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::getByYear(session, 1994, 1994); - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); + releases = Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1994, 1994))); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::getByYear(session, 1993, 1993); - ASSERT_EQ(releases.size(), 0); + releases = Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1993, 1993))); + ASSERT_EQ(releases.results.size(), 0); } } +TEST_F(DatabaseFixture, Release_writtenAfter) +{ + ScopedRelease release {session, "MyRelease"}; + ScopedTrack track {session, "MyTrack"}; + + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20}}; + + { + auto transaction {session.createUniqueTransaction()}; + track.get().modify()->setLastWriteTime(dateTime); + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + const auto releases {Release::find(session, Release::FindParameters {})}; + EXPECT_EQ(releases.results.size(), 1); + } + + { + auto transaction {session.createSharedTransaction()}; + const auto releases {Release::find(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))}; + EXPECT_EQ(releases.results.size(), 1); + } + + { + auto transaction {session.createSharedTransaction()}; + const auto releases {Release::find(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))}; + EXPECT_EQ(releases.results.size(), 0); + } +} + + diff --git a/src/libs/services/database/test/StarredArtist.cpp b/src/libs/services/database/test/StarredArtist.cpp new file mode 100644 index 00000000..22f11f8e --- /dev/null +++ b/src/libs/services/database/test/StarredArtist.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Common.hpp" +#include "services/database/StarredArtist.hpp" + +using namespace Database; + +using ScopedStarredArtist = ScopedEntity; + +TEST_F(DatabaseFixture, StarredArtist) +{ + ScopedArtist artist {session, "MyArtist"}; + ScopedUser user {session, "MyUser"}; + ScopedUser user2 {session, "MyUser2"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto starredArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_FALSE(starredArtist); + EXPECT_EQ(StarredArtist::getCount(session), 0); + + auto artists {Artist::find(session, Artist::FindParameters {})}; + EXPECT_EQ(artists.results.size(), 1); + } + + ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + { + auto transaction {session.createSharedTransaction()}; + + auto gotArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_EQ(gotArtist->getId(), starredArtist->getId()); + EXPECT_EQ(StarredArtist::getCount(session), 1); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Artist::find(session, Artist::FindParameters {})}; + EXPECT_EQ(artists.results.size(), 1); + + artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); + EXPECT_EQ(artists.results.size(), 1); + + artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); + EXPECT_EQ(artists.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, StarredArtist_dateTime) +{ + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedArtist artist2 {session, "MyArtist2"}; + ScopedUser user {session, "MyUser"}; + + ScopedStarredArtist starredArtist1 {session, artist1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + ScopedStarredArtist starredArtist2 {session, artist2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; + EXPECT_EQ(artists.results.size(), 2); + } + + { + auto transaction {session.createUniqueTransaction()}; + + starredArtist1.get().modify()->setDateTime(dateTime); + starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1)); + + auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(ArtistSortMethod::StarredDateDesc))}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId()); + EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId()); + } + { + auto transaction {session.createUniqueTransaction()}; + + starredArtist1.get().modify()->setDateTime(dateTime); + starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1)); + + auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(ArtistSortMethod::StarredDateDesc))}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId()); + EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId()); + } +} diff --git a/src/libs/services/database/test/StarredRelease.cpp b/src/libs/services/database/test/StarredRelease.cpp new file mode 100644 index 00000000..2059a785 --- /dev/null +++ b/src/libs/services/database/test/StarredRelease.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Common.hpp" +#include "services/database/StarredRelease.hpp" + +using namespace Database; + +using ScopedStarredRelease = ScopedEntity; + +TEST_F(DatabaseFixture, StarredRelease) +{ + ScopedRelease release {session, "MyRelease"}; + ScopedUser user {session, "MyUser"}; + ScopedUser user2 {session, "MyUser2"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto starredRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_FALSE(starredRelease); + EXPECT_EQ(StarredRelease::getCount(session), 0); + + auto releases {Release::find(session, Release::FindParameters {})}; + EXPECT_EQ(releases.results.size(), 1); + } + + ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + { + auto transaction {session.createSharedTransaction()}; + + auto gotRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_EQ(gotRelease->getId(), starredRelease->getId()); + EXPECT_EQ(StarredRelease::getCount(session), 1); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Release::find(session, Release::FindParameters {})}; + EXPECT_EQ(releases.results.size(), 1); + + releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); + EXPECT_EQ(releases.results.size(), 1); + + releases = Release::find(session, Release::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); + EXPECT_EQ(releases.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, StarredRelease_dateTime) +{ + ScopedRelease release1 {session, "MyRelease1"}; + ScopedRelease release2 {session, "MyRelease2"}; + ScopedUser user {session, "MyUser"}; + + ScopedStarredRelease starredRelease1 {session, release1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + ScopedStarredRelease starredRelease2 {session, release2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; + EXPECT_EQ(releases.results.size(), 2); + } + + { + auto transaction {session.createUniqueTransaction()}; + + starredRelease1.get().modify()->setDateTime(dateTime); + starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1)); + + auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(ReleaseSortMethod::StarredDateDesc))}; + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId()); + EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId()); + } + { + auto transaction {session.createUniqueTransaction()}; + + starredRelease1.get().modify()->setDateTime(dateTime); + starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1)); + + auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(ReleaseSortMethod::StarredDateDesc))}; + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId()); + EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId()); + } +} diff --git a/src/libs/services/database/test/StarredTrack.cpp b/src/libs/services/database/test/StarredTrack.cpp new file mode 100644 index 00000000..6bd00cb0 --- /dev/null +++ b/src/libs/services/database/test/StarredTrack.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Common.hpp" +#include "services/database/StarredTrack.hpp" + +using namespace Database; + +using ScopedStarredTrack = ScopedEntity; + +TEST_F(DatabaseFixture, StarredTrack) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedUser user2 {session, "MyUser2"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_FALSE(starredTrack); + EXPECT_EQ(StarredTrack::getCount(session), 0); + + auto tracks {Track::find(session, Track::FindParameters {})}; + EXPECT_EQ(tracks.results.size(), 1); + } + + ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + { + auto transaction {session.createSharedTransaction()}; + + auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_EQ(gotTrack->getId(), starredTrack->getId()); + EXPECT_EQ(StarredTrack::getCount(session), 1); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Track::find(session, Track::FindParameters {})}; + EXPECT_EQ(tracks.results.size(), 1); + + tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); + EXPECT_EQ(tracks.results.size(), 1); + + tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); + EXPECT_EQ(tracks.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, StarredTrack_dateTime) +{ + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedUser user {session, "MyUser"}; + + ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; + EXPECT_EQ(tracks.results.size(), 2); + } + + { + auto transaction {session.createUniqueTransaction()}; + + starredTrack1.get().modify()->setDateTime(dateTime); + starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1)); + + auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(TrackSortMethod::StarredDateDesc))}; + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId()); + EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId()); + } + { + auto transaction {session.createUniqueTransaction()}; + + starredTrack1.get().modify()->setDateTime(dateTime); + starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1)); + + auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(TrackSortMethod::StarredDateDesc))}; + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId()); + EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId()); + } +} diff --git a/src/libs/services/database/test/Track.cpp b/src/libs/services/database/test/Track.cpp new file mode 100644 index 00000000..3e959a70 --- /dev/null +++ b/src/libs/services/database/test/Track.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Common.hpp" + +#include + +using namespace Database; + +TEST_F(DatabaseFixture, Track) +{ + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 0); + EXPECT_EQ(Track::getCount(session), 0); + EXPECT_FALSE(Track::exists(session, 0)); + } + + ScopedTrack track {session, "MyTrackFile"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 1); + EXPECT_EQ(Track::getCount(session), 1); + EXPECT_TRUE(Track::exists(session, track.getId())); + auto myTrack {Track::find(session, track.getId())}; + ASSERT_TRUE(myTrack); + EXPECT_EQ(myTrack->getId(), track.getId()); + } +} + +TEST_F(DatabaseFixture, MultipleTracks) +{ + ScopedTrack track1 {session, "MyTrackFile1"}; + ScopedTrack track2 {session, "MyTrackFile2"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_TRUE(track1.getId() != track2.getId()); + EXPECT_TRUE(track1.get() != track2.get()); + EXPECT_FALSE(track1.get() == track2.get()); + } +} + +TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) +{ + ScopedTrack track1 {session, ""}; + ScopedTrack track2 {session, ""}; + ScopedTrack track3 {session, ""}; + ScopedTrack track4 {session, ""}; + ScopedTrack track5 {session, ""}; + ScopedTrack track6 {session, ""}; + + { + auto transaction {session.createUniqueTransaction()}; + track1.get().modify()->setName("MyTrack"); + track2.get().modify()->setName("MyTrack%"); + track3.get().modify()->setName("MyTrack%Foo"); + track4.get().modify()->setName("%MyTrack"); + track5.get().modify()->setName("Foo%MyTrack"); + track6.get().modify()->setName("M_Track"); + } + + { + auto transaction {session.createSharedTransaction()}; + + { + const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"Track"}))}; + EXPECT_EQ(tracks.results.size(), 6); + } + { + const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack"}))}; + EXPECT_EQ(tracks.results.size(), 5); + EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); })); + } + { + const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack%"}))}; + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track2.getId()); + EXPECT_EQ(tracks.results[1], track3.getId()); + } + { + const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"%MyTrack"}))}; + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track4.getId()); + EXPECT_EQ(tracks.results[1], track5.getId()); + } + } +} + +TEST_F(DatabaseFixture, Track_date) +{ + ScopedTrack track {session, "MyTrack"}; + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(track->getYear(), std::nullopt); + EXPECT_EQ(track->getOriginalYear(), std::nullopt); + } + + { + auto transaction {session.createUniqueTransaction()}; + track.get().modify()->setDate(Wt::WDate {1995, 5, 5}); + track.get().modify()->setOriginalDate(Wt::WDate {1994, 2, 2}); + } + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(track->getYear(), 1995); + EXPECT_EQ(track->getOriginalYear(), 1994); + } +} + +TEST_F(DatabaseFixture, Track_writtenAfter) +{ + ScopedTrack track {session, "MyTrack"}; + + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20}}; + + { + auto transaction {session.createUniqueTransaction()}; + track.get().modify()->setLastWriteTime(dateTime); + } + + { + auto transaction {session.createSharedTransaction()}; + const auto tracks {Track::find(session, Track::FindParameters {})}; + EXPECT_EQ(tracks.results.size(), 1); + } + + { + auto transaction {session.createSharedTransaction()}; + const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))}; + EXPECT_EQ(tracks.results.size(), 1); + } + + { + auto transaction {session.createSharedTransaction()}; + const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))}; + EXPECT_EQ(tracks.results.size(), 0); + } +} + diff --git a/src/libs/services/database/test/TrackBookmark.cpp b/src/libs/services/database/test/TrackBookmark.cpp new file mode 100644 index 00000000..d99b0bd5 --- /dev/null +++ b/src/libs/services/database/test/TrackBookmark.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Common.hpp" + +#include "services/database/TrackBookmark.hpp" + +using ScopedTrackBookmark = ScopedEntity; + +using namespace Database; + +TEST_F(DatabaseFixture, TrackBookmark) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(TrackBookmark::getCount(session), 0); + } + + ScopedTrackBookmark bookmark {session, user.lockAndGet(), track.lockAndGet()}; + + { + auto transaction {session.createUniqueTransaction()}; + + bookmark.get().modify()->setComment("MyComment"); + bookmark.get().modify()->setOffset(std::chrono::milliseconds {5}); + } + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(TrackBookmark::getCount(session), 1); + + const auto bookmarks {TrackBookmark::find(session, user.getId(), Range {})}; + ASSERT_EQ(bookmarks.results.size(), 1); + EXPECT_EQ(bookmarks.results.front(), bookmark.getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto userBookmark {TrackBookmark::find(session, user.getId(), track.getId())}; + ASSERT_TRUE(userBookmark); + EXPECT_EQ(userBookmark, bookmark.get()); + + EXPECT_EQ(userBookmark->getOffset(), std::chrono::milliseconds {5}); + EXPECT_EQ(userBookmark->getComment(), "MyComment"); + } +} diff --git a/src/libs/services/database/test/TrackFeatures.cpp b/src/libs/services/database/test/TrackFeatures.cpp new file mode 100644 index 00000000..5d919a40 --- /dev/null +++ b/src/libs/services/database/test/TrackFeatures.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Common.hpp" + +#include "services/database/TrackFeatures.hpp" + +using ScopedTrackFeatures = ScopedEntity; + +using namespace Database; + +TEST_F(DatabaseFixture, TrackFeatures) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(TrackFeatures::getCount(session), 0); + } + + ScopedTrackFeatures trackFeatures {session, track.lockAndGet(), ""}; + + { + auto transaction {session.createUniqueTransaction()}; + EXPECT_EQ(TrackFeatures::getCount(session), 1); + + auto allTrackFeatures {TrackFeatures::find(session, Range {})}; + ASSERT_EQ(allTrackFeatures.results.size(), 1); + EXPECT_EQ(allTrackFeatures.results.front(), trackFeatures.getId()); + } +} diff --git a/src/test/database/DatabaseTest.cpp b/src/libs/services/database/test/TrackList.cpp similarity index 52% rename from src/test/database/DatabaseTest.cpp rename to src/libs/services/database/test/TrackList.cpp index 1bca6150..b771e472 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/libs/services/database/test/TrackList.cpp @@ -23,192 +23,19 @@ using namespace Database; -TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease) -{ - constexpr std::size_t nbTracks {10}; - std::list tracks; - ScopedArtist artist {session, "MyArtst"}; - ScopedRelease release {session, "MyRelease"}; - - for (std::size_t i {}; i < nbTracks; ++i) - { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); - - auto transaction {session.createUniqueTransaction()}; - - TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); - tracks.back().get().modify()->setRelease(release.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Release::getAllOrphans(session).empty()); - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); - } - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(artist->getReleaseCount(), 1); - ASSERT_EQ(artist->getReleases().size(), 1); - EXPECT_EQ(artist->getReleases().front()->getId(), release.getId()); - - EXPECT_EQ(release->getTracks().size(), nbTracks); - } -} - -TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist) -{ - ScopedTrack track {session, "MyTrack"}; - ScopedRelease release {session, "MyRelease"}; - ScopedArtist artist {session, "MyArtist"}; - - { - auto transaction {session.createUniqueTransaction()}; - - auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)}; - track.get().modify()->setRelease(release.get()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - auto releases {artist->getReleases()}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); - - EXPECT_EQ(artist->getReleaseCount(), 1); - - auto artists {release->getArtists()}; - ASSERT_EQ(artists.size(), 1); - ASSERT_EQ(artists.front()->getId(), artist.getId()); - } -} - -TEST_F(DatabaseFixture, SingleUser) -{ - { - auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(User::getAll(session).empty()); - EXPECT_TRUE(User::getAllIds(session).empty()); - } - - ScopedUser user {session, "MyUser"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(user->getQueuedTrackList(session)->getCount(), 0); - EXPECT_EQ(User::getAll(session).size(), 1); - EXPECT_EQ(User::getAllIds(session).size(), 1); - } -} - -TEST_F(DatabaseFixture, SingleStarredArtist) -{ - ScopedArtist artist {session, "MyArtist"}; - ScopedTrack track {session, "MyTrack"}; - ScopedUser user {session, "MyUser"}; - - { - auto transaction {session.createUniqueTransaction()}; - - EXPECT_FALSE(user->hasStarredArtist(artist.get())); - } - - { - auto transaction {session.createUniqueTransaction()}; - - auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)}; - user.get().modify()->starArtist(artist.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_TRUE(user->hasStarredArtist(artist.get())); - - bool hasMore {}; - auto artists {Artist::getStarred(session, user.get(), {}, std::nullopt, Artist::SortMethod::BySortName, std::nullopt, hasMore)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); - EXPECT_FALSE(hasMore); - } -} - -TEST_F(DatabaseFixture, SingleStarredRelease) -{ - ScopedRelease release {session, "MyRelease"}; - ScopedTrack track {session, "MyTrack"}; - ScopedUser user {session, "MyUser"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_FALSE(user->hasStarredRelease(release.get())); - } - - { - auto transaction {session.createUniqueTransaction()}; - - track.get().modify()->setRelease(release.get()); - user.get().modify()->starRelease(release.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_TRUE(user->hasStarredRelease(release.get())); - - bool hasMore {}; - auto releases {Release::getStarred(session, user.get(), {}, std::nullopt, hasMore)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); - EXPECT_FALSE(hasMore); - } -} - -TEST_F(DatabaseFixture, SingleStarredTrack) -{ - ScopedTrack track {session, "MyTrack"}; - ScopedUser user {session, "MyUser"}; - - { - auto transaction {session.createUniqueTransaction()}; - - EXPECT_FALSE(user->hasStarredTrack(track.get())); - } - - { - auto transaction {session.createUniqueTransaction()}; - - user.get().modify()->starTrack(track.get()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - EXPECT_TRUE(user->hasStarredTrack(track.get())); - - bool hasMore {}; - auto tracks {Track::getStarred(session, user.get(), {}, std::nullopt, hasMore)}; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track.getId()); - EXPECT_FALSE(hasMore); - } -} - TEST_F(DatabaseFixture, SingleTrackList) { ScopedUser user {session, "MyUser"}; + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(TrackList::getCount(session), 0); + } + ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; { auto transaction {session.createSharedTransaction()}; - - auto trackLists {TrackList::getAll(session, user.get(), TrackList::Type::Playlist)}; - ASSERT_EQ(trackLists.size(), 1); - EXPECT_EQ(trackLists.front()->getId(), trackList.getId()); + EXPECT_EQ(TrackList::getCount(session), 1); } } @@ -231,8 +58,26 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrack) ASSERT_EQ(trackList->getCount(), tracks.size()); const auto trackIds {trackList->getTrackIds()}; - for (auto trackId : trackIds) - EXPECT_TRUE(std::any_of(std::cbegin(tracks), std::cend(tracks), [trackId](const ScopedTrack& track) { return track.getId() == trackId; })); + ASSERT_EQ(trackIds.size(), tracks.size()); + + // Same order + std::size_t i {}; + for (const ScopedTrack& track : tracks) + EXPECT_EQ(track.getId(), trackIds[i++]); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto reverseTracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; + ASSERT_EQ(reverseTracks.size(), tracks.size()); + + // Reverse order + ASSERT_TRUE(tracks.size() > 0); + std::size_t i {tracks.size() - 1}; + for (const ScopedTrack& track : tracks) + EXPECT_EQ(track.getId(), reverseTracks[i--]->getId()); } } @@ -256,7 +101,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime) auto transaction {session.createSharedTransaction()}; bool moreResults; - const auto tracks {trackList.get()->getTracksReverse({}, std::nullopt, moreResults)}; + const auto tracks {trackList.get()->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 3); EXPECT_EQ(tracks.front()->getId(), track3.getId()); EXPECT_EQ(tracks.back()->getId(), track2.getId()); @@ -285,13 +130,12 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); } { - auto transaction {session.createSharedTransaction()}; bool moreResults {}; - EXPECT_TRUE(trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesReverse({}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksReverse({}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); } { @@ -304,15 +148,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); EXPECT_EQ(releases.front()->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; EXPECT_EQ(tracks.size(), 1); } @@ -326,17 +170,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); EXPECT_EQ(artists[0]->getId(), artist2.getId()); EXPECT_EQ(artists[1]->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); EXPECT_EQ(releases[0]->getId(), release2.getId()); EXPECT_EQ(releases[1]->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); EXPECT_EQ(tracks[0]->getId(), track2.getId()); EXPECT_EQ(tracks[1]->getId(), track1.getId()); @@ -352,59 +196,170 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); EXPECT_EQ(artists[0]->getId(), artist1.getId()); EXPECT_EQ(artists[1]->getId(), artist2.getId()); - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); EXPECT_EQ(releases[0]->getId(), release1.getId()); EXPECT_EQ(releases[1]->getId(), release2.getId()); - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); EXPECT_EQ(tracks[0]->getId(), track1.getId()); EXPECT_EQ(tracks[1]->getId(), track2.getId()); } } -TEST_F(DatabaseFixture, SingleTrackSingleUserSingleBookmark) +TEST_F(DatabaseFixture, SingleTrackList_getArtists) { - ScopedTrack track {session, "MyTrack"}; ScopedUser user {session, "MyUser"}; - ScopedTrackBookmark bookmark {session, user.lockAndGet(), track.lockAndGet()}; + ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedRelease release {session, "MyRelease"}; + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedArtist artist2 {session, "MyArtist2"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(trackList->getCount(), 0); + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; + ASSERT_TRUE(artists.empty()); + } + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackListEntry::create(session, track1.get(), trackList.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(trackList->getCount(), 1); + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::ReleaseArtist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; + EXPECT_TRUE(artists.empty()); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); + } + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + TrackListEntry::create(session, track2.get(), trackList.get()); + artist1.get().modify()->setSortName("ZZZ"); + artist2.get().modify()->setSortName("AAA"); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); + EXPECT_EQ(artists.back()->getId(), artist2.getId()); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::BySortName, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists.front()->getId(), artist2.getId()); + EXPECT_EQ(artists.back()->getId(), artist1.getId()); + } +} + +TEST_F(DatabaseFixture, SingleTrackList_getReleases) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; + ScopedTrack track {session, "MyTrack"}; + ScopedRelease release {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(trackList->getCount(), 0); + bool moreResults {}; + const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)}; + ASSERT_TRUE(releases.empty()); + } { auto transaction {session.createUniqueTransaction()}; - bookmark.get().modify()->setComment("MyComment"); - bookmark.get().modify()->setOffset(std::chrono::milliseconds {5}); + track.get().modify()->setRelease(release.get()); + + TrackListEntry::create(session, track.get(), trackList.get()); } { auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(TrackBookmark::getAll(session).size(), 1); - - const auto bookmarks {TrackBookmark::getByUser(session, user.get())}; - ASSERT_EQ(bookmarks.size(), 1); - EXPECT_EQ(bookmarks.back(), bookmark.get()); - } - { - auto transaction {session.createSharedTransaction()}; - - auto userBookmark {TrackBookmark::getByUser(session, user.get(), track.get())}; - ASSERT_TRUE(userBookmark); - EXPECT_EQ(userBookmark, bookmark.get()); - - EXPECT_EQ(userBookmark->getOffset(), std::chrono::milliseconds {5}); - EXPECT_EQ(userBookmark->getComment(), "MyComment"); + ASSERT_EQ(trackList->getCount(), 1); + bool moreResults {}; + const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(releases.size(), 1); + ASSERT_EQ(releases.front()->getId(), release->getId()); } } -int main(int argc, char **argv) +TEST_F(DatabaseFixture, SingleTrackList_getTracks) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + ScopedUser user {session, "MyUser"}; + ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack1"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(trackList->getCount(), 0); + bool moreResults {}; + const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)}; + ASSERT_TRUE(tracks.empty()); + } + + { + auto transaction {session.createUniqueTransaction()}; + TrackListEntry::create(session, track1.get(), trackList.get()); + TrackListEntry::create(session, track2.get(), trackList.get()); + TrackListEntry::create(session, track1.get(), trackList.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + ASSERT_EQ(trackList->getCount(), 3); + bool moreResults {}; + const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(tracks.size(), 2); + ASSERT_EQ(tracks[0]->getId(), track1->getId()); + ASSERT_EQ(tracks[1]->getId(), track2->getId()); + } } diff --git a/src/libs/recommendation/CMakeLists.txt b/src/libs/services/recommendation/CMakeLists.txt similarity index 86% rename from src/libs/recommendation/CMakeLists.txt rename to src/libs/services/recommendation/CMakeLists.txt index 890cf3f9..a001854e 100644 --- a/src/libs/recommendation/CMakeLists.txt +++ b/src/libs/services/recommendation/CMakeLists.txt @@ -1,10 +1,10 @@ add_library(lmsrecommendation SHARED - impl/clusters/ClustersClassifier.cpp + impl/clusters/ClustersEngine.cpp impl/features/FeaturesEngineCache.cpp impl/features/FeaturesEngine.cpp impl/features/FeaturesDefs.cpp - impl/Engine.cpp + impl/RecommendationService.cpp ) target_include_directories(lmsrecommendation INTERFACE diff --git a/src/libs/recommendation/impl/ClustersClassifierCreator.hpp b/src/libs/services/recommendation/impl/ClustersEngineCreator.hpp similarity index 86% rename from src/libs/recommendation/impl/ClustersClassifierCreator.hpp rename to src/libs/services/recommendation/impl/ClustersEngineCreator.hpp index c9527ef8..e0fcc842 100644 --- a/src/libs/recommendation/impl/ClustersClassifierCreator.hpp +++ b/src/libs/services/recommendation/impl/ClustersEngineCreator.hpp @@ -21,10 +21,14 @@ #include -namespace Recommendation +namespace Database { - class IClassifier; - - std::unique_ptr createClustersClassifier(); + class Db; +} + +namespace Recommendation +{ + class IEngine; + std::unique_ptr createClustersEngine(Database::Db& db); } diff --git a/src/libs/recommendation/impl/FeaturesEngineCreator.hpp b/src/libs/services/recommendation/impl/FeaturesEngineCreator.hpp similarity index 85% rename from src/libs/recommendation/impl/FeaturesEngineCreator.hpp rename to src/libs/services/recommendation/impl/FeaturesEngineCreator.hpp index 5b948e70..5cbbde7c 100644 --- a/src/libs/recommendation/impl/FeaturesEngineCreator.hpp +++ b/src/libs/services/recommendation/impl/FeaturesEngineCreator.hpp @@ -20,10 +20,15 @@ #pragma once #include -#include "IClassifier.hpp" +#include "IEngine.hpp" + +namespace Database +{ + class Db; +} namespace Recommendation { - std::unique_ptr createFeaturesEngine(); + std::unique_ptr createFeaturesEngine(Database::Db& db); } diff --git a/src/libs/services/recommendation/impl/IEngine.hpp b/src/libs/services/recommendation/impl/IEngine.hpp new file mode 100644 index 00000000..789ef7cf --- /dev/null +++ b/src/libs/services/recommendation/impl/IEngine.hpp @@ -0,0 +1,52 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include "services/database/Types.hpp" +#include "services/database/TrackListId.hpp" +#include "services/recommendation/Types.hpp" +#include "utils/EnumSet.hpp" + +namespace Database +{ + class Db; +} + +namespace Recommendation +{ + class IEngine + { + public: + virtual ~IEngine() = default; + + virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; + virtual void requestCancelLoad() = 0; + + virtual TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0; + virtual TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const = 0; + virtual ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const = 0; + virtual ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const = 0; + }; + + std::unique_ptr createEngine(Database::Db& db); + +} // ns Recommendation + diff --git a/src/libs/services/recommendation/impl/RecommendationService.cpp b/src/libs/services/recommendation/impl/RecommendationService.cpp new file mode 100644 index 00000000..67445959 --- /dev/null +++ b/src/libs/services/recommendation/impl/RecommendationService.cpp @@ -0,0 +1,264 @@ +/* + * 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 "RecommendationService.hpp" + +#include +#include + +#include "ClustersEngineCreator.hpp" +#include "FeaturesEngineCreator.hpp" + +#include "services/database/Db.hpp" +#include "services/database/Session.hpp" +#include "services/database/ScanSettings.hpp" +#include "utils/Exception.hpp" +#include "utils/Logger.hpp" + +namespace Recommendation +{ + + static + std::string_view + engineTypeToString(EngineType engineType) + { + switch (engineType) + { + case EngineType::Clusters: return "clusters"; + case EngineType::Features: return "features"; + } + + throw LmsException {"Internal error"}; + } + + std::unique_ptr + createRecommendationService(Database::Db& db) + { + return std::make_unique(db); + } + + RecommendationService::RecommendationService(Database::Db& db) + : _db {db} + { + } + + TrackContainer + RecommendationService::findSimilarTracksFromTrackList(Database::TrackListId trackListId, std::size_t maxCount) const + { + TrackContainer res; + + std::shared_lock lock {_enginesMutex}; + for (const auto& engineType : _enginePriorities) + { + auto itEngine {_engines.find(engineType)}; + if (itEngine == std::cend(_engines)) + continue; + + res = itEngine->second->findSimilarTracksFromTrackList(trackListId, maxCount); + if (!res.empty()) + break; + } + + return res; + } + + TrackContainer + RecommendationService::findSimilarTracks(const std::vector& trackIds, std::size_t maxCount) const + { + TrackContainer res; + + std::shared_lock lock {_enginesMutex}; + for (EngineType engineType : _enginePriorities) + { + auto itEngine {_engines.find(engineType)}; + if (itEngine == std::cend(_engines)) + continue; + + const IEngine& engine {*itEngine->second}; + res = engine.findSimilarTracks(trackIds, maxCount); + if (!res.empty()) + { + LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using engine '" << engineTypeToString(engineType) << "'"; + break; + } + } + + return res; + } + + ReleaseContainer + RecommendationService::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const + { + ReleaseContainer res; + + std::shared_lock lock {_enginesMutex}; + for (EngineType engineType : _enginePriorities) + { + auto itEngine {_engines.find(engineType)}; + if (itEngine == std::cend(_engines)) + continue; + + const IEngine& engine {*itEngine->second}; + res = engine.getSimilarReleases(releaseId, maxCount); + if (!res.empty()) + { + LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using engine '" << engineTypeToString(engineType) << "'"; + break; + } + + LMS_LOG(RECOMMENDATION, DEBUG) << "No result using engine '" << engineTypeToString(engineType) << "'"; + } + + return res; + } + + ArtistContainer + RecommendationService::getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const + { + ArtistContainer res; + + std::shared_lock lock {_enginesMutex}; + for (EngineType engineType : _enginePriorities) + { + auto itEngine {_engines.find(engineType)}; + if (itEngine == std::cend(_engines)) + continue; + + LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "'"; + + const IEngine& engine {*itEngine->second}; + res = engine.getSimilarArtists(artistId, linkTypes, maxCount); + if (!res.empty()) + { + LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using engine '" << engineTypeToString(engineType) << "'"; + return res; + } + } + + return res; + } + + static + Database::ScanSettings::RecommendationEngineType + getRecommendationEngineType(Database::Session& session) + { + auto transaction {session.createSharedTransaction()}; + + return Database::ScanSettings::get(session)->getRecommendationEngineType(); + } + + void + RecommendationService::load(bool forceReload, const ProgressCallback& progressCallback) + { + using namespace Database; + + LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines..."; + + EngineContainer enginesToLoad; + + { + std::unique_lock controlLock {_controlMutex}; + + { + std::unique_lock lock {_enginesMutex}; + _engines.clear(); + } + + switch (getRecommendationEngineType(_db.getTLSSession())) + { + case ScanSettings::RecommendationEngineType::Clusters: + _enginePriorities = {EngineType::Clusters}; + enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db)); + break; + + case ScanSettings::RecommendationEngineType::Features: + _enginePriorities = {EngineType::Features, EngineType::Clusters}; + + // not same order since clusters is faster to load + enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db)); + enginesToLoad.try_emplace(EngineType::Features, createFeaturesEngine(_db)); + break; + } + + assert(_pendingEngines.empty()); + for (auto& [engineType, engine] : enginesToLoad) + _pendingEngines.push_back(engine.get()); + } + + for (auto& [engineType, engine] : enginesToLoad) + loadPendingEngine(engineType, std::move(engine), forceReload, progressCallback); + + _pendingEnginesCondvar.notify_all(); + + LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines loaded!"; + } + + void + RecommendationService::loadPendingEngine(EngineType engineType, std::unique_ptr engine, bool forceReload, const ProgressCallback& progressCallback) + { + if (!_loadCancelled) + { + LMS_LOG(RECOMMENDATION, INFO) << "Initializing engine '" << engineTypeToString(engineType) << "'..."; + + auto progress {[&](const Progress& progress) + { + progressCallback(progress); + }}; + + engine->load(forceReload, progressCallback ? progress : ProgressCallback {}); + + LMS_LOG(RECOMMENDATION, INFO) << "Initializing engine '" << engineTypeToString(engineType) << "': " << (_loadCancelled ? "aborted" : "complete"); + } + + { + std::scoped_lock lock {_controlMutex}; + _pendingEngines.erase(std::find(std::begin(_pendingEngines), std::end(_pendingEngines), engine.get())); + } + + if (!_loadCancelled) + { + std::unique_lock lock {_enginesMutex}; + _engines.emplace(engineType, std::move(engine)); + } + } + + void + RecommendationService::cancelLoad() + { + LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading..."; + + std::unique_lock controlLock {_controlMutex}; + + assert(!_loadCancelled); + _loadCancelled = true; + + LMS_LOG(RECOMMENDATION, DEBUG) << "Still " << _pendingEngines.size() << " pending engines!"; + + for (IEngine* engine : _pendingEngines) + { + engine->requestCancelLoad(); + } + + _pendingEnginesCondvar.wait(controlLock, [this] {return _pendingEngines.empty();}); + _loadCancelled = false; + + LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading DONE"; + } + +} // ns Similarity diff --git a/src/libs/services/recommendation/impl/RecommendationService.hpp b/src/libs/services/recommendation/impl/RecommendationService.hpp new file mode 100644 index 00000000..25e6b5d4 --- /dev/null +++ b/src/libs/services/recommendation/impl/RecommendationService.hpp @@ -0,0 +1,85 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "services/recommendation/IRecommendationService.hpp" +#include "IEngine.hpp" + +namespace Database +{ + class Db; +} + +namespace Recommendation +{ + enum class EngineType + { + Clusters, + Features, + }; + + class RecommendationService : public IRecommendationService + { + public: + RecommendationService(Database::Db& db); + ~RecommendationService() = default; + + RecommendationService(const RecommendationService&) = delete; + RecommendationService(RecommendationService&&) = delete; + RecommendationService& operator=(const RecommendationService&) = delete; + RecommendationService& operator=(RecommendationService&&) = delete; + + private: + void load(bool forceReload, const ProgressCallback& progressCallback) override; + void cancelLoad() override; + + TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override; + TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; + ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override; + ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const override; + + void setEnginePriorities(const std::vector& engineTypes); + void clearEngines(); + void loadPendingEngine(EngineType engineType, std::unique_ptr engine, bool forceReload, const ProgressCallback& progressCallback); + + Database::Db& _db; + + std::mutex _controlMutex; + bool _loadCancelled {}; + + using EngineContainer = std::unordered_map>; + EngineContainer _engines; + mutable std::shared_mutex _enginesMutex; + + std::vector _pendingEngines; + std::shared_mutex _pendingEnginesMutex; + std::condition_variable _pendingEnginesCondvar; + + std::vector _enginePriorities; // ordered by priority + }; + +} // ns Recommendation + diff --git a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp new file mode 100644 index 00000000..9a714ff7 --- /dev/null +++ b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 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 "ClustersEngine.hpp" + +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Db.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackList.hpp" + +namespace Recommendation { + +using namespace Database; + +std::unique_ptr createClustersEngine(Db& db) +{ + return std::make_unique(db); +} + +TrackContainer +ClusterEngine::findSimilarTracks(const std::vector& trackIds, std::size_t maxCount) const +{ + Session& dbSession {_db.getTLSSession()}; + + auto transaction {dbSession.createSharedTransaction()}; + + const auto similarTrackIds {Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount})}; + return std::move(similarTrackIds.results); +} + +TrackContainer +ClusterEngine::findSimilarTracksFromTrackList(TrackListId tracklistId, std::size_t maxCount) const +{ + Session& dbSession {_db.getTLSSession()}; + + TrackContainer res; + + { + auto transaction {dbSession.createSharedTransaction()}; + + const TrackList::pointer trackList {TrackList::find(dbSession, tracklistId)}; + if (!trackList) + return res; + + const auto tracks {trackList->getSimilarTracks(0, maxCount)}; + res.reserve(tracks.size()); + std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); }); + } + + return res; +} + +ReleaseContainer +ClusterEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const +{ + Session& dbSession {_db.getTLSSession()}; + + ReleaseContainer res; + { + auto transaction {dbSession.createSharedTransaction()}; + + auto release {Release::find(dbSession, releaseId)}; + if (!release) + return res; + + const auto releases {release->getSimilarReleases(0, maxCount)}; + res.reserve(releases.size()); + std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const auto& release) { return release->getId(); }); + } + + return res; +} + +ArtistContainer +ClusterEngine::getSimilarArtists(ArtistId artistId, EnumSet artistLinkTypes, std::size_t maxCount) const +{ + Session& dbSession {_db.getTLSSession()}; + + auto transaction {dbSession.createSharedTransaction()}; + + auto artist {Artist::find(dbSession, artistId)}; + if (!artist) + return {}; + + const auto similarArtistIds {artist->findSimilarArtists(artistLinkTypes, Range {0, maxCount})}; + return std::move(similarArtistIds.results); +} + +} // namespace Recommendation diff --git a/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp b/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp new file mode 100644 index 00000000..09c14072 --- /dev/null +++ b/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018 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 . + */ + +#pragma once + +#include "IEngine.hpp" + +namespace Recommendation +{ + + class ClusterEngine : public IEngine + { + public: + ClusterEngine(Database::Db& db) : _db {db} {} + + ClusterEngine(const ClusterEngine&) = delete; + ClusterEngine(ClusterEngine&&) = delete; + ClusterEngine& operator=(const ClusterEngine&) = delete; + ClusterEngine& operator=(ClusterEngine&&) = delete; + + private: + void load(bool, const ProgressCallback&) override {} + void requestCancelLoad() override {} + + TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override; + TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; + ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override; + ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const override; + + Database::Db& _db; + }; + +} // namespace Recommendation + diff --git a/src/libs/recommendation/impl/features/FeaturesDefs.cpp b/src/libs/services/recommendation/impl/features/FeaturesDefs.cpp similarity index 97% rename from src/libs/recommendation/impl/features/FeaturesDefs.cpp rename to src/libs/services/recommendation/impl/features/FeaturesDefs.cpp index b3de7dcb..1982b683 100644 --- a/src/libs/recommendation/impl/features/FeaturesDefs.cpp +++ b/src/libs/services/recommendation/impl/features/FeaturesDefs.cpp @@ -93,15 +93,6 @@ static const std::unordered_map featureDefinitions { "lowlevel.dissonance.min", {1}}, { "lowlevel.dissonance.var", {1}}, { "lowlevel.dynamic_complexity", {1}}, - { "lowlevel.spectral_contrast_coeffs.dmean", {6}}, - { "lowlevel.spectral_contrast_coeffs.dmean2", {6}}, - { "lowlevel.spectral_contrast_coeffs.dvar", {6}}, - { "lowlevel.spectral_contrast_coeffs.dvar2", {6}}, - { "lowlevel.spectral_contrast_coeffs.max", {6}}, - { "lowlevel.spectral_contrast_coeffs.mean", {6}}, - { "lowlevel.spectral_contrast_coeffs.median", {6}}, - { "lowlevel.spectral_contrast_coeffs.min", {6}}, - { "lowlevel.spectral_contrast_coeffs.var", {6}}, { "lowlevel.erbbands.dmean", {40}}, { "lowlevel.erbbands.dmean2", {40}}, { "lowlevel.erbbands.dvar", {40}}, diff --git a/src/libs/recommendation/impl/features/FeaturesDefs.hpp b/src/libs/services/recommendation/impl/features/FeaturesDefs.hpp similarity index 100% rename from src/libs/recommendation/impl/features/FeaturesDefs.hpp rename to src/libs/services/recommendation/impl/features/FeaturesDefs.hpp diff --git a/src/libs/recommendation/impl/features/FeaturesEngine.cpp b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp similarity index 65% rename from src/libs/recommendation/impl/features/FeaturesEngine.cpp rename to src/libs/services/recommendation/impl/features/FeaturesEngine.cpp index 03fb0db6..daee3d4b 100644 --- a/src/libs/recommendation/impl/features/FeaturesEngine.cpp +++ b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp @@ -21,13 +21,14 @@ #include -#include "database/Artist.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackArtistLink.hpp" -#include "database/TrackFeatures.hpp" -#include "database/TrackList.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Db.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackArtistLink.hpp" +#include "services/database/TrackFeatures.hpp" +#include "services/database/TrackList.hpp" #include "som/DataNormalizer.hpp" #include "utils/Logger.hpp" #include "utils/Random.hpp" @@ -35,9 +36,11 @@ namespace Recommendation { -std::unique_ptr createFeaturesEngine() +using namespace Database; + +std::unique_ptr createFeaturesEngine(Db& db) { - return std::make_unique(); + return std::make_unique(db); } const FeatureSettingsMap& @@ -48,44 +51,13 @@ FeaturesEngine::getDefaultTrainFeatureSettings() { "lowlevel.spectral_energyband_high.mean", {1}}, { "lowlevel.spectral_rolloff.median", {1}}, { "lowlevel.spectral_contrast_valleys.var", {1}}, - { "lowlevel.erbbands.mean", {1}}, - { "lowlevel.gfcc.mean", {1}}, + { "lowlevel.erbbands.mean", {1}}, + { "lowlevel.gfcc.mean", {1}}, }; return defaultTrainFeatureSettings; } -static -std::optional -getTrackFeatureValues(FeaturesEngine::FeaturesFetchFunc func, Database::TrackId trackId, const std::unordered_set& featureNames) -{ - return func(trackId, featureNames); -} - -static -std::optional -getTrackFeatureValuesFromDb(Database::Session& session, Database::TrackId trackId, const std::unordered_set& featureNames) -{ - auto func = [&](Database::TrackId trackId, const std::unordered_set& featureNames) - { - std::optional res; - - auto transaction {session.createSharedTransaction()}; - - Database::Track::pointer track {Database::Track::getById(session, trackId)}; - if (!track) - return res; - - res = track->getTrackFeatures()->getFeatureValuesMap(featureNames); - if (res->empty()) - res.reset(); - - return res; - }; - - return getTrackFeatureValues(func, trackId, featureNames); -} - static std::optional convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions) @@ -127,8 +99,8 @@ getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t return weights; } -bool -FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback) +void +FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback) { LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier..."; @@ -141,50 +113,53 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions; - std::vector trackIds; + Session& session {_db.getTLSSession()}; + + RangeResults trackFeaturesIds; { auto transaction {session.createSharedTransaction()}; - LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features..."; - trackIds = Database::Track::getAllIdsWithFeatures(session); - LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features DONE (found " << trackIds.size() << " tracks)"; + LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features..."; + trackFeaturesIds = TrackFeatures::find(session, Range {}); + LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)"; } std::vector samples; - std::vector samplesTrackIds; + std::vector samplesTrackIds; - samples.reserve(trackIds.size()); - samplesTrackIds.reserve(trackIds.size()); + samples.reserve(trackFeaturesIds.results.size()); + samplesTrackIds.reserve(trackFeaturesIds.results.size()); LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features..."; - for (Database::TrackId trackId : trackIds) + // TODO handle errors using exceptions + for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results) { if (_loadCancelled) - return false; + return; - std::optional featureValuesMap; + auto transaction {session.createSharedTransaction()}; - if (_featuresFetchFunc) - featureValuesMap = getTrackFeatureValues(_featuresFetchFunc, trackId, featureNames); - else - featureValuesMap = getTrackFeatureValuesFromDb(session, trackId, featureNames); - - if (!featureValuesMap) + TrackFeatures::pointer trackFeatures {TrackFeatures::find(session, trackFeaturesId)}; + if (!trackFeatures) continue; - std::optional inputVector {convertFeatureValuesMapToInputVector(*featureValuesMap, nbDimensions)}; + FeatureValuesMap featureValuesMap {trackFeatures->getFeatureValuesMap(featureNames)}; + if (featureValuesMap.empty()) + continue; + + std::optional inputVector {convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions)}; if (!inputVector) continue; samples.emplace_back(std::move(*inputVector)); - samplesTrackIds.emplace_back(trackId); + samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId()); } LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE"; if (samples.empty()) { LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!"; - return false; + return; } LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data..."; @@ -219,15 +194,13 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings [this] { return _loadCancelled; }); LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE"; - if (_loadCancelled) - return false; LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks..."; TrackPositions trackPositions; for (std::size_t i {}; i < samples.size(); ++i) { if (_loadCancelled) - return false; + return; const SOM::Position position {network.getClosestRefVectorPosition(samples[i])}; @@ -236,83 +209,87 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE"; - return load(session, std::move(network), std::move(trackPositions)); + load(std::move(network), std::move(trackPositions)); } -bool -FeaturesEngine::loadFromCache(Database::Session& session, const FeaturesEngineCache& cache) +void +FeaturesEngine::loadFromCache(FeaturesEngineCache cache) { LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache..."; - return load(session, std::move(cache._network), cache._trackPositions); + load(std::move(cache._network), cache._trackPositions); } -IClassifier::ResultContainer -FeaturesEngine::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId trackListId, std::size_t maxCount) const +TrackContainer +FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const { - const std::vector trackIds {[&] + const TrackContainer trackIds {[&] { - std::vector res; + TrackContainer res; + + Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - const Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)}; + const TrackList::pointer trackList {TrackList::find(session, trackListId)}; if (trackList) res = trackList->getTrackIds(); return res; }()}; - return getSimilarTracks(session, trackIds, maxCount); + return findSimilarTracks(trackIds, maxCount); } -std::vector -FeaturesEngine::getSimilarTracks(Database::Session& session, const std::vector& tracksIds, std::size_t maxCount) const +TrackContainer +FeaturesEngine::findSimilarTracks(const std::vector& tracksIds, std::size_t maxCount) const { auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)}; + Session& session {_db.getTLSSession()}; + { // Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time) auto transaction {session.createSharedTransaction()}; similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds), - [&](Database::TrackId trackId) + [&](TrackId trackId) { - return Database::Track::getById(session, trackId); // TODO exists + return !Track::exists(session, trackId); }), std::end(similarTrackIds)); } return similarTrackIds; } -std::vector -FeaturesEngine::getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const +ReleaseContainer +FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const { - auto similarReleaseIds {getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount)}; + auto similarReleaseIds {getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount)}; + Session& session {_db.getTLSSession()}; + + if (!similarReleaseIds.empty()) { // Report only existing ids auto transaction {session.createSharedTransaction()}; similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds), - [&](Database::ReleaseId releaseId) + [&](ReleaseId releaseId) { - return Database::Release::getById(session, releaseId); // TODO exists + return !Release::exists(session, releaseId); }), std::end(similarReleaseIds)); } return similarReleaseIds; } -std::vector -FeaturesEngine::getSimilarArtists(Database::Session& session, - Database::ArtistId artistId, - EnumSet linkTypes, - std::size_t maxCount) const +ArtistContainer +FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const { - auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType) + auto getSimilarArtistIdsForLinkType {[&] (TrackArtistLinkType linkType) { - std::vector similarArtistIds; + ArtistContainer similarArtistIds; const auto itArtists {_artistMatrix.find(linkType)}; if (itArtists == std::cend(_artistMatrix)) @@ -323,25 +300,26 @@ FeaturesEngine::getSimilarArtists(Database::Session& session, return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount); }}; - std::unordered_set similarArtistIds; + std::unordered_set similarArtistIds; - for (Database::TrackArtistLinkType linkType : linkTypes) + for (TrackArtistLinkType linkType : linkTypes) { const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)}; similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType)); } - std::vector res(std::cbegin(similarArtistIds), std::cend(similarArtistIds)); + ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds)); + Session& session {_db.getTLSSession()}; { // Report only existing ids auto transaction {session.createSharedTransaction()}; res.erase(std::remove_if(std::begin(res), std::end(res), - [&](Database::ArtistId artistId) - { - return Database::Artist::getById(session, artistId); // TODO exists - }), std::end(res)); + [&](ArtistId artistId) + { + return !Artist::exists(session, artistId); + }), std::end(res)); } while (res.size() > maxCount) @@ -356,29 +334,25 @@ FeaturesEngine::toCache() const return FeaturesEngineCache {*_network, _trackPositions}; } -bool -FeaturesEngine::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) +void +FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback) { if (forceReload) - { FeaturesEngineCache::invalidate(); } - else + else if (const std::optional cache {FeaturesEngineCache::read()}) { - const std::optional cache {FeaturesEngineCache::read()}; - if (cache) - return loadFromCache(session, *cache); + loadFromCache(*cache); + return; } TrainSettings trainSettings; trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings(); - const bool res {loadFromTraining(session, trainSettings, progressCallback)}; - if (res) + loadFromTraining(trainSettings, progressCallback); + if (!_loadCancelled) toCache().write(); - - return res; } void @@ -388,10 +362,8 @@ FeaturesEngine::requestCancelLoad() _loadCancelled = true; } -bool -FeaturesEngine::load(Database::Session& session, - SOM::Network network, - const TrackPositions& trackPositions) +void +FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions) { using namespace Database; @@ -406,14 +378,16 @@ FeaturesEngine::load(Database::Session& session, LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps..."; + Session& session {_db.getTLSSession()}; + for (const auto& [trackId, positions] : trackPositions) { if (_loadCancelled) - return false; + return; auto transaction {session.createSharedTransaction()}; - const Track::pointer track {Database::Track::getById(session, trackId)}; + const Track::pointer track {Track::find(session, trackId)}; if (!track) continue; @@ -445,11 +419,9 @@ FeaturesEngine::load(Database::Session& session, } } - _network = std::make_unique(std::move(network)); + _network = std::make_unique(network); LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully loaded!"; - - return true; } } // ns Recommendation diff --git a/src/libs/recommendation/impl/features/FeaturesEngine.hpp b/src/libs/services/recommendation/impl/features/FeaturesEngine.hpp similarity index 75% rename from src/libs/recommendation/impl/features/FeaturesEngine.hpp rename to src/libs/services/recommendation/impl/features/FeaturesEngine.hpp index 9ed313a4..706d7f72 100644 --- a/src/libs/recommendation/impl/features/FeaturesEngine.hpp +++ b/src/libs/services/recommendation/impl/features/FeaturesEngine.hpp @@ -29,9 +29,9 @@ #include "som/DataNormalizer.hpp" #include "som/Network.hpp" #include "utils/Utils.hpp" +#include "IEngine.hpp" #include "FeaturesEngineCache.hpp" #include "FeaturesDefs.hpp" -#include "IClassifier.hpp" namespace Database { @@ -42,38 +42,28 @@ namespace Recommendation { using FeatureWeight = double; -class FeaturesEngine : public IClassifier +class FeaturesEngine : public IEngine { public: - FeaturesEngine() = default; + FeaturesEngine(Database::Db& db) : _db {db} {} + FeaturesEngine(const FeaturesEngine&) = delete; FeaturesEngine(FeaturesEngine&&) = delete; FeaturesEngine& operator=(const FeaturesEngine&) = delete; FeaturesEngine& operator=(FeaturesEngine&&) = delete; - using FeaturesFetchFunc = std::function>>(Database::TrackId, const std::unordered_set& /*features*/)>; - // Default is to retrieve the features from the database (may be slow). - // Use this only if you want to train different searchers with some cached data - static void setFeaturesFetchFunc(FeaturesFetchFunc func) { _featuresFetchFunc = func; } - static const FeatureSettingsMap& getDefaultTrainFeatureSettings(); private: - - std::string_view getName() const override { return "Features"; } - - bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) override; + void load(bool forceReload, const ProgressCallback& progressCallback) override; void requestCancelLoad() override; - ResultContainer getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const override; - ResultContainer getSimilarTracks(Database::Session& session, const std::vector& tracksId, std::size_t maxCount) const override; - ResultContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const override; - ResultContainer getSimilarArtists(Database::Session& session, - Database::ArtistId artistId, - EnumSet linkTypes, - std::size_t maxCount) const override; + TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override; + TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; + ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override; + ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const override; - bool loadFromCache(Database::Session& session, const FeaturesEngineCache& cache); + void loadFromCache(FeaturesEngineCache cache); // Use training (may be very slow) struct TrainSettings @@ -82,7 +72,7 @@ class FeaturesEngine : public IClassifier float sampleCountPerNeuron {4}; FeatureSettingsMap featureSettingsMap; }; - bool loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback); + void loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback); template using ObjectPositions = std::unordered_map>; @@ -97,7 +87,7 @@ class FeaturesEngine : public IClassifier using ReleaseMatrix = ObjectMatrix; using TrackMatrix = ObjectMatrix; - bool load(Database::Session& session, SOM::Network network, const TrackPositions& tracksPosition); + void load(const SOM::Network& network, const TrackPositions& tracksPosition); FeaturesEngineCache toCache() const; @@ -113,6 +103,7 @@ class FeaturesEngine : public IClassifier const ObjectPositions& objectPositions, std::size_t maxCount) const; + Database::Db& _db; bool _loadCancelled {}; std::unique_ptr _network; double _networkRefVectorsDistanceMedian {}; @@ -125,8 +116,6 @@ class FeaturesEngine : public IClassifier TrackPositions _trackPositions; TrackMatrix _trackMatrix; - - static inline FeaturesFetchFunc _featuresFetchFunc; }; template diff --git a/src/libs/recommendation/impl/features/FeaturesEngineCache.cpp b/src/libs/services/recommendation/impl/features/FeaturesEngineCache.cpp similarity index 100% rename from src/libs/recommendation/impl/features/FeaturesEngineCache.cpp rename to src/libs/services/recommendation/impl/features/FeaturesEngineCache.cpp diff --git a/src/libs/recommendation/impl/features/FeaturesEngineCache.hpp b/src/libs/services/recommendation/impl/features/FeaturesEngineCache.hpp similarity index 96% rename from src/libs/recommendation/impl/features/FeaturesEngineCache.hpp rename to src/libs/services/recommendation/impl/features/FeaturesEngineCache.hpp index 08372345..a27712db 100644 --- a/src/libs/recommendation/impl/features/FeaturesEngineCache.hpp +++ b/src/libs/services/recommendation/impl/features/FeaturesEngineCache.hpp @@ -21,9 +21,8 @@ #include #include -#include -#include "database/Types.hpp" +#include "services/database/TrackId.hpp" #include "som/Network.hpp" namespace Recommendation { diff --git a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp new file mode 100644 index 00000000..45704ac9 --- /dev/null +++ b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp @@ -0,0 +1,52 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include "utils/EnumSet.hpp" +#include "services/database/TrackListId.hpp" +#include "services/database/Types.hpp" +#include "services/recommendation/Types.hpp" + +namespace Database +{ + class Db; +} + +namespace Recommendation +{ + class IRecommendationService + { + public: + virtual ~IRecommendationService() = default; + + virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; + virtual void cancelLoad() = 0; // wait for cancel done + + virtual TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0; + virtual TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const = 0; + virtual ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const = 0; + virtual ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const = 0; + }; + + std::unique_ptr createRecommendationService(Database::Db& db); + +} // ns Recommendation + diff --git a/src/libs/services/recommendation/include/services/recommendation/Types.hpp b/src/libs/services/recommendation/include/services/recommendation/Types.hpp new file mode 100644 index 00000000..bfdaa67f --- /dev/null +++ b/src/libs/services/recommendation/include/services/recommendation/Types.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include "services/database/ArtistId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" + +namespace Recommendation +{ + struct Progress + { + std::size_t totalElems {}; + std::size_t processedElems {}; + }; + using ProgressCallback = std::function; + + template + using ResultContainer = std::vector; + + using ArtistContainer = ResultContainer; + using ReleaseContainer = ResultContainer; + using TrackContainer = ResultContainer; + +} // namespace Recommendation diff --git a/src/libs/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt similarity index 94% rename from src/libs/scanner/CMakeLists.txt rename to src/libs/services/scanner/CMakeLists.txt index 5f1467b8..9aefadd2 100644 --- a/src/libs/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -1,7 +1,7 @@ add_library(lmsscanner SHARED impl/AcousticBrainzUtils.cpp - impl/Scanner.cpp + impl/ScannerService.cpp impl/ScannerStats.cpp ) diff --git a/src/libs/scanner/impl/AcousticBrainzUtils.cpp b/src/libs/services/scanner/impl/AcousticBrainzUtils.cpp similarity index 100% rename from src/libs/scanner/impl/AcousticBrainzUtils.cpp rename to src/libs/services/scanner/impl/AcousticBrainzUtils.cpp diff --git a/src/libs/scanner/impl/AcousticBrainzUtils.hpp b/src/libs/services/scanner/impl/AcousticBrainzUtils.hpp similarity index 100% rename from src/libs/scanner/impl/AcousticBrainzUtils.hpp rename to src/libs/services/scanner/impl/AcousticBrainzUtils.hpp diff --git a/src/libs/scanner/impl/Scanner.cpp b/src/libs/services/scanner/impl/ScannerService.cpp similarity index 77% rename from src/libs/scanner/impl/Scanner.cpp rename to src/libs/services/scanner/impl/ScannerService.cpp index bbc10fda..de5c94ae 100644 --- a/src/libs/scanner/impl/Scanner.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -17,23 +17,24 @@ * along with LMS. If not, see . */ -#include "Scanner.hpp" +#include "ScannerService.hpp" #include #include #include -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Release.hpp" -#include "database/ScanSettings.hpp" -#include "database/Track.hpp" -#include "database/TrackArtistLink.hpp" -#include "database/TrackFeatures.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Release.hpp" +#include "services/database/ScanSettings.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackArtistLink.hpp" +#include "services/database/TrackFeatures.hpp" #include "metadata/TagLibParser.hpp" -#include "recommendation/IEngine.hpp" +#include "services/recommendation/IRecommendationService.hpp" #include "utils/Exception.hpp" +#include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Path.hpp" #include "utils/UUID.hpp" @@ -138,7 +139,7 @@ getOrCreateArtists(Session& session, const std::vector& artist // First try to get by MBID if (artistInfo.musicBrainzArtistID) { - artist = Artist::getByMBID(session, *artistInfo.musicBrainzArtistID); + artist = Artist::find(session, *artistInfo.musicBrainzArtistID); if (!artist) artist = createArtist(session, artistInfo); else @@ -151,7 +152,7 @@ getOrCreateArtists(Session& session, const std::vector& artist // Fall back on artist name (collisions may occur) if (!artistInfo.name.empty()) { - for (const Artist::pointer& sameNamedArtist : Artist::getByName(session, artistInfo.name)) + for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name)) { // Do not fallback on artist that is correctly tagged if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID()) @@ -183,7 +184,7 @@ getOrCreateRelease(Session& session, const MetaData::Album& album) // First try to get by MBID if (album.musicBrainzAlbumID) { - release = Release::getByMBID(session, *album.musicBrainzAlbumID); + release = Release::find(session, *album.musicBrainzAlbumID); if (!release) { release = Release::create(session, album.name, album.musicBrainzAlbumID); @@ -200,7 +201,7 @@ getOrCreateRelease(Session& session, const MetaData::Album& album) // Fall back on release name (collisions may occur) if (!album.name.empty()) { - for (const Release::pointer& sameNamedRelease : Release::getByName(session, album.name)) + for (const Release::pointer& sameNamedRelease : Release::find(session, album.name)) { // do not fallback on properly tagged releases if (!sameNamedRelease->getMBID()) @@ -227,7 +228,7 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames) for (auto clusterNames : clustersNames) { - auto clusterType = ClusterType::getByName(session, clusterNames.first); + auto clusterType = ClusterType::find(session, clusterNames.first); if (!clusterType) continue; @@ -248,16 +249,19 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames) namespace Scanner { -std::unique_ptr -createScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine) +std::unique_ptr +createScannerService(Db& db, Recommendation::IRecommendationService& recommendationService) { - return std::make_unique(db, recommendationEngine); + return std::make_unique(db, recommendationService); } -Scanner::Scanner(Database::Db& db, Recommendation::IEngine& recommendationEngine) -: _recommendationEngine {recommendationEngine} +ScannerService::ScannerService(Db& db, Recommendation::IRecommendationService& recommendationService) +: _recommendationService {recommendationService} +, _skipDuplicateRecordingMBID {Service::get()->getBool("scanner-skip-duplicate-recording-mbid", false)} , _dbSession {db} { + LMS_LOG(DBUPDATER, INFO) << "skipDuplicateRecordingMBID = " << _skipDuplicateRecordingMBID; + // For now, always use TagLib _metadataParser = std::make_unique(); @@ -268,14 +272,15 @@ Scanner::Scanner(Database::Db& db, Recommendation::IEngine& recommendationEngine start(); } -Scanner::~Scanner() +ScannerService::~ScannerService() { - LMS_LOG(DBUPDATER, INFO) << "Shutting down Scanner..."; + LMS_LOG(DBUPDATER, INFO) << "Stopping service..."; stop(); + LMS_LOG(DBUPDATER, INFO) << "Service stopped!"; } void -Scanner::start() +ScannerService::start() { std::scoped_lock lock {_controlMutex}; @@ -284,8 +289,8 @@ Scanner::start() if (_abortScan) return; - _recommendationEngine.load(false, - [](const Recommendation::IEngine::Progress& progress) + _recommendationService.load(false, + [](const Recommendation::Progress& progress) { LMS_LOG(DBUPDATER, DEBUG) << "Reloading recommendation : " << progress.processedElems << "/" << progress.totalElems; }); @@ -296,18 +301,18 @@ Scanner::start() } void -Scanner::stop() +ScannerService::stop() { std::scoped_lock lock {_controlMutex}; _abortScan = true; _scheduleTimer.cancel(); - _recommendationEngine.cancelLoad(); + _recommendationService.cancelLoad(); _ioService.stop(); } void -Scanner::abortScan() +ScannerService::abortScan() { LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan..."; std::scoped_lock lock {_controlMutex}; @@ -316,7 +321,7 @@ Scanner::abortScan() _abortScan = true; _scheduleTimer.cancel(); - _recommendationEngine.cancelLoad(); + _recommendationService.cancelLoad(); _ioService.stop(); LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!"; @@ -325,7 +330,7 @@ Scanner::abortScan() } void -Scanner::requestImmediateScan(bool force) +ScannerService::requestImmediateScan(bool force) { abortScan(); _ioService.post([=]() @@ -338,7 +343,7 @@ Scanner::requestImmediateScan(bool force) } void -Scanner::requestReload() +ScannerService::requestReload() { abortScan(); _ioService.post([=]() @@ -350,8 +355,8 @@ Scanner::requestReload() }); } -Scanner::Status -Scanner::getStatus() const +ScannerService::Status +ScannerService::getStatus() const { Status res; @@ -366,7 +371,7 @@ Scanner::getStatus() const } void -Scanner::scheduleNextScan() +ScannerService::scheduleNextScan() { LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan"; @@ -420,7 +425,7 @@ Scanner::scheduleNextScan() } void -Scanner::countAllFiles(ScanStats& stats) +ScannerService::countAllFiles(ScanStats& stats) { ScanStepStats stepStats{stats.startTime, ScanProgressStep::DiscoveringFiles}; @@ -445,7 +450,7 @@ Scanner::countAllFiles(ScanStats& stats) } void -Scanner::scheduleScan(bool force, const Wt::WDateTime& dateTime) +ScannerService::scheduleScan(bool force, const Wt::WDateTime& dateTime) { auto cb {[=](boost::system::error_code ec) { @@ -474,7 +479,7 @@ Scanner::scheduleScan(bool force, const Wt::WDateTime& dateTime) } void -Scanner::scan(bool forceScan) +ScannerService::scan(bool forceScan) { _events.scanStarted.emit(); @@ -543,7 +548,7 @@ Scanner::scan(bool forceScan) } bool -Scanner::fetchTrackFeatures(Database::TrackId trackId, const UUID& recordingMBID) +ScannerService::fetchTrackFeatures(TrackId trackId, const UUID& recordingMBID) { std::map features; @@ -558,20 +563,20 @@ Scanner::fetchTrackFeatures(Database::TrackId trackId, const UUID& recordingMBID { auto uniqueTransaction {_dbSession.createUniqueTransaction()}; - Database::Track::pointer track {Database::Track::getById(_dbSession, trackId)}; + Track::pointer track {Track::find(_dbSession, trackId)}; if (!track) return false; - Database::TrackFeatures::create(_dbSession, track, data); + TrackFeatures::create(_dbSession, track, data); } return true; } void -Scanner::fetchTrackFeatures(ScanStats& stats) +ScannerService::fetchTrackFeatures(ScanStats& stats) { - if (_recommendationEngineType != ScanSettings::RecommendationEngineType::Features) + if (_recommendationServiceType != ScanSettings::RecommendationEngineType::Features) return; ScanStepStats stepStats{stats.startTime, ScanProgressStep::FetchingTrackFeatures}; @@ -580,7 +585,7 @@ Scanner::fetchTrackFeatures(ScanStats& stats) struct TrackInfo { - Database::TrackId id; + TrackId id; UUID recordingMBID; }; @@ -590,9 +595,12 @@ Scanner::fetchTrackFeatures(ScanStats& stats) auto transaction {_dbSession.createSharedTransaction()}; - auto tracks {Database::Track::getAllWithRecordingMBIDAndMissingFeatures(_dbSession)}; - for (const auto& track : tracks) + auto trackIds {Track::findWithRecordingMBIDAndMissingFeatures(_dbSession, Range {})}; + for (const TrackId trackId : trackIds.results) + { + const Track::pointer track {Track::find(_dbSession, trackId)}; res.emplace_back(TrackInfo {track->getId(), *track->getRecordingMBID()}); + } return res; }()}; @@ -619,7 +627,7 @@ Scanner::fetchTrackFeatures(ScanStats& stats) } void -Scanner::refreshScanSettings() +ScannerService::refreshScanSettings() { auto transaction {_dbSession.createSharedTransaction()}; @@ -638,7 +646,7 @@ Scanner::refreshScanSettings() [](const std::filesystem::path& extension) { return std::filesystem::path{ StringUtils::stringToLower(extension.string()) }; }); } _mediaDirectory = scanSettings->getMediaDirectory(); - _recommendationEngineType = scanSettings->getRecommendationEngineType(); + _recommendationServiceType = scanSettings->getRecommendationEngineType(); const auto clusterTypes = scanSettings->getClusterTypes(); std::set clusterTypeNames; @@ -651,7 +659,7 @@ Scanner::refreshScanSettings() } void -Scanner::notifyInProgress(const ScanStepStats& stepStats) +ScannerService::notifyInProgress(const ScanStepStats& stepStats) { { std::unique_lock lock {_statusMutex}; @@ -664,7 +672,7 @@ Scanner::notifyInProgress(const ScanStepStats& stepStats) } void -Scanner::notifyInProgressIfNeeded(const ScanStepStats& stepStats) +ScannerService::notifyInProgressIfNeeded(const ScanStepStats& stepStats) { std::chrono::system_clock::time_point now {std::chrono::system_clock::now()}; @@ -673,7 +681,7 @@ Scanner::notifyInProgressIfNeeded(const ScanStepStats& stepStats) } void -Scanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats) +ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats) { Wt::WDateTime lastWriteTime; try @@ -692,7 +700,7 @@ Scanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanSt // Skip file if last write is the same auto transaction {_dbSession.createSharedTransaction()}; - const Track::pointer track {Track::getByPath(_dbSession, file)}; + const Track::pointer track {Track::findByPath(_dbSession, file)}; if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t() && track->getScanVersion() == _scanVersion) @@ -713,7 +721,26 @@ Scanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanSt auto uniqueTransaction {_dbSession.createUniqueTransaction()}; - Track::pointer track {Track::getByPath(_dbSession, file) }; + Track::pointer track {Track::findByPath(_dbSession, file) }; + + // Skip duplicate recording MBID + if (trackInfo->recordingMBID && _skipDuplicateRecordingMBID) + { + for (Track::pointer otherTrack : Track::findByRecordingMBID(_dbSession, *trackInfo->recordingMBID)) + { + if (track && track->getId() == otherTrack->getId()) + continue; + + LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar recording MBID in '" << otherTrack->getPath().string() << "')"; + // This recording MBID already exists, just remove what we just scanned + if (track) + { + track.remove(); + stats.deletions++; + } + return; + } + } // We estimate this is an audio file if: // - we found a least one audio stream @@ -778,34 +805,36 @@ Scanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanSt track.modify()->clearArtistLinks(); // Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files for (const Artist::pointer& artist : getOrCreateArtists(_dbSession, trackInfo->artists, false)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLinkType::Artist)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, artist, TrackArtistLinkType::Artist)); for (const Artist::pointer& releaseArtist : getOrCreateArtists(_dbSession, trackInfo->albumArtists, false)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLinkType::ReleaseArtist)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist)); // Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists // We could ask MusicBrainz to get all the information, but that would heavily slow down the import process for (const Artist::pointer& conductor : getOrCreateArtists(_dbSession, trackInfo->conductorArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, conductor, Database::TrackArtistLinkType::Conductor)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, conductor, TrackArtistLinkType::Conductor)); for (const Artist::pointer& composer : getOrCreateArtists(_dbSession, trackInfo->composerArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, composer, Database::TrackArtistLinkType::Composer)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, composer, TrackArtistLinkType::Composer)); for (const Artist::pointer& lyricist : getOrCreateArtists(_dbSession, trackInfo->lyricistArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, lyricist, Database::TrackArtistLinkType::Lyricist)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, lyricist, TrackArtistLinkType::Lyricist)); for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, mixer, Database::TrackArtistLinkType::Mixer)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, mixer, TrackArtistLinkType::Mixer)); for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, producer, Database::TrackArtistLinkType::Producer)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, producer, TrackArtistLinkType::Producer)); for (const Artist::pointer& remixer : getOrCreateArtists(_dbSession, trackInfo->remixerArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, remixer, Database::TrackArtistLinkType::Remixer)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, remixer, TrackArtistLinkType::Remixer)); track.modify()->setScanVersion(_scanVersion); if (trackInfo->album) track.modify()->setRelease(getOrCreateRelease(_dbSession, *trackInfo->album)); + else + track.modify()->setRelease({}); track.modify()->setClusters(getOrCreateClusters(_dbSession, trackInfo->clusters)); track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setName(title); @@ -815,8 +844,7 @@ Scanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanSt track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0); track.modify()->setTotalTrack(trackInfo->totalTrack); track.modify()->setTotalDisc(trackInfo->totalDisc); - if (!trackInfo->discSubtitle.empty()) - track.modify()->setDiscSubtitle(trackInfo->discSubtitle); + track.modify()->setDiscSubtitle(trackInfo->discSubtitle); track.modify()->setDate(trackInfo->date); track.modify()->setOriginalDate(trackInfo->originalDate); @@ -826,18 +854,17 @@ Scanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanSt track.modify()->setRecordingMBID(trackInfo->recordingMBID); track.modify()->setTrackMBID(trackInfo->trackMBID); - track.modify()->setFeatures({}); // TODO: only if MBID changed? + if (auto trackFeatures {TrackFeatures::find(_dbSession, track->getId())}) + trackFeatures.remove(); // TODO: only if MBID changed? track.modify()->setHasCover(trackInfo->hasCover); track.modify()->setCopyright(trackInfo->copyright); track.modify()->setCopyrightURL(trackInfo->copyrightURL); - if (trackInfo->trackReplayGain) - track.modify()->setTrackReplayGain(*trackInfo->trackReplayGain); - if (trackInfo->albumReplayGain) - track.modify()->setReleaseReplayGain(*trackInfo->albumReplayGain); + track.modify()->setTrackReplayGain(trackInfo->trackReplayGain); + track.modify()->setReleaseReplayGain(trackInfo->albumReplayGain); } void -Scanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats) +ScannerService::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats) { ScanStepStats stepStats{stats.startTime, ScanProgressStep::ScanningFiles}; stepStats.totalElems = stats.filesScanned; @@ -904,7 +931,7 @@ checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDire } void -Scanner::removeMissingTracks(ScanStats& stats) +ScannerService::removeMissingTracks(ScanStats& stats) { static constexpr std::size_t batchSize {50}; @@ -922,26 +949,25 @@ Scanner::removeMissingTracks(ScanStats& stats) stepStats.totalElems = trackCount; notifyInProgress(stepStats); - std::vector> trackPaths; + RangeResults trackPaths; std::vector tracksToRemove; for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i)) { - trackPaths.clear(); tracksToRemove.clear(); { auto transaction {_dbSession.createSharedTransaction()}; - trackPaths = Track::getAllPaths(_dbSession, i, batchSize); + trackPaths = Track::findPaths(_dbSession, Range {i, batchSize}); } - for (const auto& [trackId, trackPath] : trackPaths) + for (const Track::PathResult& trackPath : trackPaths.results) { if (_abortScan) return; - if (!checkFile(trackPath, _mediaDirectory, _fileExtensions)) - tracksToRemove.push_back(trackId); + if (!checkFile(trackPath.path, _mediaDirectory, _fileExtensions)) + tracksToRemove.push_back(trackPath.trackId); stepStats.processedElems++; } @@ -952,7 +978,7 @@ Scanner::removeMissingTracks(ScanStats& stats) for (const TrackId trackId : tracksToRemove) { - Track::pointer track {Track::getById(_dbSession, trackId)}; + Track::pointer track {Track::find(_dbSession, trackId)}; if (track) { track.remove(); @@ -971,16 +997,17 @@ Scanner::removeMissingTracks(ScanStats& stats) } void -Scanner::removeOrphanEntries() +ScannerService::removeOrphanEntries() { LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters..."; { auto transaction {_dbSession.createUniqueTransaction()}; // Now process orphan Cluster (no track) - auto clusters {Cluster::getAllOrphans(_dbSession)}; - for (auto& cluster : clusters) + auto clusterIds {Cluster::findOrphans(_dbSession, Range {})}; + for (ClusterId clusterId : clusterIds.results) { + Cluster::pointer cluster {Cluster::find(_dbSession, clusterId)}; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'"; cluster.remove(); } @@ -990,9 +1017,10 @@ Scanner::removeOrphanEntries() { auto transaction {_dbSession.createUniqueTransaction()}; - auto artists {Artist::getAllOrphans(_dbSession)}; - for (auto& artist : artists) + auto artistIds {Artist::findAllOrphans(_dbSession, Range {})}; + for (const ArtistId artistId : artistIds.results) { + Artist::pointer artist {Artist::find(_dbSession, artistId)}; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; artist.remove(); } @@ -1002,9 +1030,10 @@ Scanner::removeOrphanEntries() { auto transaction {_dbSession.createUniqueTransaction()}; - auto releases {Release::getAllOrphans(_dbSession)}; - for (auto& release : releases) + auto releases {Release::findOrphans(_dbSession, Range {})}; + for (const ReleaseId releaseId : releases.results) { + Release::pointer release {Release::find(_dbSession, releaseId)}; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'"; release.remove(); } @@ -1014,19 +1043,20 @@ Scanner::removeOrphanEntries() } void -Scanner::checkDuplicatedAudioFiles(ScanStats& stats) +ScannerService::checkDuplicatedAudioFiles(ScanStats& stats) { LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files"; auto transaction {_dbSession.createSharedTransaction()}; - const std::vector tracks = Database::Track::getMBIDDuplicates(_dbSession); - for (const Track::pointer& track : tracks) + const RangeResults tracks = Track::findRecordingMBIDDuplicates(_dbSession, Range {}); + for (const TrackId trackId : tracks.results) { - if (auto trackMBID {track->getTrackMBID()}) + const Track::pointer track {Track::find(_dbSession, trackId)}; + if (auto recordingMBID {track->getRecordingMBID()}) { - LMS_LOG(DBUPDATER, INFO) << "Found duplicated Track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName(); - stats.duplicates.emplace_back(ScanDuplicate {track->getId(), DuplicateReason::SameMBID}); + LMS_LOG(DBUPDATER, INFO) << "Found duplicated recording MBID [" << recordingMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName(); + stats.duplicates.emplace_back(ScanDuplicate {track->getId(), DuplicateReason::SameRecordingMBID}); } } @@ -1034,11 +1064,11 @@ Scanner::checkDuplicatedAudioFiles(ScanStats& stats) } void -Scanner::reloadSimilarityEngine(ScanStats& stats) +ScannerService::reloadSimilarityEngine(ScanStats& stats) { ScanStepStats stepStats {stats.startTime, ScanProgressStep::ReloadingSimilarityEngine}; - auto progressCallback {[&](const Recommendation::IEngine::Progress& progress) + auto progressCallback {[&](const Recommendation::Progress& progress) { stepStats.totalElems = progress.totalElems; stepStats.processedElems = progress.processedElems; @@ -1046,7 +1076,7 @@ Scanner::reloadSimilarityEngine(ScanStats& stats) }}; notifyInProgress(stepStats); - _recommendationEngine.load(stats.nbChanges() > 0, progressCallback); + _recommendationService.load(stats.nbChanges() > 0, progressCallback); notifyInProgress(stepStats); } diff --git a/src/libs/services/scanner/impl/ScannerService.hpp b/src/libs/services/scanner/impl/ScannerService.hpp new file mode 100644 index 00000000..0d8ecf91 --- /dev/null +++ b/src/libs/services/scanner/impl/ScannerService.hpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2013 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 . + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "services/database/Types.hpp" +#include "services/database/ScanSettings.hpp" +#include "services/database/Session.hpp" +#include "metadata/IParser.hpp" +#include "services/scanner/IScannerService.hpp" +#include "utils/Path.hpp" + +class UUID; + +namespace Recommendation +{ + class IRecommendationService; +} + +namespace Scanner +{ + class ScannerService : public IScannerService + { + public: + ScannerService(Database::Db& db, Recommendation::IRecommendationService& recommendationService); + ~ScannerService(); + + ScannerService(const ScannerService&) = delete; + ScannerService(ScannerService&&) = delete; + ScannerService& operator=(const ScannerService&) = delete; + ScannerService& operator=(ScannerService&&) = delete; + + void requestReload() override; + void requestImmediateScan(bool force) override; + + Status getStatus() const override; + Events& getEvents() override { return _events; } + + private: + void start(); + void stop(); + + // Job handling + void scheduleNextScan(); + void scheduleScan(bool force, const Wt::WDateTime& dateTime = {}); + + void abortScan(); + + // Update database (scheduled callback) + void scan(bool force); + + void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats); + bool fetchTrackFeatures(Database::TrackId trackId, const UUID& MBID); + void fetchTrackFeatures(ScanStats& stats); + + // Helpers + void refreshScanSettings(); + + void countAllFiles(ScanStats& stats); + void removeMissingTracks(ScanStats& stats); + void removeOrphanEntries(); + void checkDuplicatedAudioFiles(ScanStats& stats); + void scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats); + void notifyInProgressIfNeeded(const ScanStepStats& stats); + void notifyInProgress(const ScanStepStats& stats); + void reloadSimilarityEngine(ScanStats& stats); + + Recommendation::IRecommendationService& _recommendationService; + + std::mutex _controlMutex; + std::atomic _abortScan {}; + Wt::WIOService _ioService; + boost::asio::system_timer _scheduleTimer {_ioService}; + const bool _skipDuplicateRecordingMBID {}; + Events _events; + std::chrono::system_clock::time_point _lastScanInProgressEmit {}; + Database::Session _dbSession; + std::unique_ptr _metadataParser; + + mutable std::shared_mutex _statusMutex; + State _curState {State::NotScheduled}; + std::optional _lastCompleteScanStats; + std::optional _currentScanStepStats; + Wt::WDateTime _nextScheduledScan; + + // Current scan settings + std::size_t _scanVersion {}; + Wt::WTime _startTime; + Database::ScanSettings::UpdatePeriod _updatePeriod {Database::ScanSettings::UpdatePeriod::Never}; + std::unordered_set _fileExtensions; + std::filesystem::path _mediaDirectory; + Database::ScanSettings::RecommendationEngineType _recommendationServiceType; + }; +} // Scanner + diff --git a/src/libs/scanner/impl/ScannerStats.cpp b/src/libs/services/scanner/impl/ScannerStats.cpp similarity index 96% rename from src/libs/scanner/impl/ScannerStats.cpp rename to src/libs/services/scanner/impl/ScannerStats.cpp index afc12f99..e4b71180 100644 --- a/src/libs/scanner/impl/ScannerStats.cpp +++ b/src/libs/services/scanner/impl/ScannerStats.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "scanner/ScannerStats.hpp" +#include "services/scanner/ScannerStats.hpp" namespace Scanner { diff --git a/src/libs/scanner/include/scanner/IScanner.hpp b/src/libs/services/scanner/include/services/scanner/IScannerService.hpp similarity index 85% rename from src/libs/scanner/include/scanner/IScanner.hpp rename to src/libs/services/scanner/include/services/scanner/IScannerService.hpp index 018cca98..6085f6b8 100644 --- a/src/libs/scanner/include/scanner/IScanner.hpp +++ b/src/libs/services/scanner/include/services/scanner/IScannerService.hpp @@ -31,16 +31,16 @@ namespace Database namespace Recommendation { - class IEngine; + class IRecommendationService; } namespace Scanner { - class IScanner + class IScannerService { public: - virtual ~IScanner() = default; + virtual ~IScannerService() = default; // Async requests virtual void requestReload() = 0; @@ -66,7 +66,7 @@ namespace Scanner virtual Events& getEvents() = 0; }; - std::unique_ptr createScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine); + std::unique_ptr createScannerService(Database::Db& db, Recommendation::IRecommendationService& recommendationEngine); } // Scanner diff --git a/src/libs/scanner/include/scanner/ScannerEvents.hpp b/src/libs/services/scanner/include/services/scanner/ScannerEvents.hpp similarity index 100% rename from src/libs/scanner/include/scanner/ScannerEvents.hpp rename to src/libs/services/scanner/include/services/scanner/ScannerEvents.hpp diff --git a/src/libs/scanner/include/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp similarity index 89% rename from src/libs/scanner/include/scanner/ScannerStats.hpp rename to src/libs/services/scanner/include/services/scanner/ScannerStats.hpp index 5528ce8b..53f79d52 100644 --- a/src/libs/scanner/include/scanner/ScannerStats.hpp +++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp @@ -24,22 +24,22 @@ #include #include -#include "database/Types.hpp" - -namespace Scanner { +#include "services/database/TrackId.hpp" +namespace Scanner +{ enum class ScanErrorType { - CannotReadFile, // cannot read file - CannotParseFile, // cannot parse file - NoAudioTrack, // no audio track found - BadDuration, // bad duration + CannotReadFile, // cannot read file + CannotParseFile, // cannot parse file + NoAudioTrack, // no audio track found + BadDuration, // bad duration }; enum class DuplicateReason { SameHash, - SameMBID, + SameRecordingMBID, }; struct ScanError @@ -102,6 +102,5 @@ namespace Scanner { std::size_t nbFiles() const; std::size_t nbChanges() const; }; - -} +} // namespace Scanner diff --git a/src/libs/scrobbling/CMakeLists.txt b/src/libs/services/scrobbling/CMakeLists.txt similarity index 90% rename from src/libs/scrobbling/CMakeLists.txt rename to src/libs/services/scrobbling/CMakeLists.txt index bd8a0960..2ce1bb21 100644 --- a/src/libs/scrobbling/CMakeLists.txt +++ b/src/libs/services/scrobbling/CMakeLists.txt @@ -3,9 +3,8 @@ add_library(lmsscrobbling SHARED impl/internal/InternalScrobbler.cpp impl/listenbrainz/ListenBrainzScrobbler.cpp impl/listenbrainz/ListensSynchronizer.cpp - impl/listenbrainz/SendQueue.cpp impl/listenbrainz/Utils.cpp - impl/Scrobbling.cpp + impl/ScrobblingService.cpp ) target_include_directories(lmsscrobbling INTERFACE diff --git a/src/libs/scrobbling/impl/IScrobbler.hpp b/src/libs/services/scrobbling/impl/IScrobbler.hpp similarity index 63% rename from src/libs/scrobbling/impl/IScrobbler.hpp rename to src/libs/services/scrobbling/impl/IScrobbler.hpp index f442e773..3a41242d 100644 --- a/src/libs/scrobbling/impl/IScrobbler.hpp +++ b/src/libs/services/scrobbling/impl/IScrobbler.hpp @@ -23,9 +23,10 @@ #include #include -#include - -#include "scrobbling/Listen.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackListId.hpp" +#include "services/scrobbling/Listen.hpp" namespace Database { @@ -36,18 +37,26 @@ namespace Database namespace Scrobbling { - class IScrobbler { public: virtual ~IScrobbler() = default; + // Listens virtual void listenStarted(const Listen& listen) = 0; virtual void listenFinished(const Listen& listen, std::optional duration) = 0; - virtual void addTimedListen(const TimedListen& listen) = 0; - virtual Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user) = 0; + // Feedback + virtual void onStarred(Database::UserId, Database::ArtistId) {}; + virtual void onUnstarred(Database::UserId, Database::ArtistId) {}; + virtual void onStarred(Database::UserId, Database::ReleaseId) {}; + virtual void onUnstarred(Database::UserId, Database::ReleaseId) {}; + virtual void onStarred(Database::UserId, Database::TrackId) {}; + virtual void onUnstarred(Database::UserId, Database::TrackId) {}; + +// virtual void star(Database::TrackId trackId) = 0; +// virtual void unstar(Database::TrackId trackId) = 0; }; std::unique_ptr createScrobbler(std::string_view backendName); diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.cpp b/src/libs/services/scrobbling/impl/ScrobblingService.cpp new file mode 100644 index 00000000..ac24670d --- /dev/null +++ b/src/libs/services/scrobbling/impl/ScrobblingService.cpp @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "ScrobblingService.hpp" +#include "ScrobblingService.impl.hpp" + +#include "services/database/Artist.hpp" +#include "services/database/Db.hpp" +#include "services/database/Listen.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/StarredArtist.hpp" +#include "services/database/StarredRelease.hpp" +#include "services/database/StarredTrack.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "utils/Logger.hpp" + +#include "internal/InternalScrobbler.hpp" +#include "listenbrainz/ListenBrainzScrobbler.hpp" + +namespace Scrobbling +{ + using namespace Database; + + std::unique_ptr + createScrobblingService(boost::asio::io_context& ioContext, Db& db) + { + return std::make_unique(ioContext, db); + } + + ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db) + : _db {db} + { + LMS_LOG(SCROBBLING, INFO) << "Starting service..."; + _scrobblers.emplace(Scrobbler::Internal, std::make_unique(_db)); + _scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique(ioContext, _db)); + LMS_LOG(SCROBBLING, INFO) << "Service started!"; + } + + ScrobblingService::~ScrobblingService() + { + LMS_LOG(SCROBBLING, INFO) << "Service stopped!"; + } + + void + ScrobblingService::listenStarted(const Listen& listen) + { + if (std::optional scrobbler {getUserScrobbler(listen.userId)}) + _scrobblers[*scrobbler]->listenStarted(listen); + } + + void + ScrobblingService::listenFinished(const Listen& listen, std::optional duration) + { + if (std::optional scrobbler {getUserScrobbler(listen.userId)}) + _scrobblers[*scrobbler]->listenFinished(listen, duration); + } + + void + ScrobblingService::addTimedListen(const TimedListen& listen) + { + if (std::optional scrobbler {getUserScrobbler(listen.userId)}) + _scrobblers[*scrobbler]->addTimedListen(listen); + } + + std::optional + ScrobblingService::getUserScrobbler(UserId userId) + { + std::optional scrobbler; + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + if (const User::pointer user {User::find(session, userId)}) + scrobbler = user->getScrobbler(); + + return scrobbler; + } + + ScrobblingService::ArtistContainer + ScrobblingService::getRecentArtists(UserId userId, const std::vector& clusterIds, std::optional linkType, Range range) + { + ArtistContainer res; + + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + res = Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range); + return res; + } + + ScrobblingService::ReleaseContainer + ScrobblingService::getRecentReleases(UserId userId, const std::vector& clusterIds, Range range) + { + ReleaseContainer res; + + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range); + return res; + } + + ScrobblingService::TrackContainer + ScrobblingService::getRecentTracks(UserId userId, const std::vector& clusterIds, Range range) + { + TrackContainer res; + + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range); + return res; + } + + // Top + ScrobblingService::ArtistContainer + ScrobblingService::getTopArtists(UserId userId, const std::vector& clusterIds, std::optional linkType, Range range) + { + ArtistContainer res; + + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + res = Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range); + return res; + } + + ScrobblingService::ReleaseContainer + ScrobblingService::getTopReleases(UserId userId, const std::vector& clusterIds, Range range) + { + ReleaseContainer res; + + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range); + return res; + } + + ScrobblingService::TrackContainer + ScrobblingService::getTopTracks(UserId userId, const std::vector& clusterIds, Range range) + { + TrackContainer res; + + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range); + return res; + } + + void + ScrobblingService::star(UserId userId, ArtistId artistId) + { + star(userId, artistId); + } + + void + ScrobblingService::unstar(UserId userId, ArtistId artistId) + { + unstar(userId, artistId); + } + + bool + ScrobblingService::isStarred(UserId userId, ArtistId artistId) + { + return isStarred(userId, artistId); + } + + ScrobblingService::ArtistContainer + ScrobblingService::getStarredArtists(UserId userId, const std::vector& clusterIds, + std::optional linkType, + ArtistSortMethod sortMethod, + Range range) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return {}; + + Artist::FindParameters params; + params.setStarringUser(userId, *scrobbler); + params.setClusters(clusterIds); + params.setLinkType(linkType); + params.setSortMethod(sortMethod); + params.setRange(range); + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + return Artist::find(session, params); + } + + void + ScrobblingService::star(UserId userId, ReleaseId releaseId) + { + star(userId, releaseId); + } + + void + ScrobblingService::unstar(UserId userId, ReleaseId releaseId) + { + unstar(userId, releaseId); + } + + bool + ScrobblingService::isStarred(UserId userId, ReleaseId releaseId) + { + return isStarred(userId, releaseId); + } + + ScrobblingService::ReleaseContainer + ScrobblingService::getStarredReleases(UserId userId, const std::vector& clusterIds, Range range) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return {}; + + Release::FindParameters params; + params.setStarringUser(userId, *scrobbler); + params.setClusters(clusterIds); + params.setSortMethod(ReleaseSortMethod::StarredDateDesc); + params.setRange(range); + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + return Release::find(session, params); + } + + void + ScrobblingService::star(UserId userId, TrackId trackId) + { + star(userId, trackId); + } + + void + ScrobblingService::unstar(UserId userId, TrackId trackId) + { + unstar(userId, trackId); + } + + bool + ScrobblingService::isStarred(UserId userId, TrackId trackId) + { + return isStarred(userId, trackId); + } + + ScrobblingService::TrackContainer + ScrobblingService::getStarredTracks(UserId userId, const std::vector& clusterIds, Range range) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return {}; + + Track::FindParameters params; + params.setStarringUser(userId, *scrobbler); + params.setClusters(clusterIds); + params.setSortMethod(TrackSortMethod::StarredDateDesc); + params.setRange(range); + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + return Track::find(session, params); + } +} // ns Scrobbling + diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.hpp b/src/libs/services/scrobbling/impl/ScrobblingService.hpp new file mode 100644 index 00000000..ce9efaa5 --- /dev/null +++ b/src/libs/services/scrobbling/impl/ScrobblingService.hpp @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "services/scrobbling/IScrobblingService.hpp" +#include "IScrobbler.hpp" + +namespace Scrobbling +{ + class ScrobblingService : public IScrobblingService + { + public: + ScrobblingService(boost::asio::io_context& ioContext, Database::Db& db); + ~ScrobblingService(); + + private: + void listenStarted(const Listen& listen) override; + void listenFinished(const Listen& listen, std::optional duration) override; + void addTimedListen(const TimedListen& listen) override; + + ArtistContainer getRecentArtists(Database::UserId userId, + const std::vector& clusterIds, + std::optional linkType, + Database::Range range) override; + + ReleaseContainer getRecentReleases(Database::UserId userId, + const std::vector& clusterIds, + Database::Range range) override; + + TrackContainer getRecentTracks(Database::UserId userId, + const std::vector& clusterIds, + Database::Range range) override; + + ArtistContainer getTopArtists(Database::UserId userId, + const std::vector& clusterIds, + std::optional linkType, + Database::Range range) override; + + ReleaseContainer getTopReleases(Database::UserId userId, + const std::vector& clusterIds, + Database::Range range) override; + + TrackContainer getTopTracks(Database::UserId userId, + const std::vector& clusterIds, + Database::Range range) override; + + void star(Database::UserId userId, Database::ArtistId artistId) override; + void unstar(Database::UserId userId, Database::ArtistId artistId) override; + bool isStarred(Database::UserId userId, Database::ArtistId artistId) override; + ArtistContainer getStarredArtists(Database::UserId userId, + const std::vector& clusterIds, + std::optional linkType, + Database::ArtistSortMethod sortMethod, + Database::Range range) override; + + void star(Database::UserId userId, Database::ReleaseId releaseId) override; + void unstar(Database::UserId userId, Database::ReleaseId releaseId) override; + bool isStarred(Database::UserId userId, Database::ReleaseId artistId) override; + ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; + + void star(Database::UserId userId, Database::TrackId trackId) override; + void unstar(Database::UserId userId, Database::TrackId trackId) override; + bool isStarred(Database::UserId userId, Database::TrackId trackId) override; + TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; + + std::optional getUserScrobbler(Database::UserId userId); + + template + void star(Database::UserId userId, ObjIdType id); + template + void unstar(Database::UserId userId, ObjIdType id); + template + bool isStarred(Database::UserId userId, ObjIdType id); + + Database::Db& _db; + std::unordered_map> _scrobblers; + }; + +} // ns Scrobbling + diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp b/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp new file mode 100644 index 00000000..1b7ef03d --- /dev/null +++ b/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "services/database/Db.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" + +namespace Scrobbling +{ + using namespace Database; + + template + void + ScrobblingService::star(UserId userId, ObjIdType objId) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return; + + { + Session& session {_db.getTLSSession()}; + auto transaction {session.createUniqueTransaction()}; + + typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)}; + if (!starredObj) + { + const typename ObjType::pointer obj {ObjType::find(session, objId)}; + if (!obj) + return; + + const User::pointer user {User::find(session, userId)}; + if (!user) + return; + + starredObj = StarredObjType::create(session, obj, user, *scrobbler); + } + starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime()); + } + _scrobblers[*scrobbler]->onStarred(userId, objId); + } + + template + void + ScrobblingService::unstar(UserId userId, ObjIdType objId) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return; + + { + Session& session {_db.getTLSSession()}; + auto transaction {session.createUniqueTransaction()}; + + if (typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)}) + starredObj.remove(); + } + _scrobblers[*scrobbler]->onUnstarred(userId, objId); + } + + template + bool + ScrobblingService::isStarred(UserId userId, ObjIdType objId) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return false; + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + return StarredObjType::find(session, objId, userId, *scrobbler); + } + +} // ns Scrobbling + diff --git a/src/libs/scrobbling/impl/internal/InternalScrobbler.cpp b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp similarity index 53% rename from src/libs/scrobbling/impl/internal/InternalScrobbler.cpp rename to src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp index e5b23d51..2a7572ad 100644 --- a/src/libs/scrobbling/impl/internal/InternalScrobbler.cpp +++ b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp @@ -19,23 +19,20 @@ #include "InternalScrobbler.hpp" -#include "database/Db.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackList.hpp" -#include "database/User.hpp" -#include "utils/Logger.hpp" +#include "services/database/Db.hpp" +#include "services/database/Listen.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" +#include "services/database/Track.hpp" namespace Scrobbling { - static const std::string historyTracklistName {"__scrobbler_internal_history__"}; - InternalScrobbler::InternalScrobbler(Database::Db& db) - : _db {db} + : _db {db} {} void - InternalScrobbler::listenStarted(const Listen& /*listen*/) + InternalScrobbler::listenStarted(const Listen&) { // nothing to do } @@ -43,7 +40,7 @@ namespace Scrobbling void InternalScrobbler::listenFinished(const Listen& listen, std::optional duration) { - // record tracks that have been played for at least of few seconds... + // only record tracks that have been played for at least of few seconds... if (duration && *duration < std::chrono::seconds {5}) return; @@ -54,29 +51,21 @@ namespace Scrobbling InternalScrobbler::addTimedListen(const TimedListen& listen) { Database::Session& session {_db.getTLSSession()}; - auto transaction {session.createUniqueTransaction()}; - const Database::User::pointer user {Database::User::getById(session, listen.userId)}; + if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::Internal, listen.listenedAt)) + return; + + const Database::User::pointer user {Database::User::find(session, listen.userId)}; if (!user) return; - Database::TrackList::pointer tracklist {getListensTrackList(session, user)}; - if (!tracklist) - tracklist = Database::TrackList::create(session, historyTracklistName, Database::TrackList::Type::Internal, false, user); - - const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)}; + const Database::Track::pointer track {Database::Track::find(session, listen.trackId)}; if (!track) return; - Database::TrackListEntry::create(session, track, getListensTrackList(session, user), listen.listenedAt); + auto dbListen {Database::Listen::create(session, user, track, Database::Scrobbler::Internal, listen.listenedAt)}; + dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized); } - - Database::TrackList::pointer - InternalScrobbler::getListensTrackList(Database::Session& session, Database::ObjectPtr user) - { - return Database::TrackList::get(session, historyTracklistName, Database::TrackList::Type::Internal, user); - } - } // Scrobbling diff --git a/src/libs/scrobbling/impl/internal/InternalScrobbler.hpp b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.hpp similarity index 87% rename from src/libs/scrobbling/impl/internal/InternalScrobbler.hpp rename to src/libs/services/scrobbling/impl/internal/InternalScrobbler.hpp index 45c067da..1c4d7df4 100644 --- a/src/libs/scrobbling/impl/internal/InternalScrobbler.hpp +++ b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.hpp @@ -34,14 +34,12 @@ namespace Scrobbling InternalScrobbler(Database::Db& db); private: + // IScrobbler void listenStarted(const Listen& listen) override; void listenFinished(const Listen& listen, std::optional duration) override; - void addTimedListen(const TimedListen& listen) override; - Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user) override; - - Database::Db& _db; + Database::Db& _db; }; } // Scrobbling diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp new file mode 100644 index 00000000..62a57ce0 --- /dev/null +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "ListenBrainzScrobbler.hpp" + +#include "services/database/Db.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "utils/IConfig.hpp" +#include "utils/http/IClient.hpp" +#include "utils/Logger.hpp" +#include "utils/Service.hpp" +#include "Utils.hpp" + +#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] - " + +namespace +{ + bool + canBeScrobbled(Database::Session& session, Database::TrackId trackId, std::chrono::seconds duration) + { + auto transaction {session.createSharedTransaction()}; + + const Database::Track::pointer track {Database::Track::find(session, trackId)}; + if (!track) + return false; + + const bool res {duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2)}; + if (!res) + LOG(DEBUG) << "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast(track->getDuration()).count() << "s"; + + return res; + } +} + +namespace Scrobbling::ListenBrainz +{ + Scrobbler::Scrobbler(boost::asio::io_context& ioContext, Database::Db& db) + : _ioContext {ioContext} + , _db {db} + , _baseAPIUrl {Service::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org")} + , _client {Http::createClient(_ioContext, _baseAPIUrl)} + , _listensSynchronizer {_ioContext, db, *_client} + { + LOG(INFO) << "Starting ListenBrainz scrobbler... API endpoint = '" << _baseAPIUrl; + } + + Scrobbler::~Scrobbler() + { + LOG(INFO) << "Stopped ListenBrainz scrobbler!"; + } + + void + Scrobbler::listenStarted(const Listen& listen) + { + _listensSynchronizer.enqueListenNow(listen); + } + + void + Scrobbler::listenFinished(const Listen& listen, std::optional duration) + { + if (duration && !canBeScrobbled(_db.getTLSSession(), listen.trackId, *duration)) + return; + + const TimedListen timedListen {listen, Wt::WDateTime::currentDateTime()}; + _listensSynchronizer.enqueListen(timedListen); + } + + void + Scrobbler::addTimedListen(const TimedListen& timedListen) + { + _listensSynchronizer.enqueListen(timedListen); + } +} // namespace Scrobbling::ListenBrainz + diff --git a/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp similarity index 74% rename from src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp rename to src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp index 30e85bad..4a7dc77c 100644 --- a/src/libs/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp @@ -24,7 +24,6 @@ #include "IScrobbler.hpp" #include "ListensSynchronizer.hpp" -#include "SendQueue.hpp" namespace Database { @@ -50,16 +49,12 @@ namespace Scrobbling::ListenBrainz void listenStarted(const Listen& listen) override; void listenFinished(const Listen& listen, std::optional duration) override; void addTimedListen(const TimedListen& listen) override; - Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user) override; - // Submit listens - void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint); - std::optional createSubmitListenRequestData(const Listen& listen, const Wt::WDateTime& timePoint); - - boost::asio::io_context& _ioContext; - Database::Db& _db; - SendQueue _sendQueue; - ListensSynchronizer _listensSynchronizer; + boost::asio::io_context& _ioContext; + Database::Db& _db; + std::string _baseAPIUrl; + std::unique_ptr _client; + ListensSynchronizer _listensSynchronizer; }; } // Scrobbling::ListenBrainz diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp new file mode 100644 index 00000000..329a19bb --- /dev/null +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -0,0 +1,659 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "ListenBrainzScrobbler.hpp" + +#include +#include +#include +#include +#include + +#include "services/database/Artist.hpp" +#include "services/database/Db.hpp" +#include "services/database/Listen.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackList.hpp" +#include "services/database/User.hpp" +#include "services/scrobbling/Exception.hpp" +#include "utils/IConfig.hpp" +#include "utils/http/IClient.hpp" +#include "utils/Logger.hpp" +#include "utils/Service.hpp" + +#include "Utils.hpp" + +#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz Synchronizer] - " +#define LOG_EX(sev) LMS_LOG_EX(Module::SCROBBLING, sev) << "[listenbrainz Synchronizer] - " + +namespace +{ + using namespace Scrobbling::ListenBrainz; + + std::optional + listenToJsonPayload(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint) + { + auto transaction {session.createSharedTransaction()}; + + const Database::Track::pointer track {Database::Track::find(session, listen.trackId)}; + if (!track) + return std::nullopt; + + auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; + if (artists.empty()) + artists = track->getArtists({Database::TrackArtistLinkType::ReleaseArtist}); + + if (artists.empty()) + { + LOG(DEBUG) << "Track cannot be scrobbled since it does not have any artist"; + return std::nullopt; + } + + Wt::Json::Object additionalInfo; + additionalInfo["listening_from"] = "LMS"; + if (track->getRelease()) + { + if (auto MBID {track->getRelease()->getMBID()}) + additionalInfo["release_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}}; + } + + { + Wt::Json::Array artistMBIDs; + for (const Database::Artist::pointer& artist : artists) + { + if (auto MBID {artist->getMBID()}) + artistMBIDs.push_back(Wt::Json::Value {std::string {MBID->getAsString()}}); + } + + if (!artistMBIDs.empty()) + additionalInfo["artist_mbids"] = std::move(artistMBIDs); + } + + if (auto MBID {track->getTrackMBID()}) + additionalInfo["track_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}}; + + if (auto MBID {track->getRecordingMBID()}) + additionalInfo["recording_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}}; + + if (const std::optional trackNumber {track->getTrackNumber()}) + additionalInfo["tracknumber"] = Wt::Json::Value {static_cast(*trackNumber)}; + + Wt::Json::Object trackMetadata; + trackMetadata["additional_info"] = std::move(additionalInfo); + trackMetadata["artist_name"] = Wt::Json::Value {artists.front()->getName()}; + trackMetadata["track_name"] = Wt::Json::Value {track->getName()}; + if (track->getRelease()) + trackMetadata["release_name"] = Wt::Json::Value {track->getRelease()->getName()}; + + Wt::Json::Object payload; + payload["track_metadata"] = std::move(trackMetadata); + if (timePoint.isValid()) + payload["listened_at"] = Wt::Json::Value {static_cast(timePoint.toTime_t())}; + + return payload; + } + + std::string + listenToJsonString(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint, std::string_view listenType) + { + std::string res; + + std::optional payload {listenToJsonPayload(session, listen, timePoint)}; + if (!payload) + return res; + + Wt::Json::Object root; + root["listen_type"] = Wt::Json::Value {std::string {listenType}}; + root["payload"] = Wt::Json::Array {std::move(*payload)}; + + res = Wt::Json::serialize(root); + return res; + } + + std::string + parseValidateToken(std::string_view msgBody) + { + std::string listenBrainzUserName; + + Wt::Json::ParseError error; + Wt::Json::Object root; + if (!Wt::Json::parse(std::string {msgBody}, root, error)) + { + LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what(); + return listenBrainzUserName; + } + + if (!root.get("valid").orIfNull(false)) + { + LOG(INFO) << "Invalid listenbrainz user"; + return listenBrainzUserName; + } + + listenBrainzUserName = root.get("user_name").orIfNull(""); + return listenBrainzUserName; + } + + std::optional + parseListenCount(std::string_view msgBody) + { + try + { + Wt::Json::Object root; + Wt::Json::parse(std::string {msgBody}, root); + + const Wt::Json::Object& payload {static_cast(root.get("payload"))}; + return static_cast(payload.get("count")); + } + catch (const Wt::WException& e) + { + LOG(ERROR) << "Cannot parse listen count response: " << e.what(); + return std::nullopt; + } + } + + Database::TrackId + tryMatchListen(Database::Session& session, const Wt::Json::Object& metadata) + { + using namespace Database; + + //LOG(DEBUG) << "Trying to match track' " << Wt::Json::serialize(metadata) << "'"; + + // first try to get the associated track using MBIDs, and then fallback on names + if (metadata.type("additional_info") == Wt::Json::Type::Object) + { + const Wt::Json::Object& additionalInfo = metadata.get("additional_info"); + if (std::optional recordingMBID {UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""))}) + { + const auto tracks {Track::findByRecordingMBID(session, *recordingMBID)}; + // if duplicated files, do not record it (let the user correct its database) + if (tracks.size() == 1) + return tracks.front()->getId(); + } + } + + // these fields are mandatory + const std::string trackName {static_cast(metadata.get("track_name"))}; + const std::string releaseName {static_cast(metadata.get("release_name"))}; + + auto tracks {Track::findByNameAndReleaseName(session, trackName, releaseName)}; + if (tracks.results.size() > 1) + { + tracks.results.erase(std::remove_if(std::begin(tracks.results), std::end(tracks.results), + [&](const TrackId trackId) + { + const Track::pointer track {Track::find(session, trackId)}; + + if (std::string artistName {metadata.get("artist_name").orIfNull("")}; !artistName.empty()) + { + const auto& artists {track->getArtists({TrackArtistLinkType::Artist})}; + if (std::none_of(std::begin(artists), std::end(artists), [&](const Artist::pointer& artist) { return artist->getName() == artistName; })) + return true; + } + if (metadata.type("additional_info") == Wt::Json::Type::Object) + { + const Wt::Json::Object& additionalInfo = metadata.get("additional_info"); + if (track->getTrackNumber()) + { + int otherTrackNumber {additionalInfo.get("tracknumber").orIfNull(-1)}; + if (otherTrackNumber > 0 && static_cast(otherTrackNumber) != *track->getTrackNumber()) + return true; + } + + if (auto releaseMBID {track->getRelease()->getMBID()}) + { + if (std::optional otherReleaseMBID {UUID::fromString(additionalInfo.get("release_mbid").orIfNull(""))}) + { + if (otherReleaseMBID->getAsString() != releaseMBID->getAsString()) + return true; + } + } + } + + return false; + }), std::end(tracks.results)); + } + + if (tracks.results.size() == 1) + return tracks.results.front(); + + return {}; + } + + struct ParseGetListensResult + { + Wt::WDateTime oldestEntry; + std::size_t listenCount{}; + std::vector matchedListens; + }; + ParseGetListensResult + parseGetListens(Database::Session& session, std::string_view msgBody, Database::UserId userId) + { + ParseGetListensResult result; + + try + { + Wt::Json::Object root; + Wt::Json::parse(std::string {msgBody}, root); + + const Wt::Json::Object& payload = root.get("payload"); + const Wt::Json::Array& listens = payload.get("listens"); + + LOG(DEBUG) << "Got " << listens.size() << " listens"; + + if (listens.empty()) + return result; + + auto transaction {session.createSharedTransaction()}; + + for (const Wt::Json::Value& value : listens) + { + const Wt::Json::Object& listen = value; + const Wt::WDateTime listenedAt {Wt::WDateTime::fromTime_t(static_cast(listen.get("listened_at")))}; + const Wt::Json::Object& metadata = listen.get("track_metadata"); + + if (!listenedAt.isValid()) + { + LOG(ERROR) << "bad listened_at field!"; + continue; + } + + result.listenCount++; + if (!result.oldestEntry.isValid()) + result.oldestEntry = listenedAt; + else if (listenedAt < result.oldestEntry) + result.oldestEntry = listenedAt; + + if (Database::TrackId trackId {tryMatchListen(session, metadata)}; trackId.isValid()) + result.matchedListens.emplace_back(Scrobbling::TimedListen {{userId, trackId}, listenedAt}); + } + } + catch (const Wt::WException& error) + { + LOG(ERROR) << "Cannot parse 'get-listens' result: " << error.what(); + } + + return result; + } +} + +namespace Scrobbling::ListenBrainz +{ + ListensSynchronizer::ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client) + : _ioContext {ioContext} + , _db {db} + , _client {client} + , _maxSyncListenCount {Service::get()->getULong("listenbrainz-max-sync-listen-count", 1000)} + , _syncListensPeriod {Service::get()->getULong("listenbrainz-sync-listens-period-hours", 1)} + { + LOG(INFO) << "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours"; + + scheduleSync(std::chrono::seconds {30}); + } + + void + ListensSynchronizer::enqueListen(const TimedListen& listen) + { + assert(listen.listenedAt.isValid()); + enqueListen(listen, listen.listenedAt); + } + + void + ListensSynchronizer::enqueListenNow(const Listen& listen) + { + enqueListen(listen, {}); + } + + void + ListensSynchronizer::enqueListen(const Listen& listen, const Wt::WDateTime& timePoint) + { + Http::ClientPOSTRequestParameters request; + request.relativeUrl = "/1/submit-listens"; + + if (timePoint.isValid()) + { + const TimedListen timedListen {listen, timePoint}; + // We want the listen to be sent again later in case of failure, so we just save it as pending send + saveListen(timedListen, Database::ScrobblingState::PendingAdd); + + request.priority = Http::ClientRequestParameters::Priority::Normal; + request.onSuccessFunc = [=](std::string_view) + { + _strand.dispatch([=] + { + if (saveListen(timedListen, Database::ScrobblingState::Synchronized)) + { + UserContext& context {getUserContext(listen.userId)}; + if (context.listenCount) + (*context.listenCount)++; + } + }); + }; + // on failure, this listen will be sent during the next sync + } + else + { + // We want "listen now" to appear as soon as possible + request.priority = Http::ClientRequestParameters::Priority::High; + // don't retry on failure + } + + std::string bodyText {listenToJsonString(_db.getTLSSession(), listen, timePoint, timePoint.isValid() ? "single" : "playing_now")}; + if (bodyText.empty()) + { + LOG(DEBUG) << "Cannot convert listen to json: skipping"; + return; + } + + const std::optional listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), listen.userId)}; + if (!listenBrainzToken) + { + LOG(DEBUG) << "No listenbrainz token found: skipping"; + return; + } + + request.message.addBodyText(bodyText); + request.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()}); + request.message.addHeader("Content-Type", "application/json"); + _client.sendPOSTRequest(std::move(request)); + } + + bool + ListensSynchronizer::saveListen(const TimedListen& listen, Database::ScrobblingState scrobblingState) + { + using namespace Database; + + Session& session {_db.getTLSSession()}; + auto transaction {session.createUniqueTransaction()}; + + Database::Listen::pointer dbListen {Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::ListenBrainz, listen.listenedAt)}; + if (!dbListen) + { + const User::pointer user {User::find(session, listen.userId)}; + if (!user) + return false; + + const Track::pointer track {Track::find(session, listen.trackId)}; + if (!track) + return false; + + dbListen = Database::Listen::create(session, user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt); + dbListen.modify()->setScrobblingState(scrobblingState); + + LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString(); + + return true; + } + + if (dbListen->getScrobblingState() == scrobblingState) + return false; + + dbListen.modify()->setScrobblingState(scrobblingState); + return true; + } + + void + ListensSynchronizer::enquePendingListens() + { + std::vector pendingListens; + + { + Database::Session& session {_db.getTLSSession()}; + + auto transaction {session.createUniqueTransaction()}; + + Database::Listen::FindParameters params; + params.setScrobbler(Database::Scrobbler::ListenBrainz) + .setScrobblingState(Database::ScrobblingState::PendingAdd) + .setRange(Database::Range {0, 100}); // don't flood too much? + + const Database::RangeResults results {Database::Listen::find(session, params)}; + pendingListens.reserve(results.results.size()); + + for (Database::ListenId listenId : results.results) + { + const Database::Listen::pointer listen {Database::Listen::find(session, listenId)}; + + TimedListen timedListen; + timedListen.listenedAt = listen->getDateTime(); + timedListen.userId = listen->getUser()->getId(); + timedListen.trackId = listen->getTrack()->getId(); + + pendingListens.push_back(std::move(timedListen)); + } + } + + LOG(DEBUG) << "Queing " << pendingListens.size() << " pending listen"; + + for (const TimedListen& pendingListen : pendingListens) + enqueListen(pendingListen); + } + + ListensSynchronizer::UserContext& + ListensSynchronizer::getUserContext(Database::UserId userId) + { + assert(_strand.running_in_this_thread()); + + auto itContext {_userContexts.find(userId)}; + if (itContext == std::cend(_userContexts)) + { + auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)}; + itContext = itNewContext; + } + + return itContext->second; + } + + bool + ListensSynchronizer::isSyncing() const + { + return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry) + { + const auto& [userId, context] {contextEntry}; + return context.syncing; + }); + } + + void + ListensSynchronizer::scheduleSync(std::chrono::seconds fromNow) + { + if (_syncListensPeriod.count() == 0 || _maxSyncListenCount == 0) + return; + + LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds..."; + _syncTimer.expires_after(fromNow); + _syncTimer.async_wait(boost::asio::bind_executor(_strand, [this] (const boost::system::error_code& ec) + { + if (ec == boost::asio::error::operation_aborted) + { + LOG(DEBUG) << "getListens aborted"; + return; + } + else if (ec) + { + throw Exception {"GetListens timer failure: " + std::string {ec.message()} }; + } + + startSync(); + })); + } + + void + ListensSynchronizer::startSync() + { + LOG(DEBUG) << "Starting sync!"; + + assert(!isSyncing()); + + enquePendingListens(); + + Database::RangeResults userIds; + { + Database::Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setScrobbler(Database::Scrobbler::ListenBrainz)); + } + + for (const Database::UserId userId : userIds.results) + startSync(getUserContext(userId)); + + if (!isSyncing()) + scheduleSync(_syncListensPeriod); + } + + void + ListensSynchronizer::startSync(UserContext& context) + { + context.syncing = true; + context.listenBrainzUserName = ""; + context.maxDateTime = {}; + context.fetchedListenCount = 0; + context.matchedListenCount = 0; + context.importedListenCount = 0; + + enqueValidateToken(context); + } + + void + ListensSynchronizer::onSyncEnded(UserContext& context) + { + _strand.dispatch([this, &context] + { + LOG_EX(context.importedListenCount > 0 ? Severity::INFO : Severity::DEBUG) << "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount; + context.syncing = false; + + if (!isSyncing()) + scheduleSync(_syncListensPeriod); + }); + } + + void + ListensSynchronizer::enqueValidateToken(UserContext& context) + { + assert(context.listenBrainzUserName.empty()); + + const std::optional listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), context.userId)}; + if (!listenBrainzToken) + { + onSyncEnded(context); + return; + } + + Http::ClientGETRequestParameters request; + request.priority = Http::ClientRequestParameters::Priority::Low; + request.relativeUrl = "/1/validate-token"; + request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} }; + request.onSuccessFunc = [this, &context] (std::string_view msgBody) + { + context.listenBrainzUserName = parseValidateToken(msgBody); + if (context.listenBrainzUserName.empty()) + { + onSyncEnded(context); + return; + } + enqueGetListenCount(context); + }; + request.onFailureFunc = [this, &context] + { + onSyncEnded(context); + }; + + _client.sendGETRequest(std::move(request)); + } + + void + ListensSynchronizer::enqueGetListenCount(UserContext& context) + { + assert(!context.listenBrainzUserName.empty()); + + Http::ClientGETRequestParameters request; + request.relativeUrl = "/1/user/" + std::string {context.listenBrainzUserName} + "/listen-count"; + request.priority = Http::ClientRequestParameters::Priority::Low; + request.onSuccessFunc = [=, &context] (std::string_view msgBody) + { + const auto listenCount = parseListenCount(msgBody); + if (listenCount) + LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount; + + bool needSync {listenCount && (!context.listenCount || *context.listenCount != *listenCount)}; + context.listenCount = listenCount; + + if (!needSync) + { + onSyncEnded(context); + return; + } + + context.maxDateTime = Wt::WDateTime::currentDateTime(); + enqueGetListens(context); + }; + request.onFailureFunc = [this, &context] + { + onSyncEnded(context); + }; + + _client.sendGETRequest(std::move(request)); + } + + void + ListensSynchronizer::enqueGetListens(UserContext& context) + { + assert(!context.listenBrainzUserName.empty()); + + Http::ClientGETRequestParameters request; + request.relativeUrl = "/1/user/" + context.listenBrainzUserName + "/listens?max_ts=" + std::to_string(context.maxDateTime.toTime_t()); + request.priority = Http::ClientRequestParameters::Priority::Low; + request.onSuccessFunc = [=, &context] (std::string_view msgBody) + { + processGetListensResponse(msgBody, context); + if (context.fetchedListenCount >= _maxSyncListenCount || !context.maxDateTime.isValid()) + { + onSyncEnded(context); + return; + } + + enqueGetListens(context); + }; + request.onFailureFunc = [=, &context] + { + onSyncEnded(context); + }; + + _client.sendGETRequest(std::move(request)); + } + + void + ListensSynchronizer::processGetListensResponse(std::string_view msgBody, UserContext& context) + { + Database::Session& session {_db.getTLSSession()}; + + const ParseGetListensResult parseResult {parseGetListens(session, msgBody, context.userId)}; + + context.fetchedListenCount += parseResult.listenCount; + context.matchedListenCount += parseResult.matchedListens.size(); + context.maxDateTime = parseResult.oldestEntry; + + for (const TimedListen& listen : parseResult.matchedListens) + { + if (saveListen(listen, Database::ScrobblingState::Synchronized)) + context.importedListenCount++; + } + } +} // namespace Scrobbling::ListenBrainz diff --git a/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp similarity index 70% rename from src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp rename to src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp index e2914dee..45fef878 100644 --- a/src/libs/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp @@ -24,11 +24,11 @@ #include #include #include -#include -#include "database/Types.hpp" -#include "scrobbling/Listen.hpp" -#include "SendQueue.hpp" +#include "services/database/Types.hpp" +#include "services/database/ListenId.hpp" +#include "services/database/UserId.hpp" +#include "services/scrobbling/Listen.hpp" namespace Database { @@ -38,16 +38,27 @@ namespace Database class User; } +namespace Http +{ + class IClient; +} + namespace Scrobbling::ListenBrainz { class ListensSynchronizer { public: - ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, SendQueue& sendQueue); + ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client); - void saveListen(const TimedListen& listen); + void enqueListen(const TimedListen& listen); + void enqueListenNow(const Listen& listen); private: + void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint); + bool saveListen(const TimedListen& listen, Database::ScrobblingState scrobblinState); + + void enquePendingListens(); + struct UserContext { UserContext(Database::UserId id) : userId {id} {} @@ -58,10 +69,10 @@ namespace Scrobbling::ListenBrainz UserContext& operator=(UserContext&&) = delete; const Database::UserId userId; - bool fetching {}; + bool syncing {}; std::optional listenCount {}; - // resetted at each fetch + // resetted at each sync std::string listenBrainzUserName; // need to be resolved first Wt::WDateTime maxDateTime; std::size_t fetchedListenCount{}; @@ -70,23 +81,21 @@ namespace Scrobbling::ListenBrainz }; UserContext& getUserContext(Database::UserId userId); - bool isFetching() const; - void scheduleGetListens(std::chrono::seconds fromNow); - void startGetListens(); - void startGetListens(UserContext& context); - void onGetListensEnded(UserContext& context); + bool isSyncing() const; + void scheduleSync(std::chrono::seconds fromNow); + void startSync(); + void startSync(UserContext& context); + void onSyncEnded(UserContext& context); void enqueValidateToken(UserContext& context); void enqueGetListenCount(UserContext& context); void enqueGetListens(UserContext& context); - std::optional createValidateTokenRequestData(Database::UserId userId); - std::optional createGetListensRequestData(std::string_view listenBrainzUserName, const Wt::WDateTime& maxDateTime); - void processGetListensResponse(std::string_view body, UserContext& context); + void processGetListensResponse(std::string_view body, UserContext& context); boost::asio::io_context& _ioContext; boost::asio::io_context::strand _strand {_ioContext}; Database::Db& _db; - SendQueue& _sendQueue; - boost::asio::steady_timer _getListensTimer {_ioContext}; + boost::asio::steady_timer _syncTimer {_ioContext}; + Http::IClient& _client; std::unordered_map _userContexts; diff --git a/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp b/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp new file mode 100644 index 00000000..4e9acdf6 --- /dev/null +++ b/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Utils.hpp" + +#include "services/database/Session.hpp" +#include "services/database/User.hpp" + +namespace Scrobbling::ListenBrainz::Utils +{ + std::optional + getListenBrainzToken(Database::Session& session, Database::UserId userId) + { + auto transaction {session.createSharedTransaction()}; + + const Database::User::pointer user {Database::User::find(session, userId)}; + if (!user) + return std::nullopt; + + return user->getListenBrainzToken(); + } +} diff --git a/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp b/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp new file mode 100644 index 00000000..47bd2a0e --- /dev/null +++ b/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "utils/UUID.hpp" + +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +namespace Database +{ + class Session; + class User; +} + +namespace Scrobbling::ListenBrainz::Utils +{ + std::optional getListenBrainzToken(Database::Session& session, Database::UserId userId); +} diff --git a/src/libs/scrobbling/include/scrobbling/Exception.hpp b/src/libs/services/scrobbling/include/services/scrobbling/Exception.hpp similarity index 100% rename from src/libs/scrobbling/include/scrobbling/Exception.hpp rename to src/libs/services/scrobbling/include/services/scrobbling/Exception.hpp diff --git a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp new file mode 100644 index 00000000..18b00129 --- /dev/null +++ b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include + +#include "services/scrobbling/Listen.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/Types.hpp" + +namespace Database +{ + class Db; +} + +namespace Scrobbling +{ + + class IScrobblingService + { + public: + virtual ~IScrobblingService() = default; + + // Scrobbling + virtual void listenStarted(const Listen& listen) = 0; + virtual void listenFinished(const Listen& listen, std::optional playedDuration = std::nullopt) = 0; + + virtual void addTimedListen(const TimedListen& listen) = 0; + + // Stats + using ArtistContainer = Database::RangeResults; + using ReleaseContainer = Database::RangeResults; + using TrackContainer = Database::RangeResults; + // From most recent to oldest + virtual ArtistContainer getRecentArtists(Database::UserId userId, + const std::vector& clusterIds, + std::optional linkType, + Database::Range range) = 0; + + virtual ReleaseContainer getRecentReleases(Database::UserId userId, + const std::vector& clusterIds, + Database::Range range) = 0; + + virtual TrackContainer getRecentTracks(Database::UserId userId, + const std::vector& clusterIds, + Database::Range range) = 0; + + // Top + virtual ArtistContainer getTopArtists(Database::UserId userId, + const std::vector& clusterIds, + std::optional linkType, + Database::Range) = 0; + + virtual ReleaseContainer getTopReleases(Database::UserId userId, + const std::vector& clusterIds, + Database::Range range) = 0; + + virtual TrackContainer getTopTracks(Database::UserId userId, + const std::vector& clusterIds, + Database::Range range) = 0; + + // Star + virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual ArtistContainer getStarredArtists(Database::UserId userId, + const std::vector& clusterIds, + std::optional linkType, + Database::ArtistSortMethod sortMethod, + Database::Range range) = 0; + + virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0; + virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0; + virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0; + virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + + virtual void star(Database::UserId userId, Database::TrackId trackId) = 0; + virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0; + virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0; + virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + }; + + std::unique_ptr createScrobblingService(boost::asio::io_service& ioService, Database::Db& db); + +} // ns Scrobbling + diff --git a/src/libs/scrobbling/include/scrobbling/Listen.hpp b/src/libs/services/scrobbling/include/services/scrobbling/Listen.hpp similarity index 92% rename from src/libs/scrobbling/include/scrobbling/Listen.hpp rename to src/libs/services/scrobbling/include/services/scrobbling/Listen.hpp index ae80dbac..a42ab649 100644 --- a/src/libs/scrobbling/include/scrobbling/Listen.hpp +++ b/src/libs/services/scrobbling/include/services/scrobbling/Listen.hpp @@ -21,7 +21,8 @@ #include -#include "database/Types.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/UserId.hpp" namespace Scrobbling { diff --git a/src/libs/som/CMakeLists.txt b/src/libs/som/CMakeLists.txt index 4c967429..938e835a 100644 --- a/src/libs/som/CMakeLists.txt +++ b/src/libs/som/CMakeLists.txt @@ -1,4 +1,3 @@ - add_library(lmssom SHARED impl/DataNormalizer.cpp impl/Network.cpp @@ -19,3 +18,7 @@ target_link_libraries(lmssom PUBLIC set_property(TARGET lmssom PROPERTY POSITION_INDEPENDENT_CODE ON) install(TARGETS lmssom DESTINATION lib) + +if(BUILD_TESTING) + add_subdirectory(test) +endif() diff --git a/src/test/som/CMakeLists.txt b/src/libs/som/test/CMakeLists.txt similarity index 64% rename from src/test/som/CMakeLists.txt rename to src/libs/som/test/CMakeLists.txt index 4c409913..7781e77d 100644 --- a/src/test/som/CMakeLists.txt +++ b/src/libs/som/test/CMakeLists.txt @@ -9,5 +9,7 @@ target_link_libraries(test-som PRIVATE GTest::GTest ) -gtest_discover_tests(test-som) +if (NOT CMAKE_CROSSCOMPILING) + gtest_discover_tests(test-som) +endif() diff --git a/src/test/som/SomTest.cpp b/src/libs/som/test/SomTest.cpp similarity index 99% rename from src/test/som/SomTest.cpp rename to src/libs/som/test/SomTest.cpp index 74186d85..68a71285 100644 --- a/src/test/som/SomTest.cpp +++ b/src/libs/som/test/SomTest.cpp @@ -51,7 +51,6 @@ TEST(som, Matrix) TEST(som, InputVector) { - { InputVector test1 {2}; test1[0] = 0; diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 98093601..d4f4b985 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -19,11 +19,11 @@ target_include_directories(lmssubsonic PRIVATE target_link_libraries(lmssubsonic PRIVATE lmsauth lmsav - lmscover lmsdatabase lmsrecommendation lmsscanner lmsscrobbling + lmsservice-cover lmsutils std::filesystem ) diff --git a/src/libs/subsonic/impl/ParameterParsing.hpp b/src/libs/subsonic/impl/ParameterParsing.hpp index bb8a91d3..2773a08e 100644 --- a/src/libs/subsonic/impl/ParameterParsing.hpp +++ b/src/libs/subsonic/impl/ParameterParsing.hpp @@ -20,7 +20,7 @@ #include -#include "database/Types.hpp" +#include "services/database/Types.hpp" #include "utils/String.hpp" #include "SubsonicResponse.hpp" diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index 9d73bcb4..01930783 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -23,7 +23,7 @@ #include -#include "database/Types.hpp" +#include "services/database/UserId.hpp" #include "ClientInfo.hpp" #include "ProtocolVersion.hpp" diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index dee3c082..d9a48d67 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -19,7 +19,7 @@ #include "Scan.hpp" -#include "scanner/IScanner.hpp" +#include "services/scanner/IScannerService.hpp" #include "utils/Service.hpp" namespace API::Subsonic::Scan @@ -32,10 +32,10 @@ namespace API::Subsonic::Scan { Response::Node statusResponse; - const IScanner::Status scanStatus {Service::get()->getStatus()}; + const IScannerService::Status scanStatus {Service::get()->getStatus()}; - statusResponse.setAttribute("scanning", scanStatus.currentState == IScanner::State::InProgress); - if (scanStatus.currentState == IScanner::State::InProgress) + statusResponse.setAttribute("scanning", scanStatus.currentState == IScannerService::State::InProgress); + if (scanStatus.currentState == IScannerService::State::InProgress) { std::size_t count{}; @@ -61,7 +61,7 @@ namespace API::Subsonic::Scan Response handleStartScan(RequestContext& context) { - Service::get()->requestImmediateScan(false); + Service::get()->requestImmediateScan(false); Response response {Response::createOkResponse(context.serverProtocolVersion)}; response.addNode("scanStatus", createStatusResponseNode()); diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/Stream.cpp index d3d181ed..8089779c 100644 --- a/src/libs/subsonic/impl/Stream.cpp +++ b/src/libs/subsonic/impl/Stream.cpp @@ -22,9 +22,9 @@ #include "av/TranscodeParameters.hpp" #include "av/TranscodeResourceHandlerCreator.hpp" #include "av/Types.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/User.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" #include "utils/IResourceHandler.hpp" #include "utils/Logger.hpp" #include "utils/FileResourceHandlerCreator.hpp" @@ -74,7 +74,7 @@ getStreamParameters(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; { - auto track {Track::getById(context.dbSession, id)}; + auto track {Track::find(context.dbSession, id)}; if (!track) throw RequestedDataNotFoundError {}; @@ -82,7 +82,7 @@ getStreamParameters(RequestContext& context) } { - const User::pointer user {User::getById(context.dbSession, context.userId)}; + const User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -124,7 +124,7 @@ handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Ht { auto transaction {context.dbSession.createSharedTransaction()}; - auto track {Track::getById(context.dbSession, id)}; + auto track {Track::find(context.dbSession, id)}; if (!track) throw RequestedDataNotFoundError {}; diff --git a/src/libs/subsonic/impl/SubsonicId.hpp b/src/libs/subsonic/impl/SubsonicId.hpp index 0ac2a16a..32b45715 100644 --- a/src/libs/subsonic/impl/SubsonicId.hpp +++ b/src/libs/subsonic/impl/SubsonicId.hpp @@ -19,7 +19,10 @@ #pragma once -#include "database/Types.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/TrackListId.hpp" #include "utils/String.hpp" namespace API::Subsonic diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 403e4ba4..0f4f627c 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -26,20 +26,20 @@ #include -#include "auth/IPasswordService.hpp" -#include "auth/IEnvService.hpp" -#include "cover/ICoverArtGrabber.hpp" -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackBookmark.hpp" -#include "database/TrackList.hpp" -#include "database/User.hpp" -#include "recommendation/IEngine.hpp" -#include "scrobbling/IScrobbling.hpp" +#include "services/auth/IPasswordService.hpp" +#include "services/auth/IEnvService.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Db.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackBookmark.hpp" +#include "services/database/TrackList.hpp" +#include "services/database/User.hpp" +#include "services/recommendation/IRecommendationService.hpp" +#include "services/scrobbling/IScrobblingService.hpp" +#include "services/cover/ICoverService.hpp" #include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Random.hpp" @@ -163,7 +163,7 @@ static void checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username) { - User::pointer currentUser {User::getById(context.dbSession, context.userId)}; + User::pointer currentUser {User::find(context.dbSession, context.userId)}; if (!currentUser) throw RequestedDataNotFoundError {}; @@ -177,7 +177,7 @@ checkUserTypeIsAllowed(RequestContext& context, EnumSet allo { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer currentUser {User::getById(context.dbSession, context.userId)}; + User::pointer currentUser {User::find(context.dbSession, context.userId)}; if (!currentUser) throw RequestedDataNotFoundError {}; @@ -322,11 +322,11 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: trackResponse.setAttribute("type", "music"); trackResponse.setAttribute("created", dateTimeToCreatedString(track->getLastWritten())); - if (user->hasStarredTrack(track)) + if (Service::get()->isStarred(user->getId(), track->getId())) trackResponse.setAttribute("starred", reportedStarredDate); // Report the first GENRE for this track - ClusterType::pointer clusterType {ClusterType::getByName(dbSession, genreClusterName)}; + ClusterType::pointer clusterType {ClusterType::find(dbSession, genreClusterName)}; if (clusterType) { auto clusters {track->getClusterGroups({clusterType}, 1)}; @@ -404,7 +404,7 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const if (id3) { // Report the first GENRE for this track - ClusterType::pointer clusterType {ClusterType::getByName(dbSession, genreClusterName)}; + ClusterType::pointer clusterType {ClusterType::find(dbSession, genreClusterName)}; if (clusterType) { auto clusters {release->getClusterGroups({clusterType}, 1)}; @@ -413,7 +413,7 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const } } - if (user->hasStarredRelease(release)) + if (Service::get()->isStarred(user->getId(), release->getId())) albumNode.setAttribute("starred", reportedStarredDate); return albumNode; @@ -431,7 +431,7 @@ artistToResponseNode(const User::pointer& user, const Artist::pointer& artist, b if (id3) artistNode.setAttribute("albumCount", artist->getReleaseCount()); - if (user->hasStarredArtist(artist)) + if (Service::get()->isStarred(user->getId(), artist->getId())) artistNode.setAttribute("starred", reportedStarredDate); return artistNode; @@ -499,14 +499,14 @@ handleChangePassword(RequestContext& context) checkUserIsMySelfOrAdmin(context, username); - User::pointer user {User::getByLoginName(context.dbSession, username)}; + User::pointer user {User::find(context.dbSession, username)}; if (!user) throw UserNotAuthorizedError {}; userId = user->getId(); } - Service::get()->setPassword(context.dbSession, userId, password); + Service::get()->setPassword(userId, password); } catch (const Auth::PasswordMustMatchLoginNameException&) { @@ -539,14 +539,14 @@ handleCreatePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; TrackList::pointer tracklist; if (id) { - tracklist = TrackList::getById(context.dbSession, *id); + tracklist = TrackList::find(context.dbSession, *id); if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) @@ -564,11 +564,11 @@ handleCreatePlaylistRequest(RequestContext& context) for (const TrackId trackId : trackIds) { - Track::pointer track {Track::getById(context.dbSession, trackId)}; + Track::pointer track {Track::find(context.dbSession, trackId)}; if (!track) continue; - TrackListEntry::create(context.dbSession, track, tracklist ); + TrackListEntry::create(context.dbSession, track, tracklist); } return Response::createOkResponse(context.serverProtocolVersion); @@ -586,7 +586,7 @@ handleCreateUserRequest(RequestContext& context) { auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, username)}; + User::pointer user {User::find(context.dbSession, username)}; if (user) throw UserAlreadyExistsGenericError {}; @@ -597,14 +597,14 @@ handleCreateUserRequest(RequestContext& context) auto removeCreatedUser {[&]() { auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getById(context.dbSession, userId)}; + User::pointer user {User::find(context.dbSession, userId)}; if (user) user.remove(); }}; try { - Service::get()->setPassword(context.dbSession, userId, password); + Service::get()->setPassword(userId, password); } catch (const Auth::PasswordMustMatchLoginNameException&) { @@ -633,11 +633,11 @@ handleDeletePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - TrackList::pointer tracklist {TrackList::getById(context.dbSession, id)}; + TrackList::pointer tracklist {TrackList::find(context.dbSession, id)}; if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) @@ -658,7 +658,7 @@ handleDeleteUserRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, username)}; + User::pointer user {User::find(context.dbSession, username)}; if (!user) throw RequestedDataNotFoundError {}; @@ -695,17 +695,20 @@ handleGetRandomSongsRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - auto tracks {Track::getAllRandom(context.dbSession, {}, size)}; + const auto trackIds {Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size}))}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& randomSongsNode {response.createNode("randomSongs")}; - for (const Track::pointer& track : tracks) + for (const TrackId trackId : trackIds.results) + { + const Track::pointer track {Track::find(context.dbSession, trackId)}; randomSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); + } return response; } @@ -723,69 +726,86 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) const Range range {offset, size}; - std::vector releases; + RangeResults releases; + Scrobbling::IScrobblingService& scrobbling {*Service::get()}; auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; if (type == "alphabeticalByName") { - releases = Release::getAll(context.dbSession, range); + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); + + releases = Release::find(context.dbSession, params); } else if (type == "alphabeticalByArtist") { - releases = Release::getAllOrderedByArtist(context.dbSession, offset, size); + releases = Release::findOrderedByArtist(context.dbSession, range); } else if (type == "byGenre") { // Mandatory param - std::string genre {getMandatoryParameterAs(context.parameters, "genre")}; + const std::string genre {getMandatoryParameterAs(context.parameters, "genre")}; - ClusterType::pointer clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; - if (clusterType) + if (const ClusterType::pointer clusterType {ClusterType::find(context.dbSession, genreClusterName)}) { - Cluster::pointer cluster {clusterType->getCluster(genre)}; - if (cluster) + if (const Cluster::pointer cluster {clusterType->getCluster(genre)}) { - bool more; - releases = Release::getByFilter(context.dbSession, {cluster->getId()}, {}, range, more); + Release::FindParameters params; + params.setClusters({cluster->getId()}); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); + + releases = Release::find(context.dbSession, params); } } } else if (type == "byYear") { - int fromYear {getMandatoryParameterAs(context.parameters, "fromYear")}; - int toYear {getMandatoryParameterAs(context.parameters, "toYear")}; + const int fromYear {getMandatoryParameterAs(context.parameters, "fromYear")}; + const int toYear {getMandatoryParameterAs(context.parameters, "toYear")}; - releases = Release::getByYear(context.dbSession, fromYear, toYear, range); + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::Date); + params.setRange(range); + params.setDateRange(DateRange::fromYearRange(fromYear, toYear)); + + releases = Release::find(context.dbSession, params); } else if (type == "frequent") { - bool moreResults {}; - releases = Service::get()->getTopReleases(context.dbSession, user, {}, range, moreResults); + releases = scrobbling.getTopReleases(context.userId, {}, range); } else if (type == "newest") { - bool moreResults {}; - releases = Release::getLastWritten(context.dbSession, std::nullopt, {}, range, moreResults); + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::LastWritten); + params.setRange(range); + + releases = Release::find(context.dbSession, params); } else if (type == "random") { // Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums - releases = Release::getAllRandom(context.dbSession, {}, size); + // (no seed provided by subsonic, ot it would require to store some kind of context for each user/client when iterating over the random albums) + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::Random); + params.setRange({0, size}); + + releases = Release::find(context.dbSession, params); } else if (type == "recent") { - bool moreResults {}; - releases = Service::get()->getRecentReleases(context.dbSession, user, {}, range, moreResults); + releases = scrobbling.getRecentReleases(context.userId, {}, range); } else if (type == "starred") { - bool moreResults {}; - releases = Release::getStarred(context.dbSession, user, {}, range, moreResults); + releases = scrobbling.getStarredReleases(context.userId, {}, range); } else throw NotImplementedGenericError {}; @@ -793,8 +813,11 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")}; - for (const Release::pointer& release : releases) + for (const ReleaseId releaseId : releases.results) + { + const Release::pointer release {Release::find(context.dbSession, releaseId)}; albumListNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); + } return response; } @@ -822,11 +845,11 @@ handleGetAlbumRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Release::pointer release {Release::getById(context.dbSession, id)}; + Release::pointer release {Release::find(context.dbSession, id)}; if (!release) throw RequestedDataNotFoundError {}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -851,11 +874,11 @@ handleGetArtistRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Artist::pointer artist {Artist::getById(context.dbSession, id)}; + const Artist::pointer artist {Artist::find(context.dbSession, id)}; if (!artist) throw RequestedDataNotFoundError {}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -887,7 +910,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) { auto transaction {context.dbSession.createSharedTransaction()}; - Artist::pointer artist {Artist::getById(context.dbSession, id)}; + const Artist::pointer artist {Artist::find(context.dbSession, id)}; if (!artist) throw RequestedDataNotFoundError {}; @@ -896,21 +919,18 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString()); } - auto similarArtistsId {Service::get()->getSimilarArtists(context.dbSession, - id, - {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, - count)}; + auto similarArtistsId {Service::get()->getSimilarArtists(id, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, count)}; { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - for ( const ArtistId similarArtistId : similarArtistsId ) + for (const ArtistId similarArtistId : similarArtistsId) { - Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; + const Artist::pointer similarArtist {Artist::find(context.dbSession, similarArtistId)}; if (similarArtist) artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(user, similarArtist, id3)); } @@ -933,51 +953,6 @@ handleGetArtistInfo2Request(RequestContext& context) return handleGetArtistInfoRequestCommon(context, true /* id3 */); } -static -Response -handleGetArtistsRequest(RequestContext& context) -{ - Response response {Response::createOkResponse(context.serverProtocolVersion)}; - - Response::Node& artistsNode {response.createNode("artists")}; - artistsNode.setAttribute("ignoredArticles", ""); - artistsNode.setAttribute("lastModified", reportedDummyDateULong); - - Response::Node& indexNode {artistsNode.createArrayChild("index")}; - indexNode.setAttribute("name", "?"); - - auto transaction {context.dbSession.createSharedTransaction()}; - - User::pointer user {User::getById(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - std::optional linkType; - switch (user->getSubsonicArtistListMode()) - { - case User::SubsonicArtistListMode::AllArtists: - break; - case User::SubsonicArtistListMode::ReleaseArtists: - linkType = TrackArtistLinkType::ReleaseArtist; - break; - case User::SubsonicArtistListMode::TrackArtists: - linkType = TrackArtistLinkType::Artist; - break; - } - - bool more {}; - const std::vector artists {Artist::getByFilter(context.dbSession, - {}, - {}, - linkType, - Artist::SortMethod::BySortName, - std::nullopt, more)}; - for (const Artist::pointer& artist : artists) - indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */)); - - return response; -} - static Response handleGetMusicDirectoryRequest(RequestContext& context) @@ -996,7 +971,7 @@ handleGetMusicDirectoryRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1005,16 +980,18 @@ handleGetMusicDirectoryRequest(RequestContext& context) directoryNode.setAttribute("id", idToString(RootId {})); directoryNode.setAttribute("name", "Music"); - bool moreResults{}; - auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName, std::nullopt, moreResults)}; - for (const Artist::pointer& artist : artists) + auto artistIds {Artist::find(context.dbSession, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))}; + for (const ArtistId artistId : artistIds.results) + { + const Artist::pointer artist {Artist::find(context.dbSession, artistId)}; directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */)); + } } else if (artistId) { directoryNode.setAttribute("id", idToString(*artistId)); - auto artist {Artist::getById(context.dbSession, *artistId)}; + auto artist {Artist::find(context.dbSession, *artistId)}; if (!artist) throw RequestedDataNotFoundError {}; @@ -1028,7 +1005,7 @@ handleGetMusicDirectoryRequest(RequestContext& context) { directoryNode.setAttribute("id", idToString(*releaseId)); - auto release {Release::getById(context.dbSession, *releaseId)}; + auto release {Release::find(context.dbSession, *releaseId)}; if (!release) throw RequestedDataNotFoundError {}; @@ -1058,6 +1035,88 @@ handleGetMusicFoldersRequest(RequestContext& context) return response; } +static +Response +handleGetArtistsRequestCommon(RequestContext& context, bool id3) +{ + Response response {Response::createOkResponse(context.serverProtocolVersion)}; + + Response::Node& artistsNode {response.createNode(id3 ? "artists" : "indexes")}; + artistsNode.setAttribute("ignoredArticles", ""); + artistsNode.setAttribute("lastModified", reportedDummyDateULong); + + auto transaction {context.dbSession.createSharedTransaction()}; + + User::pointer user {User::find(context.dbSession, context.userId)}; + if (!user) + throw UserNotAuthorizedError {}; + + Artist::FindParameters parameters; + parameters.setSortMethod(ArtistSortMethod::BySortName); + switch (user->getSubsonicArtistListMode()) + { + case SubsonicArtistListMode::AllArtists: + break; + case SubsonicArtistListMode::ReleaseArtists: + parameters.setLinkType(TrackArtistLinkType::ReleaseArtist); + break; + case SubsonicArtistListMode::TrackArtists: + parameters.setLinkType(TrackArtistLinkType::Artist); + break; + } + + Response::Node* currentIndexNode {}; + char currentIndex{}; + + Response::Node* unknownIndexNode {}; + auto getOrCreateUnknownIndexNode {[&] + { + if (!unknownIndexNode) + { + unknownIndexNode = &artistsNode.createArrayChild("index"); + unknownIndexNode->setAttribute("name", "?"); + } + + return unknownIndexNode; + }}; + + auto getOrCreateIndexNode {[&](char first) + { + if (!currentIndexNode || currentIndex != first) + { + currentIndexNode = &artistsNode.createArrayChild("index"); + currentIndexNode->setAttribute("name", std::string {first}); + currentIndex = first; + } + + return currentIndexNode; + }}; + + const RangeResults artists {Artist::find(context.dbSession, parameters)}; + for (const ArtistId artistId : artists.results) + { + const Artist::pointer artist {Artist::find(context.dbSession, artistId)}; + const std::string& sortName {artist->getSortName()}; + + Response::Node* indexNode{}; + if (sortName.empty() || !std::isalpha(sortName[0])) + indexNode = getOrCreateUnknownIndexNode(); + else + indexNode = getOrCreateIndexNode(std::toupper(sortName[0])); + + indexNode->addArrayChild("artist", artistToResponseNode(user, artist, id3)); + } + + return response; +} + +static +Response +handleGetIndexesRequest(RequestContext& context) +{ + return handleGetArtistsRequestCommon(context, false /* no id3 */); +} + static Response handleGetGenresRequest(RequestContext& context) @@ -1068,7 +1127,7 @@ handleGetGenresRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - const ClusterType::pointer clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; + const ClusterType::pointer clusterType {ClusterType::find(context.dbSession, genreClusterName)}; if (clusterType) { const auto clusters {clusterType->getClusters()}; @@ -1082,47 +1141,9 @@ handleGetGenresRequest(RequestContext& context) static Response -handleGetIndexesRequest(RequestContext& context) +handleGetArtistsRequest(RequestContext& context) { - Response response {Response::createOkResponse(context.serverProtocolVersion)}; - - Response::Node& artistsNode {response.createNode("indexes")}; - artistsNode.setAttribute("ignoredArticles", ""); - artistsNode.setAttribute("lastModified", reportedDummyDateULong); - - Response::Node& indexNode {artistsNode.createArrayChild("index")}; - indexNode.setAttribute("name", "?"); - - auto transaction {context.dbSession.createSharedTransaction()}; - - User::pointer user {User::getById(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - std::optional linkType; - switch (user->getSubsonicArtistListMode()) - { - case User::SubsonicArtistListMode::AllArtists: - break; - case User::SubsonicArtistListMode::ReleaseArtists: - linkType = TrackArtistLinkType::ReleaseArtist; - break; - case User::SubsonicArtistListMode::TrackArtists: - linkType = TrackArtistLinkType::Artist; - break; - } - - bool more {}; - const std::vector artists {Artist::getByFilter(context.dbSession, - {}, - {}, - linkType, - Artist::SortMethod::BySortName, - std::nullopt, more)}; - for (const Artist::pointer& artist : artists) - indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */)); - - return response; + return handleGetArtistsRequestCommon(context, true /* id3 */); } static @@ -1135,18 +1156,15 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) // Optional params std::size_t count {getParameterAs(context.parameters, "count").value_or(50)}; - const auto similarArtistIds {Service::get()->getSimilarArtists(context.dbSession, - artistId, - {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, - 5)}; + const auto similarArtistIds {Service::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, 5)}; auto transaction {context.dbSession.createSharedTransaction()}; - const Artist::pointer artist {Artist::getById(context.dbSession, artistId)}; + const Artist::pointer artist {Artist::find(context.dbSession, artistId)}; if (!artist) throw RequestedDataNotFoundError {}; - const User::pointer user {User::getById(context.dbSession, context.userId)}; + const User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1154,7 +1172,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) auto tracks {artist->getRandomTracks(count / 2)}; for (const ArtistId similarArtistId : similarArtistIds) { - const Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; + const Artist::pointer similarArtist {Artist::find(context.dbSession, similarArtistId)}; if (!similarArtist) continue; @@ -1195,31 +1213,30 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")}; + Scrobbling::IScrobblingService& scrobbling {*Service::get()}; + + for (const ArtistId artistId : scrobbling.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range {}).results) { - bool moreResults {}; - const auto artists {Artist::getStarred(context.dbSession, user, {}, std::nullopt, Artist::SortMethod::BySortName, std::nullopt, moreResults)}; - for (const Artist::pointer& artist : artists) + if (auto artist {Artist::find(context.dbSession, artistId)}) starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3)); } + for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range {}).results) { - bool moreResults {}; - const auto releases {Release::getStarred(context.dbSession, user, {}, std::nullopt, moreResults)}; - for (const Release::pointer& release : releases) + if (auto release {Release::find(context.dbSession, releaseId)}) starredNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); } + for (const TrackId trackId : scrobbling.getStarredTracks(context.userId, {} /* clusters */, Range {}).results) { - bool moreResults {}; - const auto tracks {Track::getStarred(context.dbSession, user, {}, std::nullopt, moreResults)}; - for (const Track::pointer& track : tracks) + if (auto track {Track::find(context.dbSession, trackId)}) starredNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); } @@ -1267,11 +1284,11 @@ handleGetPlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - TrackList::pointer tracklist {TrackList::getById(context.dbSession, trackListId)}; + TrackList::pointer tracklist {TrackList::find(context.dbSession, trackListId)}; if (!tracklist) throw RequestedDataNotFoundError {}; @@ -1293,16 +1310,15 @@ handleGetPlaylistsRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& playlistsNode {response.createNode("playlists")}; - auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)}; - for (const TrackList::pointer& tracklist : tracklists) - playlistsNode.addArrayChild("playlist", tracklistToResponseNode(tracklist, context.dbSession)); + auto tracklistIds {TrackList::find(context.dbSession, context.userId, TrackList::Type::Playlist, Range {})}; + for (const TrackListId trackListId : tracklistIds.results) + { + const TrackList::pointer trackList {TrackList::find(context.dbSession, trackListId)}; + playlistsNode.addArrayChild("playlist", tracklistToResponseNode(trackList, context.dbSession)); + } return response; } @@ -1322,7 +1338,7 @@ handleGetSongsByGenreRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - auto clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; + auto clusterType {ClusterType::find(context.dbSession, genreClusterName)}; if (!clusterType) throw RequestedDataNotFoundError {}; @@ -1330,17 +1346,23 @@ handleGetSongsByGenreRequest(RequestContext& context) if (!cluster) throw RequestedDataNotFoundError {}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& songsByGenreNode {response.createNode("songsByGenre")}; - bool more; - auto tracks {Track::getByFilter(context.dbSession, {cluster->getId()}, {}, Range {offset, size}, more)}; - for (const Track::pointer& track : tracks) + Track::FindParameters params; + params.setClusters({cluster->getId()}); + params.setRange({offset, size}); + + auto trackIds {Track::find(context.dbSession, params)}; + for (const TrackId trackId : trackIds.results) + { + const Track::pointer track {Track::find(context.dbSession, trackId)}; songsByGenreNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); + } return response; } @@ -1355,7 +1377,7 @@ handleGetUserRequest(RequestContext& context) checkUserIsMySelfOrAdmin(context, username); - const User::pointer user {User::getByLoginName(context.dbSession, username)}; + const User::pointer user {User::find(context.dbSession, username)}; if (!user) throw RequestedDataNotFoundError {}; @@ -1374,9 +1396,12 @@ handleGetUsersRequest(RequestContext& context) Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& usersNode {response.createNode("users")}; - const auto users {User::getAll(context.dbSession)}; - for (const User::pointer& user : users) + const auto userIds {User::find(context.dbSession, User::FindParameters {})}; + for (const UserId userId : userIds.results) + { + const User::pointer user {User::find(context.dbSession, userId)}; usersNode.addArrayChild("user", userToResponseNode(user)); + } return response; } @@ -1400,30 +1425,52 @@ handleSearchRequestCommon(RequestContext& context, bool id3) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")}; - bool more; { - auto artists {Artist::getByFilter(context.dbSession, {}, keywords, std::nullopt, Artist::SortMethod::BySortName, Range {artistOffset, artistCount}, more)}; - for (const Artist::pointer& artist : artists) + Artist::FindParameters params; + params.setKeywords(keywords); + params.setSortMethod(ArtistSortMethod::BySortName); + params.setRange({artistOffset, artistCount}); + + RangeResults artistIds {Artist::find(context.dbSession, params)}; + for (const ArtistId artistId : artistIds.results) + { + const auto artist {Artist::find(context.dbSession, artistId)}; searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3)); + } } { - auto releases {Release::getByFilter(context.dbSession, {}, keywords, Range {albumOffset, albumCount}, more)}; - for (const Release::pointer& release : releases) + Release::FindParameters params; + params.setKeywords(keywords); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange({albumOffset, albumCount}); + + RangeResults releaseIds {Release::find(context.dbSession, params)}; + for (const ReleaseId releaseId : releaseIds.results) + { + const auto release {Release::find(context.dbSession, releaseId)}; searchResult2Node.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); + } } { - auto tracks {Track::getByFilter(context.dbSession, {}, keywords, Range {songOffset, songCount}, more)}; - for (const Track::pointer& track : tracks) + Track::FindParameters params; + params.setKeywords(keywords); + params.setRange({songOffset, songCount}); + + RangeResults trackIds {Track::find(context.dbSession, params)}; + for (const TrackId trackId : trackIds.results) + { + const auto track {Track::find(context.dbSession, trackId)}; searchResult2Node.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); + } } return response; @@ -1456,38 +1503,14 @@ handleStarRequest(RequestContext& context) { StarParameters params {getStarParameters(context.parameters)}; - auto transaction {context.dbSession.createSharedTransaction()}; - - User::pointer user {User::getById(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - for (const ArtistId id : params.artistIds) - { - Artist::pointer artist {Artist::getById(context.dbSession, id)}; - if (!artist) - continue; - - user.modify()->starArtist(artist); - } + Service::get()->star(context.userId, id); for (const ReleaseId id : params.releaseIds) - { - Release::pointer release {Release::getById(context.dbSession, id)}; - if (!release) - continue; - - user.modify()->starRelease(release); - } + Service::get()->star(context.userId, id); for (const TrackId id : params.trackIds) - { - Track::pointer track {Track::getById(context.dbSession, id)}; - if (!track) - continue; - - user.modify()->starTrack(track); - } + Service::get()->star(context.userId, id); return Response::createOkResponse(context.serverProtocolVersion); } @@ -1512,39 +1535,14 @@ handleUnstarRequest(RequestContext& context) { StarParameters params {getStarParameters(context.parameters)}; - auto transaction {context.dbSession.createSharedTransaction()}; - - User::pointer user {User::getById(context.dbSession, context.userId)}; - if (!user) - throw RequestedDataNotFoundError {}; - for (const ArtistId id : params.artistIds) - { - Artist::pointer artist {Artist::getById(context.dbSession, id)}; - if (!artist) - continue; - - user.modify()->unstarArtist(artist); - } + Service::get()->unstar(context.userId, id); for (const ReleaseId id : params.releaseIds) - { - Release::pointer release {Release::getById(context.dbSession, id)}; - if (!release) - continue; - - user.modify()->unstarRelease(release); - } + Service::get()->unstar(context.userId, id); for (const TrackId id : params.trackIds) - { - Track::pointer track {Track::getById(context.dbSession, id)}; - if (!track) - continue; - - user.modify()->unstarTrack(track); - } - + Service::get()->unstar(context.userId, id); return Response::createOkResponse(context.serverProtocolVersion); } @@ -1571,13 +1569,13 @@ handleScrobble(RequestContext& context) if (!submission) { - Service::get()->listenStarted({context.userId, ids.front()}); + Service::get()->listenStarted({context.userId, ids.front()}); } else { if (times.empty()) { - Service::get()->listenFinished({context.userId, ids.front()}); + Service::get()->listenFinished({context.userId, ids.front()}); } else { @@ -1585,7 +1583,7 @@ handleScrobble(RequestContext& context) { const TrackId trackId {ids[i]}; const unsigned long time {times[i]}; - Service::get()->addTimedListen({{context.userId, trackId}, Wt::WDateTime::fromTime_t(static_cast(time / 1000))}); + Service::get()->addTimedListen({{context.userId, trackId}, Wt::WDateTime::fromTime_t(static_cast(time / 1000))}); } } } @@ -1604,7 +1602,7 @@ handleUpdateUserRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, username)}; + User::pointer user {User::find(context.dbSession, username)}; if (!user) throw RequestedDataNotFoundError {}; @@ -1617,7 +1615,7 @@ handleUpdateUserRequest(RequestContext& context) try { - Service<::Auth::IPasswordService>()->setPassword(context.dbSession, userId, decodePasswordIfNeeded(*password)); + Service<::Auth::IPasswordService>()->setPassword(userId, decodePasswordIfNeeded(*password)); } catch (const Auth::PasswordMustMatchLoginNameException&) { @@ -1652,11 +1650,11 @@ handleUpdatePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - TrackList::pointer tracklist {TrackList::getById(context.dbSession, id)}; + TrackList::pointer tracklist {TrackList::find(context.dbSession, id)}; if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) @@ -1685,7 +1683,7 @@ handleUpdatePlaylistRequest(RequestContext& context) // Add tracks for (const TrackId trackIdToAdd : trackIdsToAdd) { - Track::pointer track {Track::getById(context.dbSession, trackIdToAdd)}; + Track::pointer track {Track::find(context.dbSession, trackIdToAdd)}; if (!track) continue; @@ -1701,17 +1699,18 @@ handleGetBookmarks(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - const auto bookmarks {TrackBookmark::getByUser(context.dbSession, user)}; + const auto bookmarkIds {TrackBookmark::find(context.dbSession, user->getId(), Range {})}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& bookmarksNode {response.createNode("bookmarks")}; - for (const TrackBookmark::pointer& bookmark : bookmarks) + for (const TrackBookmarkId bookmarkId : bookmarkIds.results) { + const TrackBookmark::pointer bookmark {TrackBookmark::find(context.dbSession, bookmarkId)}; Response::Node bookmarkNode {trackBookmarkToResponseNode(bookmark)}; bookmarkNode.addArrayChild("entry", trackToResponseNode(bookmark->getTrack(), context.dbSession, user)); @@ -1726,22 +1725,22 @@ Response handleCreateBookmark(RequestContext& context) { // Mandatory params - TrackId id {getMandatoryParameterAs(context.parameters, "id")}; + TrackId trackId {getMandatoryParameterAs(context.parameters, "id")}; unsigned long position {getMandatoryParameterAs(context.parameters, "position")}; const std::optional comment {getParameterAs(context.parameters, "comment")}; auto transaction {context.dbSession.createUniqueTransaction()}; - const User::pointer user {User::getById(context.dbSession, context.userId)}; + const User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - const Track::pointer track {Track::getById(context.dbSession, id)}; + const Track::pointer track {Track::find(context.dbSession, trackId)}; if (!track) throw RequestedDataNotFoundError {}; // Replace any existing bookmark - auto bookmark {TrackBookmark::getByUser(context.dbSession, user, track)}; + auto bookmark {TrackBookmark::find(context.dbSession, user->getId(), trackId)}; if (!bookmark) bookmark = TrackBookmark::create(context.dbSession, user, track); @@ -1757,19 +1756,11 @@ Response handleDeleteBookmark(RequestContext& context) { // Mandatory params - TrackId id {getMandatoryParameterAs(context.parameters, "id")}; + TrackId trackId {getMandatoryParameterAs(context.parameters, "id")}; auto transaction {context.dbSession.createUniqueTransaction()}; - const User::pointer user {User::getById(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - const Track::pointer track {Track::getById(context.dbSession, id)}; - if (!track) - throw RequestedDataNotFoundError {}; - - auto bookmark {TrackBookmark::getByUser(context.dbSession, user, track)}; + auto bookmark {TrackBookmark::find(context.dbSession, context.userId, trackId)}; if (!bookmark) throw RequestedDataNotFoundError {}; @@ -1799,11 +1790,11 @@ handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, std::size_t size {getParameterAs(context.parameters, "size").value_or(256)}; size = Utils::clamp(size, std::size_t {32}, std::size_t {1024}); - std::shared_ptr cover; + std::shared_ptr cover; if (trackId) - cover = Service::get()->getFromTrack(context.dbSession, *trackId, size); + cover = Service::get()->getFromTrack(*trackId, size); else if (releaseId) - cover = Service::get()->getFromRelease(context.dbSession, *releaseId, size); + cover = Service::get()->getFromRelease(*releaseId, size); response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); response.setMimeType(std::string {cover->getMimeType()}); @@ -2040,22 +2031,18 @@ RequestContext SubsonicResource::buildRequestContext(const Wt::Http::Request& request) { const Wt::Http::ParameterMap& parameters {request.getParameterMap()}; - const ClientInfo clientInfo {getClientInfo(parameters)}; + const Database::UserId userId {authenticateUser(request, clientInfo)}; - Session& dbSession {_db.getTLSSession()}; - - const Database::UserId userId {authenticateUser(request, clientInfo, dbSession)}; - - return {parameters, dbSession, userId, clientInfo, getServerProtocolVersion(clientInfo.name)}; + return {parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name)}; } Database::UserId -SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo, Session& dbSession) +SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo) { if (auto *authEnvService {Service<::Auth::IEnvService>::get()}) { - const auto checkResult {authEnvService->processRequest(dbSession, request)}; + const auto checkResult {authEnvService->processRequest(request)}; if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted) throw UserNotAuthorizedError {}; @@ -2063,9 +2050,8 @@ SubsonicResource::authenticateUser(const Wt::Http::Request& request, const Clien } else if (auto *authPasswordService {Service<::Auth::IPasswordService>::get()}) { - const auto checkResult {authPasswordService->checkUserPassword(dbSession, - boost::asio::ip::address::from_string(request.clientAddress()), - clientInfo.user, clientInfo.password)}; + const auto checkResult {authPasswordService->checkUserPassword(boost::asio::ip::address::from_string(request.clientAddress()), + clientInfo.user, clientInfo.password)}; switch (checkResult.state) { diff --git a/src/libs/subsonic/impl/SubsonicResource.hpp b/src/libs/subsonic/impl/SubsonicResource.hpp index fd53029b..24c0754b 100644 --- a/src/libs/subsonic/impl/SubsonicResource.hpp +++ b/src/libs/subsonic/impl/SubsonicResource.hpp @@ -19,10 +19,12 @@ #pragma once #include +#include + #include #include -#include "database/Types.hpp" +#include "services/database/Types.hpp" #include "ClientInfo.hpp" #include "RequestContext.hpp" @@ -46,7 +48,7 @@ namespace API::Subsonic static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server); ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters); RequestContext buildRequestContext(const Wt::Http::Request& request); - Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo, Database::Session& dbSession); + Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo); const std::unordered_map _serverProtocolVersionsByClient; Database::Db& _db; diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index f6ef50b7..261ec14c 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -1,5 +1,6 @@ - add_library(lmsutils SHARED + impl/http/Client.cpp + impl/http/SendQueue.cpp impl/ChildProcess.cpp impl/ChildProcessManager.cpp impl/Config.cpp @@ -37,3 +38,6 @@ target_link_libraries(lmsutils PUBLIC install(TARGETS lmsutils DESTINATION lib) +if(BUILD_TESTING) + add_subdirectory(test) +endif() diff --git a/src/libs/utils/impl/IOContextRunner.cpp b/src/libs/utils/impl/IOContextRunner.cpp index 934b3b64..8cd1fd14 100644 --- a/src/libs/utils/impl/IOContextRunner.cpp +++ b/src/libs/utils/impl/IOContextRunner.cpp @@ -27,7 +27,7 @@ IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t : _ioService {ioService} , _work {ioService} { - LMS_LOG(UTILS, INFO) << "Starting IO Context with " << threadCount << " threads..."; + LMS_LOG(UTILS, INFO) << "Starting IO context with " << threadCount << " threads..."; for (std::size_t i {}; i < threadCount; ++i) { _threads.emplace_back([&] @@ -48,10 +48,10 @@ IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t void IOContextRunner::stop() { - LMS_LOG(UTILS, INFO) << "Stopping IO Context"; + LMS_LOG(UTILS, DEBUG) << "Stopping IO context..."; _work.reset(); _ioService.stop(); - LMS_LOG(UTILS, INFO) << "Stopped IO Context"; + LMS_LOG(UTILS, DEBUG) << "IO context stopped!"; } IOContextRunner::~IOContextRunner() diff --git a/src/libs/utils/impl/Logger.cpp b/src/libs/utils/impl/Logger.cpp index 3c0d6668..57d7a35d 100644 --- a/src/libs/utils/impl/Logger.cpp +++ b/src/libs/utils/impl/Logger.cpp @@ -31,6 +31,7 @@ const char* getModuleName(Module mod) case Module::DB: return "DB"; case Module::DBUPDATER: return "DB UPDATER"; case Module::FEATURE: return "FEATURE"; + case Module::HTTP: return "HTTP"; case Module::MAIN: return "MAIN"; case Module::METADATA: return "METADATA"; case Module::REMOTE: return "REMOTE"; diff --git a/src/libs/utils/impl/RecursiveSharedMutex.cpp b/src/libs/utils/impl/RecursiveSharedMutex.cpp index 4adaa18a..a763f4b3 100644 --- a/src/libs/utils/impl/RecursiveSharedMutex.cpp +++ b/src/libs/utils/impl/RecursiveSharedMutex.cpp @@ -24,7 +24,9 @@ void RecursiveSharedMutex::lock() { - if (_uniqueOwner == std::this_thread::get_id()) + const auto thisThreadId {std::this_thread::get_id()}; + + if (_uniqueOwner == thisThreadId) { // already locked _uniqueCount++; @@ -32,7 +34,7 @@ RecursiveSharedMutex::lock() else { _mutex.lock(); - _uniqueOwner = std::this_thread::get_id(); + _uniqueOwner = thisThreadId; assert(_uniqueCount == 0); _uniqueCount = 1; } @@ -53,10 +55,12 @@ RecursiveSharedMutex::unlock() void RecursiveSharedMutex::lock_shared() { - if (_uniqueOwner == std::this_thread::get_id()) + const auto thisThreadId {std::this_thread::get_id()}; + + if (_uniqueOwner == thisThreadId ) { // alone here, no need to lock - _sharedCounts[std::this_thread::get_id()]++; + _sharedCounts[thisThreadId]++; return; } @@ -66,7 +70,7 @@ RecursiveSharedMutex::lock_shared() { std::scoped_lock lock {_sharedCountMutex}; - auto& sharedCount {_sharedCounts[std::this_thread::get_id()]}; + auto& sharedCount {_sharedCounts[thisThreadId]}; if (sharedCount == 0) needLock = true; else @@ -80,17 +84,19 @@ RecursiveSharedMutex::lock_shared() assert(_uniqueOwner == std::thread::id {}); std::scoped_lock lock {_sharedCountMutex}; - _sharedCounts[std::this_thread::get_id()]++; + _sharedCounts[thisThreadId]++; } } void RecursiveSharedMutex::unlock_shared() { - if (_uniqueOwner == std::this_thread::get_id()) + const auto thisThreadId {std::this_thread::get_id()}; + + if (_uniqueOwner == thisThreadId ) { // alone here, no need to lock - auto& sharedCount {_sharedCounts[std::this_thread::get_id()]}; + auto& sharedCount {_sharedCounts[thisThreadId ]}; assert(sharedCount > 0); --sharedCount; @@ -102,7 +108,7 @@ RecursiveSharedMutex::unlock_shared() { std::scoped_lock lock {_sharedCountMutex}; - auto& sharedCount {_sharedCounts[std::this_thread::get_id()]}; + auto& sharedCount {_sharedCounts[thisThreadId]}; assert(sharedCount > 0); needUnlock = (--sharedCount == 0); } @@ -110,3 +116,22 @@ RecursiveSharedMutex::unlock_shared() if (needUnlock) _mutex.unlock_shared(); } + +#ifndef NDEBUG +bool +RecursiveSharedMutex::isUniqueLocked() +{ + return _uniqueOwner == std::this_thread::get_id(); +} + +bool +RecursiveSharedMutex::isSharedLocked() +{ + const auto thisThreadId {std::this_thread::get_id()}; + if (_uniqueOwner == thisThreadId ) + return true; + + std::scoped_lock lock {_sharedCountMutex}; + return _sharedCounts[thisThreadId] > 0; +} +#endif diff --git a/src/libs/utils/impl/http/Client.cpp b/src/libs/utils/impl/http/Client.cpp new file mode 100644 index 00000000..ecc9d817 --- /dev/null +++ b/src/libs/utils/impl/http/Client.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Client.hpp" +#include "utils/Exception.hpp" + +namespace Http +{ + std::unique_ptr + createClient(boost::asio::io_context& ioContext, std::string_view baseUrl) + { + return std::make_unique(ioContext, baseUrl); + } + + void + Client::sendGETRequest(ClientGETRequestParameters&& GETParams) + { + _sendQueue.sendRequest(std::make_unique(std::move(GETParams))); + } + + void + Client::sendPOSTRequest(ClientPOSTRequestParameters&& POSTParams) + { + _sendQueue.sendRequest(std::make_unique(std::move(POSTParams))); + } +} // namespace Http + diff --git a/src/libs/utils/impl/http/Client.hpp b/src/libs/utils/impl/http/Client.hpp new file mode 100644 index 00000000..f6a42b40 --- /dev/null +++ b/src/libs/utils/impl/http/Client.hpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "utils/http/IClient.hpp" +#include "SendQueue.hpp" + +namespace Http +{ + class Client final : public IClient + { + public: + Client(boost::asio::io_context& ioContext, std::string_view baseUrl) + : _ioContext {ioContext} + , _sendQueue {ioContext, baseUrl} + {} + + private: + void sendGETRequest(ClientGETRequestParameters&& request) override; + void sendPOSTRequest(ClientPOSTRequestParameters&& request) override; + + boost::asio::io_context& _ioContext; + SendQueue _sendQueue; + }; +} // namespace Http + diff --git a/src/libs/utils/impl/http/ClientRequest.hpp b/src/libs/utils/impl/http/ClientRequest.hpp new file mode 100644 index 00000000..0b9f028d --- /dev/null +++ b/src/libs/utils/impl/http/ClientRequest.hpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include "utils/http/ClientRequestParameters.hpp" + +namespace Http +{ + class ClientRequest + { + public: + ClientRequest(ClientGETRequestParameters&& GETParams) : _parameters {std::move(GETParams)} {} + ClientRequest(ClientPOSTRequestParameters&& POSTParams) : _parameters {std::move(POSTParams)} {} + + std::size_t retryCount {}; + + const ClientRequestParameters& getParameters() const + { + const ClientRequestParameters* res; + + std::visit([&](const auto& parameters) + { + res = &static_cast(parameters); + }, _parameters); + + return *res; + } + + + enum class Type + { + GET, + POST + }; + Type getType() const + { + if (std::holds_alternative(_parameters)) + return Type::GET; + else + return Type::POST; + } + + const ClientGETRequestParameters& getGETParameters() const + { + return std::get(_parameters); + } + + const ClientPOSTRequestParameters& getPOSTParameters() const + { + return std::get(_parameters); + } + + private: + std::variant _parameters; + }; +} diff --git a/src/libs/scrobbling/impl/listenbrainz/SendQueue.cpp b/src/libs/utils/impl/http/SendQueue.cpp similarity index 67% rename from src/libs/scrobbling/impl/listenbrainz/SendQueue.cpp rename to src/libs/utils/impl/http/SendQueue.cpp index aa1cfde7..8d99fc72 100644 --- a/src/libs/scrobbling/impl/listenbrainz/SendQueue.cpp +++ b/src/libs/utils/impl/http/SendQueue.cpp @@ -19,13 +19,14 @@ #include "SendQueue.hpp" +#include #include -#include "scrobbling/Exception.hpp" +#include "utils/Exception.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" -#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz SendQueue] - " +#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[Http SendQueue] - " namespace StringUtils { @@ -57,11 +58,11 @@ namespace } } -namespace Scrobbling::ListenBrainz +namespace Http { - SendQueue::SendQueue(boost::asio::io_context& ioContext, std::string_view apiBaseURL) + SendQueue::SendQueue(boost::asio::io_context& ioContext, std::string_view baseUrl) : _ioContext {ioContext} - , _apiBaseURL {apiBaseURL} + , _baseUrl {baseUrl} { _client.done().connect([this](Wt::AsioWrapper::error_code ec, const Wt::Http::Message& msg) { @@ -78,11 +79,11 @@ namespace Scrobbling::ListenBrainz } void - SendQueue::enqueueRequest(Request request) + SendQueue::sendRequest(std::unique_ptr request) { - _strand.dispatch([this, request = std::move(request)]() + boost::asio::dispatch(_strand, [this, request = std::move(request)]() mutable { - _sendQueue[request._priority].emplace_back(std::move(request)); + _sendQueue[request->getParameters().priority].emplace_back(std::move(request)); if (_state == State::Idle) sendNextQueuedRequest(); @@ -93,16 +94,17 @@ namespace Scrobbling::ListenBrainz SendQueue::sendNextQueuedRequest() { assert(_state == State::Idle); + assert(!_currentRequest); for (auto& [prio, requests] : _sendQueue) { LOG(DEBUG) << "Processing prio " << static_cast(prio) << ", request count = " << requests.size(); while (!requests.empty()) { - Request request {std::move(requests.front())}; + std::unique_ptr request {std::move(requests.front())}; requests.pop_front(); - if (!sendRequest(request._requestData)) + if (!sendRequest(*request)) continue; _state = State::Sending; @@ -113,20 +115,20 @@ namespace Scrobbling::ListenBrainz } bool - SendQueue::sendRequest(const RequestData& requestData) + SendQueue::sendRequest(const ClientRequest& request) { - const std::string url {_apiBaseURL + requestData.endpoint}; + std::string url {_baseUrl + request.getParameters().relativeUrl}; + LOG(DEBUG) << "Sending request to url '" << url << "'"; - LOG(DEBUG) << "Sending request type " << (requestData.type == RequestData::Type::GET ? "GET" : "POST") << " to url '" << url << "'"; - - bool res{}; - switch (requestData.type) + bool res {}; + switch (request.getType()) { - case RequestData::Type::GET: - res = _client.get(url, requestData.headers); + case ClientRequest::Type::GET: + res = _client.get(url, request.getGETParameters().headers); break; - case RequestData::Type::POST: - res = _client.post(url, requestData.message); + + case ClientRequest::Type::POST: + res = _client.post(url, request.getPOSTParameters().message); break; } @@ -141,48 +143,48 @@ namespace Scrobbling::ListenBrainz { if (ec == boost::asio::error::operation_aborted) { - LOG(DEBUG) << "SendQueue: client aborted"; + LOG(DEBUG) << "Client aborted"; return; } assert(_currentRequest); - Request request {std::move(*_currentRequest)}; _state = State::Idle; LOG(DEBUG) << "Client done. status = " << msg.status(); if (ec) - onClientDoneError(std::move(request), ec); + onClientDoneError(std::move(_currentRequest), ec); else - onClientDoneSuccess(std::move(request), msg); + onClientDoneSuccess(std::move(_currentRequest), msg); } void - SendQueue::onClientDoneError(Request request, Wt::AsioWrapper::error_code ec) + SendQueue::onClientDoneError(std::unique_ptr request, Wt::AsioWrapper::error_code ec) { - LOG(ERROR) << "Retry " << request._retryCount << ", client error: '" << ec.message() << "'"; + LOG(ERROR) << "Retry " << request->retryCount << ", client error: '" << ec.message() << "'"; // may be a network error, try again later throttle(_defaultRetryWaitDuration); - if (request._retryCount++ < _maxRetryCount) + if (request->retryCount++ < _maxRetryCount) { - _sendQueue[request._priority].emplace_front(std::move(request)); + _sendQueue[request->getParameters().priority].emplace_front(std::move(request)); } else { LOG(ERROR) << "Too many retries, giving up operation and throttle"; - if (request._onFailureFunc) - request._onFailureFunc(); + if (request->getParameters().onFailureFunc) + request->getParameters().onFailureFunc(); } } void - SendQueue::onClientDoneSuccess(Request request, const Wt::Http::Message& msg) + SendQueue::onClientDoneSuccess(std::unique_ptr request, const Wt::Http::Message& msg) { + const ClientRequestParameters& requestParameters {request->getParameters()}; bool mustThrottle{}; if (msg.status() == 429) { - _sendQueue[request._priority].emplace_front(std::move(request)); + _sendQueue[requestParameters.priority].emplace_front(std::move(request)); mustThrottle = true; } @@ -198,14 +200,14 @@ namespace Scrobbling::ListenBrainz { if (msg.status() == 200) { - if (request._onSuccessFunc) - request._onSuccessFunc(msg.body()); + if (requestParameters.onSuccessFunc) + requestParameters.onSuccessFunc(msg.body()); } else { LOG(ERROR) << "Send error: '" << msg.body() << "'"; - if (request._onFailureFunc) - request._onFailureFunc(); + if (requestParameters.onFailureFunc) + requestParameters.onFailureFunc(); } } @@ -226,12 +228,12 @@ namespace Scrobbling::ListenBrainz { if (ec == boost::asio::error::operation_aborted) { - LOG(DEBUG) << "SendQueue: throttle aborted"; + LOG(DEBUG) << "Throttle aborted"; return; } else if (ec) { - throw Exception {"Throttle timer failure: " + std::string {ec.message()} }; + throw LmsException {"Throttle timer failure: " + std::string {ec.message()} }; } _state = State::Idle; diff --git a/src/libs/scrobbling/impl/listenbrainz/SendQueue.hpp b/src/libs/utils/impl/http/SendQueue.hpp similarity index 52% rename from src/libs/scrobbling/impl/listenbrainz/SendQueue.hpp rename to src/libs/utils/impl/http/SendQueue.hpp index 9878d5d8..e9eb4180 100644 --- a/src/libs/scrobbling/impl/listenbrainz/SendQueue.hpp +++ b/src/libs/utils/impl/http/SendQueue.hpp @@ -20,19 +20,22 @@ #pragma once #include +#include +#include #include #include #include #include +#include "ClientRequest.hpp" -namespace Scrobbling::ListenBrainz +namespace Http { class SendQueue { public: - SendQueue(boost::asio::io_context& ioContext, std::string_view apiBaseURL); + SendQueue(boost::asio::io_context& ioContext, std::string_view baseUrl); ~SendQueue(); SendQueue(const SendQueue&) = delete; @@ -40,58 +43,14 @@ namespace Scrobbling::ListenBrainz SendQueue& operator=(const SendQueue&) = delete; SendQueue& operator=(const SendQueue&&) = delete; - // generic queue operations - struct RequestData - { - enum class Type - { - GET, - POST, - }; - - Type type; - std::string endpoint; // relative URL to the base API - std::vector headers; // used by GET - Wt::Http::Message message; // used by POST - }; - - class Request - { - public: - enum class Priority - { - High, - Normal, - Low, - }; - - Request(RequestData requestData) : _requestData {std::move(requestData)} {} - - using OnSuccessFunc = std::function; - using OnFailureFunc = std::function; - - void setOnSuccessFunc(OnSuccessFunc onSuccessFunc) { _onSuccessFunc = onSuccessFunc; } - void setOnFailureFunc(OnFailureFunc onFailureFunc) { _onFailureFunc = onFailureFunc; } - void setPriority(Priority priority) { _priority = priority; } - - private: - friend class SendQueue; - RequestData _requestData; - Priority _priority {Priority::Normal}; - std::size_t _retryCount {}; - OnSuccessFunc _onSuccessFunc; - OnFailureFunc _onFailureFunc; - }; - - std::string_view getAPIBaseURL() const { return _apiBaseURL; } - void enqueueRequest(Request request); + void sendRequest(std::unique_ptr request); private: void sendNextQueuedRequest(); - bool sendRequest(const RequestData& request); + bool sendRequest(const ClientRequest& request); void onClientDone(Wt::AsioWrapper::error_code ec, const Wt::Http::Message& msg); - void onClientDoneError(Request request, Wt::AsioWrapper::error_code ec); - void onClientDoneSuccess(Request request, const Wt::Http::Message& msg); + void onClientDoneError(std::unique_ptr request, Wt::AsioWrapper::error_code ec); + void onClientDoneSuccess(std::unique_ptr request, const Wt::Http::Message& msg); void throttle(std::chrono::seconds duration); const std::size_t _maxRetryCount {2}; @@ -99,20 +58,21 @@ namespace Scrobbling::ListenBrainz const std::chrono::seconds _minRetryWaitDuration {1}; const std::chrono::seconds _maxRetryWaitDuration {300}; + boost::asio::io_context& _ioContext; + boost::asio::io_context::strand _strand {_ioContext}; + boost::asio::steady_timer _throttleTimer {_ioContext}; + std::string _baseUrl; + enum class State { Idle, Throttled, Sending, }; - boost::asio::io_context& _ioContext; - boost::asio::io_context::strand _strand {_ioContext}; - boost::asio::steady_timer _throttleTimer {_ioContext}; - std::string _apiBaseURL; - State _state {State::Idle}; - Wt::Http::Client _client {_ioContext}; - std::map> _sendQueue; - std::optional _currentRequest; + State _state {State::Idle}; + Wt::Http::Client _client {_ioContext}; + std::map>> _sendQueue; + std::unique_ptr _currentRequest; }; } // namespace Scrobbling::ListenBrainz diff --git a/src/libs/utils/include/utils/Logger.hpp b/src/libs/utils/include/utils/Logger.hpp index 4da7f4ba..d7faf078 100644 --- a/src/libs/utils/include/utils/Logger.hpp +++ b/src/libs/utils/include/utils/Logger.hpp @@ -43,6 +43,7 @@ enum class Module DB, DBUPDATER, FEATURE, + HTTP, MAIN, METADATA, REMOTE, @@ -84,5 +85,6 @@ class Logger virtual void processLog(const Log& log) = 0; }; -#define LMS_LOG(module, severity) Log(Service::get(), Module::module, Severity::severity).getOstream() +#define LMS_LOG(module, severity) Log(Service::get(), Module::module, Severity::severity).getOstream() +#define LMS_LOG_EX(module, severity) Log(Service::get(), module, severity).getOstream() diff --git a/src/libs/utils/include/utils/RecursiveSharedMutex.hpp b/src/libs/utils/include/utils/RecursiveSharedMutex.hpp index 84f05453..57f6631e 100644 --- a/src/libs/utils/include/utils/RecursiveSharedMutex.hpp +++ b/src/libs/utils/include/utils/RecursiveSharedMutex.hpp @@ -34,6 +34,10 @@ class RecursiveSharedMutex void lock_shared(); void unlock_shared(); +#ifndef NDEBUG + bool isSharedLocked(); + bool isUniqueLocked(); +#endif // NDEBUG private: std::shared_mutex _mutex; std::thread::id _uniqueOwner; diff --git a/src/libs/utils/include/utils/http/ClientRequestParameters.hpp b/src/libs/utils/include/utils/http/ClientRequestParameters.hpp new file mode 100644 index 00000000..94fc1cdd --- /dev/null +++ b/src/libs/utils/include/utils/http/ClientRequestParameters.hpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include + +namespace Http +{ + struct ClientRequestParameters + { + enum class Priority + { + High, + Normal, + Low, + }; + + Priority priority {Priority::Normal}; + std::string relativeUrl; // relative to baseUrl used by the client + + using OnSuccessFunc = std::function; + OnSuccessFunc onSuccessFunc; + + using OnFailureFunc = std::function; + OnFailureFunc onFailureFunc; + }; + + struct ClientGETRequestParameters final : public ClientRequestParameters + { + std::vector headers; + }; + + struct ClientPOSTRequestParameters final : public ClientRequestParameters + { + Wt::Http::Message message; + }; + +} // namespace Http + diff --git a/src/libs/utils/include/utils/http/IClient.hpp b/src/libs/utils/include/utils/http/IClient.hpp new file mode 100644 index 00000000..09df5ee5 --- /dev/null +++ b/src/libs/utils/include/utils/http/IClient.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "utils/http/ClientRequestParameters.hpp" + +namespace Http +{ + class IClient + { + public: + virtual ~IClient() = default; + + virtual void sendGETRequest(ClientGETRequestParameters&& request) = 0; + virtual void sendPOSTRequest(ClientPOSTRequestParameters&& request) = 0; + }; + + std::unique_ptr createClient(boost::asio::io_context& ioContext, std::string_view baseUrl); +} // namespace Http + diff --git a/src/test/utils/CMakeLists.txt b/src/libs/utils/test/CMakeLists.txt similarity index 72% rename from src/test/utils/CMakeLists.txt rename to src/libs/utils/test/CMakeLists.txt index 1b355f9b..9f33170b 100644 --- a/src/test/utils/CMakeLists.txt +++ b/src/libs/utils/test/CMakeLists.txt @@ -12,5 +12,7 @@ target_link_libraries(test-utils PRIVATE GTest::GTest ) -gtest_discover_tests(test-utils) +if (NOT CMAKE_CROSSCOMPILING) + gtest_discover_tests(test-utils) +endif() diff --git a/src/test/utils/RecursiveSharedMutex.cpp b/src/libs/utils/test/RecursiveSharedMutex.cpp similarity index 100% rename from src/test/utils/RecursiveSharedMutex.cpp rename to src/libs/utils/test/RecursiveSharedMutex.cpp diff --git a/src/test/utils/String.cpp b/src/libs/utils/test/String.cpp similarity index 100% rename from src/test/utils/String.cpp rename to src/libs/utils/test/String.cpp diff --git a/src/test/utils/Utils.cpp b/src/libs/utils/test/Utils.cpp similarity index 100% rename from src/test/utils/Utils.cpp rename to src/libs/utils/test/Utils.cpp diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 77570c7e..33dad4b1 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -51,11 +51,11 @@ target_include_directories(lms PRIVATE target_link_libraries(lms PRIVATE lmsav lmsauth - lmscover lmsdatabase lmsrecommendation lmsscanner lmsscrobbling + lmsservice-cover lmssubsonic lmsutils Wt::Wt diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 98c0cec6..8ad33f19 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -25,16 +25,17 @@ #include #include -#include "auth/IAuthTokenService.hpp" -#include "auth/IPasswordService.hpp" -#include "auth/IEnvService.hpp" -#include "cover/ICoverArtGrabber.hpp" -#include "database/Db.hpp" -#include "database/Session.hpp" -#include "scanner/IScanner.hpp" -#include "recommendation/IEngine.hpp" +#include "image/IRawImage.hpp" +#include "services/auth/IAuthTokenService.hpp" +#include "services/auth/IPasswordService.hpp" +#include "services/auth/IEnvService.hpp" +#include "services/cover/ICoverService.hpp" +#include "services/database/Db.hpp" +#include "services/database/Session.hpp" +#include "services/recommendation/IRecommendationService.hpp" +#include "services/scanner/IScannerService.hpp" +#include "services/scrobbling/IScrobblingService.hpp" #include "subsonic/SubsonicResource.hpp" -#include "scrobbling/IScrobbling.hpp" #include "ui/LmsApplication.hpp" #include "ui/LmsApplicationManager.hpp" #include "utils/IChildProcessManager.hpp" @@ -44,6 +45,16 @@ #include "utils/String.hpp" #include "utils/WtLogger.hpp" +static +std::size_t +getThreadCount() +{ + const unsigned long configHttpServerThreadCount {Service::get()->getULong("http-server-thread-count", 0)}; + + // Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg) + return configHttpServerThreadCount ? configHttpServerThreadCount : std::max(2, std::thread::hardware_concurrency()); +} + static std::vector generateWtConfig(std::string execPath) @@ -54,7 +65,6 @@ generateWtConfig(std::string execPath) const std::filesystem::path wtLogFilePath {Service::get()->getPath("log-file", "/var/log/lms.log")}; const std::filesystem::path wtAccessLogFilePath {Service::get()->getPath("access-log-file", "/var/log/lms.access.log")}; const std::filesystem::path wtResourcesPath {Service::get()->getPath("wt-resources", "/usr/share/Wt/resources")}; - const unsigned long configHttpServerThreadCount {Service::get()->getULong("http-server-thread-count", 0)}; args.push_back(execPath); args.push_back("--config=" + wtConfigPath.string()); @@ -81,11 +91,7 @@ generateWtConfig(std::string execPath) if (!wtAccessLogFilePath.empty()) args.push_back("--accesslog=" + wtAccessLogFilePath.string()); - { - // Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg) - const unsigned long httpServerThreadCount {configHttpServerThreadCount ? configHttpServerThreadCount : std::max(2, std::thread::hardware_concurrency())}; - args.push_back("--threads=" + std::to_string(httpServerThreadCount)); - } + args.push_back("--threads=" + std::to_string(getThreadCount())); // Generate the wt_config.xml file boost::property_tree::ptree pt; @@ -126,7 +132,7 @@ generateWtConfig(std::string execPath) static void -proxyScannerEventsToApplication(Scanner::IScanner& scanner, Wt::WServer& server) +proxyScannerEventsToApplication(Scanner::IScannerService& scanner, Wt::WServer& server) { auto postAll {[](Wt::WServer& server, std::function cb) { @@ -218,10 +224,10 @@ int main(int argc, char* argv[]) Wt::WServer server {argv[0]}; server.setServerConfiguration(wtServerArgs.size(), const_cast(&wtArgv[0])); - IOContextRunner ioContextRunner {ioContext, std::max(2, std::thread::hardware_concurrency())}; + IOContextRunner ioContextRunner {ioContext, getThreadCount()}; // Initializing a connection pool to the database that will be shared along services - Database::Db database {config->getPath("working-dir") / "lms.db"}; + Database::Db database {config->getPath("working-dir") / "lms.db", getThreadCount()}; { Database::Session session {database}; session.prepareTables(); @@ -232,7 +238,6 @@ int main(int argc, char* argv[]) // Service initialization order is important (reverse-order for deinit) Service childProcessManagerService {createChildProcessManager(ioContext)}; - Service authTokenService; Service authPasswordService; Service authEnvService; @@ -240,32 +245,29 @@ int main(int argc, char* argv[]) const std::string authenticationBackend {StringUtils::stringToLower(config->getString("authentication-backend", "internal"))}; if (authenticationBackend == "internal" || authenticationBackend == "pam") { - authTokenService.assign(Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))); - authPasswordService.assign(Auth::createPasswordService(authenticationBackend, config->getULong("login-throttler-max-entriees", 10000), *authTokenService.get())); + authTokenService.assign(Auth::createAuthTokenService(database, config->getULong("login-throttler-max-entriees", 10000))); + authPasswordService.assign(Auth::createPasswordService(authenticationBackend, database, config->getULong("login-throttler-max-entriees", 10000), *authTokenService.get())); } else if (authenticationBackend == "http-headers") { - authEnvService.assign(Auth::createEnvService(authenticationBackend)); + authEnvService.assign(Auth::createEnvService(authenticationBackend, database)); } else throw LmsException {"Bad value '" + authenticationBackend + "' for 'authentication-backend'"}; - Service coverArtService {CoverArt::createGrabber(argv[0], - server.appRoot() + "/images/unknown-cover.jpg", - config->getULong("cover-max-cache-size", 30) * 1000 * 1000, - config->getULong("cover-max-file-size", 10) * 1000 * 1000, - config->getULong("cover-jpeg-quality", 75))}; - Service recommendationEngineService {Recommendation::createEngine(database)}; - Service scannerService {Scanner::createScanner(/*ioContext,*/ database, *recommendationEngineService)}; + Image::init(argv[0]); + Service coverService {Cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg")}; + Service recommendationService {Recommendation::createRecommendationService(database)}; + Service scannerService {Scanner::createScannerService(database, *recommendationService)}; scannerService->getEvents().scanComplete.connect([&] { // Flush cover cache even if no changes: // covers may be external files that changed and we don't keep track of them - coverArtService->flushCache(); + coverService->flushCache(); }); - Service scrobblingService {Scrobbling::createScrobbling(ioContext, database)}; + Service scrobblingService {Scrobbling::createScrobblingService(ioContext, database)}; std::unique_ptr subsonicResource; diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index 5e1ec8ea..2db329bc 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -28,10 +28,10 @@ #include #include -#include "auth/IAuthTokenService.hpp" -#include "auth/IPasswordService.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" +#include "services/auth/IAuthTokenService.hpp" +#include "services/auth/IPasswordService.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" @@ -49,7 +49,7 @@ static void createAuthToken(Database::UserId userId, const Wt::WDateTime& expiry) { - const std::string secret {Service<::Auth::IAuthTokenService>::get()->createAuthToken(LmsApp->getDbSession(), userId, expiry)}; + const std::string secret {Service<::Auth::IAuthTokenService>::get()->createAuthToken(userId, expiry)}; LmsApp->setCookie(authCookieName, secret, @@ -67,7 +67,7 @@ processAuthToken(const Wt::WEnvironment& env) if (!authCookie) return std::nullopt; - const auto res {Service<::Auth::IAuthTokenService>::get()->processAuthToken(LmsApp->getDbSession(), boost::asio::ip::address::from_string(env.clientAddress()), *authCookie)}; + const auto res {Service<::Auth::IAuthTokenService>::get()->processAuthToken(boost::asio::ip::address::from_string(env.clientAddress()), *authCookie)}; switch (res.state) { case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Denied: @@ -109,7 +109,7 @@ class AuthModel : public Wt::WFormModel { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginNameField).toUTF8())}; + Database::User::pointer user {Database::User::find(LmsApp->getDbSession(), valueText(LoginNameField).toUTF8())}; user.modify()->setLastLogin(Wt::WDateTime::currentDateTime()); _userId = user->getId(); @@ -131,7 +131,6 @@ class AuthModel : public Wt::WFormModel if (field == PasswordField) { const auto checkResult {Service<::Auth::IPasswordService>::get()->checkUserPassword( - LmsApp->getDbSession(), boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), valueText(LoginNameField).toUTF8(), valueText(PasswordField).toUTF8())}; @@ -206,7 +205,7 @@ Auth::Auth() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::User::pointer demoUser {Database::User::getDemo(LmsApp->getDbSession())}; + Database::User::pointer demoUser {Database::User::findDemoUser(LmsApp->getDbSession())}; if (demoUser) { model->setValue(AuthModel::LoginNameField, demoUser->getLoginName()); diff --git a/src/lms/ui/Auth.hpp b/src/lms/ui/Auth.hpp index 5fda238b..305e9a4d 100644 --- a/src/lms/ui/Auth.hpp +++ b/src/lms/ui/Auth.hpp @@ -19,21 +19,23 @@ #pragma once +#include #include -#include "database/Types.hpp" +#include "services/database/UserId.hpp" -namespace UserInterface { - -std::optional -processAuthToken(const Wt::WEnvironment& env); - -class Auth : public Wt::WTemplateFormView +namespace UserInterface { - public: - Auth(); - Wt::Signal userLoggedIn; -}; + std::optional + processAuthToken(const Wt::WEnvironment& env); + + class Auth : public Wt::WTemplateFormView + { + public: + Auth(); + + Wt::Signal userLoggedIn; + }; } // namespace UserInterface diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index a2d0b4e3..c15f05f1 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -28,16 +28,16 @@ #include #include -#include "auth/IEnvService.hpp" -#include "auth/IPasswordService.hpp" -#include "cover/ICoverArtGrabber.hpp" -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" -#include "scrobbling/IScrobbling.hpp" +#include "services/auth/IEnvService.hpp" +#include "services/auth/IPasswordService.hpp" +#include "services/cover/ICoverService.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Db.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" +#include "services/scrobbling/IScrobblingService.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" @@ -69,7 +69,7 @@ LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplica { if (auto *authEnvService {Service<::Auth::IEnvService>::get()}) { - const auto checkResult {authEnvService->processEnv(db.getTLSSession(), env)}; + const auto checkResult {authEnvService->processEnv(env)}; if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted) { LMS_LOG(UI, ERROR) << "Cannot authenticate user from environment!"; @@ -101,7 +101,7 @@ LmsApplication::getUser() if (!_authenticatedUser) return {}; - return Database::User::getById(getDbSession(), _authenticatedUser->userId); + return Database::User::find(getDbSession(), _authenticatedUser->userId); } Database::UserId @@ -247,7 +247,7 @@ LmsApplication::processPasswordAuth() void LmsApplication::setTheme() { - Database::User::UITheme theme {Database::User::defaultUITheme}; + Database::UITheme theme {Database::User::defaultUITheme}; { auto transaction {getDbSession().createSharedTransaction()}; if (const auto user {getUser()}) @@ -548,14 +548,14 @@ LmsApplication::createHome() { LMS_LOG(UI, DEBUG) << "Received ScrobbleListenNow from player for trackId = " << trackId.toString(); const Scrobbling::Listen listen {getUserId(), trackId}; - Service::get()->listenStarted(listen); + Service::get()->listenStarted(listen); }); _mediaPlayer->scrobbleListenFinished.connect([this](Database::TrackId trackId, unsigned durationMs) { LMS_LOG(UI, DEBUG) << "Received ScrobbleListenFinished from player for trackId = " << trackId.toString() << ", duration = " << (durationMs / 1000) << "s"; const std::chrono::milliseconds duration {durationMs}; const Scrobbling::Listen listen {getUserId(), trackId}; - Service::get()->listenFinished(listen, std::chrono::duration_cast(duration)); + Service::get()->listenFinished(listen, std::chrono::duration_cast(duration)); }); _mediaPlayer->playbackEnded.connect([this] diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 7f8f9446..080c03f9 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -23,7 +23,10 @@ #include -#include "scanner/ScannerEvents.hpp" +#include "services/database/Object.hpp" +#include "services/database/UserId.hpp" +#include "services/database/Types.hpp" +#include "services/scanner/ScannerEvents.hpp" namespace Database { diff --git a/src/lms/ui/LmsApplicationException.hpp b/src/lms/ui/LmsApplicationException.hpp index 0671a6c4..463bd899 100644 --- a/src/lms/ui/LmsApplicationException.hpp +++ b/src/lms/ui/LmsApplicationException.hpp @@ -19,41 +19,38 @@ #pragma once -#include "database/Types.hpp" +#include "services/database/Types.hpp" #include "utils/Exception.hpp" -namespace UserInterface { - - -class LmsApplicationException : public LmsException +namespace UserInterface { - public: - LmsApplicationException(const Wt::WString& error) : LmsException {error.toUTF8()} {} -}; + class LmsApplicationException : public LmsException + { + public: + LmsApplicationException(const Wt::WString& error) : LmsException {error.toUTF8()} {} + }; -class ArtistNotFoundException : public LmsApplicationException -{ - public: - ArtistNotFoundException() : LmsApplicationException {Wt::WString::tr("Lms.Error.artist-not-found")} {} -}; + class ArtistNotFoundException : public LmsApplicationException + { + public: + ArtistNotFoundException() : LmsApplicationException {Wt::WString::tr("Lms.Error.artist-not-found")} {} + }; -class ReleaseNotFoundException : public LmsApplicationException -{ - public: - ReleaseNotFoundException() : LmsApplicationException {Wt::WString::tr("Lms.Error.release-not-found")} {} -}; + class ReleaseNotFoundException : public LmsApplicationException + { + public: + ReleaseNotFoundException() : LmsApplicationException {Wt::WString::tr("Lms.Error.release-not-found")} {} + }; -class UserNotFoundException : public LmsApplicationException -{ - public: - UserNotFoundException() : LmsApplicationException {Wt::WString::tr("Lms.Error.user-not-found")} {} -}; - -class UserNotAllowedException : public LmsApplicationException -{ - public: - UserNotAllowedException() : LmsApplicationException {Wt::WString::tr("Lms.Error.user-not-allowed")} {} -}; - -} + class UserNotFoundException : public LmsApplicationException + { + public: + UserNotFoundException() : LmsApplicationException {Wt::WString::tr("Lms.Error.user-not-found")} {} + }; + class UserNotAllowedException : public LmsApplicationException + { + public: + UserNotAllowedException() : LmsApplicationException {Wt::WString::tr("Lms.Error.user-not-allowed")} {} + }; +} // ns UserInterface diff --git a/src/lms/ui/LmsApplicationManager.hpp b/src/lms/ui/LmsApplicationManager.hpp index 1d7761f7..5a81aec1 100644 --- a/src/lms/ui/LmsApplicationManager.hpp +++ b/src/lms/ui/LmsApplicationManager.hpp @@ -25,7 +25,7 @@ #include -#include "database/Types.hpp" +#include "services/database/UserId.hpp" namespace UserInterface { diff --git a/src/lms/ui/LmsTheme.cpp b/src/lms/ui/LmsTheme.cpp index d56cbe5a..e5d9f3cc 100644 --- a/src/lms/ui/LmsTheme.cpp +++ b/src/lms/ui/LmsTheme.cpp @@ -24,7 +24,7 @@ namespace UserInterface { -LmsTheme::LmsTheme(Database::User::UITheme theme) +LmsTheme::LmsTheme(Database::UITheme theme) : _theme {theme} { setVersion(Wt::BootstrapVersion::v3); @@ -32,7 +32,7 @@ LmsTheme::LmsTheme(Database::User::UITheme theme) } void -LmsTheme::setTheme(Database::User::UITheme theme) +LmsTheme::setTheme(Database::UITheme theme) { if (theme == _theme) return; @@ -59,11 +59,11 @@ LmsTheme::styleSheets() const } std::vector -LmsTheme::getStyleSheets(Database::User::UITheme theme) +LmsTheme::getStyleSheets(Database::UITheme theme) { switch (theme) { - case Database::User::UITheme::Dark: + case Database::UITheme::Dark: return { {"css/fonts.css"}, @@ -73,7 +73,7 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme) {"css/lms-darkly.css"}, }; - case Database::User::UITheme::Light: + case Database::UITheme::Light: return { {"css/fonts.css"}, diff --git a/src/lms/ui/LmsTheme.hpp b/src/lms/ui/LmsTheme.hpp index 10ee14c2..912137a6 100644 --- a/src/lms/ui/LmsTheme.hpp +++ b/src/lms/ui/LmsTheme.hpp @@ -24,23 +24,21 @@ #include #include -#include "database/User.hpp" +#include "services/database/Types.hpp" -namespace UserInterface { - -class LmsTheme : public Wt::WBootstrapTheme +namespace UserInterface { - public: - LmsTheme(Database::User::UITheme theme); + class LmsTheme : public Wt::WBootstrapTheme + { + public: + LmsTheme(Database::UITheme theme); - void setTheme(Database::User::UITheme theme); + void setTheme(Database::UITheme theme); - private: - - std::vector styleSheets() const override; - static std::vector getStyleSheets(Database::User::UITheme theme); - - Database::User::UITheme _theme; -}; + private: + std::vector styleSheets() const override; + static std::vector getStyleSheets(Database::UITheme theme); + Database::UITheme _theme; + }; } diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 4ee65e06..43f37e76 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -25,13 +25,13 @@ #include "utils/Logger.hpp" -#include "database/Artist.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackList.hpp" -#include "database/Types.hpp" -#include "database/User.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackList.hpp" +#include "services/database/Types.hpp" +#include "services/database/User.hpp" #include "resource/CoverResource.hpp" #include "resource/AudioTranscodeResource.hpp" @@ -119,10 +119,10 @@ bitrateFromString(const std::string& str) if (!value) return std::nullopt; - if (Database::User::audioTranscodeAllowedBitrates.find(*value) != std::cend(Database::User::audioTranscodeAllowedBitrates)) - return *value; + if (!Database::isAudioBitrateAllowed(*value)) + return std::nullopt; - return std::nullopt; + return *value; } static @@ -241,7 +241,7 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + const auto track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (!track) return; diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index d4f1700b..40307c4b 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -27,7 +27,8 @@ #include #include -#include "database/Types.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/Types.hpp" namespace UserInterface { diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index 846069b0..f7fb957b 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -22,13 +22,14 @@ #include #include -#include "database/Cluster.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackList.hpp" -#include "database/User.hpp" -#include "recommendation/IEngine.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackList.hpp" +#include "services/database/User.hpp" +#include "services/scrobbling/IScrobblingService.hpp" +#include "services/recommendation/IRecommendationService.hpp" #include "utils/Logger.hpp" #include "utils/Random.hpp" #include "utils/Service.hpp" @@ -131,7 +132,7 @@ PlayQueue::PlayQueue() if (LmsApp->getUser()->isDemo()) { LMS_LOG(UI, DEBUG) << "Removing tracklist id " << _tracklistId.toString(); - auto tracklist = Database::TrackList::getById(LmsApp->getDbSession(), _tracklistId); + auto tracklist = Database::TrackList::find(LmsApp->getDbSession(), _tracklistId); if (tracklist) tracklist.remove(); } @@ -160,7 +161,11 @@ PlayQueue::PlayQueue() loadTrack(trackPos, false); }); - trackList = LmsApp->getUser()->getQueuedTrackList(LmsApp->getDbSession()); + + static const std::string queuedListName {"__queued_tracks__"}; + trackList = Database::TrackList::find(LmsApp->getDbSession(), queuedListName, Database::TrackList::Type::Internal, LmsApp->getUserId()); + if (!trackList) + trackList = Database::TrackList::create(LmsApp->getDbSession(), queuedListName, Database::TrackList::Type::Internal, false, LmsApp->getUser()); } else { @@ -192,7 +197,7 @@ PlayQueue::updateRadioBtn() Database::TrackList::pointer PlayQueue::getTrackList() const { - return Database::TrackList::getById(LmsApp->getDbSession(), _tracklistId); + return Database::TrackList::find(LmsApp->getDbSession(), _tracklistId); } bool @@ -326,7 +331,7 @@ PlayQueue::enqueueTracks(const std::vector& trackIds) std::size_t nbTracksToEnqueue {tracklist->getCount() + trackIds.size() > _nbMaxEntries ? _nbMaxEntries - tracklist->getCount() : trackIds.size()}; for (const Database::TrackId trackId : trackIds) { - Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (!track) continue; @@ -409,6 +414,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) Wt::WTemplate* entry = _entriesContainer->addNew(Wt::WString::tr("Lms.PlayQueue.template.entry")); + entry->bindString("is-selected", ""); entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; @@ -449,7 +455,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); } - entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); + entry->bindString("duration", durationToString(track->getDuration()), Wt::TextFormat::Plain); Wt::WText* playBtn {entry->bindNew("play-btn", Wt::WString::tr("Lms.PlayQueue.template.play-btn"), Wt::TextFormat::XHTML)}; playBtn->clicked().connect([=] @@ -487,27 +493,14 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) { Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; - bool isStarred {}; - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - if (auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}) - isStarred = LmsApp->getUser()->hasStarredTrack(track); - } - + const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), trackId)}; popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star")) ->triggered().connect(moreBtn, [=] { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; - if (!track) - return; - if (isStarred) - LmsApp->getUser().modify()->unstarTrack(track); + Service::get()->unstar(LmsApp->getUserId(), trackId); else - LmsApp->getUser().modify()->starTrack(track); + Service::get()->star(LmsApp->getUserId(), trackId); }); popup->addItem(Wt::WString::tr("Lms.Explore.download")) ->setLink(Wt::WLink {std::make_unique(trackId)}); @@ -519,7 +512,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) void PlayQueue::enqueueRadioTracks() { - const auto similarTrackIds {Service::get()->getSimilarTracksFromTrackList(LmsApp->getDbSession(), _tracklistId, 3)}; + const auto similarTrackIds {Service::get()->findSimilarTracksFromTrackList(_tracklistId, 3)}; std::vector trackToAddIds(std::cbegin(similarTrackIds), std::cend(similarTrackIds)); Random::shuffleContainer(trackToAddIds); diff --git a/src/lms/ui/PlayQueue.hpp b/src/lms/ui/PlayQueue.hpp index 1bbb996a..b90b28ce 100644 --- a/src/lms/ui/PlayQueue.hpp +++ b/src/lms/ui/PlayQueue.hpp @@ -25,7 +25,8 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Object.hpp" +#include "services/database/TrackListId.hpp" #include "PlayQueueAction.hpp" namespace Similarity diff --git a/src/lms/ui/PlayQueueAction.hpp b/src/lms/ui/PlayQueueAction.hpp index fffd8133..c2a45b34 100644 --- a/src/lms/ui/PlayQueueAction.hpp +++ b/src/lms/ui/PlayQueueAction.hpp @@ -22,11 +22,12 @@ #include #include -#include "database/Types.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" namespace UserInterface { - enum class PlayQueueAction { Play, diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 1f25251e..7bbc5672 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -34,8 +34,9 @@ #include "common/UUIDValidator.hpp" #include "common/ValueStringModel.hpp" -#include "auth/IPasswordService.hpp" -#include "database/Session.hpp" +#include "services/auth/IPasswordService.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" #include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" @@ -139,7 +140,7 @@ class SettingsModel : public Wt::WFormModel User::pointer user {LmsApp->getUser()}; { - const User::UITheme newTheme {Wt::asNumber(value(DarkModeField)) ? User::UITheme::Dark : User::UITheme::Light}; + const UITheme newTheme {Wt::asNumber(value(DarkModeField)) ? UITheme::Dark : UITheme::Light}; LmsTheme* lmsTheme {static_cast(LmsApp->theme().get())}; lmsTheme->setTheme(newTheme); @@ -197,7 +198,7 @@ class SettingsModel : public Wt::WFormModel if (_authPasswordService && !valueText(PasswordField).empty()) { - _authPasswordService->setPassword(LmsApp->getDbSession(), user->getId(), valueText(PasswordField).toUTF8()); + _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); } } @@ -208,7 +209,7 @@ class SettingsModel : public Wt::WFormModel User::pointer user {LmsApp->getUser()}; - setValue(DarkModeField, user->getUITheme() == User::UITheme::Dark); + setValue(DarkModeField, user->getUITheme() == UITheme::Dark); { const auto settings {*LmsApp->getMediaPlayer().getSettings()}; @@ -333,10 +334,10 @@ class SettingsModel : public Wt::WFormModel _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported); _transcodeBitrateModel = std::make_shared>(); - for (const Bitrate bitrate : User::audioTranscodeAllowedBitrates) + visitAllowedAudioBitrates([&](const Bitrate bitrate) { _transcodeBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate); - } + }); _transcodeFormatModel = std::make_shared>(); _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.mp3"), AudioFormat::MP3); @@ -351,10 +352,10 @@ class SettingsModel : public Wt::WFormModel _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.track"), MediaPlayer::Settings::ReplayGain::Mode::Track); _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.release"), MediaPlayer::Settings::ReplayGain::Mode::Release); - _subsonicArtistListModeModel = std::make_shared>(); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), User::SubsonicArtistListMode::ReleaseArtists); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), User::SubsonicArtistListMode::TrackArtists); + _subsonicArtistListModeModel = std::make_shared>(); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), SubsonicArtistListMode::AllArtists); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), SubsonicArtistListMode::ReleaseArtists); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), SubsonicArtistListMode::TrackArtists); _scrobblerModel = std::make_shared>(); _scrobblerModel->add(Wt::WString::tr("Lms.Settings.scrobbling.scrobbler.internal"), Scrobbler::Internal); @@ -368,7 +369,7 @@ class SettingsModel : public Wt::WFormModel std::shared_ptr> _transcodeBitrateModel; std::shared_ptr> _transcodeFormatModel; std::shared_ptr _replayGainModeModel; - std::shared_ptr> _subsonicArtistListModeModel; + std::shared_ptr> _subsonicArtistListModeModel; std::shared_ptr _scrobblerModel; }; diff --git a/src/lms/ui/TrackStringUtils.cpp b/src/lms/ui/TrackStringUtils.cpp index 8bd8cc39..79abede5 100644 --- a/src/lms/ui/TrackStringUtils.cpp +++ b/src/lms/ui/TrackStringUtils.cpp @@ -23,7 +23,7 @@ #include std::string -trackDurationToString(std::chrono::milliseconds msDuration) +durationToString(std::chrono::milliseconds msDuration) { const std::chrono::seconds duration {std::chrono::duration_cast(msDuration)}; diff --git a/src/lms/ui/TrackStringUtils.hpp b/src/lms/ui/TrackStringUtils.hpp index e07aec65..2af0b2fc 100644 --- a/src/lms/ui/TrackStringUtils.hpp +++ b/src/lms/ui/TrackStringUtils.hpp @@ -23,5 +23,5 @@ #include std::string -trackDurationToString(std::chrono::milliseconds msDuration); +durationToString(std::chrono::milliseconds msDuration); diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index d410b4cd..53cec972 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -26,10 +26,10 @@ #include #include -#include "database/Cluster.hpp" -#include "database/ScanSettings.hpp" -#include "database/Session.hpp" -#include "scanner/IScanner.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/ScanSettings.hpp" +#include "services/database/Session.hpp" +#include "services/scanner/IScannerService.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" @@ -240,7 +240,7 @@ DatabaseSettingsView::refreshView() { model->saveData(); - Service::get()->requestImmediateScan(false); + Service::get()->requestImmediateScan(false); LmsApp->notifyMsg(LmsApplication::MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved")); } @@ -257,7 +257,7 @@ DatabaseSettingsView::refreshView() immScanBtn->clicked().connect([=] { - Service::get()->requestImmediateScan(false); + Service::get()->requestImmediateScan(false); }); t->updateView(model.get()); diff --git a/src/lms/ui/admin/InitWizardView.cpp b/src/lms/ui/admin/InitWizardView.cpp index b187f472..efb14db4 100644 --- a/src/lms/ui/admin/InitWizardView.cpp +++ b/src/lms/ui/admin/InitWizardView.cpp @@ -24,9 +24,9 @@ #include #include -#include "auth/IPasswordService.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" +#include "services/auth/IPasswordService.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" @@ -65,12 +65,12 @@ class InitWizardModel : public Wt::WFormModel // Check if a user already exist // If it's the case, just do nothing - if (!Database::User::getAll(LmsApp->getDbSession()).empty()) + if (Database::User::getCount(LmsApp->getDbSession()) > 0) throw LmsException {"Admin user already created"}; Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8())}; user.modify()->setType(Database::UserType::ADMIN); - Service<::Auth::IPasswordService>::get()->setPassword(LmsApp->getDbSession(), user->getId(), valueText(PasswordField).toUTF8()); + Service<::Auth::IPasswordService>::get()->setPassword(user->getId(), valueText(PasswordField).toUTF8()); } bool validateField(Field field) diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index f01cfc71..6b5153db 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -28,9 +28,9 @@ #include #include -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "scanner/IScanner.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/scanner/IScannerService.hpp" #include "utils/Service.hpp" #include "LmsApplication.hpp" @@ -90,13 +90,13 @@ class ReportResource : public Wt::WResource for (const auto& duplicate : _stats.duplicates) { - const auto& track {Database::Track::getById(LmsApp->getDbSession(), duplicate.trackId)}; + const auto& track {Database::Track::find(LmsApp->getDbSession(), duplicate.trackId)}; if (!track) continue; response.out() << track->getPath().string(); if (auto mbid {track->getTrackMBID()}) - response.out() << " (Track MBID " << mbid->getAsString() << ")"; + response.out() << " (Recording MBID " << mbid->getAsString() << ")"; response.out() << " - " << duplicateReasonToWString(duplicate.reason).toUTF8() << '\n'; } @@ -122,7 +122,7 @@ class ReportResource : public Wt::WResource switch (reason) { case Scanner::DuplicateReason::SameHash: return Wt::WString::tr("Lms.Admin.ScannerController.same-hash"); - case Scanner::DuplicateReason::SameMBID: return Wt::WString::tr("Lms.Admin.ScannerController.same-mbid"); + case Scanner::DuplicateReason::SameRecordingMBID: return Wt::WString::tr("Lms.Admin.ScannerController.same-mbid"); } return "?"; } @@ -163,20 +163,20 @@ ScannerController::refreshContents() actionBtn->actionButton()->setText(Wt::WString::tr("Lms.Admin.ScannerController.scan-now")); actionBtn->actionButton()->clicked().connect([] { - Service::get()->requestImmediateScan(false); + Service::get()->requestImmediateScan(false); }); auto popup = std::make_unique(); popup->addItem(Wt::WString::tr("Lms.Admin.ScannerController.force-scan-now")); popup->itemSelected().connect([] { - Service::get()->requestImmediateScan(true); + Service::get()->requestImmediateScan(true); }); actionBtn->dropDownButton()->setMenu(std::move(popup)); actionBtn->dropDownButton()->addStyleClass("btn-primary"); - const IScanner::Status status {Service::get()->getStatus()}; + const IScannerService::Status status {Service::get()->getStatus()}; if (status.lastCompleteScanStats) { bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status") @@ -200,16 +200,16 @@ ScannerController::refreshContents() switch (status.currentState) { - case IScanner::State::NotScheduled: + case IScannerService::State::NotScheduled: bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled")); bindEmpty("step-status"); break; - case IScanner::State::Scheduled: + case IScannerService::State::Scheduled: bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled") .arg(status.nextScheduledScan.toString())); bindEmpty("step-status"); break; - case IScanner::State::InProgress: + case IScannerService::State::InProgress: bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress") .arg(static_cast(status.currentScanStepStats->currentStep) + 1) .arg(Scanner::ScanProgressStepCount)); diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index f27901b9..218526f1 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -27,9 +27,9 @@ #include -#include "auth/IPasswordService.hpp" -#include "database/User.hpp" -#include "database/Session.hpp" +#include "services/auth/IPasswordService.hpp" +#include "services/database/User.hpp" +#include "services/database/Session.hpp" #include "utils/IConfig.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" @@ -53,7 +53,7 @@ class UserModel : public Wt::WFormModel static inline const Field PasswordField {"password"}; static inline const Field DemoField {"demo"}; - UserModel(std::optional userId, ::Auth::IPasswordService* authPasswordService) + UserModel(std::optional userId, ::Auth::IPasswordService* authPasswordService) : _userId {userId} , _authPasswordService {authPasswordService} { @@ -82,28 +82,28 @@ class UserModel : public Wt::WFormModel if (_userId) { // Update user - Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; + User::pointer user {User::find(LmsApp->getDbSession(), *_userId)}; if (!user) throw UserNotFoundException {}; if (_authPasswordService && !valueText(PasswordField).empty()) - _authPasswordService->setPassword(LmsApp->getDbSession(), user->getId(), valueText(PasswordField).toUTF8()); + _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); } else { // Check races with other endpoints (subsonic API...) - Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; + User::pointer user {User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; if (user) throw UserNotAllowedException {}; // Create user - user = Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8()); + user = User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8()); if (Wt::asNumber(value(DemoField))) - user.modify()->setType(Database::UserType::DEMO); + user.modify()->setType(UserType::DEMO); if (_authPasswordService) - _authPasswordService->setPassword(LmsApp->getDbSession(), user->getId(), valueText(PasswordField).toUTF8()); + _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); } } @@ -115,20 +115,20 @@ class UserModel : public Wt::WFormModel auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; + const User::pointer user {User::find(LmsApp->getDbSession(), *_userId)}; if (!user) throw UserNotFoundException {}; else if (user == LmsApp->getUser()) throw UserNotAllowedException {}; } - Database::UserType getUserType() const + UserType getUserType() const { if (_userId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; + const User::pointer user {User::find(LmsApp->getDbSession(), *_userId)}; return user->getType(); } @@ -141,7 +141,7 @@ class UserModel : public Wt::WFormModel { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; + const User::pointer user {User::find(LmsApp->getDbSession(), *_userId)}; return user->getLoginName(); } @@ -156,7 +156,7 @@ class UserModel : public Wt::WFormModel { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; + const User::pointer user {User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; if (user) error = Wt::WString::tr("Lms.Admin.User.user-already-exists"); } @@ -164,7 +164,7 @@ class UserModel : public Wt::WFormModel { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - if (Wt::asNumber(value(DemoField)) && Database::User::getDemo(LmsApp->getDbSession())) + if (Wt::asNumber(value(DemoField)) && User::findDemoUser(LmsApp->getDbSession())) error = Wt::WString::tr("Lms.Admin.User.demo-account-already-exists"); } @@ -176,7 +176,7 @@ class UserModel : public Wt::WFormModel return false; } - std::optional _userId; + std::optional _userId; ::Auth::IPasswordService* _authPasswordService {}; }; @@ -196,7 +196,7 @@ UserView::refreshView() if (!wApp->internalPathMatches("/admin/user")) return; - const std::optional userId {StringUtils::readAs(wApp->internalPathNextPart("/admin/user/"))}; + const std::optional userId {StringUtils::readAs(wApp->internalPathNextPart("/admin/user/"))}; clear(); @@ -212,7 +212,7 @@ UserView::refreshView() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *userId)}; + const User::pointer user {User::find(LmsApp->getDbSession(), *userId)}; if (!user) throw UserNotFoundException {}; diff --git a/src/lms/ui/admin/UsersView.cpp b/src/lms/ui/admin/UsersView.cpp index 50bc58e7..e7292ef6 100644 --- a/src/lms/ui/admin/UsersView.cpp +++ b/src/lms/ui/admin/UsersView.cpp @@ -23,9 +23,9 @@ #include #include -#include "auth/IPasswordService.hpp" -#include "database/User.hpp" -#include "database/Session.hpp" +#include "services/auth/IPasswordService.hpp" +#include "services/database/User.hpp" +#include "services/database/Session.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" @@ -33,8 +33,10 @@ namespace UserInterface { +using namespace Database; + UsersView::UsersView() - : Wt::WTemplate(Wt::WString::tr("Lms.Admin.Users.template")) + : Wt::WTemplate {Wt::WString::tr("Lms.Admin.Users.template")} { addFunction("tr", &Wt::WTemplate::Functions::tr); @@ -45,7 +47,7 @@ UsersView::UsersView() setCondition("if-can-create-user", true); Wt::WPushButton* addBtn = bindNew("add-btn", Wt::WString::tr("Lms.Admin.Users.add")); - addBtn->clicked().connect([]() + addBtn->clicked().connect([] { LmsApp->setInternalPath("/admin/user", true); }); @@ -69,10 +71,10 @@ UsersView::refreshView() auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - auto users = Database::User::getAll(LmsApp->getDbSession()); - for (const auto& user : users) + const User::IdType currentUserId {LmsApp->getUser()}; + for (const UserId userId : User::find(LmsApp->getDbSession(), User::FindParameters {}).results) { - const Database::UserId userId {user->getId()}; + const User::pointer user {User::find(LmsApp->getDbSession(), userId)}; Wt::WTemplate* entry {_container->addNew(Wt::WString::tr("Lms.Admin.Users.template.entry"))}; @@ -86,7 +88,7 @@ UsersView::refreshView() } // Don't edit ourself this way - if (LmsApp->getUser() == user) + if (user->getId() == currentUserId) continue; entry->setCondition("if-edit", true); @@ -110,7 +112,7 @@ UsersView::refreshView() { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), userId)}; + User::pointer user {User::find(LmsApp->getDbSession(), userId)}; if (user) user.remove(); diff --git a/src/lms/ui/common/LoginNameValidator.cpp b/src/lms/ui/common/LoginNameValidator.cpp index d1574d3f..5fc121cf 100644 --- a/src/lms/ui/common/LoginNameValidator.cpp +++ b/src/lms/ui/common/LoginNameValidator.cpp @@ -20,7 +20,7 @@ #include "LoginNameValidator.hpp" #include -#include "database/User.hpp" +#include "services/database/User.hpp" namespace UserInterface { diff --git a/src/lms/ui/common/PasswordValidator.cpp b/src/lms/ui/common/PasswordValidator.cpp index 443d556f..2979739e 100644 --- a/src/lms/ui/common/PasswordValidator.cpp +++ b/src/lms/ui/common/PasswordValidator.cpp @@ -21,7 +21,7 @@ #include -#include "auth/IPasswordService.hpp" +#include "services/auth/IPasswordService.hpp" #include "utils/Service.hpp" #include "LmsApplication.hpp" @@ -80,7 +80,6 @@ namespace UserInterface return Wt::WValidator::validate(input); const auto checkResult {Service<::Auth::IPasswordService>::get()->checkUserPassword( - LmsApp->getDbSession(), boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), LmsApp->getUserLoginName(), input.toUTF8())}; diff --git a/src/lms/ui/common/PasswordValidator.hpp b/src/lms/ui/common/PasswordValidator.hpp index d504dbc1..e3b5a708 100644 --- a/src/lms/ui/common/PasswordValidator.hpp +++ b/src/lms/ui/common/PasswordValidator.hpp @@ -22,8 +22,8 @@ #include #include -#include "database/Types.hpp" -#include "auth/Types.hpp" +#include "services/database/Types.hpp" +#include "services/auth/Types.hpp" namespace UserInterface { diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp index 49e19731..f1df91a8 100644 --- a/src/lms/ui/explore/ArtistCollector.cpp +++ b/src/lms/ui/explore/ArtistCollector.cpp @@ -19,10 +19,10 @@ #include "ArtistCollector.hpp" -#include "database/Artist.hpp" -#include "database/User.hpp" -#include "database/TrackList.hpp" -#include "scrobbling/IScrobbling.hpp" +#include "services/database/Artist.hpp" +#include "services/database/User.hpp" +#include "services/database/TrackList.hpp" +#include "services/scrobbling/IScrobblingService.hpp" #include "utils/Service.hpp" #include "Filters.hpp" #include "LmsApplication.hpp" @@ -31,100 +31,92 @@ namespace UserInterface { using namespace Database; - std::vector> - ArtistCollector::get(std::optional range, bool& moreResults) + RangeResults + ArtistCollector::get(Database::Range range) { + Scrobbling::IScrobblingService& scrobbling {*Service::get()}; + range = getActualRange(range); - std::vector artists; + RangeResults artists; switch (getMode()) { case Mode::Random: - artists = getRandomArtists(range, moreResults); + artists = getRandomArtists(range); break; case Mode::Starred: - artists = Artist::getStarred(LmsApp->getDbSession(), - LmsApp->getUser(), - getFilters().getClusterIds(), - _linkType, - Artist::SortMethod::BySortName, - range, moreResults); + artists = scrobbling.getStarredArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, ArtistSortMethod::StarredDateDesc, range); break; case Mode::RecentlyPlayed: - artists = Service::get()->getRecentArtists(LmsApp->getDbSession(), LmsApp->getUser(), - getFilters().getClusterIds(), - _linkType, - range, moreResults); + artists = scrobbling.getRecentArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range); break; case Mode::MostPlayed: - artists = Service::get()->getTopArtists(LmsApp->getDbSession(), LmsApp->getUser(), - getFilters().getClusterIds(), - _linkType, - range, moreResults); + artists = scrobbling.getTopArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range); break; case Mode::RecentlyAdded: - artists = Artist::getLastWritten(LmsApp->getDbSession(), - std::nullopt, // after - getFilters().getClusterIds(), - _linkType, - range, moreResults); - break; - - case Mode::Search: - artists = Database::Artist::getByFilter(LmsApp->getDbSession(), - getFilters().getClusterIds(), - getSearchKeywords(), - std::nullopt, // no link - Database::Artist::SortMethod::BySortName, - range, moreResults); - break; - - case Mode::All: - artists = Artist::getByFilter(LmsApp->getDbSession(), - getFilters().getClusterIds(), - {}, - _linkType, - Artist::SortMethod::BySortName, - range, moreResults); - break; - } - - if (range && getMaxCount() && (range->offset + range->limit == *getMaxCount())) - moreResults = false; - - return artists; - } - - std::vector - ArtistCollector::getRandomArtists(std::optional range, bool& moreResults) - { - std::vector artists; - - assert(getMode() == Mode::Random); - - if (_randomArtists.empty()) - _randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), getFilters().getClusterIds(), _linkType, getMaxCount()); - - { - auto itBegin {std::cbegin(_randomArtists) + std::min(range ? range->offset : 0, _randomArtists.size())}; - auto itEnd {std::cbegin(_randomArtists) + std::min(range ? range->offset + range->limit : _randomArtists.size(), _randomArtists.size())}; - - for (auto it {itBegin}; it != itEnd; ++it) { - Artist::pointer artist {Artist::getById(LmsApp->getDbSession(), *it)}; - if (artist) - artists.push_back(artist); + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::LastWritten); + params.setRange(range); + + artists = Artist::find(LmsApp->getDbSession(), params); + break; } - moreResults = (itEnd != std::cend(_randomArtists)); + case Mode::Search: + { + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setKeywords(getSearchKeywords()); + params.setSortMethod(ArtistSortMethod::BySortName); + params.setRange(range); + + artists = Artist::find(LmsApp->getDbSession(), params); + break; + } + + case Mode::All: + { + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::BySortName); + params.setRange(range); + + artists = Artist::find(LmsApp->getDbSession(), params); + break; + } } + if (range.offset + range.size == getMaxCount()) + artists.moreResults = false; + return artists; } + RangeResults + ArtistCollector::getRandomArtists(Range range) + { + assert(getMode() == Mode::Random); + + if (!_randomArtists) + { + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::Random); + params.setRange(Range {0, getMaxCount()}); + + _randomArtists = Artist::find(LmsApp->getDbSession(), params); + } + + return _randomArtists->getSubRange(range); + } } // ns UserInterface diff --git a/src/lms/ui/explore/ArtistCollector.hpp b/src/lms/ui/explore/ArtistCollector.hpp index 05734af0..ab9d43fc 100644 --- a/src/lms/ui/explore/ArtistCollector.hpp +++ b/src/lms/ui/explore/ArtistCollector.hpp @@ -20,11 +20,10 @@ #pragma once #include -#include #include "DatabaseCollectorBase.hpp" - -#include "database/Types.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/Types.hpp" namespace Database { @@ -38,13 +37,13 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - std::vector> get(std::optional range, bool& moreResults); - void reset() { _randomArtists.clear(); } + Database::RangeResults get(Database::Range range); + void reset() { _randomArtists.reset(); } void setArtistLinkType(std::optional linkType) { _linkType = linkType; } private: - std::vector> getRandomArtists(std::optional range, bool& moreResults); - std::vector _randomArtists; + Database::RangeResults getRandomArtists(Range range); + std::optional> _randomArtists; std::optional _linkType; }; } // ns UserInterface diff --git a/src/lms/ui/explore/ArtistListHelpers.cpp b/src/lms/ui/explore/ArtistListHelpers.cpp index 85adb362..e16de3e2 100644 --- a/src/lms/ui/explore/ArtistListHelpers.cpp +++ b/src/lms/ui/explore/ArtistListHelpers.cpp @@ -20,7 +20,7 @@ #include -#include "database/Artist.hpp" +#include "services/database/Artist.hpp" #include "LmsApplication.hpp" namespace UserInterface::ArtistListHelpers diff --git a/src/lms/ui/explore/ArtistListHelpers.hpp b/src/lms/ui/explore/ArtistListHelpers.hpp index 02659bf7..d26a5182 100644 --- a/src/lms/ui/explore/ArtistListHelpers.hpp +++ b/src/lms/ui/explore/ArtistListHelpers.hpp @@ -23,7 +23,7 @@ #include -#include "database/Types.hpp" +#include "services/database/Object.hpp" namespace Database { diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 0e803db5..2c14fead 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -25,14 +25,15 @@ #include #include -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Release.hpp" -#include "database/ScanSettings.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/User.hpp" -#include "recommendation/IEngine.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Release.hpp" +#include "services/database/ScanSettings.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "services/scrobbling/IScrobblingService.hpp" +#include "services/recommendation/IRecommendationService.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" @@ -78,14 +79,14 @@ extractArtistIdFromInternalPath() if (mbid) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - if (const Database::Artist::pointer artist {Database::Artist::getByMBID(LmsApp->getDbSession(), *mbid)}) + if (const Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), *mbid)}) return artist->getId(); } return std::nullopt; } - return StringUtils::readAs(wApp->internalPathNextPart("/artist/")); + return StringUtils::readAs(wApp->internalPathNextPart("/artist/")); } void @@ -102,14 +103,11 @@ Artist::refreshView() if (!artistId) throw ArtistNotFoundException {}; - const auto similarArtistIds {Service::get()->getSimilarArtists(LmsApp->getDbSession(), - *artistId, - {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, - 5)}; + const auto similarArtistIds {Service::get()->getSimilarArtists(*artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, 5)}; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), *artistId)}; + const Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), *artistId)}; if (!artist) throw ArtistNotFoundException {}; @@ -168,26 +166,14 @@ Artist::refreshView() artistsAction.emit(PlayQueueAction::PlayLast, {_artistId}); }); - bool isStarred {}; - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - if (auto artist {Database::Artist::getById(LmsApp->getDbSession(), *artistId)}) - isStarred = LmsApp->getUser()->hasStarredArtist(artist); - } + const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), _artistId)}; popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star")) ->triggered().connect(this, [=] { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - auto artist {Database::Artist::getById(LmsApp->getDbSession(), *artistId)}; - if (!artist) - return; - if (isStarred) - LmsApp->getUser().modify()->unstarArtist(artist); + Service::get()->unstar(LmsApp->getUserId(), _artistId); else - LmsApp->getUser().modify()->starArtist(artist); + Service::get()->star(LmsApp->getUserId(), _artistId); }); popup->addItem(Wt::WString::tr("Lms.Explore.download")) ->setLink(Wt::WLink {std::make_unique(*artistId)}); @@ -198,7 +184,7 @@ Artist::refreshView() } void -Artist::refreshReleases(const Database::ObjectPtr& artist) +Artist::refreshReleases(const ObjectPtr& artist) { const auto releases {artist->getReleases(_filters->getClusterIds())}; if (releases.empty()) @@ -214,7 +200,7 @@ Artist::refreshReleases(const Database::ObjectPtr& artist) } void -Artist::refreshNonReleaseTracks(const Database::ObjectPtr& artist) +Artist::refreshNonReleaseTracks(const ObjectPtr& artist) { if (!artist->hasNonReleaseTracks()) return; @@ -230,7 +216,7 @@ Artist::refreshNonReleaseTracks(const Database::ObjectPtr& art } void -Artist::refreshSimilarArtists(const std::vector& similarArtistsId) +Artist::refreshSimilarArtists(const std::vector& similarArtistsId) { if (similarArtistsId.empty()) return; @@ -238,9 +224,9 @@ Artist::refreshSimilarArtists(const std::vector& similarArti setCondition("if-has-similar-artists", true); Wt::WContainerWidget* similarArtistsContainer {bindNew("similar-artists")}; - for (const Database::ArtistId artistId : similarArtistsId) + for (const ArtistId artistId : similarArtistsId) { - const Database::Artist::pointer similarArtist{Database::Artist::getById(LmsApp->getDbSession(), artistId)}; + const Database::Artist::pointer similarArtist {Database::Artist::find(LmsApp->getDbSession(), artistId)}; if (!similarArtist) continue; @@ -266,27 +252,24 @@ Artist::refreshLinks(const Database::Artist::pointer& artist) void Artist::addSomeNonReleaseTracks() { - bool moreResults {}; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + const Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), _artistId)}; + if (!artist) + return; + + const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {static_cast(_trackContainer->getCount()), _tracksBatchSize})}; + bool moreResults {tracks.moreResults}; + + for (const Track::pointer& track : tracks.results) { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)}; - if (!artist) - return; - - const auto tracks {artist->getNonReleaseTracks(std::nullopt, Database::Range {static_cast(_trackContainer->getCount()), _tracksBatchSize}, moreResults)}; - - for (const auto& track : tracks) + if (_trackContainer->getCount() == _tracksMaxCount) { - if (_trackContainer->getCount() == _tracksMaxCount) - { - moreResults = false; - break; - } - - _trackContainer->add(TrackListHelpers::createEntry(track, tracksAction)); + moreResults = false; + break; } + + _trackContainer->add(TrackListHelpers::createEntry(track, tracksAction)); } _trackContainer->setHasMore(moreResults); diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index 18b3894c..26255a6a 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -25,7 +25,7 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Object.hpp" #include "PlayQueueAction.hpp" namespace Database diff --git a/src/lms/ui/explore/ArtistsView.cpp b/src/lms/ui/explore/ArtistsView.cpp index 3d71b94c..d7ae20ed 100644 --- a/src/lms/ui/explore/ArtistsView.cpp +++ b/src/lms/ui/explore/ArtistsView.cpp @@ -21,8 +21,9 @@ #include -#include "database/Session.hpp" -#include "database/TrackArtistLink.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Session.hpp" +#include "services/database/TrackArtistLink.hpp" #include "utils/EnumSet.hpp" #include "utils/Logger.hpp" @@ -109,7 +110,7 @@ Artists::refreshView(ArtistCollector::Mode mode) } void -Artists::refreshView(std::optional linkType) +Artists::refreshView(std::optional linkType) { _artistCollector.setArtistLinkType(linkType); refreshView(); @@ -120,13 +121,13 @@ Artists::refreshArtistLinkTypes() { std::shared_ptr linkTypeModel {std::static_pointer_cast(_linkType->model())}; - EnumSet usedLinkTypes; + EnumSet usedLinkTypes; { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - usedLinkTypes = Database::TrackArtistLink::getUsedTypes(LmsApp->getDbSession()); + usedLinkTypes = TrackArtistLink::findUsedTypes(LmsApp->getDbSession()); } - auto addTypeIfUsed {[&](Database::TrackArtistLinkType linkType, std::string_view stringKey) + auto addTypeIfUsed {[&](TrackArtistLinkType linkType, std::string_view stringKey) { if (!usedLinkTypes.contains(linkType)) return; @@ -151,17 +152,16 @@ Artists::refreshArtistLinkTypes() void Artists::addSome() { - bool moreResults {}; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + const auto artistIds {_artistCollector.get(Range {static_cast(_container->getCount()), _batchSize})}; + for (const ArtistId artistId : artistIds.results) { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const auto artists {_artistCollector.get(Range {static_cast(_container->getCount()), _batchSize}, moreResults)}; - for (const auto& artist : artists) + if (const auto artist {Artist::find(LmsApp->getDbSession(), artistId)}) _container->add(ArtistListHelpers::createEntry(artist)); } - _container->setHasMore(moreResults); + _container->setHasMore(artistIds.moreResults); } } // namespace UserInterface diff --git a/src/lms/ui/explore/ArtistsView.hpp b/src/lms/ui/explore/ArtistsView.hpp index f7e61e22..350f683d 100644 --- a/src/lms/ui/explore/ArtistsView.hpp +++ b/src/lms/ui/explore/ArtistsView.hpp @@ -25,7 +25,7 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Types.hpp" #include "ArtistCollector.hpp" namespace UserInterface diff --git a/src/lms/ui/explore/DatabaseCollectorBase.cpp b/src/lms/ui/explore/DatabaseCollectorBase.cpp index a4e9cdfc..7a161fd7 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.cpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.cpp @@ -23,28 +23,23 @@ namespace UserInterface { - DatabaseCollectorBase::DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::optional maxCount) + DatabaseCollectorBase::DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::size_t maxCount) : _filters {filters} , _mode {defaultMode} , _maxCount {maxCount} { } - std::optional - DatabaseCollectorBase::getActualRange(std::optional range) const + DatabaseCollectorBase::Range + DatabaseCollectorBase::getActualRange(Range range) const { - if (std::optional maxCount {getMaxCount()}) - { - if (range) - range->limit = std::min(*maxCount - range->offset, range->limit); - else - range = Range {0, *maxCount}; - } + assert(range.offset < _maxCount); + range.size = std::min(_maxCount - range.offset, range.size); return range; } - std::optional + std::size_t DatabaseCollectorBase::getMaxCount() const { return _maxCount; diff --git a/src/lms/ui/explore/DatabaseCollectorBase.hpp b/src/lms/ui/explore/DatabaseCollectorBase.hpp index 224f7657..66732c32 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.hpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.hpp @@ -25,7 +25,7 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Types.hpp" namespace UserInterface { @@ -49,17 +49,16 @@ namespace UserInterface All }; - DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::optional maxCount = std::nullopt); + DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::size_t maxCount); Mode getMode() const { return _mode; } void setMode(Mode mode) { _mode = mode; } - void setMaxCount(std::size_t maxCount) { _maxCount = maxCount; } void setSearch(std::string_view search); protected: - std::optional getActualRange(std::optional range) const; - std::optional getMaxCount() const; - Filters& getFilters() { return _filters; } + Range getActualRange(Range range) const; + std::size_t getMaxCount() const; + Filters& getFilters() { return _filters; } const std::vector& getSearchKeywords() const { return _searchKeywords; } private: @@ -67,7 +66,7 @@ namespace UserInterface std::string _searchText; std::vector _searchKeywords; Mode _mode; - std::optional _maxCount; + std::size_t _maxCount; }; } // ns UserInterface diff --git a/src/lms/ui/explore/Explore.cpp b/src/lms/ui/explore/Explore.cpp index 8c821fdb..bf9b1239 100644 --- a/src/lms/ui/explore/Explore.cpp +++ b/src/lms/ui/explore/Explore.cpp @@ -23,10 +23,10 @@ #include #include -#include "database/Artist.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" #include "utils/Logger.hpp" #include "LmsApplication.hpp" @@ -138,7 +138,7 @@ getArtistsTracks(Database::Session& session, const std::vector -#include "database/Types.hpp" +#include "services/database/Types.hpp" #include "PlayQueueAction.hpp" -namespace UserInterface { - -class Filters; -class SearchView; - -class Explore : public Wt::WTemplate +namespace UserInterface { - public: - Explore(Filters* filters); + class Filters; + class SearchView; - void search(const Wt::WString& searchText); + class Explore : public Wt::WTemplate + { + public: + Explore(Filters* filters); - PlayQueueActionTrackSignal tracksAction; + void search(const Wt::WString& searchText); - private: + PlayQueueActionTrackSignal tracksAction; - void handleArtistsAction(PlayQueueAction action, const std::vector& artistsId); - void handleReleasesAction(PlayQueueAction action, const std::vector& releasesId); - void handleTracksAction(PlayQueueAction action, const std::vector& tracksId); - - Filters* _filters {}; - SearchView* _search {}; -}; + private: + void handleArtistsAction(PlayQueueAction action, const std::vector& artistsId); + void handleReleasesAction(PlayQueueAction action, const std::vector& releasesId); + void handleTracksAction(PlayQueueAction action, const std::vector& tracksId); + Filters* _filters {}; + SearchView* _search {}; + }; } // namespace UserInterface diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index f8ef1c74..b5807b1a 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -24,13 +24,15 @@ #include #include -#include "database/Cluster.hpp" -#include "database/Session.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Session.hpp" #include "LmsApplication.hpp" namespace UserInterface { +using namespace Database; + void Filters::showDialog() { @@ -52,13 +54,18 @@ Filters::showDialog() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const auto types {Database::ClusterType::getAllUsed(LmsApp->getDbSession())}; - for (const Database::ClusterType::pointer& type : types) - typeCombo->addItem(Wt::WString::fromUTF8(type->getName())); - - if (!types.empty()) + const auto clusterTypesIds {ClusterType::findUsed(LmsApp->getDbSession(), Range {})}; + for (const ClusterTypeId clusterTypeId : clusterTypesIds.results) { - for (const Database::Cluster::pointer& cluster : types.front()->getClusters()) + const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypeId)}; + typeCombo->addItem(Wt::WString::fromUTF8(clusterType->getName())); + } + + if (!clusterTypesIds.results.empty()) + { + const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypesIds.results.front())}; + + for (const Cluster::pointer cluster : clusterType->getClusters()) { if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName())); @@ -74,9 +81,9 @@ Filters::showDialog() auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - auto clusterType = Database::ClusterType::getByName(LmsApp->getDbSession(), name); + auto clusterType = ClusterType::find(LmsApp->getDbSession(), name); - for (const Database::Cluster::pointer& cluster : clusterType->getClusters()) + for (const Cluster::pointer& cluster : clusterType->getClusters()) { if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName())); @@ -98,16 +105,16 @@ Filters::showDialog() const std::string value {valueCombo->valueText().toUTF8()}; // TODO use a model to store the cluster.id() values - Database::ClusterId clusterId {}; + ClusterId clusterId {}; { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(LmsApp->getDbSession(), type)}; + ClusterType::pointer clusterType {ClusterType::find(LmsApp->getDbSession(), type)}; if (!clusterType) return; - Database::Cluster::pointer cluster {clusterType->getCluster(value)}; + Cluster::pointer cluster {clusterType->getCluster(value)}; if (!cluster) return; @@ -121,7 +128,7 @@ Filters::showDialog() } void -Filters::add(Database::ClusterId clusterId) +Filters::add(ClusterId clusterId) { Wt::WInteractWidget* filter {}; @@ -129,7 +136,7 @@ Filters::add(Database::ClusterId clusterId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::Cluster::pointer cluster {Database::Cluster::getById(LmsApp->getDbSession(), clusterId)}; + Cluster::pointer cluster {Cluster::find(LmsApp->getDbSession(), clusterId)}; if (!cluster) return; @@ -144,7 +151,7 @@ Filters::add(Database::ClusterId clusterId) filter->clicked().connect([=] { _filters->removeWidget(filter); - _clusterIds.erase(std::remove_if(std::begin(_clusterIds), std::end(_clusterIds), [clusterId](Database::ClusterId id) { return id == clusterId; }), std::end(_clusterIds)); + _clusterIds.erase(std::remove_if(std::begin(_clusterIds), std::end(_clusterIds), [clusterId](ClusterId id) { return id == clusterId; }), std::end(_clusterIds)); _sigUpdated.emit(); }); diff --git a/src/lms/ui/explore/Filters.hpp b/src/lms/ui/explore/Filters.hpp index 079d52c2..0f96398a 100644 --- a/src/lms/ui/explore/Filters.hpp +++ b/src/lms/ui/explore/Filters.hpp @@ -24,31 +24,27 @@ #include #include -#include "database/Types.hpp" +#include "services/database/ClusterId.hpp" #include "Filters.hpp" -namespace UserInterface { - -class Filters : public Wt::WTemplate +namespace UserInterface { - public: - Filters(); + class Filters : public Wt::WTemplate + { + public: + Filters(); - void add(Database::ClusterId clusterId); + void add(Database::ClusterId clusterId); + const std::vector& getClusterIds() const { return _clusterIds; } + Wt::Signal<>& updated() { return _sigUpdated; } - const std::vector& getClusterIds() const { return _clusterIds; } - - Wt::Signal<>& updated() { return _sigUpdated; } - - private: - - void showDialog(); - - Wt::WContainerWidget *_filters; - Wt::Signal<> _sigUpdated; - std::vector _clusterIds; -}; + private: + void showDialog(); + Wt::WContainerWidget *_filters; + Wt::Signal<> _sigUpdated; + std::vector _clusterIds; + }; } // namespace UserInterface diff --git a/src/lms/ui/explore/ReleaseCollector.cpp b/src/lms/ui/explore/ReleaseCollector.cpp index 473278df..e109f038 100644 --- a/src/lms/ui/explore/ReleaseCollector.cpp +++ b/src/lms/ui/explore/ReleaseCollector.cpp @@ -21,11 +21,11 @@ #include -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" -#include "database/TrackList.hpp" -#include "scrobbling/IScrobbling.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" +#include "services/database/TrackList.hpp" +#include "services/scrobbling/IScrobblingService.hpp" #include "utils/Service.hpp" #include "Filters.hpp" #include "LmsApplication.hpp" @@ -34,90 +34,89 @@ namespace UserInterface { using namespace Database; - std::vector - ReleaseCollector::get(std::optional range, bool& moreResults) + RangeResults + ReleaseCollector::get(Database::Range range) { + Scrobbling::IScrobblingService& scrobbling {*Service::get()}; + range = getActualRange(range); - std::vector releases; + RangeResults releases; switch (getMode()) { case Mode::Random: - releases = getRandomReleases(range, moreResults); + releases = getRandomReleases(range); break; case Mode::Starred: - releases = Release::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults); + releases = scrobbling.getStarredReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case ReleaseCollector::Mode::RecentlyPlayed: - releases = Service::get()->getRecentReleases(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults); + releases = scrobbling.getRecentReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case Mode::MostPlayed: - releases = Service::get()->getTopReleases(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults); + releases = scrobbling.getTopReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case Mode::RecentlyAdded: - releases = Release::getLastWritten(LmsApp->getDbSession(), std::nullopt, getFilters().getClusterIds(), range, moreResults); - break; - - case Mode::Search: - releases = Release::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), getSearchKeywords(), range, moreResults); - break; - - case Mode::All: - releases = Release::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), {}, range, moreResults); - break; - } - - if (range && getMaxCount() && (range->offset + range->limit == *getMaxCount())) - moreResults = false; - - return releases; - } - - std::vector - ReleaseCollector::getAll() - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - bool moreResults; - const auto releases {get(std::nullopt, moreResults)}; - - std::vector res; - res.reserve(releases.size()); - std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const Release::pointer& release) { return release->getId(); }); - - return res; - } - - std::vector - ReleaseCollector::getRandomReleases(std::optional range, bool& moreResults) - { - std::vector releases; - - assert(getMode() == Mode::Random); - - if (_randomReleases.empty()) - _randomReleases = Release::getAllIdsRandom(LmsApp->getDbSession(), getFilters().getClusterIds(), getMaxCount()); - - { - auto itBegin {std::cbegin(_randomReleases) + std::min(range ? range->offset : 0, _randomReleases.size())}; - auto itEnd {std::cbegin(_randomReleases) + std::min(range ? range->offset + range->limit : _randomReleases.size(), _randomReleases.size())}; - - for (auto it {itBegin}; it != itEnd; ++it) { - Release::pointer release {Release::getById(LmsApp->getDbSession(), *it)}; - if (release) - releases.push_back(release); + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(ReleaseSortMethod::LastWritten); + params.setRange(range); + + releases = Release::find(LmsApp->getDbSession(), params); + break; } - moreResults = (itEnd != std::cend(_randomReleases)); + case Mode::Search: + { + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setKeywords(getSearchKeywords()); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); + + releases = Release::find(LmsApp->getDbSession(), params); + break; + } + + case Mode::All: + { + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); + + releases = Release::find(LmsApp->getDbSession(), params); + break; + } } + if (range.offset + range.size == getMaxCount()) + releases.moreResults = false; + return releases; } + RangeResults + ReleaseCollector::getRandomReleases(Range range) + { + assert(getMode() == Mode::Random); + + if (!_randomReleases) + { + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(ReleaseSortMethod::Random); + params.setRange({0, getMaxCount()}); + _randomReleases = Release::find(LmsApp->getDbSession(), params); + } + + return _randomReleases->getSubRange(range); + } + } // ns UserInterface diff --git a/src/lms/ui/explore/ReleaseCollector.hpp b/src/lms/ui/explore/ReleaseCollector.hpp index 3882cc80..924aabe6 100644 --- a/src/lms/ui/explore/ReleaseCollector.hpp +++ b/src/lms/ui/explore/ReleaseCollector.hpp @@ -23,6 +23,10 @@ #include "DatabaseCollectorBase.hpp" +#include "services/database/Object.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/Types.hpp" + namespace Database { class Release; @@ -35,13 +39,12 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - std::vector> get(std::optional range, bool& moreResults); - std::vector getAll(); - void reset() { _randomReleases.clear(); } + Database::RangeResults get(Database::Range range); + void reset() { _randomReleases.reset(); } private: - std::vector> getRandomReleases(std::optional range, bool& moreResults); - std::vector _randomReleases; + Database::RangeResults getRandomReleases(Range range); + std::optional> _randomReleases; }; } // ns UserInterface diff --git a/src/lms/ui/explore/ReleaseListHelpers.cpp b/src/lms/ui/explore/ReleaseListHelpers.cpp index 71c5daa5..e2c40f65 100644 --- a/src/lms/ui/explore/ReleaseListHelpers.cpp +++ b/src/lms/ui/explore/ReleaseListHelpers.cpp @@ -23,8 +23,8 @@ #include #include -#include "database/Artist.hpp" -#include "database/Release.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Release.hpp" #include "resource/CoverResource.hpp" #include "LmsApplication.hpp" @@ -33,7 +33,6 @@ using namespace Database; namespace UserInterface::ReleaseListHelpers { - static std::unique_ptr createEntryInternal(const Release::pointer& release, const std::string& templateKey, const Artist::pointer& artist, const bool showYear) @@ -104,6 +103,5 @@ namespace UserInterface::ReleaseListHelpers { return createEntry(release, artist, true); } - } // namespace UserInterface diff --git a/src/lms/ui/explore/ReleaseListHelpers.hpp b/src/lms/ui/explore/ReleaseListHelpers.hpp index 3aad6698..41550b85 100644 --- a/src/lms/ui/explore/ReleaseListHelpers.hpp +++ b/src/lms/ui/explore/ReleaseListHelpers.hpp @@ -22,7 +22,7 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Object.hpp" namespace Database { diff --git a/src/lms/ui/explore/ReleasePopup.cpp b/src/lms/ui/explore/ReleasePopup.cpp index 7224683f..ae8aefcb 100644 --- a/src/lms/ui/explore/ReleasePopup.cpp +++ b/src/lms/ui/explore/ReleasePopup.cpp @@ -21,15 +21,16 @@ #include -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/User.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" +#include "services/scrobbling/IScrobblingService.hpp" #include "resource/DownloadResource.hpp" +#include "utils/Service.hpp" #include "LmsApplication.hpp" namespace UserInterface { - void displayReleasePopupMenu(Wt::WInteractWidget& target, Database::ReleaseId releaseId, @@ -48,33 +49,18 @@ namespace UserInterface releasesAction.emit(PlayQueueAction::PlayLast, {releaseId}); }); - bool isStarred {}; - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - if (auto release {Database::Release::getById(LmsApp->getDbSession(), releaseId)}) - isStarred = LmsApp->getUser()->hasStarredRelease(release); - } - + const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), releaseId)}; popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star")) ->triggered().connect(&target, [=] { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - auto release {Database::Release::getById(LmsApp->getDbSession(), releaseId)}; - if (!release) - return; - if (isStarred) - LmsApp->getUser().modify()->unstarRelease(release); + Service::get()->unstar(LmsApp->getUserId(), releaseId); else - LmsApp->getUser().modify()->starRelease(release); + Service::get()->star(LmsApp->getUserId(), releaseId); }); popup->addItem(Wt::WString::tr("Lms.Explore.download")) ->setLink(Wt::WLink {std::make_unique(releaseId)}); popup->popup(&target); } - } // namespace UserInterface - diff --git a/src/lms/ui/explore/ReleasePopup.hpp b/src/lms/ui/explore/ReleasePopup.hpp index 5ffb4f48..7bf35ad8 100644 --- a/src/lms/ui/explore/ReleasePopup.hpp +++ b/src/lms/ui/explore/ReleasePopup.hpp @@ -20,9 +20,8 @@ #pragma once #include -#include -#include "database/Types.hpp" +#include "services/database/Types.hpp" #include "PlayQueueAction.hpp" namespace UserInterface diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index ceb82d90..13ac9672 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -25,12 +25,12 @@ #include #include -#include "database/Cluster.hpp" -#include "database/Release.hpp" -#include "database/ScanSettings.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "recommendation/IEngine.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Release.hpp" +#include "services/database/ScanSettings.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/recommendation/IRecommendationService.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" @@ -78,14 +78,14 @@ extractReleaseIdFromInternalPath() if (mbid) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - if (const Database::Release::pointer release {Database::Release::getByMBID(LmsApp->getDbSession(), *mbid)}) + if (const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), *mbid)}) return release->getId(); } return std::nullopt; } - return StringUtils::readAs(wApp->internalPathNextPart("/release/")); + return StringUtils::readAs(wApp->internalPathNextPart("/release/")); } @@ -101,11 +101,11 @@ Release::refreshView() if (!releaseId) throw ReleaseNotFoundException {}; - auto similarReleasesIds {Service::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 6)}; + auto similarReleasesIds {Service::get()->getSimilarReleases(*releaseId, 6)}; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), *releaseId)}; + const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), *releaseId)}; if (!release) throw ReleaseNotFoundException {}; @@ -129,6 +129,8 @@ Release::refreshView() } } + bindString("duration", durationToString(release->getDuration()), Wt::TextFormat::Plain); + refreshReleaseArtists(release); { @@ -218,7 +220,7 @@ Release::refreshView() entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); - const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; + const auto artists {track->getArtists({TrackArtistLinkType::Artist})}; if (variousArtists && !artists.empty()) { entry->setCondition("if-has-artists", true); @@ -250,9 +252,9 @@ Release::refreshView() displayTrackPopupMenu(*moreBtn, trackId, tracksAction); }); - entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); + entry->bindString("duration", durationToString(track->getDuration()), Wt::TextFormat::Plain); - LmsApp->getMediaPlayer().trackLoaded.connect(entry, [=] (Database::TrackId loadedTrackId) + LmsApp->getMediaPlayer().trackLoaded.connect(entry, [=] (TrackId loadedTrackId) { entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : ""); }); @@ -270,12 +272,12 @@ Release::refreshView() void Release::refreshReleaseArtists(const Database::Release::pointer& release) { - std::vector> artists; + std::vector> artists; artists = release->getReleaseArtists(); if (artists.empty()) { - artists = release->getArtists(Database::TrackArtistLinkType::Artist); + artists = release->getArtists(TrackArtistLinkType::Artist); if (artists.size() > 1) { setCondition("if-has-various-release-artists", true); @@ -340,7 +342,7 @@ Release::refreshLinks(const Database::Release::pointer& release) } void -Release::refreshSimilarReleases(const std::vector& similarReleasesId) +Release::refreshSimilarReleases(const std::vector& similarReleasesId) { if (similarReleasesId.empty()) return; @@ -348,9 +350,9 @@ Release::refreshSimilarReleases(const std::vector& similarR setCondition("if-has-similar-releases", true); auto* similarReleasesContainer {bindNew("similar-releases")}; - for (const Database::ReleaseId id : similarReleasesId) + for (const ReleaseId id : similarReleasesId) { - const Database::Release::pointer similarRelease{Database::Release::getById(LmsApp->getDbSession(), id)}; + const Database::Release::pointer similarRelease {Database::Release::find(LmsApp->getDbSession(), id)}; if (!similarRelease) continue; diff --git a/src/lms/ui/explore/ReleaseView.hpp b/src/lms/ui/explore/ReleaseView.hpp index d3700eaa..41f9fbf6 100644 --- a/src/lms/ui/explore/ReleaseView.hpp +++ b/src/lms/ui/explore/ReleaseView.hpp @@ -22,7 +22,7 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Object.hpp" #include "PlayQueueAction.hpp" namespace Database @@ -32,7 +32,6 @@ namespace Database namespace UserInterface { - class Filters; class Release : public Wt::WTemplate { @@ -51,6 +50,5 @@ namespace UserInterface Filters* _filters {}; }; - } // namespace UserInterface diff --git a/src/lms/ui/explore/ReleasesView.cpp b/src/lms/ui/explore/ReleasesView.cpp index 93c56428..6ac2637b 100644 --- a/src/lms/ui/explore/ReleasesView.cpp +++ b/src/lms/ui/explore/ReleasesView.cpp @@ -23,7 +23,8 @@ #include #include -#include "database/Session.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" #include "common/InfiniteScrollingContainer.hpp" #include "ReleaseListHelpers.hpp" #include "Filters.hpp" @@ -115,23 +116,24 @@ Releases::refreshView(ReleaseCollector::Mode mode) void Releases::addSome() { - bool moreResults {}; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + const auto releaseIds {_releaseCollector.get(Range {static_cast(_container->getCount()), _batchSize})}; + for (const ReleaseId releaseId : releaseIds.results) { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const auto releases {_releaseCollector.get(Range {static_cast(_container->getCount()), _batchSize}, moreResults)}; - for (const auto& release : releases) + if (const Release::pointer release {Release::find(LmsApp->getDbSession(), releaseId)}) _container->add(ReleaseListHelpers::createEntry(release)); } - _container->setHasMore(moreResults); + _container->setHasMore(releaseIds.moreResults); } -std::vector +std::vector Releases::getAllReleases() { - return _releaseCollector.getAll(); + RangeResults releaseIds {_releaseCollector.get(Range {})}; + + return std::move(releaseIds.results); } } // namespace UserInterface diff --git a/src/lms/ui/explore/ReleasesView.hpp b/src/lms/ui/explore/ReleasesView.hpp index f2a9b91b..9661678f 100644 --- a/src/lms/ui/explore/ReleasesView.hpp +++ b/src/lms/ui/explore/ReleasesView.hpp @@ -24,7 +24,7 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Types.hpp" #include "PlayQueueAction.hpp" #include "ReleaseCollector.hpp" @@ -41,7 +41,6 @@ namespace UserInterface PlayQueueActionReleaseSignal releasesAction; private: - void refreshView(); void refreshView(ReleaseCollector::Mode mode); diff --git a/src/lms/ui/explore/SearchView.cpp b/src/lms/ui/explore/SearchView.cpp index 90a950e8..ac573799 100644 --- a/src/lms/ui/explore/SearchView.cpp +++ b/src/lms/ui/explore/SearchView.cpp @@ -25,10 +25,10 @@ #include #include -#include "database/Artist.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" #include "common/InfiniteScrollingContainer.hpp" #include "common/LoadingIndicator.hpp" @@ -38,9 +38,10 @@ #include "ReleaseListHelpers.hpp" #include "TrackListHelpers.hpp" +using namespace Database; + namespace UserInterface { - SearchView::SearchView(Filters* filters) : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")} , _filters {filters} @@ -134,17 +135,22 @@ namespace UserInterface SearchView::addSomeArtists() { InfiniteScrollingContainer& results {getResultContainer(Mode::Artist)}; - bool moreResults {}; { + using namespace Database; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Range range {results.getCount(), getBatchSize(Mode::Artist)}; - for (const auto& artist : _artistCollector.get(range, moreResults)) + const Range range {results.getCount(), getBatchSize(Mode::Artist)}; + const RangeResults artistIds {_artistCollector.get(range)}; + for (const ArtistId artistId : artistIds.results) + { + const Artist::pointer artist {Artist::find(LmsApp->getDbSession(), artistId)}; results.add(ArtistListHelpers::createEntry(artist)); - } + } - results.setHasMore(moreResults); + results.setHasMore(artistIds.moreResults); + } getItemMenu(Mode::Artist).setDisabled(results.getCount() == 0); } @@ -156,14 +162,21 @@ namespace UserInterface bool moreResults {}; { + using namespace Database; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Range range {results.getCount(), getBatchSize(Mode::Release)}; - for (const auto& release : _releaseCollector.get(range, moreResults)) - results.add(ReleaseListHelpers::createEntry(release)); - } + const Range range {results.getCount(), getBatchSize(Mode::Release)}; + const RangeResults releaseIds {_releaseCollector.get(range)}; - results.setHasMore(moreResults); + for (const ReleaseId releaseId : releaseIds.results) + { + const Release::pointer release {Release::find(LmsApp->getDbSession(), releaseId)}; + results.add(ReleaseListHelpers::createEntry(release)); + } + + results.setHasMore(moreResults); + } getItemMenu(Mode::Release).setDisabled(results.getCount() == 0); } @@ -172,22 +185,24 @@ namespace UserInterface SearchView::addSomeTracks() { InfiniteScrollingContainer& results {getResultContainer(Mode::Track)}; - bool moreResults {}; - { + using namespace Database; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Range range {results.getCount(), getBatchSize(Mode::Track)}; - for (const auto& track : _trackCollector.get(range, moreResults)) - results.add(TrackListHelpers::createEntry(track, tracksAction)); - } + const Range range {results.getCount(), getBatchSize(Mode::Track)}; + const RangeResults trackIds {_trackCollector.get(range)}; - results.setHasMore(moreResults); + for (const TrackId trackId : trackIds.results) + { + const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}; + results.add(TrackListHelpers::createEntry(track, tracksAction)); + } + + results.setHasMore(trackIds.moreResults); + } getItemMenu(Mode::Track).setDisabled(results.getCount() == 0); } - - - } // namespace UserInterface diff --git a/src/lms/ui/explore/TrackCollector.cpp b/src/lms/ui/explore/TrackCollector.cpp index 26495626..bbefe4cd 100644 --- a/src/lms/ui/explore/TrackCollector.cpp +++ b/src/lms/ui/explore/TrackCollector.cpp @@ -21,11 +21,11 @@ #include -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackList.hpp" -#include "database/User.hpp" -#include "scrobbling/IScrobbling.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackList.hpp" +#include "services/database/User.hpp" +#include "services/scrobbling/IScrobblingService.hpp" #include "utils/Service.hpp" #include "Filters.hpp" #include "LmsApplication.hpp" @@ -34,89 +34,85 @@ namespace UserInterface { using namespace Database; - std::vector - TrackCollector::get(std::optional range, bool& moreResults) + RangeResults + TrackCollector::get(Range range) { + Scrobbling::IScrobblingService& scrobbling {*Service::get()}; range = getActualRange(range); - std::vector releases; + RangeResults tracks; switch (getMode()) { case Mode::Random: - releases = getRandomTracks(range, moreResults); + tracks = getRandomTracks(range); break; case Mode::Starred: - releases = Track::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults); + tracks = scrobbling.getStarredTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case TrackCollector::Mode::RecentlyPlayed: - releases = Service::get()->getRecentTracks(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults); + tracks = scrobbling.getRecentTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case Mode::MostPlayed: - releases = Service::get()->getTopTracks(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults); + tracks = scrobbling.getTopTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case Mode::RecentlyAdded: - releases = Track::getLastWritten(LmsApp->getDbSession(), std::nullopt, getFilters().getClusterIds(), range, moreResults); - break; - - case Mode::Search: - releases = Track::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), getSearchKeywords(), range, moreResults); - break; - - case Mode::All: - releases = Track::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), {}, range, moreResults); - break; - } - - if (range && getMaxCount() && (range->offset + range->limit == *getMaxCount())) - moreResults = false; - - return releases; - } - - std::vector - TrackCollector::getAll() - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - bool moreResults; - const auto tracks {get(std::nullopt, moreResults)}; - - std::vector res; - res.reserve(tracks.size()); - std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Track::pointer& track) { return track->getId(); }); - - return res; - } - - std::vector - TrackCollector::getRandomTracks(std::optional range, bool& moreResults) - { - std::vector releases; - - assert(getMode() == Mode::Random); - - if (_randomTracks.empty()) - _randomTracks = Track::getAllIdsRandom(LmsApp->getDbSession(), getFilters().getClusterIds(), getMaxCount()); - - { - auto itBegin {std::cbegin(_randomTracks) + std::min(range ? range->offset : 0, _randomTracks.size())}; - auto itEnd {std::cbegin(_randomTracks) + std::min(range ? range->offset + range->limit : _randomTracks.size(), _randomTracks.size())}; - - for (auto it {itBegin}; it != itEnd; ++it) { - Track::pointer release {Track::getById(LmsApp->getDbSession(), *it)}; - if (release) - releases.push_back(release); + Track::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(TrackSortMethod::LastWritten); + params.setRange(range); + + tracks = Track::find(LmsApp->getDbSession(), params); + break; } - moreResults = (itEnd != std::cend(_randomTracks)); + case Mode::Search: + { + Track::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setKeywords(getSearchKeywords()); + params.setRange(range); + + tracks = Track::find(LmsApp->getDbSession(), params); + break; + } + + case Mode::All: + { + Track::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setRange(range); + + tracks = Track::find(LmsApp->getDbSession(), params); + break; + } } - return releases; + if (range.offset + range.size == getMaxCount()) + tracks.moreResults = false; + + return tracks; + } + + RangeResults + TrackCollector::getRandomTracks(Range range) + { + assert(getMode() == Mode::Random); + + if (!_randomTracks) + { + Track::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(TrackSortMethod::Random); + params.setRange({0, getMaxCount()}); + _randomTracks = Track::find(LmsApp->getDbSession(), params); + } + + return _randomTracks->getSubRange(range); } } // ns UserInterface diff --git a/src/lms/ui/explore/TrackCollector.hpp b/src/lms/ui/explore/TrackCollector.hpp index 79011adf..a87f9759 100644 --- a/src/lms/ui/explore/TrackCollector.hpp +++ b/src/lms/ui/explore/TrackCollector.hpp @@ -21,6 +21,8 @@ #include +#include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" #include "DatabaseCollectorBase.hpp" namespace Database @@ -35,13 +37,12 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - std::vector> get(std::optional range, bool& moreResults); - std::vector getAll(); - void reset() { _randomTracks.clear(); } + Database::RangeResults get(Database::Range range); + void reset() { _randomTracks.reset(); } private: - std::vector> getRandomTracks(std::optional range, bool& moreResults); - std::vector _randomTracks; + Database::RangeResults getRandomTracks(Range range); + std::optional> _randomTracks; }; } // ns UserInterface diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index ba36cb9d..49509385 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -24,9 +24,9 @@ #include #include -#include "database/Artist.hpp" -#include "database/Release.hpp" -#include "database/Track.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Release.hpp" +#include "services/database/Track.hpp" #include "resource/DownloadResource.hpp" #include "resource/CoverResource.hpp" #include "LmsApplication.hpp" @@ -87,7 +87,7 @@ namespace UserInterface::TrackListHelpers cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); } - entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); + entry->bindString("duration", durationToString(track->getDuration()), Wt::TextFormat::Plain); Wt::WText* playBtn = entry->bindNew("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML); playBtn->clicked().connect([trackId, &tracksAction] diff --git a/src/lms/ui/explore/TrackListHelpers.hpp b/src/lms/ui/explore/TrackListHelpers.hpp index 2e3ea306..00b322f9 100644 --- a/src/lms/ui/explore/TrackListHelpers.hpp +++ b/src/lms/ui/explore/TrackListHelpers.hpp @@ -22,7 +22,7 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Object.hpp" #include "PlayQueueAction.hpp" namespace Database diff --git a/src/lms/ui/explore/TrackPopup.cpp b/src/lms/ui/explore/TrackPopup.cpp index 8884347c..7020d2d4 100644 --- a/src/lms/ui/explore/TrackPopup.cpp +++ b/src/lms/ui/explore/TrackPopup.cpp @@ -21,15 +21,16 @@ #include -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/User.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "services/scrobbling/IScrobblingService.hpp" +#include "utils/Service.hpp" #include "resource/DownloadResource.hpp" #include "LmsApplication.hpp" namespace UserInterface { - void displayTrackPopupMenu(Wt::WInteractWidget& target, Database::TrackId trackId, @@ -43,26 +44,20 @@ namespace UserInterface tracksAction.emit(PlayQueueAction::PlayLast, {trackId}); }); - bool isStarred {}; - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - if (auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}) - isStarred = LmsApp->getUser()->hasStarredTrack(track); - } + const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), trackId)}; popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star")) ->triggered().connect(&target, [=] { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + auto track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (!track) return; if (isStarred) - LmsApp->getUser().modify()->unstarTrack(track); + Service::get()->unstar(LmsApp->getUserId(), trackId); else - LmsApp->getUser().modify()->starTrack(track); + Service::get()->star(LmsApp->getUserId(), trackId); }); popup->addItem(Wt::WString::tr("Lms.Explore.download")) ->setLink(Wt::WLink {std::make_unique(trackId)}); diff --git a/src/lms/ui/explore/TrackPopup.hpp b/src/lms/ui/explore/TrackPopup.hpp index 1597d994..9ffe7474 100644 --- a/src/lms/ui/explore/TrackPopup.hpp +++ b/src/lms/ui/explore/TrackPopup.hpp @@ -22,7 +22,7 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Types.hpp" #include "PlayQueueAction.hpp" namespace UserInterface diff --git a/src/lms/ui/explore/TracksView.cpp b/src/lms/ui/explore/TracksView.cpp index daec0fcd..1b602ca8 100644 --- a/src/lms/ui/explore/TracksView.cpp +++ b/src/lms/ui/explore/TracksView.cpp @@ -23,7 +23,8 @@ #include #include -#include "database/Session.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" #include "utils/Logger.hpp" #include "common/InfiniteScrollingContainer.hpp" @@ -118,24 +119,25 @@ Tracks::refreshView(TrackCollector::Mode mode) void Tracks::addSome() { - bool moreResults {}; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + const auto trackIds {_trackCollector.get(Range {static_cast(_container->getCount()), _batchSize})}; + + for (const TrackId trackId : trackIds.results) { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const auto tracks {_trackCollector.get(Range {static_cast(_container->getCount()), _batchSize}, moreResults)}; - - for (const auto& track : tracks) + if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}) _container->add(TrackListHelpers::createEntry(track, tracksAction)); } - _container->setHasMore(moreResults); + _container->setHasMore(trackIds.moreResults); } std::vector Tracks::getAllTracks() { - return _trackCollector.getAll(); + RangeResults trackIds {_trackCollector.get(Range {})}; + + return std::move(trackIds.results); } diff --git a/src/lms/ui/explore/TracksView.hpp b/src/lms/ui/explore/TracksView.hpp index ac011e01..37e7b4c5 100644 --- a/src/lms/ui/explore/TracksView.hpp +++ b/src/lms/ui/explore/TracksView.hpp @@ -24,7 +24,7 @@ #include #include -#include "database/Types.hpp" +#include "services/database/Types.hpp" #include "PlayQueueAction.hpp" #include "TrackCollector.hpp" diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp index def318df..792128c0 100644 --- a/src/lms/ui/resource/AudioFileResource.cpp +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -23,8 +23,8 @@ #include #include "av/IAudioFile.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" #include "utils/FileResourceHandlerCreator.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" @@ -51,7 +51,7 @@ getTrackPathFromTrackId(Database::TrackId trackId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (!track) { LOG(ERROR) << "Missing track"; diff --git a/src/lms/ui/resource/AudioFileResource.hpp b/src/lms/ui/resource/AudioFileResource.hpp index fda116eb..589ac51e 100644 --- a/src/lms/ui/resource/AudioFileResource.hpp +++ b/src/lms/ui/resource/AudioFileResource.hpp @@ -21,29 +21,21 @@ #include -#include "database/Types.hpp" +#include "services/database/TrackId.hpp" - -namespace UserInterface { - -class AudioFileResource : public Wt::WResource +namespace UserInterface { - public: - ~AudioFileResource(); + class AudioFileResource : public Wt::WResource + { + public: + ~AudioFileResource(); - std::string getUrl(Database::TrackId trackId) const; + std::string getUrl(Database::TrackId trackId) const; - private: - - static constexpr std::size_t _chunkSize {262144}; - - void handleRequest(const Wt::Http::Request& request, - Wt::Http::Response& response) override; - -}; + private: + static constexpr std::size_t _chunkSize {262144}; + void handleRequest(const Wt::Http::Request& request, + Wt::Http::Response& response) override; + }; } // namespace UserInterface - - - - diff --git a/src/lms/ui/resource/AudioTranscodeResource.cpp b/src/lms/ui/resource/AudioTranscodeResource.cpp index 66ecac06..c8982fbd 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.cpp +++ b/src/lms/ui/resource/AudioTranscodeResource.cpp @@ -25,9 +25,9 @@ #include "av/TranscodeParameters.hpp" #include "av/TranscodeResourceHandlerCreator.hpp" #include "av/Types.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/User.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" @@ -41,7 +41,6 @@ namespace StringUtils std::optional readAs(std::string_view str) { - auto encodedFormat {readAs(str)}; if (!encodedFormat) return std::nullopt; @@ -150,7 +149,7 @@ readTranscodeParameters(const Wt::Http::Request& request) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), *trackId)}; + const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)}; if (!track) { LOG(ERROR) << "Missing track"; @@ -159,7 +158,7 @@ readTranscodeParameters(const Wt::Http::Request& request) parameters.file = track->getPath(); - if (Database::User::audioTranscodeAllowedBitrates.find(*bitrate) == std::cend(Database::User::audioTranscodeAllowedBitrates)) + if (!Database::isAudioBitrateAllowed(*bitrate)) { LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed"; return std::nullopt; diff --git a/src/lms/ui/resource/AudioTranscodeResource.hpp b/src/lms/ui/resource/AudioTranscodeResource.hpp index ef516e91..a4240b15 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.hpp +++ b/src/lms/ui/resource/AudioTranscodeResource.hpp @@ -22,29 +22,28 @@ #include #include -#include "database/Types.hpp" +#include "services/database/TrackId.hpp" namespace Database { class User; } -namespace UserInterface { - -class AudioTranscodeResource : public Wt::WResource +namespace UserInterface { - public: - ~AudioTranscodeResource(); + class AudioTranscodeResource : public Wt::WResource + { + public: + ~AudioTranscodeResource(); - // Url depends on the user since settings are used in parameters - std::string getUrl(Database::TrackId trackId) const; + // Url depends on the user since settings are used in parameters + std::string getUrl(Database::TrackId trackId) const; - void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); - - private: - static constexpr std::size_t _chunkSize {262144}; -}; + void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); + private: + static constexpr std::size_t _chunkSize {262144}; + }; } // namespace UserInterface diff --git a/src/lms/ui/resource/CoverResource.cpp b/src/lms/ui/resource/CoverResource.cpp index a2978c6b..6b4c5a42 100644 --- a/src/lms/ui/resource/CoverResource.cpp +++ b/src/lms/ui/resource/CoverResource.cpp @@ -22,8 +22,8 @@ #include #include -#include "cover/ICoverArtGrabber.hpp" -#include "database/Track.hpp" +#include "services/cover/ICoverService.hpp" +#include "services/database/Track.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" @@ -83,7 +83,7 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons return; } - std::shared_ptr cover; + std::shared_ptr cover; if (trackIdStr) { @@ -96,7 +96,7 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons return; } - cover = Service::get()->getFromTrack(LmsApp->getDbSession(), *trackId, *size); + cover = Service::get()->getFromTrack(*trackId, *size); } else if (releaseIdStr) { @@ -106,7 +106,7 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons if (!releaseId) return; - cover = Service::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, *size); + cover = Service::get()->getFromRelease(*releaseId, *size); } else { diff --git a/src/lms/ui/resource/CoverResource.hpp b/src/lms/ui/resource/CoverResource.hpp index 1e8cb8ad..533a96f1 100644 --- a/src/lms/ui/resource/CoverResource.hpp +++ b/src/lms/ui/resource/CoverResource.hpp @@ -20,11 +20,11 @@ #pragma once #include -#include "database/Types.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" namespace UserInterface { - class CoverResource : public Wt::WResource { public: @@ -45,6 +45,5 @@ namespace UserInterface private: void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; }; - } // namespace UserInterface diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index 998096a5..f87d4645 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -26,10 +26,10 @@ #include #include -#include "database/Artist.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" #include "utils/Zipper.hpp" @@ -176,7 +176,7 @@ DownloadArtistResource::DownloadArtistResource(Database::ArtistId artistId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), artistId)}; + Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), artistId)}; if (artist) suggestFileName(getArtistPathName(artist) + ".zip"); } @@ -186,7 +186,7 @@ DownloadArtistResource::createZipper() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)}; + const Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), _artistId)}; if (!artist) { LOG(DEBUG) << "Cannot find artist"; @@ -201,7 +201,7 @@ DownloadReleaseResource::DownloadReleaseResource(Database::ReleaseId releaseId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), releaseId)}; + Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), releaseId)}; if (release) suggestFileName(getReleasePathName(release) + ".zip"); } @@ -212,7 +212,7 @@ DownloadReleaseResource::createZipper() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), _releaseId)}; + const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), _releaseId)}; if (!release) { LOG(DEBUG) << "Cannot find release"; @@ -227,7 +227,7 @@ DownloadTrackResource::DownloadTrackResource(Database::TrackId trackId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (track) suggestFileName(getTrackPathName(track) + ".zip"); } @@ -237,7 +237,7 @@ DownloadTrackResource::createZipper() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), _trackId)}; + const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), _trackId)}; if (!track) { LOG(DEBUG) << "Cannot find track"; diff --git a/src/lms/ui/resource/DownloadResource.hpp b/src/lms/ui/resource/DownloadResource.hpp index 581a55ec..9203d0af 100644 --- a/src/lms/ui/resource/DownloadResource.hpp +++ b/src/lms/ui/resource/DownloadResource.hpp @@ -22,53 +22,53 @@ #include #include -#include "database/Types.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" #include "utils/Zipper.hpp" -namespace UserInterface { - -class DownloadResource : public Wt::WResource +namespace UserInterface { - public: - static constexpr std::size_t bufferSize {32768}; + class DownloadResource : public Wt::WResource + { + public: + static constexpr std::size_t bufferSize {32768}; - ~DownloadResource(); + ~DownloadResource(); - private: + private: + void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; + virtual std::unique_ptr createZipper() = 0; + }; - void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; - virtual std::unique_ptr createZipper() = 0; -}; + class DownloadArtistResource : public DownloadResource + { + public: + DownloadArtistResource(Database::ArtistId artistId); -class DownloadArtistResource : public DownloadResource -{ - public: - DownloadArtistResource(Database::ArtistId artistId); + private: + std::unique_ptr createZipper() override; + Database::ArtistId _artistId; + }; - private: - std::unique_ptr createZipper() override; - Database::ArtistId _artistId; -}; + class DownloadReleaseResource : public DownloadResource + { + public: + DownloadReleaseResource(Database::ReleaseId releaseId); -class DownloadReleaseResource : public DownloadResource -{ - public: - DownloadReleaseResource(Database::ReleaseId releaseId); + private: + std::unique_ptr createZipper() override; + Database::ReleaseId _releaseId; + }; - private: - std::unique_ptr createZipper() override; - Database::ReleaseId _releaseId; -}; - -class DownloadTrackResource : public DownloadResource -{ - public: - DownloadTrackResource(Database::TrackId trackId); - - private: - std::unique_ptr createZipper() override; - Database::TrackId _trackId; -}; + class DownloadTrackResource : public DownloadResource + { + public: + DownloadTrackResource(Database::TrackId trackId); + private: + std::unique_ptr createZipper() override; + Database::TrackId _trackId; + }; } // namespace UserInterface diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt deleted file mode 100644 index ea5a1fe2..00000000 --- a/src/test/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ - -add_subdirectory(database) -add_subdirectory(som) -add_subdirectory(utils) - diff --git a/src/test/database/CMakeLists.txt b/src/test/database/CMakeLists.txt deleted file mode 100644 index 0ad6756d..00000000 --- a/src/test/database/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ - -add_executable(test-database - Artist.cpp - Cluster.cpp - DatabaseTest.cpp - Release.cpp - Track.cpp - ) - -target_link_libraries(test-database PRIVATE - lmsdatabase - GTest::GTest - ) - -gtest_discover_tests(test-database) - diff --git a/src/test/database/Track.cpp b/src/test/database/Track.cpp deleted file mode 100644 index ac919bff..00000000 --- a/src/test/database/Track.cpp +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2021 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "Common.hpp" - -#include - -using namespace Database; - -TEST_F(DatabaseFixture, SingleTrack) -{ - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(Track::getCount(session), 0); - } - - ScopedTrack track {session, "MyTrackFile"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(Track::getAll(session).size(), 1); - EXPECT_EQ(Track::getCount(session), 1); - } -} - -TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) -{ - ScopedTrack track1 {session, ""}; - ScopedTrack track2 {session, ""}; - ScopedTrack track3 {session, ""}; - ScopedTrack track4 {session, ""}; - ScopedTrack track5 {session, ""}; - ScopedTrack track6 {session, ""}; - - { - auto transaction {session.createUniqueTransaction()}; - track1.get().modify()->setName("MyTrack"); - track2.get().modify()->setName("MyTrack%"); - track3.get().modify()->setName("MyTrack%Foo"); - track4.get().modify()->setName("%MyTrack"); - track5.get().modify()->setName("Foo%MyTrack"); - track6.get().modify()->setName("M_Track"); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool more; - { - const auto tracks {Track::getByFilter(session, {}, {"Track"}, std::nullopt, more)}; - EXPECT_EQ(tracks.size(), 6); - } - { - const auto tracks {Track::getByFilter(session, {}, {"MyTrack"}, std::nullopt, more)}; - EXPECT_EQ(tracks.size(), 5); - EXPECT_TRUE(std::none_of(std::cbegin(tracks), std::cend(tracks), [&](const Track::pointer& track) { return track->getId() == track6.getId(); })); - } - { - const auto tracks {Track::getByFilter(session, {}, {"MyTrack%"}, std::nullopt, more)}; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track2.getId()); - EXPECT_EQ(tracks[1]->getId(), track3.getId()); - } - { - const auto tracks {Track::getByFilter(session, {}, {"%MyTrack"}, std::nullopt, more)}; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track4.getId()); - EXPECT_EQ(tracks[1]->getId(), track5.getId()); - } - } -} - -TEST_F(DatabaseFixture, SingleTrackDate) -{ - ScopedTrack track {session, "MyTrack"}; - - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(track->getYear(), std::nullopt); - EXPECT_EQ(track->getOriginalYear(), std::nullopt); - } - - { - auto transaction {session.createUniqueTransaction()}; - track.get().modify()->setDate(Wt::WDate {1995, 5, 5}); - track.get().modify()->setOriginalDate(Wt::WDate {1994, 2, 2}); - } - - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(track->getYear(), 1995); - EXPECT_EQ(track->getOriginalYear(), 1994); - } -} - - - diff --git a/src/tools/cover/CMakeLists.txt b/src/tools/cover/CMakeLists.txt index 141e2983..b7131b97 100644 --- a/src/tools/cover/CMakeLists.txt +++ b/src/tools/cover/CMakeLists.txt @@ -4,7 +4,7 @@ add_executable(lms-cover ) target_link_libraries(lms-cover PRIVATE - lmscover + lmsservice-cover Boost::program_options ) diff --git a/src/tools/cover/LmsCover.cpp b/src/tools/cover/LmsCover.cpp index 6a9e2767..36291e5b 100644 --- a/src/tools/cover/LmsCover.cpp +++ b/src/tools/cover/LmsCover.cpp @@ -24,11 +24,11 @@ #include -#include "cover/ICoverArtGrabber.hpp" -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" +#include "services/database/Db.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/cover/ICoverService.hpp" #include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" @@ -36,18 +36,20 @@ static void -dumpTrackCovers(Database::Session& session, CoverArt::ImageSize width) +dumpTrackCovers(Database::Session& session, Image::ImageSize width) { - std::vector trackIds; + using namespace Database; + + RangeResults trackIds; { auto transaction {session.createSharedTransaction()}; - trackIds = Database::Track::getAllIds(session); + trackIds = Database::Track::find(session, Database::Track::FindParameters {}); } - for (const Database::TrackId trackId : trackIds) + for (const Database::TrackId trackId : trackIds.results) { std::cout << "Getting cover for track id " << trackId.toString() << std::endl; - Service::get()->getFromTrack(session, trackId, width); + Service::get()->getFromTrack(trackId, width); } } @@ -82,15 +84,11 @@ int main(int argc, char *argv[]) } Service config {createConfig(vm["conf"].as())}; - - Service coverArtService {CoverArt::createGrabber(argv[0], - vm["default-cover"].as(), - config->getULong("cover-max-cache-size", 30) * 1000 * 1000, - config->getULong("cover-max-file-size", 10) * 1000 * 1000, - config->getULong("cover-jpeg-quality", vm["quality"].as()) - )}; - Database::Db db {config->getPath("working-dir") / "lms.db"}; + Service coverArtService {Cover::createCoverService(db, argv[0], vm["default-cover"].as())}; + + coverArtService->setJpegQuality(config->getULong("cover-jpeg-quality", vm["quality"].as())); + Database::Session session {db}; if (vm.count("tracks")) diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index 0a1f4a3c..5dec1cb3 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -24,40 +24,43 @@ #include -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Db.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/Types.hpp" +#include "services/recommendation/IRecommendationService.hpp" #include "utils/IConfig.hpp" #include "utils/Service.hpp" #include "utils/StreamLogger.hpp" -#include "recommendation/IEngine.hpp" + +using namespace Database; static void -dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) +dumpTracksRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount) { - const std::vector trackIds {[&]() + const RangeResults trackIds {[&]() { auto transaction {session.createSharedTransaction()}; - return Database::Track::getAllIds(session); + return Track::find(session, Track::FindParameters {}); }()}; - std::cout << "*** Tracks (" << trackIds.size() << ") ***" << std::endl; - for (Database::TrackId trackId : trackIds) + std::cout << "*** Tracks (" << trackIds.results.size() << ") ***" << std::endl; + for (const TrackId trackId : trackIds.results) { - auto trackToString = [&](Database::TrackId trackId) + auto trackToString = [&](const TrackId trackId) { std::string res; auto transaction {session.createSharedTransaction()}; - Database::Track::pointer track {Database::Track::getById(session, trackId)}; + const Track::pointer track {Track::find(session, trackId)}; res += track->getName(); if (track->getRelease()) res += " [" + track->getRelease()->getName() + "]"; - for (auto artist : track->getArtists({Database::TrackArtistLinkType::Artist})) + for (auto artist : track->getArtists({TrackArtistLinkType::Artist})) res += " - " + artist->getName(); for (auto cluster : track->getClusters()) res += " {" + cluster->getType()->getName() + "-"+ cluster->getName() + "}"; @@ -66,61 +69,61 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng }; std::cout << "Processing track '" << trackToString(trackId) << std::endl; - for (Database::TrackId similarTrackId : engine.getSimilarTracks(session, {trackId}, maxSimilarityCount)) + for (TrackId similarTrackId : recommendationService.findSimilarTracks({trackId}, maxSimilarityCount)) std::cout << "\t- Similar track '" << trackToString(similarTrackId) << std::endl; } } static void -dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) +dumpReleasesRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount) { - const std::vector releaseIds = std::invoke([&]() + const RangeResults releaseIds {std::invoke([&]() { auto transaction {session.createSharedTransaction()}; - return Database::Release::getAllIds(session); - }); + return Release::find(session, Release::FindParameters {}); + })}; std::cout << "*** Releases ***" << std::endl; - for (Database::ReleaseId releaseId : releaseIds) + for (ReleaseId releaseId : releaseIds.results) { - auto releaseToString = [&](Database::ReleaseId releaseId) + auto releaseToString = [&](ReleaseId releaseId) -> std::string { auto transaction {session.createSharedTransaction()}; - Database::Release::pointer release {Database::Release::getById(session, releaseId)}; + Release::pointer release {Release::find(session, releaseId)}; return release->getName(); }; std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl; - for (Database::ReleaseId similarReleaseId : engine.getSimilarReleases(session, releaseId, maxSimilarityCount)) + for (ReleaseId similarReleaseId : recommendationService.getSimilarReleases(releaseId, maxSimilarityCount)) std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl; } } static void -dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& engine, unsigned maxSimilarityCount) +dumpArtistsRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount) { - const std::vector artistIds = std::invoke([&]() + const RangeResults artistIds = std::invoke([&]() { auto transaction {session.createSharedTransaction()}; - return Database::Artist::getAllIds(session); + return Artist::find(session, Artist::FindParameters {}); }); std::cout << "*** Artists ***" << std::endl; - for (Database::ArtistId artistId : artistIds) + for (ArtistId artistId : artistIds.results) { - auto artistToString = [&](Database::ArtistId artistId) + auto artistToString = [&](ArtistId artistId) { auto transaction {session.createSharedTransaction()}; - Database::Artist::pointer artist {Database::Artist::getById(session, artistId)}; + Artist::pointer artist {Artist::find(session, artistId)}; return artist->getName(); }; std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl; - for (Database::ArtistId similarArtistId : engine.getSimilarArtists(session, artistId, {Database::TrackArtistLinkType::Artist, Database::TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount)) + for (ArtistId similarArtistId : recommendationService.getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount)) { std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl; } @@ -158,28 +161,28 @@ int main(int argc, char *argv[]) Service config {createConfig(vm["conf"].as())}; - Database::Db db {config->getPath("working-dir") / "lms.db"}; - Database::Session session {db}; + Db db {config->getPath("working-dir") / "lms.db"}; + Session session {db}; - std::cout << "Creating recommendation engine..." << std::endl; - const auto engine {Recommendation::createEngine(db)}; - std::cout << "Recommendation engine created!" << std::endl; + std::cout << "Creating recommendation recommendationService..." << std::endl; + const auto recommendationService {Recommendation::createRecommendationService(db)}; + std::cout << "Recommendation recommendationService created!" << std::endl; - std::cout << "Loading recommendation engine..." << std::endl; - engine->load(false); + std::cout << "Loading recommendation recommendationService..." << std::endl; + recommendationService->load(false); unsigned maxSimilarityCount {vm["max"].as()}; - std::cout << "Recommendation engine loaded!" << std::endl; + std::cout << "Recommendation recommendationService loaded!" << std::endl; if (vm.count("tracks")) - dumpTracksRecommendation(db, *engine, maxSimilarityCount); + dumpTracksRecommendation(db, *recommendationService, maxSimilarityCount); if (vm.count("releases")) - dumpReleasesRecommendation(db, *engine, maxSimilarityCount); + dumpReleasesRecommendation(db, *recommendationService, maxSimilarityCount); if (vm.count("artists")) - dumpArtistsRecommendation(db, *engine, maxSimilarityCount); + dumpArtistsRecommendation(db, *recommendationService, maxSimilarityCount); } catch( std::exception& e) {