Skip to content

(wip) song/DirectorySongFilter: new filter#2419

Open
thjbdvlt wants to merge 4 commits intoMusicPlayerDaemon:masterfrom
thjbdvlt:filter-dir
Open

(wip) song/DirectorySongFilter: new filter#2419
thjbdvlt wants to merge 4 commits intoMusicPlayerDaemon:masterfrom
thjbdvlt:filter-dir

Conversation

@thjbdvlt
Copy link

@thjbdvlt thjbdvlt commented Jan 29, 2026

Adds a new filter directory similar to base but non-recursive.

See #2418

(The commit adds a GetDirectory method on LightSong struct, similar to GetURI. It could be done from within the filter functions, but I think that other (future) features could use this, e.g. group directory or list directory.)

In its current state (a partial and unoptimized copy of BasSongFilter), it's still very slow!

}

[[gnu::pure]]
std::string GetDirectory() const noexcept {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method creates an allocated copy of the directory field, but I find that rather useless, what's the point of that copy? I think the whole method is not useful at all. You create a copy on the heap only to get a C string, but you already have a C string.

Copy link
Author

@thjbdvlt thjbdvlt Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, i remove the allocation.

I actually added the method mostly to create a string if directory is null:

if (directory == nullptr)
      return std::string("");

But it seems rather useless, I remove the method. It's also prettier with ternary operator:

  return StringIsEqual(value.c_str(), song.directory == nullptr ? "" : song.directory);

@MaxKellermann
Copy link
Member

Please add protocol documentation

@thjbdvlt thjbdvlt changed the title song/DirectorySongFilter: new filter (wip) song/DirectorySongFilter: new filter Jan 30, 2026
@thjbdvlt
Copy link
Author

I'm really sorry: I thought it was easier to implement that it's actually. Thus I thought that my kinda superficial C++ knowledge would do it; it doesn't.
I've now specified "work-in-progress", and I'll come with a better proposal, maybe by suclassing or wrapping BaseSongFilter (because of functions HasOtherThanBase and others). Or maybe by adding a recursive field in BaseSongFilter if it has no impact on performance.

@MaxKellermann
Copy link
Member

I forgot about HasOtherThanBase - but it seems this method is obsolete and can be removed; the last user was removed 8 years ago in commit 9cc960a

I thought this PR was good, I just didn't like the unnecessary allocation.

@thjbdvlt
Copy link
Author

But it (or WithoutBasePrefix/GetBase) seems to do a great job for optimization. I didn't figured out yet how exactly and how DirectorySongFilter can take advantage of it without duplicating code.

My directory filter is much slower.

I'm probably not using MPD protocol very well (there's a loop in which I call list album base DIR, because I need an array of Albums,Artists,Directory tuples, and I would actually like to do list album group albumartist group directory), but the difference is huge; I just can't use my new filter in my client while base is instantaneous.

@MaxKellermann
Copy link
Member

True, your code is completely unoptimized. It's a naive approach - and it works. I like naive code that just works!

But of course, it's at the same time somewhat clumsy. A search with your filter will traverse the whole database and check the URI of each and every song. The right approach would be to go to that one Directory object and apply the rest of the filter on all Song objects inside that directory. So, for the common case, your code would never actually be called; your new class would just be a marker, just like BaseSongFilter is - its code is just as well not called most of the time.

First step towards optimizing it: SongFilter::GetBase() should recognize your new class, too. That way, you get acceptable performance because only a small branch will be searched. At this point, yes, you could consider renaming BaseSongFilter to DirectorySongFilter and add the recursive flag - you had that idea, and I think it's a good idea. That would automatically give you this optimization.

Next: in DatabaseSelection::DatabaseSelection(), copy the recursive flag. Then the search will not visit subdirectories.

For experts: make your new feature work with ProxyDatabasePlugin. This requires adding the feature to libmpdclient, checking libmpdclient version at compile time, check other MPD version at runtime.

@thjbdvlt
Copy link
Author

Thank you so much! Your hints gave me good insights into the code. This leads to a solution that's both faster and more concise.

Now I have a new question.

To access BaseSongFilter.recursive flag from within DatabaseSelection(), I need to loop over filters in the same way GetBase, HasOtherThanBase and WithoutBasePrefix do:

for (const auto &i : filter->GetItems()) {
  const auto *f = dynamic_cast<const BaseSongFilter *>(i.get());
  if (f != nullptr && !f->recursive) {
    recursive = false;
    break;
  }
}

I can either make recursive flag public and put the loop directly inside DatabaseSelection(), since it's used only once and is very small, or I can add a method to the ...Base... toolkit. A new method (e.g. HasNonRecursiveFlag) seems more consistent and helps to keep DatabaseSelection code small, but also seems a bit unnecessary.

@MaxKellermann
Copy link
Member

Don't use negated names. Call it IsRecursive(), not HasNonRecursiveFlag(). It's too easy to get double negation wrong.
And I'd rather have this getter method than making the field public. Sure, getters are ugly boilerplate, but I tend to prefer that and making the field inaccessible. Whoever has a non-const reference to such an object should not be able to manipulate the field, as these objects are supposed to be immutable.


/* BaseSongFilter may have an explicit non-recursive flag set */
if (recursive && filter != nullptr) {
for (const auto &i : filter->GetItems()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that this traverses the filter list again (was already done by SongFilter::GetBase()). This seems fragile because you never know if the two copies of that loop are looking at the same BaseSongFilter instance.
Maybe refactor SongFilter::GetBase() to something that returns both the base directory and the recursive flag? Something like:

struct Base { const char *uri; bool recursive; };
Base GetBase() ...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I didn't like to do the same loop twice, so I'm doing this.
It also avoids a SongBase::IsRecursive() (only BaseSongFilter::IsRecursive()).

@MaxKellermann
Copy link
Member

When you do fixups for unmerged commits, please don't add fixup commits on top; instead, amend the existing commit.
This PR has 4 commits, but they do just one single feature, therefore should result in only one commit.
A mention in NEWS would be good.

template<typename V>
explicit BaseSongFilter(V &&_value)
:value(std::forward<V>(_value)) {}
:value(std::forward<V>(_value)), recursive(true) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than adding an initializer here, it should be on the declaration of the field to specify that this is the default value for all cases where none was given.
But I'm thinking we shouldn't have two constructors; only your new one should exist, and all old callers should be changed to pass the recursive flag (being true for them). When this feature exists, recursiveness should be an explicit decision with no default, because having an implicit default seems more confusing than not.

@thjbdvlt
Copy link
Author

When you do fixups for unmerged commits, please don't add fixup commits on top; instead, amend the existing commit. This PR has 4 commits, but they do just one single feature, therefore should result in only one commit. A mention in NEWS would be good.

I'm sorry. May I rebase and push --force to have a single commit?

Adds a new filter `directory` similar to `base` but non-recursive.
See MusicPlayerDaemon#2418
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`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants