From 5a621d332103d8070c557eee37057d07f16e8576 Mon Sep 17 00:00:00 2001 From: thjbbvlt Date: Thu, 29 Jan 2026 20:50:25 +0100 Subject: [PATCH 1/4] song/DirectorySongFilter: new filter Adds a new filter `directory` similar to `base` but non-recursive. See https://github.com/MusicPlayerDaemon/MPD/issues/2418 --- src/song/DirectorySongFilter.cxx | 19 +++++++++++++++++++ src/song/DirectorySongFilter.hxx | 31 +++++++++++++++++++++++++++++++ src/song/Filter.cxx | 17 +++++++++++++++++ src/song/LightSong.hxx | 7 +++++++ src/song/meson.build | 1 + 5 files changed, 75 insertions(+) create mode 100644 src/song/DirectorySongFilter.cxx create mode 100644 src/song/DirectorySongFilter.hxx diff --git a/src/song/DirectorySongFilter.cxx b/src/song/DirectorySongFilter.cxx new file mode 100644 index 0000000000..208023ae51 --- /dev/null +++ b/src/song/DirectorySongFilter.cxx @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#include "DirectorySongFilter.hxx" +#include "Escape.hxx" +#include "LightSong.hxx" +#include "util/StringAPI.hxx" + +std::string +DirectorySongFilter::ToExpression() const noexcept +{ + return "(directory \"" + EscapeFilterString(value) + "\")"; +} + +bool +DirectorySongFilter::Match(const LightSong &song) const noexcept +{ + return StringIsEqual(value.c_str(), song.GetDirectory().c_str()); +} diff --git a/src/song/DirectorySongFilter.hxx b/src/song/DirectorySongFilter.hxx new file mode 100644 index 0000000000..e9d597cc8e --- /dev/null +++ b/src/song/DirectorySongFilter.hxx @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#ifndef MPD_DIRECTORY_SONG_FILTER_HXX +#define MPD_DIRECTORY_SONG_FILTER_HXX + +#include "ISongFilter.hxx" + +class DirectorySongFilter final : public ISongFilter { + std::string value; + +public: + DirectorySongFilter(const DirectorySongFilter &) = default; + + template + explicit DirectorySongFilter(V &&_value) + :value(std::forward(_value)) {} + + const char *GetValue() const noexcept { + return value.c_str(); + } + + ISongFilterPtr Clone() const noexcept override { + return std::make_unique(*this); + } + + std::string ToExpression() const noexcept override; + bool Match(const LightSong &song) const noexcept override; +}; + +#endif diff --git a/src/song/Filter.cxx b/src/song/Filter.cxx index fe05592927..81ab8e6d50 100644 --- a/src/song/Filter.cxx +++ b/src/song/Filter.cxx @@ -6,6 +6,7 @@ #include "NotSongFilter.hxx" #include "UriSongFilter.hxx" #include "BaseSongFilter.hxx" +#include "DirectorySongFilter.hxx" #include "TagSongFilter.hxx" #include "ModifiedSinceSongFilter.hxx" #include "AddedSinceSongFilter.hxx" @@ -43,6 +44,7 @@ enum { LOCATE_TAG_FILE_TYPE, LOCATE_TAG_ANY_TYPE, LOCATE_TAG_ADDED_SINCE, + LOCATE_TAG_DIRECTORY_TYPE, }; /** @@ -62,6 +64,9 @@ locate_parse_type(const char *str) noexcept if (strcmp(str, "base") == 0) return LOCATE_TAG_BASE_TYPE; + if (strcmp(str, "directory") == 0) + return LOCATE_TAG_DIRECTORY_TYPE; + if (strcmp(str, "modified-since") == 0) return LOCATE_TAG_MODIFIED_SINCE; @@ -386,6 +391,12 @@ SongFilter::ParseExpression(const char *&s, bool fold_case, bool strip_diacritic s = StripLeft(s + 1); return std::make_unique(std::move(value)); + } else if (type == LOCATE_TAG_DIRECTORY_TYPE) { + auto value = ExpectQuoted(s); + if (*s != ')') + throw std::runtime_error("')' expected"); + s = StripLeft(s + 1); + return std::make_unique(std::move(value)); } else if (type == LOCATE_TAG_AUDIO_FORMAT) { bool mask; if (s[0] == '=' && s[1] == '=') @@ -459,6 +470,12 @@ SongFilter::Parse(const char *tag_string, const char *value, bool fold_case, boo and_filter.AddItem(std::make_unique(value)); break; + case LOCATE_TAG_DIRECTORY_TYPE: + if (!uri_safe_local(value)) + throw std::runtime_error("Bad URI"); + and_filter.AddItem(std::make_unique(value)); + break; + case LOCATE_TAG_MODIFIED_SINCE: and_filter.AddItem(std::make_unique(ParseTimeStamp(value))); break; diff --git a/src/song/LightSong.hxx b/src/song/LightSong.hxx index e4328128c9..179245c5eb 100644 --- a/src/song/LightSong.hxx +++ b/src/song/LightSong.hxx @@ -106,6 +106,13 @@ struct LightSong { return result; } + [[gnu::pure]] + std::string GetDirectory() const noexcept { + if (directory == nullptr) + return std::string(""); + return std::string(directory); + } + [[gnu::pure]] SignedSongTime GetDuration() const noexcept; }; diff --git a/src/song/meson.build b/src/song/meson.build index 1f6863dcab..10b272de58 100644 --- a/src/song/meson.build +++ b/src/song/meson.build @@ -5,6 +5,7 @@ song = static_library( 'StringFilter.cxx', 'UriSongFilter.cxx', 'BaseSongFilter.cxx', + 'DirectorySongFilter.cxx', 'TagSongFilter.cxx', 'ModifiedSinceSongFilter.cxx', 'AddedSinceSongFilter.cxx', From 5a0b3b27b42bacb00e6e3b25f33a551e0ca512ff Mon Sep 17 00:00:00 2001 From: thjbbvlt Date: Thu, 29 Jan 2026 21:21:16 +0100 Subject: [PATCH 2/4] song/DirectorySongFilter: minor change --- src/song/DirectorySongFilter.cxx | 2 +- src/song/LightSong.hxx | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/song/DirectorySongFilter.cxx b/src/song/DirectorySongFilter.cxx index 208023ae51..8cfa29739d 100644 --- a/src/song/DirectorySongFilter.cxx +++ b/src/song/DirectorySongFilter.cxx @@ -15,5 +15,5 @@ DirectorySongFilter::ToExpression() const noexcept bool DirectorySongFilter::Match(const LightSong &song) const noexcept { - return StringIsEqual(value.c_str(), song.GetDirectory().c_str()); + return StringIsEqual(value.c_str(), song.directory == nullptr ? "" : song.directory); } diff --git a/src/song/LightSong.hxx b/src/song/LightSong.hxx index 179245c5eb..e4328128c9 100644 --- a/src/song/LightSong.hxx +++ b/src/song/LightSong.hxx @@ -106,13 +106,6 @@ struct LightSong { return result; } - [[gnu::pure]] - std::string GetDirectory() const noexcept { - if (directory == nullptr) - return std::string(""); - return std::string(directory); - } - [[gnu::pure]] SignedSongTime GetDuration() const noexcept; }; From ba8ba729fadeb71dadaa99520149335088ab4024 Mon Sep 17 00:00:00 2001 From: thjbbvlt Date: Thu, 29 Jan 2026 21:30:33 +0100 Subject: [PATCH 3/4] song/DirectoryFilterSong: documents the filter --- doc/protocol.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/protocol.rst b/doc/protocol.rst index 5272fc4a3c..8a219af2f1 100644 --- a/doc/protocol.rst +++ b/doc/protocol.rst @@ -213,6 +213,8 @@ of: songs in the given directory (relative to the music directory). +- ``(directory 'VALUE')``: non-recursive version of ``(base 'VALUE')``. + - ``(modified-since 'VALUE')``: compares the file's time stamp with the given value (ISO 8601 or UNIX time stamp). From 354b7a1124639ea140c2dabd8bcd062d63f99308 Mon Sep 17 00:00:00 2001 From: thjbbvlt Date: Fri, 30 Jan 2026 15:16:32 +0100 Subject: [PATCH 4/4] song/DirectorySongFilter: recursive BaseSongFilter Adds `directory` filter that is a non-recursive version of BaseSongFilter. This is done by adding a `recursive` flag on BaseSongFilter that the `base` filter sets to `true` and the `directory` filter sets to `false`. --- src/db/Selection.cxx | 10 ++++++---- src/song/BaseSongFilter.hxx | 10 ++++++++-- src/song/DirectorySongFilter.cxx | 19 ------------------- src/song/DirectorySongFilter.hxx | 31 ------------------------------- src/song/Filter.cxx | 18 ++++++++---------- src/song/Filter.hxx | 11 ++++++++--- src/song/meson.build | 1 - 7 files changed, 30 insertions(+), 70 deletions(-) delete mode 100644 src/song/DirectorySongFilter.cxx delete mode 100644 src/song/DirectorySongFilter.hxx diff --git a/src/db/Selection.cxx b/src/db/Selection.cxx index 4df4d9ed8d..a5bac32f72 100644 --- a/src/db/Selection.cxx +++ b/src/db/Selection.cxx @@ -3,6 +3,7 @@ #include "Selection.hxx" #include "song/Filter.hxx" +#include "song/BaseSongFilter.hxx" DatabaseSelection::DatabaseSelection(const char *_uri, bool _recursive, const SongFilter *_filter) noexcept @@ -10,10 +11,11 @@ DatabaseSelection::DatabaseSelection(const char *_uri, bool _recursive, { /* optimization: if the caller didn't specify a base URI, pick the one from SongFilter */ - if (uri.empty() && filter != nullptr) { - auto base = filter->GetBase(); - if (base != nullptr) - uri = base; + if (filter != nullptr) { + Base base = filter->GetBase(); + if (uri.empty() && base.uri != nullptr) + uri = base.uri; + recursive = base.recursive; } } diff --git a/src/song/BaseSongFilter.hxx b/src/song/BaseSongFilter.hxx index c5ada242d6..7e3baa897d 100644 --- a/src/song/BaseSongFilter.hxx +++ b/src/song/BaseSongFilter.hxx @@ -8,18 +8,24 @@ class BaseSongFilter final : public ISongFilter { std::string value; + bool recursive; public: + BaseSongFilter(const BaseSongFilter &) = default; template - explicit BaseSongFilter(V &&_value) - :value(std::forward(_value)) {} + explicit BaseSongFilter(V &&_value, bool _recursive) + :value(std::forward(_value)), recursive(_recursive) {} const char *GetValue() const noexcept { return value.c_str(); } + bool IsRecursive() const noexcept { + return recursive; + } + ISongFilterPtr Clone() const noexcept override { return std::make_unique(*this); } diff --git a/src/song/DirectorySongFilter.cxx b/src/song/DirectorySongFilter.cxx deleted file mode 100644 index 8cfa29739d..0000000000 --- a/src/song/DirectorySongFilter.cxx +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "DirectorySongFilter.hxx" -#include "Escape.hxx" -#include "LightSong.hxx" -#include "util/StringAPI.hxx" - -std::string -DirectorySongFilter::ToExpression() const noexcept -{ - return "(directory \"" + EscapeFilterString(value) + "\")"; -} - -bool -DirectorySongFilter::Match(const LightSong &song) const noexcept -{ - return StringIsEqual(value.c_str(), song.directory == nullptr ? "" : song.directory); -} diff --git a/src/song/DirectorySongFilter.hxx b/src/song/DirectorySongFilter.hxx deleted file mode 100644 index e9d597cc8e..0000000000 --- a/src/song/DirectorySongFilter.hxx +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_DIRECTORY_SONG_FILTER_HXX -#define MPD_DIRECTORY_SONG_FILTER_HXX - -#include "ISongFilter.hxx" - -class DirectorySongFilter final : public ISongFilter { - std::string value; - -public: - DirectorySongFilter(const DirectorySongFilter &) = default; - - template - explicit DirectorySongFilter(V &&_value) - :value(std::forward(_value)) {} - - const char *GetValue() const noexcept { - return value.c_str(); - } - - ISongFilterPtr Clone() const noexcept override { - return std::make_unique(*this); - } - - std::string ToExpression() const noexcept override; - bool Match(const LightSong &song) const noexcept override; -}; - -#endif diff --git a/src/song/Filter.cxx b/src/song/Filter.cxx index 81ab8e6d50..1dd42b98ee 100644 --- a/src/song/Filter.cxx +++ b/src/song/Filter.cxx @@ -6,7 +6,6 @@ #include "NotSongFilter.hxx" #include "UriSongFilter.hxx" #include "BaseSongFilter.hxx" -#include "DirectorySongFilter.hxx" #include "TagSongFilter.hxx" #include "ModifiedSinceSongFilter.hxx" #include "AddedSinceSongFilter.hxx" @@ -390,13 +389,13 @@ SongFilter::ParseExpression(const char *&s, bool fold_case, bool strip_diacritic throw std::runtime_error("')' expected"); s = StripLeft(s + 1); - return std::make_unique(std::move(value)); + return std::make_unique(std::move(value), true); } else if (type == LOCATE_TAG_DIRECTORY_TYPE) { auto value = ExpectQuoted(s); if (*s != ')') throw std::runtime_error("')' expected"); s = StripLeft(s + 1); - return std::make_unique(std::move(value)); + return std::make_unique(std::move(value), false); } else if (type == LOCATE_TAG_AUDIO_FORMAT) { bool mask; if (s[0] == '=' && s[1] == '=') @@ -467,13 +466,13 @@ SongFilter::Parse(const char *tag_string, const char *value, bool fold_case, boo if (!uri_safe_local(value)) throw std::runtime_error("Bad URI"); - and_filter.AddItem(std::make_unique(value)); + and_filter.AddItem(std::make_unique(value, true)); break; case LOCATE_TAG_DIRECTORY_TYPE: if (!uri_safe_local(value)) throw std::runtime_error("Bad URI"); - and_filter.AddItem(std::make_unique(value)); + and_filter.AddItem(std::make_unique(value, false)); break; case LOCATE_TAG_MODIFIED_SINCE: @@ -574,16 +573,15 @@ SongFilter::HasFoldCase() const noexcept }); } -const char * +const Base SongFilter::GetBase() const noexcept { for (const auto &i : and_filter.GetItems()) { const auto *f = dynamic_cast(i.get()); if (f != nullptr) - return f->GetValue(); + return Base{f->GetValue(), f->IsRecursive()}; } - - return nullptr; + return Base{nullptr, true}; } SongFilter @@ -603,7 +601,7 @@ SongFilter::WithoutBasePrefix(const std::string_view prefix) const noexcept ++s; if (*s != 0) - result.and_filter.AddItem(std::make_unique(s)); + result.and_filter.AddItem(std::make_unique(s, f->IsRecursive())); continue; } diff --git a/src/song/Filter.hxx b/src/song/Filter.hxx index 8139bd3e02..30b902975a 100644 --- a/src/song/Filter.hxx +++ b/src/song/Filter.hxx @@ -29,6 +29,11 @@ enum TagType : uint8_t; struct LightSong; +/** + * Base URI and recursive flag. + */ +struct Base { const char *uri; bool recursive; }; + class SongFilter { AndSongFilter and_filter; @@ -80,11 +85,11 @@ public: bool HasFoldCase() const noexcept; /** - * Returns the "base" specification (if there is one) or - * nullptr. + * Returns the "base" specification, containing URI (if any) and + * recursive flag (default to true). */ [[gnu::pure]] - const char *GetBase() const noexcept; + const Base GetBase() const noexcept; /** * Create a copy of the filter with the given prefix stripped diff --git a/src/song/meson.build b/src/song/meson.build index 10b272de58..1f6863dcab 100644 --- a/src/song/meson.build +++ b/src/song/meson.build @@ -5,7 +5,6 @@ song = static_library( 'StringFilter.cxx', 'UriSongFilter.cxx', 'BaseSongFilter.cxx', - 'DirectorySongFilter.cxx', 'TagSongFilter.cxx', 'ModifiedSinceSongFilter.cxx', 'AddedSinceSongFilter.cxx',