Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add PlaylistQuery plugin #2380

Closed
wants to merge 3 commits into from

Conversation

RobinMcCorkell
Copy link

This plugin provides support for queries on M3U playlist files, giving some of the features requested in #123. In my workflow, this allows me to create and manage playlists with a music client (e.g. ncmpcpp) then leverage Beets to transform those tracks, e.g. exporting and converting them into a separate directory for consumption on another device.

It uses a flexible field type definition to add the playlist:... query pattern. I considered using prefixes, but that would make for strange behaviour when combined with a field selector. This approach adds a dynamic 'field' to items that allow for matching.

Two configuration parameters are present: playlist_dir is required, and defines the location of the playlist directory. relative_to defines how relative paths in M3U files are specified, as relative to the base music directory or relative to the playlist. It defaults to paths being relative to the base directory, since that is what ncmpcpp does.

TODO:

  • Add documentation
  • Write tests?

I wanted to get this PR open so people can start commenting on my code

Copy link
Member

@sampsyo sampsyo left a comment

Choose a reason for hiding this comment

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

This is a cool idea! Here are a few comments. It also looks like Travis has some style suggestions:
https://travis-ci.org/beetbox/beets/jobs/190643595#L851-L853

item_types = {'playlist': PlaylistType()}

def __init__(self):
super(PlaylistQueryPlugin, self).__init__()
Copy link
Member

Choose a reason for hiding this comment

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

You'll likely want to set defaults for the plugin's configuration here.

Copy link
Author

Choose a reason for hiding this comment

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

I think the documentation could benefit from a small section on how to specify config defaults for plugins, it is currently missing so I had to look at other plugins for examples.

super(PlaylistQueryPlugin, self).__init__()
self.register_listener('library_opened', self.library_opened)

PlaylistQuery.playlist_dir = self.config['playlist_dir'].as_filename()
Copy link
Member

Choose a reason for hiding this comment

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

Using global state to keep track of this could be a little problematic for testing—the right thing to do here might be to look up the configuration option directly in the query class.

Copy link
Author

Choose a reason for hiding this comment

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

How will this work with access needed to lib? The Query objects don't get any context, so the best place I thought to put it was in the library_opened event on PlaylistQueryPlugin, which in turn requires some sort of global state passed to PlaylistQuery for instances to use. Unless there's an API that I haven't seen?

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, yes, that is a problem. We haven't had a case where a query needed a reference to the database before—maybe we need to add that capability?

Copy link
Author

Choose a reason for hiding this comment

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

At the moment a Query object is constructed with the field and pattern, which makes it very clean. I think it is unwise to expand that in the default case, since most of the time it will go unused. However, I can see potential for the Builder pattern here - have a Plugin object instantiate Query objects that it controls.

So currently, construct_query_part() in dbcore.queryparse directly constructs the selected query_class. Instead, it should know which Plugin object corresponds to each query class, then calls on that object to construct the desired class. The default implementation in BeetsPlugin would do what construct_query_part() does at the moment (i.e. check if we have a FieldQuery, and construct in two different ways) but can be overridden in custom plugins to do more complex things, passing in other information if desired.

Copy link
Member

Choose a reason for hiding this comment

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

Sure; good idea. Another relatively simple way would be to use a closure—that is, the plugin would just set self.item_types to reference a class that can access lib from the outer context.

I suppose one way to judge whether we've done it "right" is to ask whether the plugin would break if beets were ever to support multiple libraries for some reason—if there weren't a single, global Library object but queries would work on whatever library they're querying.

Copy link
Author

Choose a reason for hiding this comment

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

I can't think of a good way to avoid using global state somewhere, without rewriting how the entire mechanism for registering queries works. Closures wouldn't work easily, since the PlaylistQuery class is defined inside PlaylistType, of which an instance belongs to PlaylistQueryPlugin, so the PlaylistQuery class inside the (now global) PlaylistType instance needs to be changed on library_opened events, taking you right back to mutable global state.

I don't think these two configuration variables as global state is necessarily a problem, though. If you consider PlaylistQueryPlugin and PlaylistQuery to be separate units of code, then each can be unit tested independently, where the global state isn't such a big deal.

if relative_to == 'playlist':
PlaylistQuery.relative_path = PlaylistQuery.playlist_dir
else:
PlaylistQuery.relative_path = lib.directory
Copy link
Member

Choose a reason for hiding this comment

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

Is there a difference between setting playlist_dir to be the same as directory vs. setting relative_to to base?

Copy link
Author

Choose a reason for hiding this comment

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

Yes: playlist_dir affects where playlists are stored (and referenced from, so a playlist:foobar looks up $playlist_dir/foobar.m3u) while relative_to affects how relative paths inside the M3U file are handled. There is no formal M3U spec (https://en.wikipedia.org/wiki/M3U#File_format) so paths may be relative to the playlist file (relative_to = playlist) or relative to the base music directory (ncmpcpp does this, relative_to = base)

Copy link
Member

Choose a reason for hiding this comment

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

Aha, that makes sense! Thanks for clarifying.

playlist_dir = None

def __init__(self, field, pattern, fast=True):
super(PlaylistQuery, self).__init__(field, pattern, fast)
Copy link
Member

Choose a reason for hiding this comment

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

I think you want fast=False here unless you implement a clause method (i.e., a pure-SQL way of executing the query).

@sampsyo
Copy link
Member

sampsyo commented Jan 10, 2017

This is a nifty idea. I'm intrigued by the low-complexity approach of using a directory full of m3u files as the playlist store. Eventually, for the full set of #123 features, we probably want to keep playlists in the database itself. But for now, I'm curious about using this approach to prototype some of what we'd do eventually with that full-fledged implementation.

@terenc3
Copy link

terenc3 commented Dec 18, 2017

@Xenopathic Any progress here?

@Holzhaus
Copy link
Contributor

I'd really like to see this feature merged.

@RobinMcCorkell
Copy link
Author

I'm no longer using Beets, so I'm going to close this PR. Anyone else is welcome to take my work so far and complete it, since my work is licensed under the same MIT license that Beets is.

@Holzhaus Holzhaus mentioned this pull request Dec 28, 2018
Holzhaus added a commit to Holzhaus/beets that referenced this pull request Feb 15, 2019
Adds M3U playlist support as a query to beets and thus partially
resolves issue beetbox#123. The implementation is heavily based on beetbox#2380 by
Robin McCorkell.

It supports referencing playlists by absolute path:

    $ beet ls playlist:/path/to/someplaylist.m3u

It also supports referencing playlists by name. The playlist is then
seached in the playlist_dir and the ".m3u" extension is appended to the
name:

    $ beet ls playlist:anotherplaylist

The configuration for the plugin looks like this:

    playlist:
        relative_to: library
        playlist_dir: /path/to/playlists

The relative_to option specifies how relative paths in playlists are
handled. By default, paths are relative to the "library" directory. It
also possible to make them relative to the "playlist" or set the option
or set it to a fixed path.
Holzhaus added a commit to Holzhaus/beets that referenced this pull request Feb 15, 2019
Adds M3U playlist support as a query to beets and thus partially
resolves issue beetbox#123. The implementation is heavily based on beetbox#2380 by
Robin McCorkell.

It supports referencing playlists by absolute path:

    $ beet ls playlist:/path/to/someplaylist.m3u

It also supports referencing playlists by name. The playlist is then
seached in the playlist_dir and the ".m3u" extension is appended to the
name:

    $ beet ls playlist:anotherplaylist

The configuration for the plugin looks like this:

    playlist:
        relative_to: library
        playlist_dir: /path/to/playlists

The relative_to option specifies how relative paths in playlists are
handled. By default, paths are relative to the "library" directory. It
also possible to make them relative to the "playlist" or set the option
or set it to a fixed path.
Holzhaus added a commit to Holzhaus/beets that referenced this pull request Feb 15, 2019
Adds M3U playlist support as a query to beets and thus partially
resolves issue beetbox#123. The implementation is heavily based on beetbox#2380 by
Robin McCorkell.

It supports referencing playlists by absolute path:

    $ beet ls playlist:/path/to/someplaylist.m3u

It also supports referencing playlists by name. The playlist is then
seached in the playlist_dir and the ".m3u" extension is appended to the
name:

    $ beet ls playlist:anotherplaylist

The configuration for the plugin looks like this:

    playlist:
        relative_to: library
        playlist_dir: /path/to/playlists

The relative_to option specifies how relative paths in playlists are
handled. By default, paths are relative to the "library" directory. It
also possible to make them relative to the "playlist" or set the option
or set it to a fixed path.
@Holzhaus Holzhaus mentioned this pull request Feb 15, 2019
Holzhaus added a commit to Holzhaus/beets that referenced this pull request Feb 15, 2019
Adds M3U playlist support as a query to beets and thus partially
resolves issue beetbox#123. The implementation is heavily based on beetbox#2380 by
Robin McCorkell.

It supports referencing playlists by absolute path:

    $ beet ls playlist:/path/to/someplaylist.m3u

It also supports referencing playlists by name. The playlist is then
seached in the playlist_dir and the ".m3u" extension is appended to the
name:

    $ beet ls playlist:anotherplaylist

The configuration for the plugin looks like this:

    playlist:
        relative_to: library
        playlist_dir: /path/to/playlists

The relative_to option specifies how relative paths in playlists are
handled. By default, paths are relative to the "library" directory. It
also possible to make them relative to the "playlist" or set the option
or set it to a fixed path.
Holzhaus added a commit to Holzhaus/beets that referenced this pull request Feb 17, 2019
Adds M3U playlist support as a query to beets and thus partially
resolves issue beetbox#123. The implementation is heavily based on beetbox#2380 by
Robin McCorkell.

It supports referencing playlists by absolute path:

    $ beet ls playlist:/path/to/someplaylist.m3u

It also supports referencing playlists by name. The playlist is then
seached in the playlist_dir and the ".m3u" extension is appended to the
name:

    $ beet ls playlist:anotherplaylist

The configuration for the plugin looks like this:

    playlist:
        relative_to: library
        playlist_dir: /path/to/playlists

The relative_to option specifies how relative paths in playlists are
handled. By default, paths are relative to the "library" directory. It
also possible to make them relative to the "playlist" or set the option
or set it to a fixed path.
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.

4 participants