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
This commit is contained in:
Max Kellermann 2021-10-14 13:03:54 +02:00
parent f7622ca332
commit 37bd6de658
9 changed files with 47 additions and 12 deletions

1
NEWS
View File

@ -9,6 +9,7 @@ ver 0.23 (not yet released)
- proxy: require MPD 0.20 or later - proxy: require MPD 0.20 or later
- proxy: require libmpdclient 2.11 or later - proxy: require libmpdclient 2.11 or later
- proxy: split search into chunks to avoid exceeding the output buffer - 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 - upnp: support libnpupnp instead of libupnp
* archive * archive
- zzip, iso9660: ignore file names which are invalid UTF-8 - zzip, iso9660: ignore file names which are invalid UTF-8

View File

@ -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. - 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 yes|no**
- Compress the database file using gzip? Enabled by default (if built with zlib). - 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 proxy
----- -----

View File

@ -109,21 +109,21 @@ Directory::FindChild(std::string_view name) const noexcept
return nullptr; return nullptr;
} }
bool Song *
Directory::TargetExists(std::string_view _target) const noexcept Directory::LookupTargetSong(std::string_view _target) noexcept
{ {
StringView target{_target}; StringView target{_target};
if (target.SkipPrefix("../")) { if (target.SkipPrefix("../")) {
if (parent == nullptr) if (parent == nullptr)
return false; return nullptr;
return parent->TargetExists(target); return parent->LookupTargetSong(target);
} }
/* sorry for the const_cast ... */ /* sorry for the const_cast ... */
const auto lr = const_cast<Directory *>(this)->LookupDirectory(target); const auto lr = LookupDirectory(target);
return lr.directory->FindSong(lr.rest) != nullptr; return lr.directory->FindSong(lr.rest);
} }
void void
@ -229,6 +229,7 @@ Directory::Sort() noexcept
void void
Directory::Walk(bool recursive, const SongFilter *filter, Directory::Walk(bool recursive, const SongFilter *filter,
bool hide_playlist_targets,
const VisitDirectory& visit_directory, const VisitSong& visit_song, const VisitDirectory& visit_directory, const VisitSong& visit_song,
const VisitPlaylist& visit_playlist) const const VisitPlaylist& visit_playlist) const
{ {
@ -247,7 +248,10 @@ Directory::Walk(bool recursive, const SongFilter *filter,
} }
if (visit_song) { if (visit_song) {
for (auto &song : songs){ for (auto &song : songs) {
if (hide_playlist_targets && song.in_playlist)
continue;
const auto song2 = song.Export(); const auto song2 = song.Export();
if (filter == nullptr || filter->Match(song2)) if (filter == nullptr || filter->Match(song2))
visit_song(song2); visit_song(song2);
@ -265,6 +269,7 @@ Directory::Walk(bool recursive, const SongFilter *filter,
if (recursive) if (recursive)
child.Walk(recursive, filter, child.Walk(recursive, filter,
hide_playlist_targets,
visit_directory, visit_song, visit_directory, visit_song,
visit_playlist); visit_playlist);
} }

View File

@ -214,7 +214,7 @@ public:
LookupResult LookupDirectory(std::string_view uri) noexcept; LookupResult LookupDirectory(std::string_view uri) noexcept;
[[gnu::pure]] [[gnu::pure]]
bool TargetExists(std::string_view target) const noexcept; Song *LookupTargetSong(std::string_view target) noexcept;
[[gnu::pure]] [[gnu::pure]]
bool IsEmpty() const noexcept { bool IsEmpty() const noexcept {
@ -303,6 +303,7 @@ public:
* Caller must lock #db_mutex. * Caller must lock #db_mutex.
*/ */
void Walk(bool recursive, const SongFilter *match, void Walk(bool recursive, const SongFilter *match,
bool hide_playlist_targets,
const VisitDirectory& visit_directory, const VisitSong& visit_song, const VisitDirectory& visit_directory, const VisitSong& visit_song,
const VisitPlaylist& visit_playlist) const; const VisitPlaylist& visit_playlist) const;

View File

@ -60,6 +60,7 @@ inline SimpleDatabase::SimpleDatabase(const ConfigBlock &block)
#ifdef ENABLE_ZLIB #ifdef ENABLE_ZLIB
compress(block.GetBlockValue("compress", true)), compress(block.GetBlockValue("compress", true)),
#endif #endif
hide_playlist_targets(block.GetBlockValue("hide_playlist_targets", true)),
cache_path(block.GetPath("cache_directory")) cache_path(block.GetPath("cache_directory"))
{ {
if (path.IsNull()) if (path.IsNull())
@ -306,6 +307,7 @@ SimpleDatabase::Visit(const DatabaseSelection &selection,
visit_directory(r.directory->Export()); visit_directory(r.directory->Export());
r.directory->Walk(selection.recursive, selection.filter, r.directory->Walk(selection.recursive, selection.filter,
hide_playlist_targets,
visit_directory, visit_song, visit_directory, visit_song,
visit_playlist); visit_playlist);
helper.Commit(); helper.Commit();

View File

@ -44,6 +44,8 @@ class SimpleDatabase : public Database {
bool compress; bool compress;
#endif #endif
bool hide_playlist_targets;
/** /**
* The path where cache files for Mount() are located. * The path where cache files for Mount() are located.
*/ */

View File

@ -101,6 +101,12 @@ struct Song {
*/ */
AudioFormat audio_format = AudioFormat::Undefined(); 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<typename F> template<typename F>
Song(F &&_filename, Directory &_parent) noexcept Song(F &&_filename, Directory &_parent) noexcept
:parent(_parent), filename(std::forward<F>(_filename)) {} :parent(_parent), filename(std::forward<F>(_filename)) {}

View File

@ -141,10 +141,18 @@ UpdateWalk::PurgeDanglingFromPlaylists(Directory &directory) noexcept
directory.ForEachSongSafe([&](Song &song){ directory.ForEachSongSafe([&](Song &song){
if (!song.target.empty() && if (!song.target.empty() &&
!PathTraitsUTF8::IsAbsoluteOrHasScheme(song.target.c_str()) && !PathTraitsUTF8::IsAbsoluteOrHasScheme(song.target.c_str())) {
!directory.TargetExists(song.target)) { Song *target = directory.LookupTargetSong(song.target.c_str());
if (target == nullptr) {
/* the target does not exist: remove
the virtual song */
editor.DeleteSong(directory, &song); editor.DeleteSong(directory, &song);
modified = true; modified = true;
} else {
/* the target exists: mark it (for
option "hide_playlist_targets") */
target->in_playlist = true;
}
} }
}); });
} }

View File

@ -88,6 +88,9 @@ private:
/** /**
* Remove all virtual songs inside playlists whose "target" * Remove all virtual songs inside playlists whose "target"
* field points to a non-existing song file. * 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; void PurgeDanglingFromPlaylists(Directory &directory) noexcept;