diff --git a/README.md b/README.md index 2270e3bf..7cda5d98 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ _LMS_ provides several ways to help you find the music you like: * ... __Note__: the recommendation engine supports two modes: +* **Audio similarity** (default): uses MusicNN embeddings. Extraction throughput depends heavily on both the number of scanner threads and the speed of your storage. Typical reported speeds range from ~1k tracks/hour on a Raspberry Pi 4 to ~25k tracks/hour on an Intel Core i5-13500. * **Tag-based**: uses metadata tags `genre`, `mood`, `grouping`, and `language`. -* **Audio similarity**: uses MusicNN embeddings. Extraction throughput depends heavily on both the number of scanner threads and the speed of your storage. Typical reported speeds range from ~1k tracks/hour on a Raspberry Pi 4 (3 threads) to ~25k tracks/hour on an Intel Core i5-13500 (10 threads). To speed up extraction, consider tweaking `scanner-thread-count` in `lms.conf` (defaults to half the number of logical CPUs). ## About tags _LMS_ primarily relies on tags to organize your music collection but also supports browsing by directory using the [Subsonic/OpenSubsonic API](SUBSONIC.md). diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index a5d7530c..a38822e6 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -180,17 +180,15 @@ namespace lms::db bool Session::migrateSchemaIfNeeded() { - const bool migrationPerformed{ Migration::doDbMigration(*this) }; + return Migration::doDbMigration(*this); + } - // TODO: move this elsewhere - { - auto uniqueTransaction{ createWriteTransaction() }; + void Session::createScanSettingsIfNeeded(RecommendationEngineType defaultRecommendationEngineType) + { + auto uniqueTransaction{ createWriteTransaction() }; - if (!ScanSettings::find(*this)) - create(); - } - - return migrationPerformed; + if (!ScanSettings::find(*this)) + create().modify()->setRecommendationEngineType(defaultRecommendationEngineType); } void Session::createIndexesIfNeeded() diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index a551b17d..47bd2125 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -28,6 +28,7 @@ #include "database/Transaction.hpp" #include "database/Types.hpp" +#include "database/objects/Types.hpp" namespace lms::db { @@ -58,6 +59,7 @@ namespace lms::db void prepareTablesIfNeeded(); // need to run only once at startup bool migrateSchemaIfNeeded(); // returns true if migration was performed + void createScanSettingsIfNeeded(RecommendationEngineType defaultRecommendationEngineType = RecommendationEngineType::Clusters); void createIndexesIfNeeded(); void vacuumIfNeeded(); void vacuum(); diff --git a/src/libs/database/include/database/objects/ScanSettings.hpp b/src/libs/database/include/database/objects/ScanSettings.hpp index d8e68db1..e14aca6d 100644 --- a/src/libs/database/include/database/objects/ScanSettings.hpp +++ b/src/libs/database/include/database/objects/ScanSettings.hpp @@ -29,6 +29,7 @@ #include "database/IdType.hpp" #include "database/Object.hpp" +#include "database/objects/Types.hpp" LMS_DECLARE_IDTYPE(ScanSettingsId) @@ -49,14 +50,6 @@ namespace lms::db Hourly, }; - // Do not modify values (just add) - enum class RecommendationEngineType - { - Clusters = 0, - None = 2, - AudioSimilarity = 3, - }; - ScanSettings() = default; static pointer find(Session& session, std::string_view name = ""); diff --git a/src/libs/database/include/database/objects/Types.hpp b/src/libs/database/include/database/objects/Types.hpp index 851e66b9..052f9b61 100644 --- a/src/libs/database/include/database/objects/Types.hpp +++ b/src/libs/database/include/database/objects/Types.hpp @@ -114,6 +114,13 @@ namespace lms::db PubDateDesc, }; + enum class RecommendationEngineType + { + Clusters = 0, + None = 2, + AudioSimilarity = 3, + }; + enum class ReleaseArtistLinkSortMethod { None, diff --git a/src/libs/database/test/Migration.cpp b/src/libs/database/test/Migration.cpp index 90815466..c1be13d2 100644 --- a/src/libs/database/test/Migration.cpp +++ b/src/libs/database/test/Migration.cpp @@ -344,6 +344,7 @@ VALUES // Now perform full migration db.getTLSSession().migrateSchemaIfNeeded(); + db.getTLSSession().createScanSettingsIfNeeded(); // Now perform some dummy finds to ensure all fields are correctly mapped { diff --git a/src/libs/services/recommendation/impl/RecommendationService.cpp b/src/libs/services/recommendation/impl/RecommendationService.cpp index 3d12ba29..8682f79c 100644 --- a/src/libs/services/recommendation/impl/RecommendationService.cpp +++ b/src/libs/services/recommendation/impl/RecommendationService.cpp @@ -32,35 +32,35 @@ namespace lms::recommendation { namespace { - db::ScanSettings::RecommendationEngineType getRecommendationEngineType(db::Session& session) + db::RecommendationEngineType getRecommendationEngineType(db::Session& session) { auto transaction{ session.createReadTransaction() }; return db::ScanSettings::find(session)->getRecommendationEngineType(); } - EngineType toEngineType(db::ScanSettings::RecommendationEngineType type) + EngineType toEngineType(db::RecommendationEngineType type) { switch (type) { - case db::ScanSettings::RecommendationEngineType::None: + case db::RecommendationEngineType::None: return EngineType::None; - case db::ScanSettings::RecommendationEngineType::Clusters: + case db::RecommendationEngineType::Clusters: return EngineType::Clusters; - case db::ScanSettings::RecommendationEngineType::AudioSimilarity: + case db::RecommendationEngineType::AudioSimilarity: return EngineType::AudioSimilarity; } return EngineType::None; } - std::unique_ptr createEngine(db::ScanSettings::RecommendationEngineType type, db::IDb& db) + std::unique_ptr createEngine(db::RecommendationEngineType type, db::IDb& db) { switch (type) { - case db::ScanSettings::RecommendationEngineType::Clusters: + case db::RecommendationEngineType::Clusters: return std::make_unique(db); - case db::ScanSettings::RecommendationEngineType::AudioSimilarity: + case db::RecommendationEngineType::AudioSimilarity: return std::make_unique(db); - case db::ScanSettings::RecommendationEngineType::None: + case db::RecommendationEngineType::None: return nullptr; } return nullptr; @@ -139,7 +139,7 @@ namespace lms::recommendation return false; } - db::ScanSettings::RecommendationEngineType RecommendationService::prepareReload() + db::RecommendationEngineType RecommendationService::prepareReload() { const auto type{ getRecommendationEngineType(_db.getTLSSession()) }; std::unique_lock lock{ _mutex }; diff --git a/src/libs/services/recommendation/impl/RecommendationService.hpp b/src/libs/services/recommendation/impl/RecommendationService.hpp index 2caeea70..699ef7ac 100644 --- a/src/libs/services/recommendation/impl/RecommendationService.hpp +++ b/src/libs/services/recommendation/impl/RecommendationService.hpp @@ -59,7 +59,7 @@ namespace lms::recommendation ArtistResults findSimilarArtists(db::ArtistId artistId, core::EnumSet linkTypes, std::size_t maxCount) const override; TrackResults findTrackSimilarityPath(db::TrackId startTrackId, db::TrackId endTrackId, std::size_t maxCount) const override; - db::ScanSettings::RecommendationEngineType prepareReload(); + db::RecommendationEngineType prepareReload(); db::IDb& _db; mutable std::shared_mutex _mutex; diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index d1ce1a5e..c385b392 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -126,7 +126,7 @@ namespace lms::scanner settings->allowArtistMBIDFallback = scanSettings->getAllowMBIDArtistMerge(); settings->artistImageFallbackToRelease = scanSettings->getArtistImageFallbackToRelease(); - settings->extractMusicNNEmbeddings = scanSettings->getRecommendationEngineType() == db::ScanSettings::RecommendationEngineType::AudioSimilarity; + settings->extractMusicNNEmbeddings = scanSettings->getRecommendationEngineType() == db::RecommendationEngineType::AudioSimilarity; settings->musicnnModelPath = core::Service::get()->getPath("musicnn-model-path", "/usr/share/lms/models/MSD_musicnn_embedding.onnx"); settings->musicnnMaxPatchCountPerTrack = core::Service::get()->getULong("musicnn-max-patch-count-per-track", 20); diff --git a/src/lms/main.cpp b/src/lms/main.cpp index a409b759..3fd5fff4 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -36,6 +36,7 @@ #include "core/SystemPaths.hpp" #include "audio/IAudioOutput.hpp" +#include "audio/IMusicNNEmbeddingExtractor.hpp" #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/profiling/IQueryProfiler.hpp" @@ -436,6 +437,7 @@ namespace lms db::Session session{ *database }; session.prepareTablesIfNeeded(); bool migrationPerformed{ session.migrateSchemaIfNeeded() }; + session.createScanSettingsIfNeeded(audio::canExtractMusicNNEmbeddings() ? db::RecommendationEngineType::AudioSimilarity : db::RecommendationEngineType::Clusters); session.createIndexesIfNeeded(); // As this may be quite long, we only do it during startup diff --git a/src/lms/ui/admin/ScanSettingsView.cpp b/src/lms/ui/admin/ScanSettingsView.cpp index 44caa2c2..1c8bca51 100644 --- a/src/lms/ui/admin/ScanSettingsView.cpp +++ b/src/lms/ui/admin/ScanSettingsView.cpp @@ -44,7 +44,7 @@ namespace lms::ui { namespace { - using RecommendationEngineTypeModel = ValueStringModel; + using RecommendationEngineTypeModel = ValueStringModel; class RecommendationEngineValidator : public Wt::WValidator { @@ -63,7 +63,7 @@ namespace lms::ui std::string inputStr{ input.toUTF8() }; const auto row{ _model->getRowFromString(inputStr) }; - if (row && _model->getValue(*row) == db::ScanSettings::RecommendationEngineType::AudioSimilarity) + if (row && _model->getValue(*row) == db::RecommendationEngineType::AudioSimilarity) { if (!core::Service::get()->isEngineTypeSupported(recommendation::EngineType::AudioSimilarity)) return Wt::WValidator::Result{ Wt::ValidationState::Invalid, Wt::WString::tr("Lms.Admin.Database.recommendation-engine-not-supported") }; @@ -247,10 +247,10 @@ namespace lms::ui _updateStartTimeModel->add(time.toString(), time); } - _recommendationEngineTypeModel = std::make_shared>(); - _recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.audio-similarity"), db::ScanSettings::RecommendationEngineType::AudioSimilarity); - _recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.clusters"), db::ScanSettings::RecommendationEngineType::Clusters); - _recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.none"), db::ScanSettings::RecommendationEngineType::None); + _recommendationEngineTypeModel = std::make_shared>(); + _recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.audio-similarity"), db::RecommendationEngineType::AudioSimilarity); + _recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.clusters"), db::RecommendationEngineType::Clusters); + _recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.none"), db::RecommendationEngineType::None); } std::shared_ptr _updatePeriodModel; diff --git a/src/tools/db-generator/LmsDbGenerator.cpp b/src/tools/db-generator/LmsDbGenerator.cpp index c27db8a5..8042b10a 100644 --- a/src/tools/db-generator/LmsDbGenerator.cpp +++ b/src/tools/db-generator/LmsDbGenerator.cpp @@ -282,6 +282,7 @@ int main(int argc, char* argv[]) db::Session session{ *db }; session.prepareTablesIfNeeded(); session.migrateSchemaIfNeeded(); + session.createScanSettingsIfNeeded(); session.createIndexesIfNeeded(); std::cout << "Starting generation..." << std::endl;