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

Playlist plugin #3145

Merged
merged 14 commits into from
Feb 17, 2019
Merged

Playlist plugin #3145

merged 14 commits into from
Feb 17, 2019

Conversation

Holzhaus
Copy link
Contributor

Adds M3U playlist support as a query to beets and thus partially
resolves issue #123. The implementation is heavily based on #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
Copy link
Contributor Author

One of the main problems is that the query is rather slow with large libraries. Since key in model_cls._fields evaluates to False, the col_clause() implemention will not be used. Any ideas?

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.

Cool! This is a really neat prototype.

I think the issue you're running into with the query is a bit more fundamental than it might seem at first: field-qualified queries like foo:bar are currently quite intrinsically tied to the existence of a field called foo (either built-in or flexible). You're introducing a type for a "fake" field called playlist here, which works but isn't really what the query system is expecting. Because PlaylistQuery is a FieldQuery, the system really wants to do a playlist-style query of some field (and that field, here, is always called playlist).

I agree that the syntax playlist:foo would be desirable for this plugin. So maybe it's worth digging a little bit into building special support for these "pseudo-fields" for invoking special queries with field-like syntax but without querying actual, underlying fields. For what it's worth, the random plugin (which currently does not define a query) could use this too: a query term like random:5 could use the same mechanism.

@@ -345,7 +345,7 @@ def _sort_candidates(candidates):
return sorted(candidates, key=lambda match: match.distance)


def _add_candidate(items, results, info):
def _add_candidate(items, results, info, force=False):
Copy link
Member

Choose a reason for hiding this comment

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

This addition of the force parameter seems to be an unrelated change that got included in this PR by mistake?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, sorry about that.

config = beets.config['playlist']

# Get the full path to the playlist
if os.path.isabs(beets.util.syspath(pattern)):
Copy link
Member

Choose a reason for hiding this comment

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

I might recommend using isfile. That way, anything that refers to an actual file on disk will work, and if the file doesn't exist, we'll fall back on searching playlist_dir.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would introduce a race condition in case the file is deleted between the isfile and the open call. With proper error handling this wouldn't be too bad, but I restructured the code a bit so that isfile is not needed at all.

Copy link
Member

Choose a reason for hiding this comment

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

Looks perfect. 👍

relative_to = beets.util.bytestring_path(relative_to)

self.paths = []
with open(beets.util.syspath(playlist_path), 'rb') as f:
Copy link
Member

Choose a reason for hiding this comment

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

It might be nice to have some error handling here in case the playlist does not exist (either as a plain filename or in the centralized playlist directory).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. It would be nice to print some error message to the console, but I don't know how. I don't really want to assign the self._log attribute of PlaylistPlugin to the PlaylistQuery class, but I don't see another way to do this.

Copy link
Member

Choose a reason for hiding this comment

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

Good point. Threading through the plugin-specific log object here seems too hard. Maybe we can work something out as part of the more general "pseudo-field" query infrastructure.

# Playlist is empty
return '0', ()
clause = 'BYTELOWER(path) IN ({0})'.format(
', '.join('BYTELOWER(?)' for path in self.paths))
Copy link
Member

Choose a reason for hiding this comment

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

I note that this query is always case-insensitive. That's probably fine, but it's probably worth noting, at least in the docstring for this class. (See the built-in PathQuery if you're curious—it can either be case-sensitive or case-insensitive, depending on the filesystem.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, the problem is that Playlists can contain multiple entries from different filesystems. I should probably make this case sensitive - case-insensitivity is more a convenience feature for FAT/NTFS users and not actually needed.

Copy link
Member

Choose a reason for hiding this comment

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

Also a good point! Case-sensitive sounds fine; we can revisit this if it gets inconvenient.

- **relative_to**: Interpret paths in the playlist files relative to a base
directory. It is also possible to set it to ``playlist`` to use the
playlist's parent directory as base directory.
Default: ``library``
Copy link
Member

Choose a reason for hiding this comment

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

It's probably a good idea to mention explicitly that there are three options here: playlist, library, or a path.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

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.
Implement the col_clause method for faster, sqlite-based querying. This
will only make a difference if the "fast" kwarg is set to True.
@Holzhaus
Copy link
Contributor Author

Holzhaus commented Feb 17, 2019

For some reason some testcase on Windows test keep throwing errors:
https://ci.appveyor.com/project/beetbox/beets/builds/22435461/job/prrm5wikfjp8xvm2#L122

Might be related to spaces in the path, but I'm not sure and I can't fix this since I don't have Windows. The other tests should work fine now.

@Holzhaus
Copy link
Contributor Author

Looks like spaces in the path were indeed the problem. Should work now.

The performance issue could be fixed via #3149. If that PR is merged, we only need to override the check_fast method in PlaylistQuery with this one:

@classmethod
def check_fast(self, key, model_cls):
    return issubclass(model_cls, beets.library.Item)

@Holzhaus
Copy link
Contributor Author

Is anything else missing? Should I squash the commits?

@sampsyo
Copy link
Member

sampsyo commented Feb 17, 2019

Awesome! This looks perfect. Thank you for tracking down the Windows problem even without access to a Windows machine—that was heroic. ✨

No need for a squash from my perspective. I'll merge this now. 🚀

@sampsyo sampsyo merged commit 4f1a468 into beetbox:master Feb 17, 2019
sampsyo added a commit that referenced this pull request Feb 17, 2019
sampsyo added a commit that referenced this pull request Feb 17, 2019
sampsyo added a commit that referenced this pull request Feb 17, 2019
As discussed here:
#3145 (review)

This would replace the need for #3149.
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