diff --git a/.gitignore b/.gitignore index b8617e347..d3419b0be 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ CMakeCache.txt CMakeFiles/ build/ -.vscode/ \ No newline at end of file +.vscode/ +cmake-build-*/ +.idea/ \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md index b97d2fe60..97ad02d65 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -33,6 +33,8 @@ Please refer to [Deployment](#deployment) for further configuration options. __Note__: this installation process and the default values of the configuration files have been written for _Debian Bookworm_. Therefore, you may have to adapt commands and/or paths in order to fit to your distribution. ### Build dependencies __Notes__: +* Wt by default uses bundled Sqlite, which does not support needed features. It will probably need to be compiled with + `-DUSE_SYSTEM_SQLITE3=ON` for cmake. * a C++17 compiler is needed * ffmpeg version 4 minimum is required ```sh diff --git a/approot/artists.xml b/approot/artists.xml index 129388602..d28c69fff 100644 --- a/approot/artists.xml +++ b/approot/artists.xml @@ -6,7 +6,9 @@
${link-type class="me-1"}
+ ${} ${search class="form-control form-control-sm me-1" type="search"} + ${} ${sort-mode}
@@ -14,7 +16,18 @@ - ${name class="text-decoration-none link-secondary"} +
+
+ ${cover} +
+
+
+
+
${name class="text-decoration-none link-secondary"}
+
+
+
+
diff --git a/approot/main.xml b/approot/main.xml index 0dea95e88..51d1e1169 100644 --- a/approot/main.xml +++ b/approot/main.xml @@ -34,6 +34,11 @@ ${filters class="d-flex align-items-center me-auto mb-2 mb-md-0"}
+ ${} ${search class="form-control form-control-sm me-1" type="search"} + ${} ${sort-mode}
@@ -28,7 +30,7 @@ ${cover class="shadow-sm"} ${release-name class="d-block text-truncate text-nowrap text-decoration-none link-success"} - ${}${artist-name class="d-block text-truncate text-nowrap"}${} + ${}${artists class="d-block text-truncate text-nowrap"}${} ${}
${year}
${
} diff --git a/approot/settings.xml b/approot/settings.xml index ae63455fd..becd61eb8 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -5,6 +5,28 @@
+ ${tr:Lms.Settings.interface} +
+ ${tr:Lms.Settings.interface-settings-need-refresh} +
+
+ ${interface-enable-multisearch class="form-check-input"} + +
+ ${interface-enable-multisearch-info} +
+
+
+ ${interface-enable-singlesearch class="form-check-input"} + +
+ ${interface-enable-singlesearch-info} +
+
${tr:Lms.Settings.audio}
${tr:Lms.Settings.audio-settings-are-local} diff --git a/approot/tracks.xml b/approot/tracks.xml index 23bc4d735..497ceeec0 100644 --- a/approot/tracks.xml +++ b/approot/tracks.xml @@ -15,7 +15,9 @@
+ ${} ${search class="form-control form-control-sm me-1" type="search"} + ${} ${sort-mode}
diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt index be2bd834c..6e45f402f 100644 --- a/src/libs/av/CMakeLists.txt +++ b/src/libs/av/CMakeLists.txt @@ -1,4 +1,4 @@ -pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat) +pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET libavcodec libavutil libavformat) add_library(lmsav SHARED impl/AudioFile.cpp diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index b5e5a194a..cb1a0d9e9 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(lmsdatabase SHARED + impl/AnyMedium.cpp impl/Artist.cpp impl/AuthToken.cpp impl/Cluster.cpp diff --git a/src/libs/database/impl/AnyMedium.cpp b/src/libs/database/impl/AnyMedium.cpp new file mode 100644 index 000000000..4621d8cef --- /dev/null +++ b/src/libs/database/impl/AnyMedium.cpp @@ -0,0 +1,102 @@ +#include "database/AnyMedium.hpp" + +#include +#include + +#include + +#include "Utils.hpp" + +using namespace lms::db; + +AnyMediumId any_medium::fromString(const std::string& type, const Wt::Dbo::dbo_default_traits::IdType id) +{ + if (type == "artist") + return ArtistId(id); + if (type == "release") + return ReleaseId(id); + if (type == "track") + return TrackId(id); + + throw std::logic_error("unknown medium type"); +} + +RangeResults any_medium::findIds(Session& session, Type type, const std::vector& keywords, + std::span clusters, MediaLibraryId mediaLibrary, + std::optional range) +{ + using Columns = std::tuple; + + session.checkReadTransaction(); + + auto media_library_query = session.getDboSession()->query("SELECT json_each.value FROM json_each(media_library_ids)").where("json_each.value = ?").bind(mediaLibrary.getValue()); + + auto cluster_query = session.getDboSession()->query("SELECT json_each.value FROM json_each(cluster_ids)"); + for (auto cluster_id : clusters) + cluster_query.orWhere("json_each.value = ?").bind(cluster_id.getValue()); + + auto query = session.getDboSession()->query(R"( + SELECT type, id, sum(weight) AS v + FROM keywords + )"); + + for (const std::string_view keyword : keywords) + { + query.orWhere("value LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%"); + } + + if (mediaLibrary != MediaLibraryId{}) + query.where("EXISTS(" + media_library_query.asString() + ")").bindSubqueryValues(media_library_query); + + if (!clusters.empty()) + query.where("EXISTS(" + cluster_query.asString() + ")").bindSubqueryValues(cluster_query); + + switch (type) + { + case Type::ALL: + break; + case Type::RELEASES: + query.where("type = 'release'"); + break; + case Type::ARTISTS: + query.where("type = 'artist'"); + break; + case Type::TRACKS: + query.where("type = 'track'"); + break; + } + + query + .groupBy("type, id") + .orderBy("v DESC"); + + auto columns = utils::execRangeQuery(query, range); + + auto results = std::vector(); + results.reserve(columns.results.size()); + for (const auto& [type, id, _] : columns.results) + results.emplace_back(fromString(type, id)); + + return { + columns.range, + results, + columns.moreResults + }; +} + +std::ostream& lms::db::operator<<(std::ostream& os, const AnyMediumId& v) +{ + std::visit([&os](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) + os << "Artist(" << arg.getValue() << ")"; + else if constexpr (std::is_same_v) + os << "Release(" << arg.getValue() << ")"; + else if constexpr (std::is_same_v) + os << "Track(" << arg.getValue() << ")"; + else + static_assert(false, "inexhaustible patterns"); + }, + v); + return os; +} diff --git a/src/libs/database/impl/IdTypeTraits.hpp b/src/libs/database/impl/IdTypeTraits.hpp index 2313bb4b4..775f76065 100644 --- a/src/libs/database/impl/IdTypeTraits.hpp +++ b/src/libs/database/impl/IdTypeTraits.hpp @@ -23,7 +23,7 @@ #include -#include "database/Types.hpp" +#include "database/IdType.hpp" namespace Wt::Dbo { diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 4dacba8b3..93a80bcd0 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -35,7 +35,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 59 }; + static constexpr Version LMS_DATABASE_VERSION{ 60 }; } VersionInfo::VersionInfo() @@ -476,6 +476,13 @@ SELECT session.getDboSession()->execute("UPDATE scan_settings SET audio_file_extensions = audio_file_extensions || ' .dsf'"); } + void migrateFromV59(Session& session) + { + // Searcher choice + session.getDboSession()->execute("ALTER TABLE user ADD interface_enable_multisearch BOOL NOT NULL DEFAULT(FALSE)"); + session.getDboSession()->execute("ALTER TABLE user ADD interface_enable_singlesearch BOOL NOT NULL DEFAULT(TRUE)"); + } + bool doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -484,33 +491,35 @@ SELECT using MigrationFunction = std::function; - const std::map migrationFunctions{ - { 33, migrateFromV33 }, - { 34, migrateFromV34 }, - { 35, migrateFromV35 }, - { 36, migrateFromV36 }, - { 37, migrateFromV37 }, - { 38, migrateFromV38 }, - { 39, migrateFromV39 }, - { 40, migrateFromV40 }, - { 41, migrateFromV41 }, - { 42, migrateFromV42 }, - { 43, migrateFromV43 }, - { 44, migrateFromV44 }, - { 45, migrateFromV45 }, - { 46, migrateFromV46 }, - { 47, migrateFromV47 }, - { 48, migrateFromV48 }, - { 49, migrateFromV49 }, - { 50, migrateFromV50 }, - { 51, migrateFromV51 }, - { 52, migrateFromV52 }, - { 53, migrateFromV53 }, - { 54, migrateFromV54 }, - { 55, migrateFromV55 }, - { 56, migrateFromV56 }, - { 57, migrateFromV57 }, - { 58, migrateFromV58 }, + const std::map migrationFunctions + { + {33, migrateFromV33}, + {34, migrateFromV34}, + {35, migrateFromV35}, + {36, migrateFromV36}, + {37, migrateFromV37}, + {38, migrateFromV38}, + {39, migrateFromV39}, + {40, migrateFromV40}, + {41, migrateFromV41}, + {42, migrateFromV42}, + {43, migrateFromV43}, + {44, migrateFromV44}, + {45, migrateFromV45}, + {46, migrateFromV46}, + {47, migrateFromV47}, + {48, migrateFromV48}, + {49, migrateFromV49}, + {50, migrateFromV50}, + {51, migrateFromV51}, + {52, migrateFromV52}, + {53, migrateFromV53}, + {54, migrateFromV54}, + {55, migrateFromV55}, + {56, migrateFromV56}, + {57, migrateFromV57}, + {58, migrateFromV58}, + {59, migrateFromV59}, }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index d0c5c90da..2f6602860 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -236,6 +236,111 @@ namespace lms::db LMS_LOG(DB, INFO, "Indexes created!"); } + void Session::createViewsIfNeeded() + { + LMS_SCOPED_TRACE_OVERVIEW("Database", "ViewCreation"); + LMS_LOG(DB, INFO, "Creating views..."); + + auto transaction{ createWriteTransaction() }; + _session.execute(R"( + CREATE TABLE IF NOT EXISTS keywords AS + WITH tracks AS (WITH keywords AS (SELECT id, 5 as weight, name + FROM track + UNION ALL + SELECT DISTINCT track_id, 1, artist.name + FROM track_artist_link + JOIN artist ON track_artist_link.artist_id = artist.id), + media_libs AS (SELECT id, json_array(media_library_id) as media_library_ids + FROM track + WHERE media_library_id IS NOT NULL), + clusters AS (SELECT track_id as id, json_group_array(cluster_id) as cluster_ids + FROM track_cluster + GROUP BY track_id) + SELECT keywords.id, + weight, + name, + IFNULL(media_libs.media_library_ids, json_array()) AS media_library_ids, + IFNULL(clusters.cluster_ids, json_array()) AS cluster_ids + FROM keywords + LEFT JOIN media_libs ON media_libs.id = keywords.id + LEFT JOIN clusters ON clusters.id = keywords.id), + + artists AS (WITH keywords AS (SELECT id, 5 as weight, name + FROM artist), + media_libs AS (SELECT artist_id as id, json_group_array(media_library_id) as media_library_ids + FROM (SELECT DISTINCT artist_id, media_library_id + FROM track_artist_link + JOIN track ON track_artist_link.track_id = track.id + WHERE media_library_id IS NOT NULL) + GROUP BY artist_id), + clusters AS (SELECT artist_id as id, json_group_array(cluster_id) as cluster_ids + FROM (SELECT DISTINCT artist_id, cluster_id + FROM track_artist_link + JOIN track_cluster on track_artist_link.track_id = track_cluster.track_id) + GROUP BY artist_id) + SELECT keywords.id, + weight, + name, + IFNULL(media_libs.media_library_ids, json_array()) AS media_library_ids, + IFNULL(clusters.cluster_ids, json_array()) AS cluster_ids + FROM keywords + LEFT JOIN media_libs ON media_libs.id = keywords.id + LEFT JOIN clusters ON clusters.id = keywords.id), + + releases AS (WITH keywords AS (SELECT id, 5 as weight, name + FROM "release" + UNION ALL + SELECT id, 1 as weight, artist_display_name + FROM "release" + UNION ALL + SELECT DISTINCT r.id, 1, a.name + FROM "release" r + JOIN track t ON r.id = t.release_id + JOIN track_artist_link tal ON tal.track_id = t.id + JOIN artist a ON a.id = tal.artist_id), + media_libs AS (SELECT release_id as id, json_group_array(media_library_id) as media_library_ids + FROM (SELECT DISTINCT release_id, media_library_id + FROM track + WHERE media_library_id IS NOT NULL) + GROUP BY release_id), + clusters AS (SELECT release_id as id, json_group_array(cluster_id) as cluster_ids + FROM (SELECT DISTINCT release_id, cluster_id + FROM track + JOIN track_cluster on track.id = track_cluster.track_id) + GROUP BY release_id) + SELECT keywords.id, + weight, + name, + IFNULL(media_libs.media_library_ids, json_array()) AS media_library_ids, + IFNULL(clusters.cluster_ids, json_array()) AS cluster_ids + FROM keywords + LEFT JOIN media_libs ON media_libs.id = keywords.id + LEFT JOIN clusters ON clusters.id = keywords.id) + + SELECT "track" as type, id, weight, name as value, media_library_ids, cluster_ids + FROM tracks + UNION ALL + SELECT "artist", id, weight, name, media_library_ids, cluster_ids + FROM artists + UNION ALL + SELECT "release", id, weight, name, media_library_ids, cluster_ids + FROM releases + )"); + + LMS_LOG(DB, INFO, "Views created!"); + } + + void Session::dropViews() + { + LMS_SCOPED_TRACE_OVERVIEW("Database", "ViewDestruction"); + LMS_LOG(DB, INFO, "Dropping views..."); + + auto transaction{ createWriteTransaction() }; + _session.execute("DROP TABLE IF EXISTS keywords"); + + LMS_LOG(DB, INFO, "Views dropped!"); + } + void Session::vacuumIfNeeded() { long pageCount{}; diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp index 944a24f75..e21d4f925 100644 --- a/src/libs/database/impl/User.cpp +++ b/src/libs/database/impl/User.cpp @@ -21,12 +21,9 @@ #include "core/ILogger.hpp" #include "database/Artist.hpp" -#include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" -#include "IdTypeTraits.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" namespace lms::db @@ -88,7 +85,7 @@ namespace lms::db User::pointer User::find(Session& session, std::string_view name) { - return utils::fetchQuerySingleResult(session.getDboSession()->find().where("login_name = ?").bind(name)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("login_name = ?").bind(std::string(name))); } void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate) diff --git a/src/libs/database/impl/Utils.hpp b/src/libs/database/impl/Utils.hpp index 2c7386d2b..3e228edcd 100644 --- a/src/libs/database/impl/Utils.hpp +++ b/src/libs/database/impl/Utils.hpp @@ -28,6 +28,7 @@ #include "core/ITraceLogger.hpp" #include "database/Types.hpp" +#include "IdTypeTraits.hpp" namespace lms::db::utils { diff --git a/src/libs/database/include/database/AnyMedium.hpp b/src/libs/database/include/database/AnyMedium.hpp new file mode 100644 index 000000000..dc7c2b43c --- /dev/null +++ b/src/libs/database/include/database/AnyMedium.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +#include "ArtistId.hpp" +#include "ClusterId.hpp" +#include "MediaLibraryId.hpp" +#include "ReleaseId.hpp" +#include "TrackId.hpp" +#include "Types.hpp" + +namespace lms::db +{ + class Session; + using AnyMediumId = std::variant; + + std::ostream& operator<<(std::ostream& os, const AnyMediumId& v); + + namespace any_medium + { + enum class Type + { + ALL, + RELEASES, + ARTISTS, + TRACKS + }; + + AnyMediumId fromString(const std::string& type, Wt::Dbo::dbo_default_traits::IdType id); + RangeResults findIds(Session& session, Type type, const std::vector& keywords, + std::span clusters, MediaLibraryId mediaLibrary, + std::optional range); + } // namespace any_medium +} // namespace lms::db diff --git a/src/libs/database/include/database/ArtistId.hpp b/src/libs/database/include/database/ArtistId.hpp index b9320263f..535da5318 100644 --- a/src/libs/database/include/database/ArtistId.hpp +++ b/src/libs/database/include/database/ArtistId.hpp @@ -21,4 +21,9 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(ArtistId) +namespace lms::db +{ + class Artist; +} + +LMS_DECLARE_IDTYPE(ArtistId, lms::db::Artist) diff --git a/src/libs/database/include/database/AuthTokenId.hpp b/src/libs/database/include/database/AuthTokenId.hpp index 3abf86540..cdc29906d 100644 --- a/src/libs/database/include/database/AuthTokenId.hpp +++ b/src/libs/database/include/database/AuthTokenId.hpp @@ -21,4 +21,8 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(AuthTokenId) +namespace lms::db { + class AuthToken; +} + +LMS_DECLARE_IDTYPE(AuthTokenId, lms::db::AuthToken) diff --git a/src/libs/database/include/database/ClusterId.hpp b/src/libs/database/include/database/ClusterId.hpp index 4ee317416..b9c6173e2 100644 --- a/src/libs/database/include/database/ClusterId.hpp +++ b/src/libs/database/include/database/ClusterId.hpp @@ -21,5 +21,11 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(ClusterId) -LMS_DECLARE_IDTYPE(ClusterTypeId) +namespace lms::db +{ + class Cluster; + class ClusterType; +} // namespace lms::db + +LMS_DECLARE_IDTYPE(ClusterId, lms::db::Cluster) +LMS_DECLARE_IDTYPE(ClusterTypeId, lms::db::ClusterType) diff --git a/src/libs/database/include/database/IdType.hpp b/src/libs/database/include/database/IdType.hpp index bbfb202ab..4e105f4b6 100644 --- a/src/libs/database/include/database/IdType.hpp +++ b/src/libs/database/include/database/IdType.hpp @@ -48,13 +48,14 @@ namespace lms::db Wt::Dbo::dbo_default_traits::IdType _id{ Wt::Dbo::dbo_default_traits::invalidId() }; }; -#define LMS_DECLARE_IDTYPE(name) \ +#define LMS_DECLARE_IDTYPE(name, target) \ namespace lms::db \ { \ class name : public IdType \ { \ public: \ using IdType::IdType; \ + using Target = target; \ auto operator<=>(const name& other) const = default; \ }; \ } \ diff --git a/src/libs/database/include/database/ListenId.hpp b/src/libs/database/include/database/ListenId.hpp index 271a33572..87432961b 100644 --- a/src/libs/database/include/database/ListenId.hpp +++ b/src/libs/database/include/database/ListenId.hpp @@ -21,4 +21,9 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(ListenId) +namespace lms::db +{ + class Listen; +} + +LMS_DECLARE_IDTYPE(ListenId, lms::db::Listen) diff --git a/src/libs/database/include/database/MediaLibraryId.hpp b/src/libs/database/include/database/MediaLibraryId.hpp index fcd2e5329..a495390b4 100644 --- a/src/libs/database/include/database/MediaLibraryId.hpp +++ b/src/libs/database/include/database/MediaLibraryId.hpp @@ -21,4 +21,9 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(MediaLibraryId) +namespace lms::db +{ + class MediaLibrary; +} + +LMS_DECLARE_IDTYPE(MediaLibraryId, lms::db::MediaLibrary) diff --git a/src/libs/database/include/database/ReleaseId.hpp b/src/libs/database/include/database/ReleaseId.hpp index c588661c8..e53695165 100644 --- a/src/libs/database/include/database/ReleaseId.hpp +++ b/src/libs/database/include/database/ReleaseId.hpp @@ -21,4 +21,9 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(ReleaseId) +namespace lms::db +{ + class Release; +} + +LMS_DECLARE_IDTYPE(ReleaseId, lms::db::Release) diff --git a/src/libs/database/include/database/ReleaseTypeId.hpp b/src/libs/database/include/database/ReleaseTypeId.hpp index e8261a7ea..803894135 100644 --- a/src/libs/database/include/database/ReleaseTypeId.hpp +++ b/src/libs/database/include/database/ReleaseTypeId.hpp @@ -21,4 +21,9 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(ReleaseTypeId) +namespace lms::db +{ + class ReleaseType; +} + +LMS_DECLARE_IDTYPE(ReleaseTypeId, lms::db::ReleaseType) diff --git a/src/libs/database/include/database/ScanSettings.hpp b/src/libs/database/include/database/ScanSettings.hpp index 91ffd4380..af8a2ac52 100644 --- a/src/libs/database/include/database/ScanSettings.hpp +++ b/src/libs/database/include/database/ScanSettings.hpp @@ -31,7 +31,11 @@ #include "database/IdType.hpp" #include "database/Object.hpp" -LMS_DECLARE_IDTYPE(ScanSettingsId) +namespace lms::db { + class ScanSettings; +} + +LMS_DECLARE_IDTYPE(ScanSettingsId, lms::db::ScanSettings) namespace lms::db { diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index 80aafd3f4..e07c3df67 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -97,6 +97,8 @@ namespace lms::db void prepareTablesIfNeeded(); // need to run only once at startup bool migrateSchemaIfNeeded(); // returns true if migration was performed void createIndexesIfNeeded(); + void createViewsIfNeeded(); + void dropViews(); void vacuumIfNeeded(); void vacuum(); void refreshTracingLoggerStats(); diff --git a/src/libs/database/include/database/StarredArtistId.hpp b/src/libs/database/include/database/StarredArtistId.hpp index 2ff326781..3208b68e6 100644 --- a/src/libs/database/include/database/StarredArtistId.hpp +++ b/src/libs/database/include/database/StarredArtistId.hpp @@ -21,4 +21,9 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(StarredArtistId) +namespace lms::db +{ + class StarredArtist; +} + +LMS_DECLARE_IDTYPE(StarredArtistId, lms::db::StarredArtist) diff --git a/src/libs/database/include/database/StarredReleaseId.hpp b/src/libs/database/include/database/StarredReleaseId.hpp index bb0a63c03..6bd181008 100644 --- a/src/libs/database/include/database/StarredReleaseId.hpp +++ b/src/libs/database/include/database/StarredReleaseId.hpp @@ -21,4 +21,9 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(StarredReleaseId) +namespace lms::db +{ + class StarredRelease; +} + +LMS_DECLARE_IDTYPE(StarredReleaseId, lms::db::StarredRelease) diff --git a/src/libs/database/include/database/StarredTrackId.hpp b/src/libs/database/include/database/StarredTrackId.hpp index 54568afd4..1907155e2 100644 --- a/src/libs/database/include/database/StarredTrackId.hpp +++ b/src/libs/database/include/database/StarredTrackId.hpp @@ -21,4 +21,9 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(StarredTrackId) +namespace lms::db +{ + class StarredTrack; +} + +LMS_DECLARE_IDTYPE(StarredTrackId, lms::db::StarredTrack) diff --git a/src/libs/database/include/database/TrackArtistLink.hpp b/src/libs/database/include/database/TrackArtistLink.hpp index 5e823b938..d0d1591e4 100644 --- a/src/libs/database/include/database/TrackArtistLink.hpp +++ b/src/libs/database/include/database/TrackArtistLink.hpp @@ -33,7 +33,12 @@ #include "database/TrackId.hpp" #include "database/Types.hpp" -LMS_DECLARE_IDTYPE(TrackArtistLinkId) +namespace lms::db +{ + class TrackArtistLink; +} + +LMS_DECLARE_IDTYPE(TrackArtistLinkId, lms::db::TrackArtistLink) namespace lms::db { diff --git a/src/libs/database/include/database/TrackBookmark.hpp b/src/libs/database/include/database/TrackBookmark.hpp index 61a606a2c..160b270d6 100644 --- a/src/libs/database/include/database/TrackBookmark.hpp +++ b/src/libs/database/include/database/TrackBookmark.hpp @@ -30,7 +30,12 @@ #include "database/Types.hpp" #include "database/UserId.hpp" -LMS_DECLARE_IDTYPE(TrackBookmarkId) +namespace lms::db +{ + class TrackBookmark; +} + +LMS_DECLARE_IDTYPE(TrackBookmarkId, lms::db::TrackBookmark) namespace lms::db { diff --git a/src/libs/database/include/database/TrackFeatures.hpp b/src/libs/database/include/database/TrackFeatures.hpp index 4ff8e1e9d..fd027b52c 100644 --- a/src/libs/database/include/database/TrackFeatures.hpp +++ b/src/libs/database/include/database/TrackFeatures.hpp @@ -32,7 +32,12 @@ #include "database/TrackId.hpp" #include "database/Types.hpp" -LMS_DECLARE_IDTYPE(TrackFeaturesId) +namespace lms::db +{ + class TrackFeatures; +} + +LMS_DECLARE_IDTYPE(TrackFeaturesId, lms::db::TrackFeatures) namespace lms::db { diff --git a/src/libs/database/include/database/TrackId.hpp b/src/libs/database/include/database/TrackId.hpp index a2bbd415f..1a758ffc6 100644 --- a/src/libs/database/include/database/TrackId.hpp +++ b/src/libs/database/include/database/TrackId.hpp @@ -21,4 +21,9 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(TrackId) +namespace lms::db +{ + class Track; +} + +LMS_DECLARE_IDTYPE(TrackId, lms::db::Track) diff --git a/src/libs/database/include/database/TrackListId.hpp b/src/libs/database/include/database/TrackListId.hpp index 85945b53a..8239f3920 100644 --- a/src/libs/database/include/database/TrackListId.hpp +++ b/src/libs/database/include/database/TrackListId.hpp @@ -21,5 +21,12 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(TrackListId) -LMS_DECLARE_IDTYPE(TrackListEntryId) +namespace lms::db +{ + class TrackListEntry; + class TrackList; +} // namespace lms::db + +LMS_DECLARE_IDTYPE(TrackListId, lms::db::TrackList) + +LMS_DECLARE_IDTYPE(TrackListEntryId, lms::db::TrackListEntry) diff --git a/src/libs/database/include/database/User.hpp b/src/libs/database/include/database/User.hpp index d26a29b9b..cefa99b6e 100644 --- a/src/libs/database/include/database/User.hpp +++ b/src/libs/database/include/database/User.hpp @@ -70,6 +70,8 @@ namespace lms::db static inline constexpr std::size_t MinNameLength{ 3 }; static inline constexpr std::size_t MaxNameLength{ 15 }; + static inline constexpr bool defaultInterfaceEnableMultisearch{ false }; + static inline constexpr bool defaultInterfaceEnableSinglesearch{ true }; static inline constexpr bool defaultSubsonicEnableTranscodingByDefault{ false }; static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS }; static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 }; @@ -101,6 +103,8 @@ namespace lms::db _passwordHash = passwordHash.hash; } void setType(UserType type) { _type = type; } + void setInterfaceEnableMultisearch(bool value) { _interfaceEnableMultisearch = value; } + void setInterfaceEnableSinglesearch(bool value) { _interfaceEnableSinglesearch = value; } void setSubsonicEnableTranscodingByDefault(bool value) { _subsonicEnableTranscodingByDefault = value; } void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; } void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate); @@ -118,6 +122,8 @@ namespace lms::db bool isAdmin() const { return _type == UserType::ADMIN; } bool isDemo() const { return _type == UserType::DEMO; } UserType getType() const { return _type; } + bool getInterfaceEnableMultisearch() const { return _interfaceEnableMultisearch; } + bool getInterfaceEnableSinglesearch() const { return _interfaceEnableSinglesearch; } bool getSubsonicEnableTranscodingByDefault() const { return _subsonicEnableTranscodingByDefault; } TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; } Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; } @@ -138,6 +144,8 @@ namespace lms::db Wt::Dbo::field(a, _passwordSalt, "password_salt"); Wt::Dbo::field(a, _passwordHash, "password_hash"); Wt::Dbo::field(a, _lastLogin, "last_login"); + Wt::Dbo::field(a, _interfaceEnableMultisearch, "interface_enable_multisearch"); + Wt::Dbo::field(a, _interfaceEnableSinglesearch, "interface_enable_singlesearch"); Wt::Dbo::field(a, _subsonicEnableTranscodingByDefault, "subsonic_enable_transcoding_by_default"); Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputFormat, "subsonic_default_transcode_format"); Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputBitrate, "subsonic_default_transcode_bitrate"); @@ -173,6 +181,8 @@ namespace lms::db UserType _type{ UserType::REGULAR }; // User defined settings + bool _interfaceEnableMultisearch{ defaultInterfaceEnableMultisearch }; + bool _interfaceEnableSinglesearch{ defaultInterfaceEnableSinglesearch }; SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode }; bool _subsonicEnableTranscodingByDefault{ defaultSubsonicEnableTranscodingByDefault }; TranscodingOutputFormat _subsonicDefaultTranscodingOutputFormat{ defaultSubsonicTranscodingOutputFormat }; diff --git a/src/libs/database/include/database/UserId.hpp b/src/libs/database/include/database/UserId.hpp index ee6e0f2dd..a9212e001 100644 --- a/src/libs/database/include/database/UserId.hpp +++ b/src/libs/database/include/database/UserId.hpp @@ -21,4 +21,9 @@ #include "database/IdType.hpp" -LMS_DECLARE_IDTYPE(UserId) +namespace lms::db +{ + class User; +} + +LMS_DECLARE_IDTYPE(UserId, lms::db::User) diff --git a/src/libs/database/test/AnyMedium.cpp b/src/libs/database/test/AnyMedium.cpp new file mode 100644 index 000000000..4341699de --- /dev/null +++ b/src/libs/database/test/AnyMedium.cpp @@ -0,0 +1,442 @@ +#include +#include + +#include "Common.hpp" + +namespace lms::db +{ + struct Keyword + { + AnyMediumId id; + int weight{}; + std::string value; + std::unordered_set media_library_ids; + std::unordered_set cluster_ids; + + [[nodiscard]] auto tie() const + { + return std::tie(id, weight, value, media_library_ids, cluster_ids); + } + + bool operator==(const Keyword& rhs) const + { + return tie() == rhs.tie(); + } + }; + + std::ostream& operator<<(std::ostream& os, const Keyword& v) + { + os << v.id << ", " << v.weight << ", " << v.value << ", ["; + + for (auto& e : v.media_library_ids) + os << e.getValue() << ", "; + + os << "], ["; + + for (auto& e : v.cluster_ids) + os << e.getValue() << ", "; + + os << "]"; + + return os; + } + + struct KeywordIds + { + AnyMediumId id; + std::string value; + + KeywordIds(AnyMediumId id, + std::string value) + : id(id), value(std::move(value)) + { + } + + explicit KeywordIds(const Keyword& keyword) + : id(keyword.id), value(keyword.value) + { + } + + [[nodiscard]] auto tie() const + { + return std::tie(id, value); + } + + bool operator==(const KeywordIds& rhs) const + { + return tie() == rhs.tie(); + } + }; +} // namespace lms::db + +template<> +struct std::hash +{ + std::size_t operator()(const lms::db::KeywordIds& s) const noexcept + { + std::size_t seed = std::hash{}(s.id); + seed = seed ^ (std::hash{}(s.value) << 1); + + return seed; + } +}; + +namespace Wt::Dbo +{ + template<> + struct query_result_traits + { + static void getFields(Session& session, + std::vector* aliases, + std::vector& result) + { + query_result_traits::getFields(session, aliases, result); + query_result_traits::getFields(session, aliases, result); + query_result_traits::getFields(session, aliases, result); + query_result_traits::getFields(session, aliases, result); + query_result_traits::getFields(session, aliases, result); + query_result_traits::getFields(session, aliases, result); + } + + static lms::db::Keyword load(Session& session, + SqlStatement& statement, + int& column) + { + auto type = query_result_traits::load(session, statement, column); + auto id = query_result_traits::load(session, statement, column); + auto weight = query_result_traits::load(session, statement, column); + auto value = query_result_traits::load(session, statement, column); + auto media_library_ids = query_result_traits::load(session, statement, column); + auto cluster_ids = query_result_traits::load(session, statement, column); + + auto media_library_ids_vec = std::unordered_set(media_library_ids.size()); + for (auto& v : media_library_ids) + media_library_ids_vec.emplace(v.toNumber()); + + auto cluster_ids_vec = std::unordered_set(cluster_ids.size()); + for (auto& v : cluster_ids) + cluster_ids_vec.emplace(v.toNumber()); + + return { lms::db::any_medium::fromString(type, id), weight, value, media_library_ids_vec, cluster_ids_vec }; + } + }; +} // namespace Wt::Dbo + +namespace lms::db::tests +{ + class KeywordsFixture : public DatabaseFixture + { + std::unordered_map results; + + protected: + void collectResults() + { + resetViews(); + + auto transaction{ session.createReadTransaction() }; + + auto r = session.getDboSession()->query( + "SELECT type, id, weight, value, media_library_ids, cluster_ids FROM keywords"); + for (auto& kw : r.resultList()) + { + results.emplace(KeywordIds(kw), kw); + } + } + + bool hasResult(AnyMediumId id, const std::string& keyword) const + { + return results.contains({ id, keyword }); + } + + const Keyword& getResult(AnyMediumId id, const std::string& keyword) const + { + return results.at({ id, keyword }); + } + }; + + TEST_F(KeywordsFixture, keywords_artist_simple) + { + const ScopedArtist artist{ session, "MyArtist" }; + + collectResults(); + + EXPECT_TRUE(hasResult(artist.getId(), "MyArtist")); + EXPECT_TRUE(getResult(artist.getId(), "MyArtist").cluster_ids.empty()); + EXPECT_TRUE(getResult(artist.getId(), "MyArtist").media_library_ids.empty()); + } + + TEST_F(KeywordsFixture, keywords_artist_with_track_and_media_library) + { + ScopedArtist artist{ session, "MyArtist" }; + ScopedTrack track{ session }; + ScopedMediaLibrary library{ session }; + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setMediaLibrary(library.get()); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + collectResults(); + + EXPECT_EQ(getResult(artist.getId(), "MyArtist").media_library_ids, std::unordered_set{ library.getId() }); + } + + TEST_F(KeywordsFixture, keywords_artist_with_track_and_cluster) + { + ScopedArtist artist{ session, "MyArtist" }; + ScopedTrack track{ session }; + ScopedClusterType clusterType{ session, "MyClusterType" }; + ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; + { + auto transaction{ session.createWriteTransaction() }; + cluster.get().modify()->addTrack(track.get()); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + collectResults(); + + EXPECT_EQ(getResult(artist.getId(), "MyArtist").cluster_ids, std::unordered_set{ cluster.getId() }); + } + + TEST_F(KeywordsFixture, keywords_track) + { + ScopedTrack track{ session }; + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setName("MyTrack"); + } + + collectResults(); + + EXPECT_TRUE(hasResult(track.getId(), "MyTrack")); + EXPECT_TRUE(getResult(track.getId(), "MyTrack").cluster_ids.empty()); + EXPECT_TRUE(getResult(track.getId(), "MyTrack").media_library_ids.empty()); + } + + TEST_F(KeywordsFixture, keywords_track_with_artist) + { + ScopedTrack track{ session }; + ScopedArtist artist{ session, "MyArtist" }; + { + auto transaction{ session.createWriteTransaction() }; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + collectResults(); + + EXPECT_TRUE(hasResult(track.getId(), "MyArtist")); + EXPECT_TRUE(getResult(track.getId(), "MyArtist").cluster_ids.empty()); + EXPECT_TRUE(getResult(track.getId(), "MyArtist").media_library_ids.empty()); + } + + TEST_F(KeywordsFixture, keywords_track_with_cluster) + { + ScopedTrack track{ session }; + ScopedClusterType clusterType{ session, "MyClusterType" }; + ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setName("MyTrack"); + cluster.get().modify()->addTrack(track.get()); + } + + collectResults(); + + EXPECT_EQ(getResult(track.getId(), "MyTrack").cluster_ids, std::unordered_set{ cluster.getId() }); + } + + TEST_F(KeywordsFixture, keywords_track_with_media_library) + { + ScopedTrack track{ session }; + ScopedMediaLibrary library{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setName("MyTrack"); + track.get().modify()->setMediaLibrary(library.get()); + } + + collectResults(); + + EXPECT_EQ(getResult(track.getId(), "MyTrack").media_library_ids, std::unordered_set{ library.getId() }); + } + + TEST_F(KeywordsFixture, keywords_release) + { + ScopedRelease release{ session, "MyRelease" }; + + collectResults(); + + EXPECT_TRUE(hasResult(release.getId(), "MyRelease")); + EXPECT_TRUE(getResult(release.getId(), "MyRelease").cluster_ids.empty()); + EXPECT_TRUE(getResult(release.getId(), "MyRelease").media_library_ids.empty()); + } + + TEST_F(KeywordsFixture, keywords_release_with_artist_display_name) + { + ScopedRelease release{ session, "MyRelease" }; + + { + auto transaction{ session.createWriteTransaction() }; + release.get().modify()->setArtistDisplayName("MyArtist"); + } + + collectResults(); + + EXPECT_TRUE(hasResult(release.getId(), "MyArtist")); + EXPECT_TRUE(getResult(release.getId(), "MyArtist").cluster_ids.empty()); + EXPECT_TRUE(getResult(release.getId(), "MyArtist").media_library_ids.empty()); + } + + TEST_F(KeywordsFixture, keywords_release_with_track_artist) + { + ScopedRelease release{ session, "MyRelease" }; + ScopedTrack track{ session }; + ScopedArtist artist{ session, "MyArtist" }; + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setRelease(release.get()); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + collectResults(); + + EXPECT_TRUE(hasResult(track.getId(), "MyArtist")); + EXPECT_TRUE(getResult(track.getId(), "MyArtist").cluster_ids.empty()); + EXPECT_TRUE(getResult(track.getId(), "MyArtist").media_library_ids.empty()); + } + + TEST_F(KeywordsFixture, keywords_release_with_cluster) + { + ScopedRelease release{ session, "MyRelease" }; + ScopedTrack track{ session }; + ScopedClusterType clusterType{ session, "MyClusterType" }; + ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setRelease(release.get()); + cluster.get().modify()->addTrack(track.get()); + } + + collectResults(); + + EXPECT_EQ(getResult(release.getId(), "MyRelease").cluster_ids, std::unordered_set{ cluster.getId() }); + } + + TEST_F(KeywordsFixture, keywords_release_with_media_library) + { + ScopedRelease release{ session, "MyRelease" }; + ScopedTrack track{ session }; + ScopedMediaLibrary library{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setRelease(release.get()); + track.get().modify()->setMediaLibrary(library.get()); + } + + collectResults(); + + EXPECT_EQ(getResult(release.getId(), "MyRelease").media_library_ids, std::unordered_set{ library.getId() }); + } + + TEST_F(DatabaseFixture, MediumId_find_emptyDatabase) + { + auto transaction{ session.createReadTransaction() }; + + auto result = any_medium::findIds(session, any_medium::Type::ALL, {}, {}, {}, std::nullopt); + EXPECT_TRUE(result.results.empty()); + EXPECT_FALSE(result.moreResults); + } + + TEST_F(DatabaseFixture, MediumId_find_no_filters) + { + const ScopedArtist artist{ session, "MyArtist" }; + const ScopedRelease release{ session, "MyRelease" }; + const ScopedTrack track{ session }; + + resetViews(); + + const auto expected = std::unordered_set{ artist.getId(), release.getId(), track.getId() }; + + auto transaction{ session.createReadTransaction() }; + + const auto result = any_medium::findIds(session, any_medium::Type::ALL, {}, {}, {}, std::nullopt); + const auto result_set = std::unordered_set{ result.results.begin(), result.results.end() }; + + EXPECT_EQ(result_set, expected); + EXPECT_FALSE(result.moreResults); + } + + TEST_F(DatabaseFixture, MediumId_find_keyword) + { + const ScopedArtist artist{ session, "MyArtist" }; + const ScopedRelease release{ session, "MyRelease" }; + const ScopedTrack track{ session }; + + resetViews(); + + const auto expected = std::unordered_set{ artist.getId(), release.getId() }; + + auto transaction{ session.createReadTransaction() }; + + const auto result = any_medium::findIds(session, any_medium::Type::ALL, { "My" }, {}, {}, std::nullopt); + const auto result_set = std::unordered_set{ result.results.begin(), result.results.end() }; + + EXPECT_EQ(result_set, expected); + EXPECT_FALSE(result.moreResults); + } + + TEST_F(DatabaseFixture, MediumId_find_media_library) + { + const ScopedArtist artist{ session, "MyArtist" }; + const ScopedRelease release{ session, "MyRelease" }; + ScopedTrack track{ session }; + ScopedMediaLibrary library{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setMediaLibrary(library.get()); + } + + resetViews(); + + const auto expected = std::unordered_set{ track.getId() }; + + auto transaction{ session.createReadTransaction() }; + + const auto result = any_medium::findIds(session, any_medium::Type::ALL, {}, {}, library.getId(), std::nullopt); + const auto result_set = std::unordered_set{ result.results.begin(), result.results.end() }; + + EXPECT_EQ(result_set, expected); + EXPECT_FALSE(result.moreResults); + } + + TEST_F(DatabaseFixture, MediumId_find_cluster) + { + const ScopedArtist artist{ session, "MyArtist" }; + const ScopedRelease release{ session, "MyRelease" }; + ScopedTrack track{ session }; + ScopedClusterType clusterType{ session, "MyClusterType" }; + ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; + + { + auto transaction{ session.createWriteTransaction() }; + cluster.get().modify()->addTrack(track.get()); + } + + resetViews(); + + const auto expected = std::unordered_set{ track.getId() }; + + auto transaction{ session.createReadTransaction() }; + + ClusterId clusters[1] = { cluster.getId() }; + const auto result = any_medium::findIds(session, any_medium::Type::ALL, {}, clusters, {}, std::nullopt); + const auto result_set = std::unordered_set{ result.results.begin(), result.results.end() }; + + EXPECT_EQ(result_set, expected); + EXPECT_FALSE(result.moreResults); + } +} // namespace lms::db::tests diff --git a/src/libs/database/test/CMakeLists.txt b/src/libs/database/test/CMakeLists.txt index 451581c85..f28e6af71 100644 --- a/src/libs/database/test/CMakeLists.txt +++ b/src/libs/database/test/CMakeLists.txt @@ -1,5 +1,6 @@ add_executable(test-database + AnyMedium.cpp Artist.cpp Cluster.cpp Common.cpp diff --git a/src/libs/database/test/Common.cpp b/src/libs/database/test/Common.cpp index b61399b7a..785ef1b92 100644 --- a/src/libs/database/test/Common.cpp +++ b/src/libs/database/test/Common.cpp @@ -62,6 +62,7 @@ namespace lms::db::tests db::Session s{ _tmpDb->getDb() }; s.prepareTablesIfNeeded(); s.createIndexesIfNeeded(); + s.createViewsIfNeeded(); } } @@ -91,6 +92,14 @@ namespace lms::db::tests EXPECT_EQ(User::getCount(session), 0); } + void DatabaseFixture::resetViews() + { + auto transaction = session.createWriteTransaction(); + + session.dropViews(); + session.createViewsIfNeeded(); + } + TEST_F(DatabaseFixture, vacuum) { session.vacuum(); diff --git a/src/libs/database/test/Common.hpp b/src/libs/database/test/Common.hpp index 45f53db5c..107e081d5 100644 --- a/src/libs/database/test/Common.hpp +++ b/src/libs/database/test/Common.hpp @@ -42,14 +42,14 @@ namespace lms::db::tests { - template + template class [[nodiscard]] ScopedEntity { public: using IdType = typename T::IdType; - template - ScopedEntity(db::Session& session, Args&&... args) + template + ScopedEntity(db::Session& session, Args&& ...args) : _session{ session } { auto transaction{ _session.createWriteTransaction() }; @@ -113,8 +113,7 @@ namespace lms::db::tests class ScopedFileDeleter final { public: - ScopedFileDeleter(const std::filesystem::path& path) - : _path{ path } {} + ScopedFileDeleter(const std::filesystem::path& path) : _path{ path } {} ~ScopedFileDeleter() { std::filesystem::remove(_path); } private: @@ -155,5 +154,7 @@ namespace lms::db::tests public: db::Session session{ _tmpDb->getDb() }; + + void resetViews(); }; -} // namespace lms::db::tests \ No newline at end of file +} \ No newline at end of file diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index dbe27c9c1..13dd33c42 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(lmsscanner SHARED impl/ScanStepComputeClusterStats.cpp impl/ScanStepDiscoverFiles.cpp impl/ScanStepOptimize.cpp + impl/ScanStepRecreateViews.cpp impl/ScanStepRemoveOrphanDbFiles.cpp impl/ScanStepScanFiles.cpp ) diff --git a/src/libs/services/scanner/impl/ScanStepRecreateViews.cpp b/src/libs/services/scanner/impl/ScanStepRecreateViews.cpp new file mode 100644 index 000000000..f89581246 --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepRecreateViews.cpp @@ -0,0 +1,25 @@ +#include "ScanStepRecreateViews.hpp" + +#include "core/ILogger.hpp" +#include "database/Db.hpp" +#include "database/Session.hpp" +#include "database/Track.hpp" + +namespace lms::scanner +{ + void ScanStepRecreateViews::process(ScanContext&) + { + using namespace db; + + if (_abortScan) + return; + + Session& session{ _db.getTLSSession() }; + + auto transaction{ session.createWriteTransaction() }; + session.dropViews(); + session.createViewsIfNeeded(); + + LMS_LOG(DBUPDATER, DEBUG, "Views recreated"); + } +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/ScanStepRecreateViews.hpp b/src/libs/services/scanner/impl/ScanStepRecreateViews.hpp new file mode 100644 index 000000000..00c8f1ee1 --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepRecreateViews.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "ScanStepBase.hpp" + +namespace lms::scanner +{ + class ScanStepRecreateViews : public ScanStepBase + { + public: + using ScanStepBase::ScanStepBase; + + private: + core::LiteralString getStepName() const override { return "Recreate views"; } + ScanStep getStep() const override { return ScanStep::RecreateViews; } + void process(ScanContext& context) override; + }; +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index 852d28d20..d99dab1e4 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -35,6 +35,7 @@ #include "ScanStepComputeClusterStats.hpp" #include "ScanStepDiscoverFiles.hpp" #include "ScanStepOptimize.hpp" +#include "ScanStepRecreateViews.hpp" #include "ScanStepRemoveOrphanDbFiles.hpp" #include "ScanStepScanFiles.hpp" @@ -344,6 +345,7 @@ namespace lms::scanner _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); + _scanSteps.push_back(std::make_unique(params)); } ScannerSettings ScannerService::readSettings() diff --git a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp index b056faf81..b41dda531 100644 --- a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp +++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp @@ -66,6 +66,7 @@ namespace lms::scanner DiscoverFiles, FetchTrackFeatures, Optimize, + RecreateViews, ReloadSimilarityEngine, ScanFiles, }; diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index c67e9ce6b..25f2c32df 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -36,6 +36,9 @@ add_executable(lms ui/explore/DatabaseCollectorBase.cpp ui/explore/Explore.cpp ui/explore/Filters.cpp + ui/explore/MultisearchCollector.cpp + ui/explore/MultisearchListHelpers.cpp + ui/explore/MultisearchView.cpp ui/explore/PlayQueueController.cpp ui/explore/ReleaseCollector.cpp ui/explore/ReleaseHelpers.cpp @@ -51,7 +54,7 @@ add_executable(lms ui/resource/AudioTranscodingResource.cpp ui/resource/CoverResource.cpp ui/resource/DownloadResource.cpp - ) +) target_include_directories(lms PRIVATE ui/ diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 63cb13096..d70af99da 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -281,6 +281,10 @@ namespace lms bool migrationPerformed{ session.migrateSchemaIfNeeded() }; session.createIndexesIfNeeded(); + if (migrationPerformed) + session.dropViews(); + session.createViewsIfNeeded(); + // As this may be quite long, we only do it during startup if (migrationPerformed) session.vacuum(); diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 8624bd38a..50f1ae4e9 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -93,6 +93,7 @@ namespace lms::ui res->use(appRoot + "mediaplayer"); res->use(appRoot + "messages"); res->use(appRoot + "misc"); + res->use(appRoot + "multisearch"); res->use(appRoot + "notifications"); res->use(appRoot + "playqueue"); res->use(appRoot + "release"); @@ -140,6 +141,7 @@ namespace lms::ui { "/tracks", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracks") }, { "/tracklists", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracklists") }, { "/tracklist", IdxExplore, false, std::nullopt }, + { "/multisearch", IdxExplore, true, std::nullopt }, { "/playqueue", IdxPlayQueue, false, Wt::WString::tr("Lms.PlayQueue.playqueue") }, { "/settings", IdxSettings, false, Wt::WString::tr("Lms.Settings.settings") }, { "/admin/libraries", IdxAdminLibraries, true, Wt::WString::tr("Lms.Admin.MediaLibraries.media-libraries") }, @@ -420,6 +422,7 @@ namespace lms::ui navbar->bindNew("tracklists", Wt::WLink{ Wt::LinkType::InternalPath, "/tracklists" }, Wt::WString::tr("Lms.Explore.tracklists")); Filters* filters{ navbar->bindNew("filters") }; + navbar->bindString("username", getUserLoginName(), Wt::TextFormat::Plain); navbar->bindNew("settings", Wt::WLink{ Wt::LinkType::InternalPath, "/settings" }, Wt::WString::tr("Lms.Settings.menu-settings")); @@ -449,7 +452,16 @@ namespace lms::ui mainStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default std::unique_ptr playQueue{ std::make_unique() }; - Explore* explore{ mainStack->addNew(*filters, *playQueue) }; + + auto* searEdit = navbar->bindNew("multisearch"); + + { + auto transaction = LmsApp->getDbSession().createReadTransaction(); + if (LmsApp->getUser()->getInterfaceEnableMultisearch()) + navbar->setCondition("if-multisearch-enabled", true); + } + + Explore* explore{ mainStack->addNew(*filters, *playQueue, *searEdit) }; _playQueue = mainStack->addWidget(std::move(playQueue)); mainStack->addNew(); diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index f82c8445a..f8cff2637 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -51,6 +51,8 @@ namespace lms::ui { public: // Associate each field with a unique string literal. + static inline const Field InterfaceEnableMultisearchField{ "interface-enable-multisearch" }; + static inline const Field InterfaceEnableSinglesearchField{ "interface-enable-singlesearch" }; static inline const Field TranscodingModeField{ "transcoding-mode" }; static inline const Field TranscodeFormatField{ "transcoding-output-format" }; static inline const Field TranscodeBitrateField{ "transcoding-output-bitrate" }; @@ -79,6 +81,8 @@ namespace lms::ui { initializeModels(); + addField(InterfaceEnableMultisearchField); + addField(InterfaceEnableSinglesearchField); addField(TranscodingModeField); addField(TranscodeBitrateField); addField(TranscodeFormatField); @@ -136,6 +140,14 @@ namespace lms::ui User::pointer user{ LmsApp->getUser() }; + { + bool interfaceEnableMultisearchField{ Wt::asNumber(value(InterfaceEnableMultisearchField)) != 0 }; + user.modify()->setInterfaceEnableMultisearch(interfaceEnableMultisearchField); + + bool interfaceEnableSinglesearchField{ Wt::asNumber(value(InterfaceEnableSinglesearchField)) != 0 }; + user.modify()->setInterfaceEnableSinglesearch(interfaceEnableSinglesearchField); + } + { MediaPlayer::Settings settings; @@ -204,6 +216,11 @@ namespace lms::ui User::pointer user{ LmsApp->getUser() }; + { + setValue(InterfaceEnableMultisearchField, user->getInterfaceEnableMultisearch()); + setValue(InterfaceEnableSinglesearchField, user->getInterfaceEnableSinglesearch()); + } + { const auto settings{ *LmsApp->getMediaPlayer().getSettings() }; @@ -425,6 +442,15 @@ namespace lms::ui t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm)); } + // Interface + { + // Enable multisearch + t->setFormWidget(SettingsModel::InterfaceEnableMultisearchField, std::make_unique()); + + // Enable singlesearch + t->setFormWidget(SettingsModel::InterfaceEnableSinglesearchField, std::make_unique()); + } + // Audio { // Transcode diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index 1fef41584..e95a04c11 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -80,6 +80,15 @@ namespace lms::ui::utils return cover; } + std::unique_ptr createCover(db::ArtistId artistId, CoverResource::Size size) + { + auto cover{ std::make_unique() }; + cover->setImageLink(LmsApp->getCoverResource()->getArtistUrl(artistId, size)); + cover->setStyleClass("Lms-cover img-fluid"); // HACK + cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK + return cover; + } + std::unique_ptr createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete) { auto res{ std::make_unique(Wt::WString{ canDelete ? " " : "" } + name, Wt::TextFormat::UnsafeXHTML) }; @@ -151,6 +160,29 @@ namespace lms::ui::utils return clusterContainer; } + std::unique_ptr createArtistAnchorList(const std::vector& artists, std::string_view cssAnchorClass) + { + using namespace db; + + std::unique_ptr artistContainer{ std::make_unique() }; + + bool firstArtist{ true }; + + for (const auto& artist : artists) + { + if (!firstArtist) + artistContainer->addNew(" · "); + + auto anchor{ createArtistAnchor(artist) }; + anchor->addStyleClass("text-decoration-none"); // hack + anchor->addStyleClass(std::string{ cssAnchorClass }); + artistContainer->addWidget(std::move(anchor)); + firstArtist = false; + } + + return artistContainer; + } + std::unique_ptr createArtistAnchorList(const std::vector& artistIds, std::string_view cssAnchorClass) { using namespace db; @@ -179,6 +211,40 @@ namespace lms::ui::utils return artistContainer; } + std::unique_ptr createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector& artists, std::string_view cssAnchorClass) + { + using namespace db; + + std::size_t matchCount{}; + std::string_view::size_type currentOffset{}; + + auto result{ std::make_unique() }; + + // consider order is guaranteed + we will likely succeed + for (const auto& artist : artists) + { + const auto pos{ displayName.find(artist->getName(), currentOffset) }; + if (pos == std::string_view::npos) + break; + + assert(pos >= currentOffset); + if (pos != currentOffset) + result->addNew(std::string{ displayName.substr(currentOffset, pos - currentOffset) }, Wt::TextFormat::Plain); + + auto anchor{ createArtistAnchor(artist) }; + anchor->addStyleClass("text-decoration-none"); // hack + anchor->addStyleClass(std::string{ cssAnchorClass }); // hack + result->addWidget(std::move(anchor)); + currentOffset = pos + artist->getName().size(); + matchCount += 1; + } + + if (matchCount != artists.size()) + return createArtistAnchorList(artists, cssAnchorClass); + + return result; + } + std::unique_ptr createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector& artistIds, std::string_view cssAnchorClass) { using namespace db; diff --git a/src/lms/ui/Utils.hpp b/src/lms/ui/Utils.hpp index aab8de548..62bf8d4d8 100644 --- a/src/lms/ui/Utils.hpp +++ b/src/lms/ui/Utils.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -54,16 +55,18 @@ namespace lms::ui::utils { std::string durationToString(std::chrono::milliseconds msDuration); - std::unique_ptr createCover(db::ReleaseId releaseId, CoverResource::Size size); - std::unique_ptr createCover(db::TrackId trackId, CoverResource::Size size); + std::unique_ptr createCover(db::ReleaseId releaseId, CoverResource::Size size); + std::unique_ptr createCover(db::TrackId trackId, CoverResource::Size size); + std::unique_ptr createCover(db::ArtistId artistId, CoverResource::Size size); std::unique_ptr createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete = false); std::unique_ptr createFilterCluster(db::ClusterId clusterId, bool canDelete = false); std::unique_ptr createFilterClustersForTrack(db::ObjectPtr track, Filters& filters); - std::unique_ptr createArtistAnchorList(const std::vector& artistIds, std::string_view cssAnchorClass = "link-success"); - std::unique_ptr createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector& artistIds, std::string_view cssAnchorClass = "link-success"); - std::unique_ptr createArtistsAnchorsForRelease(db::ObjectPtr release, db::ArtistId omitIfMatchThisArtist = {}, std::string_view cssAnchorClass = "link-success"); + std::unique_ptr createArtistAnchorList(const std::vector& artistIds, std::string_view cssAnchorClass = "link-success"); + std::unique_ptr createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector& artistIds, std::string_view cssAnchorClass = "link-success"); + std::unique_ptr createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector& artistIds, std::string_view cssAnchorClass = "link-success"); + std::unique_ptr createArtistsAnchorsForRelease(db::ObjectPtr release, db::ArtistId omitIfMatchThisArtist = {}, std::string_view cssAnchorClass = "link-success"); Wt::WLink createArtistLink(db::ObjectPtr artist); std::unique_ptr createArtistAnchor(db::ObjectPtr artist, bool setText = true); diff --git a/src/lms/ui/explore/ArtistListHelpers.cpp b/src/lms/ui/explore/ArtistListHelpers.cpp index c8dbb0387..e27cbda70 100644 --- a/src/lms/ui/explore/ArtistListHelpers.cpp +++ b/src/lms/ui/explore/ArtistListHelpers.cpp @@ -21,16 +21,33 @@ #include "database/Artist.hpp" #include "database/Session.hpp" -#include "LmsApplication.hpp" #include "Utils.hpp" +#include +#include + namespace lms::ui::ArtistListHelpers { + void bindName(Template& entry, const db::ObjectPtr& artist) + { + entry.bindWidget("name", utils::createArtistAnchor(artist)); + } + + void bindCover(Template& entry, const db::ObjectPtr& artist) + { + Wt::WAnchor* anchor = entry.bindWidget("cover", utils::createArtistAnchor(artist, false)); + auto cover = utils::createCover(artist->getId(), CoverResource::Size::Small); + cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK + anchor->setImage(std::move(cover)); + } + std::unique_ptr createEntry(const db::ObjectPtr& artist) { - auto res{ std::make_unique(Wt::WString::tr("Lms.Explore.Artists.template.entry")) }; - res->bindWidget("name", utils::createArtistAnchor(artist)); + auto entry = std::make_unique