diff --git a/src/libs/database/impl/Directory.cpp b/src/libs/database/impl/Directory.cpp index 0b20237f..f994cc0c 100644 --- a/src/libs/database/impl/Directory.cpp +++ b/src/libs/database/impl/Directory.cpp @@ -19,6 +19,7 @@ #include "database/Directory.hpp" +#include "database/MediaLibrary.hpp" #include "database/Session.hpp" #include "IdTypeTraits.hpp" @@ -43,12 +44,15 @@ namespace lms::db query.groupBy("d.id"); } + if (params.mediaLibrary.isValid()) + query.where("d.media_library_id = ?").bind(params.mediaLibrary); + if (params.parentDirectory.isValid()) query.where("d.parent_directory_id = ?").bind(params.parentDirectory); if (params.release.isValid()) query.where("t.release_id = ?").bind(params.release); - + if (params.artist.isValid()) { query.join("artist a ON a.id = t_a_l.artist_id") diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index e28d1233..d1d5de1e 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{ 61 }; + static constexpr Version LMS_DATABASE_VERSION{ 62 }; } VersionInfo::VersionInfo() @@ -631,6 +631,39 @@ SELECT } // namespace + void migrateFromV61(Session& session) + { + // Added a media_library_id in Directory + session.getDboSession()->execute(R"( +CREATE TABLE IF NOT EXISTS "directory_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "absolute_path" text not null, + "name" text not null, + "parent_directory_id" bigint, + "media_library_id" bigint, + constraint "fk_directory_parent_directory" foreign key ("parent_directory_id") references "directory" ("id") on delete cascade deferrable initially deferred, + constraint "fk_directory_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred + ))"); + + // Migrate data, with the new directory_id field set to null + session.getDboSession()->execute(R"(INSERT INTO directory_backup +SELECT + id, + version, + absolute_path, + name, + parent_directory_id, + NULL + FROM directory)"); + + session.getDboSession()->execute("DROP TABLE directory"); + session.getDboSession()->execute("ALTER TABLE directory_backup RENAME TO directory"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } + bool doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -668,6 +701,7 @@ SELECT { 58, migrateFromV58 }, { 59, migrateFromV59 }, { 60, migrateFromV60 }, + { 61, migrateFromV61 }, }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 03c9a2f7..73819aac 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -183,6 +183,7 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS directory_id_idx ON directory(id)"); _session.execute("CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)"); + _session.execute("CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)"); _session.execute("CREATE INDEX IF NOT EXISTS image_artist_idx ON image(artist_id)"); _session.execute("CREATE INDEX IF NOT EXISTS image_directory_idx ON image(directory_id)"); diff --git a/src/libs/database/include/database/Directory.hpp b/src/libs/database/include/database/Directory.hpp index 7cecbc69..ca74e156 100644 --- a/src/libs/database/include/database/Directory.hpp +++ b/src/libs/database/include/database/Directory.hpp @@ -21,22 +21,24 @@ #include #include -#include #include #include +#include #include #include "core/EnumSet.hpp" #include "database/ArtistId.hpp" #include "database/DirectoryId.hpp" -#include "database/ReleaseId.hpp" +#include "database/MediaLibraryId.hpp" #include "database/Object.hpp" +#include "database/ReleaseId.hpp" #include "database/Types.hpp" namespace lms::db { class Session; + class MediaLibrary; class Directory final : public Object { @@ -52,6 +54,7 @@ namespace lms::db core::EnumSet trackArtistLinkTypes; // and for these link types DirectoryId parentDirectory; // If set, directories that have this parent bool withNoTrack{}; // If set, directories that do not contain any track + MediaLibraryId mediaLibrary; // If set, directories in this library FindParameters& setRange(std::optional _range) { @@ -84,6 +87,11 @@ namespace lms::db withNoTrack = _withNoTrack; return *this; } + FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary) + { + mediaLibrary = _mediaLibrary; + return *this; + } }; // find @@ -100,10 +108,12 @@ namespace lms::db const std::filesystem::path& getAbsolutePath() const { return _absolutePath; } std::string_view getName() const { return _name; } ObjectPtr getParentDirectory() const { return _parent; } + ObjectPtr getMediaLibrary() const { return _mediaLibrary; } // setters void setAbsolutePath(const std::filesystem::path& p); void setParent(ObjectPtr parent); + void setMediaLibrary(ObjectPtr mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); } template void persist(Action& a) @@ -112,6 +122,7 @@ namespace lms::db Wt::Dbo::field(a, _name, "name"); Wt::Dbo::belongsTo(a, _parent, "parent_directory", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete directories on media library removal, we want to wait for the next scan to have a chance to migrate files } private: @@ -123,5 +134,6 @@ namespace lms::db std::string _name; Wt::Dbo::ptr _parent; + Wt::Dbo::ptr _mediaLibrary; }; } // namespace lms::db diff --git a/src/libs/services/scanner/impl/ScanStepAssociateArtistImages.cpp b/src/libs/services/scanner/impl/ScanStepAssociateArtistImages.cpp index 66d8ba0e..26c1bc0b 100644 --- a/src/libs/services/scanner/impl/ScanStepAssociateArtistImages.cpp +++ b/src/libs/services/scanner/impl/ScanStepAssociateArtistImages.cpp @@ -205,6 +205,9 @@ namespace lms::scanner void ScanStepAssociateArtistImages::process(ScanContext& context) { + if (_abortScan) + return; + if (context.stats.nbChanges() == 0) return; @@ -224,6 +227,9 @@ namespace lms::scanner ArtistImageAssociationContainer artistImageAssociations; while (fetchNextArtistImagesToUpdate(searchContext, artistImageAssociations)) { + if (_abortScan) + return; + updateArtistImages(session, artistImageAssociations); context.currentStepStats.processedElems += readBatchSize; _progressCallback(context.currentStepStats); diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp index ffaa9c51..a59342e9 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp @@ -104,18 +104,20 @@ namespace lms::scanner return res; } - Directory::pointer getOrCreateDirectory(Session& session, const std::filesystem::path& path, const std::filesystem::path& rootPath) + Directory::pointer getOrCreateDirectory(Session& session, const std::filesystem::path& path, const MediaLibrary::pointer& mediaLibrary) { Directory::pointer directory{ Directory::find(session, path) }; if (!directory) { Directory::pointer parentDirectory; - if (path != rootPath) - parentDirectory = getOrCreateDirectory(session, path.parent_path(), rootPath); + if (path != mediaLibrary->getPath()) + parentDirectory = getOrCreateDirectory(session, path.parent_path(), mediaLibrary); directory = session.create(path); directory.modify()->setParent(parentDirectory); + directory.modify()->setMediaLibrary(mediaLibrary); } + // Don't update library if it does not match, will be updated elsewhere return directory; } @@ -350,12 +352,13 @@ namespace lms::scanner std::vector scanResults; + std::filesystem::path currentDirectory; core::pathUtils::exploreFilesRecursive( mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path) { LMS_SCOPED_TRACE_DETAILED("Scanner", "OnExploreFile"); if (_abortScan) - return false; + return false; // stop iterating if (ec) { @@ -364,22 +367,33 @@ namespace lms::scanner } else { - bool fileToProcess{}; + bool fileMatched{}; if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedAudioFileExtensions)) { - fileToProcess = true; + fileMatched = true; if (checkAudioFileNeedScan(context, path, mediaLibrary)) _fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::AudioFile); } else if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedImageFileExtensions)) { - fileToProcess = true; + fileMatched = true; if (checkImageFileNeedScan(context, path)) _fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::ImageFile); } - if (fileToProcess) + if (fileMatched) { + // Not very efficient way to update media_library for directories + if (path.has_parent_path()) + { + const std::filesystem::path directory{ path.parent_path() }; + if (directory != currentDirectory) + { + updateDirectoryIfNeeded(currentDirectory, mediaLibrary); + currentDirectory = directory; + } + } + context.currentStepStats.processedElems++; _progressCallback(context.currentStepStats); } @@ -415,17 +429,19 @@ namespace lms::scanner return false; } + if (context.scanOptions.fullScan) + return true; + bool needUpdateLibrary{}; - if (!context.scanOptions.fullScan) + db::Session& dbSession{ _db.getTLSSession() }; + { + auto transaction{ dbSession.createReadTransaction() }; + // Skip file if last write is the same - db::Session& dbSession{ _db.getTLSSession() }; - auto transaction{ _db.getTLSSession().createReadTransaction() }; - const Track::pointer track{ Track::findByPath(dbSession, file) }; - if (track - && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t() + && track->getLastWriteTime() == lastWriteTime && track->getScanVersion() == _settings.scanVersion) { // this file may have been moved from one library to another, then we just need to update the media library id instead of a full rescan @@ -442,8 +458,7 @@ namespace lms::scanner if (needUpdateLibrary) { - db::Session& dbSession{ _db.getTLSSession() }; - auto transaction{ _db.getTLSSession().createWriteTransaction() }; + auto transaction{ dbSession.createWriteTransaction() }; Track::pointer track{ Track::findByPath(dbSession, file) }; assert(track); @@ -632,8 +647,9 @@ namespace lms::scanner track.modify()->setFileSize(fileInfo->fileSize); track.modify()->setLastWriteTime(fileInfo->lastWriteTime); - track.modify()->setMediaLibrary(MediaLibrary::find(dbSession, libraryInfo.id)); // may be null if settings are updated in // => next scan will correct this - track.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), libraryInfo.rootDirectory)); + MediaLibrary::pointer mediaLibrary{ MediaLibrary::find(dbSession, libraryInfo.id) }; // may be null if settings are updated in // => next scan will correct this + track.modify()->setMediaLibrary(mediaLibrary); + track.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), mediaLibrary)); 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 @@ -762,7 +778,8 @@ namespace lms::scanner image.modify()->setFileSize(fileInfo->fileSize); image.modify()->setHeight(imageInfo->height); image.modify()->setWidth(imageInfo->width); - image.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), libraryInfo.rootDirectory)); + MediaLibrary::pointer mediaLibrary{ MediaLibrary::find(dbSession, libraryInfo.id) }; // may be null if settings are updated in // => next scan will correct this + image.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), mediaLibrary)); if (added) { @@ -775,4 +792,34 @@ namespace lms::scanner stats.updates++; } } + + void ScanStepScanFiles::updateDirectoryIfNeeded(const std::filesystem::path& dirPath, const ScannerSettings::MediaLibraryInfo& libraryInfo) + { + db::Session& dbSession{ _db.getTLSSession() }; + + const bool needUpdateLibrary{ [&] { + auto transaction{ dbSession.createReadTransaction() }; + + Directory::pointer directory{ Directory::find(dbSession, dirPath) }; + if (!directory) + return false; // we create directories only of we find images or tracks inside + + MediaLibrary::pointer currentLibrary{ directory->getMediaLibrary() }; + return !currentLibrary || currentLibrary->getId() != libraryInfo.id; + }() }; + + if (needUpdateLibrary) + { + auto transaction{ dbSession.createWriteTransaction() }; + + Directory::pointer directory{ Directory::find(dbSession, dirPath) }; + assert(directory); + directory.modify()->setMediaLibrary(MediaLibrary::find(dbSession, libraryInfo.id)); // may be null if settings are updated in // => next scan will correct this + } + + if (dirPath != libraryInfo.rootDirectory && dirPath.has_parent_path()) + { + updateDirectoryIfNeeded(dirPath.parent_path(), libraryInfo); + } + } } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.hpp b/src/libs/services/scanner/impl/ScanStepScanFiles.hpp index fd9687d9..6c94d21a 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.hpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.hpp @@ -49,6 +49,8 @@ namespace lms::scanner void processAudioFileScanData(ScanContext& context, const std::filesystem::path& path, const metadata::Track* trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo); void processImageFileScanData(ScanContext& context, const std::filesystem::path& path, const ImageInfo* imageInfo, const ScannerSettings::MediaLibraryInfo& libraryInfo); + void updateDirectoryIfNeeded(const std::filesystem::path& directory, const ScannerSettings::MediaLibraryInfo& libraryInfo); + std::unique_ptr _metadataParser; const std::vector _extraTagsToParse; diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index a5087945..e0337491 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -116,7 +116,7 @@ namespace lms::api::subsonic void findRequestedArtistDirectories(RequestContext& context, const std::vector& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode) { - // For now, no need to "accelerate" all this + // For now, no need to optimize all this // Find all the directories that match the name and that do not contain any track (considered by the legacy API as artists) const std::size_t artistCount{ getParameterAs(context.parameters, "artistCount").value_or(20) }; if (artistCount == 0) @@ -126,12 +126,12 @@ namespace lms::api::subsonic throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize }; const std::size_t artistOffset{ getParameterAs(context.parameters, "artistOffset").value_or(0) }; - + Directory::FindParameters params; params.setKeywords(keywords); - params.setRange(Range{artistOffset, artistCount}); + params.setRange(Range{ artistOffset, artistCount }); params.setWithNoTrack(true); - // TODO media + params.setMediaLibrary(mediaLibrary); Directory::find(context.dbSession, params, [&](const Directory::pointer& directory) { Response::Node childNode; @@ -333,10 +333,7 @@ namespace lms::api::subsonic } } } - } // namespace - namespace - { Response handleSearchRequestCommon(RequestContext& context, bool id3) { // Mandatory params