From f06c33cb71f800ed60ffed41ef282a8413ddca4d Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sun, 15 Mar 2015 12:02:21 +0100 Subject: [PATCH] Smartplaylist: update only if item changed --- beetsplug/smartplaylist.py | 101 +++++++++++++++++++++++++------------ test/test_smartplaylist.py | 41 +++++++++++++++ 2 files changed, 110 insertions(+), 32 deletions(-) create mode 100644 test/test_smartplaylist.py diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index a9c0a25aa..ab63c9de8 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -21,30 +21,13 @@ from __future__ import (division, absolute_import, print_function, from beets.plugins import BeetsPlugin from beets import ui from beets.util import mkdirall, normpath, syspath +from beets.library import Item, Album, parse_query_parts +from beets.dbcore import OrQuery import os -def _items_for_query(lib, queries, album): - """Get the matching items for a list of queries. - - `queries` can either be a single string or a list of strings. In the - latter case, the results from each query are concatenated. `album` - indicates whether the queries are item-level or album-level. - """ - if isinstance(queries, basestring): - queries = [queries] - if album: - for query in queries: - for album in lib.albums(query): - for item in album.items(): - yield item - else: - for query in queries: - for item in lib.items(query): - yield item - - class SmartPlaylistPlugin(BeetsPlugin): + def __init__(self): super(SmartPlaylistPlugin, self).__init__() self.config.add({ @@ -54,42 +37,96 @@ class SmartPlaylistPlugin(BeetsPlugin): 'playlists': [] }) + self._matched_playlists = None + self._unmatched_playlists = None + if self.config['auto']: self.register_listener('database_change', self.db_change) def commands(self): def update(lib, opts, args): + self.build_queries() + self._matched_playlists = self._unmatched_playlists self.update_playlists(lib) spl_update = ui.Subcommand('splupdate', help='update the smart playlists') spl_update.func = update return [spl_update] + def build_queries(self): + """ + Instanciate queries for the playlists. + + Each playlist has 2 queries: one or items one for albums. We must also + remember its name. _unmatched_playlists is a set of tuples + (name, q, album_q). + """ + self._unmatched_playlists = set() + self._matched_playlists = set() + + for playlist in self.config['playlists'].get(list): + playlist_data = (playlist['name'],) + for key, Model in (('query', Item), ('album_query', Album)): + qs = playlist.get(key) + # FIXME sort mgmt + if qs is None: + query = None + sort = None + elif isinstance(qs, basestring): + query, sort = parse_query_parts(qs, Model) + else: + query = OrQuery([parse_query_parts(q, Model)[0] + for q in qs]) + sort = None + playlist_data += (query,) + + self._unmatched_playlists.add(playlist_data) + def db_change(self, lib, model): - self.register_listener('cli_exit', self.update_playlists) + if self._unmatched_playlists is None: + self.build_queries() + + for playlist in self._unmatched_playlists: + n, q, a_q = playlist + if a_q and isinstance(model, Album): + matches = a_q.match(model) + elif q and isinstance(model, Item): + matches = q.match(model) or q.match(model.get_album()) + else: + matches = False + + if matches: + self._log.debug("{0} will be updated because of {1}", n, model) + self._matched_playlists.add(playlist) + self.register_listener('cli_exit', self.update_playlists) + + self._unmatched_playlists -= self._matched_playlists def update_playlists(self, lib): - self._log.info("Updating smart playlists...") - playlists = self.config['playlists'].get(list) + self._log.info("Updating {0} smart playlists...", + len(self._matched_playlists)) + playlist_dir = self.config['playlist_dir'].as_filename() relative_to = self.config['relative_to'].get() if relative_to: relative_to = normpath(relative_to) - for playlist in playlists: - self._log.debug(u"Creating playlist {0[name]}", playlist) + for playlist in self._matched_playlists: + name, query, album_query = playlist + self._log.debug(u"Creating playlist {0}", name) items = [] - if 'album_query' in playlist: - items.extend(_items_for_query(lib, playlist['album_query'], - True)) - if 'query' in playlist: - items.extend(_items_for_query(lib, playlist['query'], False)) + + if query: + items.extend(lib.items(query)) + if album_query: + for album in lib.albums(album_query): + items.extend(album.items()) m3us = {} # 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(playlist['name'], True) + m3u_name = item.evaluate_template(name, True) if m3u_name not in m3us: m3us[m3u_name] = [] item_path = item.path @@ -104,4 +141,4 @@ class SmartPlaylistPlugin(BeetsPlugin): with open(syspath(m3u_path), 'w') as f: for path in m3us[m3u]: f.write(path + b'\n') - self._log.info("{0} playlists updated", len(playlists)) + self._log.info("{0} playlists updated", len(self._matched_playlists)) diff --git a/test/test_smartplaylist.py b/test/test_smartplaylist.py new file mode 100644 index 000000000..90aca4e9b --- /dev/null +++ b/test/test_smartplaylist.py @@ -0,0 +1,41 @@ +# This file is part of beets. +# Copyright 2015, Bruno Cauet. +# +# 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. + +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +from test._common import unittest +from beetsplug import smartplaylist +from beets import config, ui + +from test.helper import TestHelper + + +class SmartPlaylistTest(unittest.TestCase): + def test_build_queries(self): + pass + + def test_db_changes(self): + pass + + def test_playlist_update(self): + pass + + +class SmartPlaylistCLITest(unittest.TestCase, TestHelper): + def test_import(self): + pass + + def test_splupdate(self): + pass