From 4312fd39143f386059fe4f3128327a849d8dcd61 Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 08:12:22 -0500 Subject: [PATCH 01/11] Working smart playlist plugin --- beetsplug/smartplaylist.py | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 beetsplug/smartplaylist.py diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py new file mode 100644 index 000000000..e4dfc9d14 --- /dev/null +++ b/beetsplug/smartplaylist.py @@ -0,0 +1,54 @@ +from __future__ import print_function + +from beets.plugins import BeetsPlugin +from beets import config, ui +from beets.util import syspath +import os + +database_changed = False +library = None + + +def update_playlists(lib): + playlists = config['smartplaylist']['playlists'].get(list) + playlist_dir = config['smartplaylist']['playlist_dir'].get(unicode) + relative_to = config['smartplaylist']['relative_to'].get(unicode) + for playlist in playlists: + items = lib.items(playlist['query']) + paths = [os.path.relpath(item.path, relative_to) for item in items] + basename = playlist['name'].encode('utf8') + m3u_path = os.path.join(playlist_dir, basename) + with open(syspath(m3u_path), 'w') as f: + for path in paths: + f.write(path + '\n') + + +class SmartPlaylistPlugin(BeetsPlugin): + def __init__(self): + super(SmartPlaylistPlugin, self).__init__() + self.config.add({ + 'mpd_music_dir': u'', + 'playlists': [] + }) + + def commands(self): + def update(lib, opts, args): + update_playlists(lib) + spl_update = ui.Subcommand('spl_update', + help='update the smart playlists') + spl_update.func = update + return [spl_update] + + +@SmartPlaylistPlugin.listen('database_change') +def handle_change(lib): + global library + global database_changed + library = lib + database_changed = True + + +@SmartPlaylistPlugin.listen('cli_exit') +def update(): + if database_changed: + update_playlists(library) From 70240bf4b1ddd0c8aa0f6e8980bd9ab17f89bec9 Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 08:25:18 -0500 Subject: [PATCH 02/11] Fix optional relative_to --- beetsplug/smartplaylist.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index e4dfc9d14..34531c7c6 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -2,7 +2,7 @@ from __future__ import print_function from beets.plugins import BeetsPlugin from beets import config, ui -from beets.util import syspath +from beets.util import normpath, syspath import os database_changed = False @@ -12,10 +12,16 @@ library = None def update_playlists(lib): playlists = config['smartplaylist']['playlists'].get(list) playlist_dir = config['smartplaylist']['playlist_dir'].get(unicode) - relative_to = config['smartplaylist']['relative_to'].get(unicode) + relative_to = config['smartplaylist']['relative_to'].get() + if relative_to: + relative_to = normpath(relative_to) + for playlist in playlists: items = lib.items(playlist['query']) - paths = [os.path.relpath(item.path, relative_to) for item in items] + if relative_to: + paths = [os.path.relpath(item.path, relative_to) for item in items] + else: + paths = [item.path for item in items] basename = playlist['name'].encode('utf8') m3u_path = os.path.join(playlist_dir, basename) with open(syspath(m3u_path), 'w') as f: @@ -27,7 +33,7 @@ class SmartPlaylistPlugin(BeetsPlugin): def __init__(self): super(SmartPlaylistPlugin, self).__init__() self.config.add({ - 'mpd_music_dir': u'', + 'relative_to': None, 'playlists': [] }) From 480d51443c132c54952774f20d847ffbd8012f5a Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 08:31:39 -0500 Subject: [PATCH 03/11] Add print statements --- beetsplug/smartplaylist.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 34531c7c6..98ee6328e 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -10,6 +10,7 @@ library = None def update_playlists(lib): + print("Updating smart playlists...") playlists = config['smartplaylist']['playlists'].get(list) playlist_dir = config['smartplaylist']['playlist_dir'].get(unicode) relative_to = config['smartplaylist']['relative_to'].get() @@ -27,6 +28,7 @@ def update_playlists(lib): with open(syspath(m3u_path), 'w') as f: for path in paths: f.write(path + '\n') + print("... Done") class SmartPlaylistPlugin(BeetsPlugin): From d134e9c461af2c9b67673aa97fc15a302dcbc58c Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 08:42:29 -0500 Subject: [PATCH 04/11] Add comments --- beetsplug/smartplaylist.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 98ee6328e..33dabad0c 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -1,3 +1,19 @@ +# This file is part of beets. +# Copyright 2013, Dang Mai . +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Generates smart playlists based on beets queries. +""" from __future__ import print_function from beets.plugins import BeetsPlugin @@ -5,6 +21,8 @@ from beets import config, ui from beets.util import normpath, syspath import os +# Global variables so that smartplaylist can detect database changes and run +# only once before beets exits. database_changed = False library = None From 5a7d7e1bb59eb8e7b6035d2f7bdc9ec7e374e9cd Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 09:28:50 -0500 Subject: [PATCH 05/11] Change command to splupdate --- beetsplug/smartplaylist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 33dabad0c..c84f1c612 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -60,7 +60,7 @@ class SmartPlaylistPlugin(BeetsPlugin): def commands(self): def update(lib, opts, args): update_playlists(lib) - spl_update = ui.Subcommand('spl_update', + spl_update = ui.Subcommand('splupdate', help='update the smart playlists') spl_update.func = update return [spl_update] From f4107703fb4d75adf9ec750d0b6e191cc7d0f695 Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 09:30:36 -0500 Subject: [PATCH 06/11] Add documentation for plugin --- docs/plugins/index.rst | 2 ++ docs/plugins/smartplaylist.rst | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 docs/plugins/smartplaylist.rst diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 898795150..2217f4d91 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -59,6 +59,7 @@ disabled by default, but you can turn them on as described above. ihate convert info + smartplaylist Autotagger Extensions '''''''''''''''''''''' @@ -92,6 +93,7 @@ Interoperability * :doc:`mpdupdate`: Automatically notifies `MPD`_ whenever the beets library changes. * :doc:`importfeeds`: Keep track of imported files via ``.m3u`` playlist file(s) or symlinks. +* :doc:`smartplaylist`: Generate smart playlists based on beets queries. Miscellaneous ''''''''''''' diff --git a/docs/plugins/smartplaylist.rst b/docs/plugins/smartplaylist.rst new file mode 100644 index 000000000..80b44df1d --- /dev/null +++ b/docs/plugins/smartplaylist.rst @@ -0,0 +1,42 @@ +Smart Playlist Plugin +===================== + +``smartplaylist`` is a plugin to generate smart playlists in m3u format based on +beets queries every time your library changes. This plugin is specifically +created to work well with `MPD`_'s playlist functionality. + +.. _MPD: http://mpd.wikia.com/wiki/Music_Player_Daemon_Wiki + +To use it, enable the plugin by putting ``smartplaylist`` in the ``plugins`` +section in your ``config.yaml``. Then configure your smart playlists like the +following example:: + + smartplaylist: + relative_to: ~/Music + playlist_dir: ~/.mpd/playlists + playlists: + - query: '' + name: all.m3u + + - query: 'artist:Beatles' + name: beatles.m3u + +If you intend to use this plugin to generate playlists for MPD, you should set +``relative_to`` to your MPD music directory (by default, ``relative_to`` is +``None``, and the absolute paths to your music files will be generated). + +``playlist_dir`` is where the generated playlist files will be put. + +You can generate as many playlists as you want by adding them to the +``playlists`` section, using the normal querying format (see +:doc:`/reference/query`) for ``query`` and the file name to be generated for +``name`` (*note*: if you have existing files with the same names, you should +back them up, as they will be overwritten when the plugin runs). + +If you add a smart playlist to your ``config.yaml`` file and don't want to wait +until the next time your library changes for ``smartplugin`` to run, you can +invoke it manually from the command-line:: + + $ beet splupdate + +which will generate your new smart playlists. \ No newline at end of file From 895ee7de3fbe51f7bde3d59a9ed98c282252704d Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 09:50:27 -0500 Subject: [PATCH 07/11] Fix playlist_dir not resolving relative path --- beetsplug/smartplaylist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index c84f1c612..c8ff0f9be 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -42,7 +42,7 @@ def update_playlists(lib): else: paths = [item.path for item in items] basename = playlist['name'].encode('utf8') - m3u_path = os.path.join(playlist_dir, basename) + m3u_path = normpath(os.path.join(playlist_dir, basename)) with open(syspath(m3u_path), 'w') as f: for path in paths: f.write(path + '\n') From 6bc02023548634360157cc1db88fbd1c2947d15f Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 12:23:57 -0500 Subject: [PATCH 08/11] Add ability to use tag in m3u names --- beetsplug/smartplaylist.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index c8ff0f9be..365f2f802 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -28,6 +28,7 @@ library = None def update_playlists(lib): + from beets.util.functemplate import Template print("Updating smart playlists...") playlists = config['smartplaylist']['playlists'].get(list) playlist_dir = config['smartplaylist']['playlist_dir'].get(unicode) @@ -37,15 +38,24 @@ def update_playlists(lib): for playlist in playlists: items = lib.items(playlist['query']) - if relative_to: - paths = [os.path.relpath(item.path, relative_to) for item in items] - else: - paths = [item.path for item in items] + m3us = {} basename = playlist['name'].encode('utf8') - m3u_path = normpath(os.path.join(playlist_dir, basename)) - with open(syspath(m3u_path), 'w') as f: - for path in paths: - f.write(path + '\n') + # As we allow tags in the m3u names, we'll need to iterate through + # the items and generate the correct m3u file names. + for item in items: + m3u_name = item.evaluate_template(Template(basename), lib=lib) + if not (m3u_name in m3us): + m3us[m3u_name] = [] + if relative_to: + m3us[m3u_name].append(os.path.relpath(item.path, relative_to)) + else: + m3us[m3u_name].append(item.path) + # Now iterate through the m3us that we need to generate + for m3u in m3us: + m3u_path = normpath(os.path.join(playlist_dir, m3u)) + with open(syspath(m3u_path), 'w') as f: + for path in m3us[m3u]: + f.write(path + '\n') print("... Done") From 89b19f3e6721fa6152897580b5799695daf39227 Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 12:47:44 -0500 Subject: [PATCH 09/11] Documentation about template in m3u names --- docs/plugins/smartplaylist.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/plugins/smartplaylist.rst b/docs/plugins/smartplaylist.rst index 80b44df1d..56fd39bd2 100644 --- a/docs/plugins/smartplaylist.rst +++ b/docs/plugins/smartplaylist.rst @@ -33,6 +33,15 @@ You can generate as many playlists as you want by adding them to the ``name`` (*note*: if you have existing files with the same names, you should back them up, as they will be overwritten when the plugin runs). +For more advanced usage, you can also specify metadata (see +:doc:`/reference/pathformat/`) in the ``name`` field, for example:: + + - query: 'year::201(0|1)' + name: 'ReleasedIn$year.m3u' + +This will query all the songs in 2010 and 2011, and generate the 2 playlist +files `ReleasedIn2010.m3u` and `ReleasedIn2011.m3u` using those songs. + If you add a smart playlist to your ``config.yaml`` file and don't want to wait until the next time your library changes for ``smartplugin`` to run, you can invoke it manually from the command-line:: From f5838692d711e6c8d0b8f0dc7716ea28707df4f2 Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 21:27:00 -0500 Subject: [PATCH 10/11] Add default for playlist_dir --- beetsplug/smartplaylist.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 365f2f802..dc4fe2b92 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -53,6 +53,7 @@ def update_playlists(lib): # Now iterate through the m3us that we need to generate for m3u in m3us: m3u_path = normpath(os.path.join(playlist_dir, m3u)) + import pdb; pdb.set_trace() with open(syspath(m3u_path), 'w') as f: for path in m3us[m3u]: f.write(path + '\n') @@ -64,6 +65,7 @@ class SmartPlaylistPlugin(BeetsPlugin): super(SmartPlaylistPlugin, self).__init__() self.config.add({ 'relative_to': None, + 'playlist_dir': u'.', 'playlists': [] }) From 397ad441a8d064be41c31000356f677532e9f1b4 Mon Sep 17 00:00:00 2001 From: Dang Mai Date: Wed, 30 Jan 2013 21:28:54 -0500 Subject: [PATCH 11/11] Remove debugging statement Silly me for forgetting to check the pdb statement I just put in ... --- beetsplug/smartplaylist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index dc4fe2b92..44aae61b5 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -53,7 +53,6 @@ def update_playlists(lib): # Now iterate through the m3us that we need to generate for m3u in m3us: m3u_path = normpath(os.path.join(playlist_dir, m3u)) - import pdb; pdb.set_trace() with open(syspath(m3u_path), 'w') as f: for path in m3us[m3u]: f.write(path + '\n')