From 37bd6de658b881e91305edc2777e51e991e53250 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 14 Oct 2021 13:03:54 +0200 Subject: [PATCH] db/simple: add option to hide CUE target songs This reduces duplicates in the music database by hiding the original song file when it is referenced by a CUE sheet. Closes https://github.com/MusicPlayerDaemon/MPD/issues/1275 --- NEWS | 1 + doc/plugins.rst | 7 +++++++ src/db/plugins/simple/Directory.cxx | 19 ++++++++++++------- src/db/plugins/simple/Directory.hxx | 3 ++- .../plugins/simple/SimpleDatabasePlugin.cxx | 2 ++ .../plugins/simple/SimpleDatabasePlugin.hxx | 2 ++ src/db/plugins/simple/Song.hxx | 6 ++++++ src/db/update/Playlist.cxx | 16 ++++++++++++---- src/db/update/Walk.hxx | 3 +++ 9 files changed, 47 insertions(+), 12 deletions(-) diff --git a/NEWS b/NEWS index 564c9b985..996ac24fa 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,7 @@ ver 0.23 (not yet released) - proxy: require MPD 0.20 or later - proxy: require libmpdclient 2.11 or later - proxy: split search into chunks to avoid exceeding the output buffer + - simple: add option to hide CUE target songs - upnp: support libnpupnp instead of libupnp * archive - zzip, iso9660: ignore file names which are invalid UTF-8 diff --git a/doc/plugins.rst b/doc/plugins.rst index 382426dbc..d7ee8262d 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -23,6 +23,13 @@ The default plugin. Stores a copy of the database in memory. A file is used for - The path of the cache directory for additional storages mounted at runtime. This setting is necessary for the **mount** protocol command. * - **compress yes|no** - Compress the database file using gzip? Enabled by default (if built with zlib). + * - **hide_playlist_targets yes|no** + - Hide songs which are referenced by playlists? Thas is, + playlist files which are represented in the database as virtual + directories (playlist plugin setting ``as_directory``). This + option is enabled by default and avoids duplicate songs; one + copy for the original file, and another copy in the virtual + directory of a CUE file referring to it. proxy ----- diff --git a/src/db/plugins/simple/Directory.cxx b/src/db/plugins/simple/Directory.cxx index b03994730..308b9f300 100644 --- a/src/db/plugins/simple/Directory.cxx +++ b/src/db/plugins/simple/Directory.cxx @@ -109,21 +109,21 @@ Directory::FindChild(std::string_view name) const noexcept return nullptr; } -bool -Directory::TargetExists(std::string_view _target) const noexcept +Song * +Directory::LookupTargetSong(std::string_view _target) noexcept { StringView target{_target}; if (target.SkipPrefix("../")) { if (parent == nullptr) - return false; + return nullptr; - return parent->TargetExists(target); + return parent->LookupTargetSong(target); } /* sorry for the const_cast ... */ - const auto lr = const_cast(this)->LookupDirectory(target); - return lr.directory->FindSong(lr.rest) != nullptr; + const auto lr = LookupDirectory(target); + return lr.directory->FindSong(lr.rest); } void @@ -229,6 +229,7 @@ Directory::Sort() noexcept void Directory::Walk(bool recursive, const SongFilter *filter, + bool hide_playlist_targets, const VisitDirectory& visit_directory, const VisitSong& visit_song, const VisitPlaylist& visit_playlist) const { @@ -247,7 +248,10 @@ Directory::Walk(bool recursive, const SongFilter *filter, } if (visit_song) { - for (auto &song : songs){ + for (auto &song : songs) { + if (hide_playlist_targets && song.in_playlist) + continue; + const auto song2 = song.Export(); if (filter == nullptr || filter->Match(song2)) visit_song(song2); @@ -265,6 +269,7 @@ Directory::Walk(bool recursive, const SongFilter *filter, if (recursive) child.Walk(recursive, filter, + hide_playlist_targets, visit_directory, visit_song, visit_playlist); } diff --git a/src/db/plugins/simple/Directory.hxx b/src/db/plugins/simple/Directory.hxx index 50728fb45..e13b556ff 100644 --- a/src/db/plugins/simple/Directory.hxx +++ b/src/db/plugins/simple/Directory.hxx @@ -214,7 +214,7 @@ public: LookupResult LookupDirectory(std::string_view uri) noexcept; [[gnu::pure]] - bool TargetExists(std::string_view target) const noexcept; + Song *LookupTargetSong(std::string_view target) noexcept; [[gnu::pure]] bool IsEmpty() const noexcept { @@ -303,6 +303,7 @@ public: * Caller must lock #db_mutex. */ void Walk(bool recursive, const SongFilter *match, + bool hide_playlist_targets, const VisitDirectory& visit_directory, const VisitSong& visit_song, const VisitPlaylist& visit_playlist) const; diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.cxx b/src/db/plugins/simple/SimpleDatabasePlugin.cxx index 90dda7a01..58588723a 100644 --- a/src/db/plugins/simple/SimpleDatabasePlugin.cxx +++ b/src/db/plugins/simple/SimpleDatabasePlugin.cxx @@ -60,6 +60,7 @@ inline SimpleDatabase::SimpleDatabase(const ConfigBlock &block) #ifdef ENABLE_ZLIB compress(block.GetBlockValue("compress", true)), #endif + hide_playlist_targets(block.GetBlockValue("hide_playlist_targets", true)), cache_path(block.GetPath("cache_directory")) { if (path.IsNull()) @@ -306,6 +307,7 @@ SimpleDatabase::Visit(const DatabaseSelection &selection, visit_directory(r.directory->Export()); r.directory->Walk(selection.recursive, selection.filter, + hide_playlist_targets, visit_directory, visit_song, visit_playlist); helper.Commit(); diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.hxx b/src/db/plugins/simple/SimpleDatabasePlugin.hxx index 9a8bd42f9..6b51c8015 100644 --- a/src/db/plugins/simple/SimpleDatabasePlugin.hxx +++ b/src/db/plugins/simple/SimpleDatabasePlugin.hxx @@ -44,6 +44,8 @@ class SimpleDatabase : public Database { bool compress; #endif + bool hide_playlist_targets; + /** * The path where cache files for Mount() are located. */ diff --git a/src/db/plugins/simple/Song.hxx b/src/db/plugins/simple/Song.hxx index e4a176c38..8866c9947 100644 --- a/src/db/plugins/simple/Song.hxx +++ b/src/db/plugins/simple/Song.hxx @@ -101,6 +101,12 @@ struct Song { */ AudioFormat audio_format = AudioFormat::Undefined(); + /** + * Is this song referenced by at least one playlist file that + * is part of the database? + */ + bool in_playlist = false; + template Song(F &&_filename, Directory &_parent) noexcept :parent(_parent), filename(std::forward(_filename)) {} diff --git a/src/db/update/Playlist.cxx b/src/db/update/Playlist.cxx index 3c3c1aaf9..6403eb504 100644 --- a/src/db/update/Playlist.cxx +++ b/src/db/update/Playlist.cxx @@ -141,10 +141,18 @@ UpdateWalk::PurgeDanglingFromPlaylists(Directory &directory) noexcept directory.ForEachSongSafe([&](Song &song){ if (!song.target.empty() && - !PathTraitsUTF8::IsAbsoluteOrHasScheme(song.target.c_str()) && - !directory.TargetExists(song.target)) { - editor.DeleteSong(directory, &song); - modified = true; + !PathTraitsUTF8::IsAbsoluteOrHasScheme(song.target.c_str())) { + Song *target = directory.LookupTargetSong(song.target.c_str()); + if (target == nullptr) { + /* the target does not exist: remove + the virtual song */ + editor.DeleteSong(directory, &song); + modified = true; + } else { + /* the target exists: mark it (for + option "hide_playlist_targets") */ + target->in_playlist = true; + } } }); } diff --git a/src/db/update/Walk.hxx b/src/db/update/Walk.hxx index 29295d1bf..f7461c905 100644 --- a/src/db/update/Walk.hxx +++ b/src/db/update/Walk.hxx @@ -88,6 +88,9 @@ private: /** * Remove all virtual songs inside playlists whose "target" * field points to a non-existing song file. + * + * It also looks up all target songs and sets their + * "in_playlist" field. */ void PurgeDanglingFromPlaylists(Directory &directory) noexcept;