From 288902a572c0ff92b9019cb0f2f633e9c8a4af42 Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Thu, 14 Jun 2012 18:47:40 -0400 Subject: [PATCH 01/21] add experimental fuzzy matching plugin fuzzy is a command which tries to be like the list command but using fuzzy matching. --- beetsplug/fuzzy_list.py | 84 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 beetsplug/fuzzy_list.py diff --git a/beetsplug/fuzzy_list.py b/beetsplug/fuzzy_list.py new file mode 100644 index 000000000..ca409dcdf --- /dev/null +++ b/beetsplug/fuzzy_list.py @@ -0,0 +1,84 @@ +# This file is part of beets. +# Copyright 2011, Philippe Mongeau. +# +# 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. + +"""Get a random song or album from the library. +""" +from beets.plugins import BeetsPlugin +from beets.ui import Subcommand, decargs, print_ +from beets.util.functemplate import Template +import random +import difflib + +def fuzzy_score(query, item): + return difflib.SequenceMatcher(a=query, b=item).quick_ratio() + +def is_match(query, item, verbose=False): + query = ' '.join(query) + s = max(fuzzy_score(query, i) for i in (item.artist, + item.album, + item.title)) + if s > 0.7: return (True, s) if verbose else True + else: return (False, s) if verbose else False + +def fuzzy_list(lib, config, opts, args): + query = decargs(args) + path = opts.path + fmt = opts.format + verbose = opts.verbose + + if fmt is None: + # If no specific template is supplied, use a default + if opts.album: + fmt = u'$albumartist - $album' + else: + fmt = u'$artist - $album - $title' + template = Template(fmt) + + if opts.album: + objs = list(lib.albums()) + else: + objs = list(lib.items()) + + # matches = [i for i in objs if is_match(query, i)] + + if opts.album: + for album in objs: + if path: + print_(album.item_dir()) + else: + print_(album.evaluate_template(template)) + else: + for item in objs: + if is_match(query, item): + if path: + print_(item.path) + else: + print_(item.evaluate_template(template, lib)) + if verbose: print is_match(query,item, True)[1] + +fuzzy_cmd = Subcommand('fuzzy', + help='list items using fuzzy matching') +fuzzy_cmd.parser.add_option('-a', '--album', action='store_true', + help='choose an album instead of track') +fuzzy_cmd.parser.add_option('-p', '--path', action='store_true', + help='print the path of the matched item') +fuzzy_cmd.parser.add_option('-f', '--format', action='store', + help='print with custom format', default=None) +fuzzy_cmd.parser.add_option('-v', '--verbose', action='store_true', + help='output scores for matches') +fuzzy_cmd.func = fuzzy_list + +class Random(BeetsPlugin): + def commands(self): + return [fuzzy_cmd] From 7187d93303b7575511a77416c5e036cef67d9a39 Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Thu, 14 Jun 2012 18:55:47 -0400 Subject: [PATCH 02/21] implement album option for the fuzzy plugin --- beetsplug/fuzzy_list.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/beetsplug/fuzzy_list.py b/beetsplug/fuzzy_list.py index ca409dcdf..adbeb9447 100644 --- a/beetsplug/fuzzy_list.py +++ b/beetsplug/fuzzy_list.py @@ -23,11 +23,13 @@ import difflib def fuzzy_score(query, item): return difflib.SequenceMatcher(a=query, b=item).quick_ratio() -def is_match(query, item, verbose=False): +def is_match(query, item, album=False, verbose=False): query = ' '.join(query) - s = max(fuzzy_score(query, i) for i in (item.artist, - item.album, - item.title)) + + if album: values = [item.albumartist, item.album] + else: values = [item.artist, item.album, item.title] + + s = max(fuzzy_score(query, i) for i in values) if s > 0.7: return (True, s) if verbose else True else: return (False, s) if verbose else False @@ -54,10 +56,12 @@ def fuzzy_list(lib, config, opts, args): if opts.album: for album in objs: - if path: - print_(album.item_dir()) - else: - print_(album.evaluate_template(template)) + if is_match(query, album, album=True): + if path: + print_(album.item_dir()) + else: + print_(album.evaluate_template(template)) + if verbose: print is_match(query,album, album=True, verbose=True)[1] else: for item in objs: if is_match(query, item): @@ -65,7 +69,7 @@ def fuzzy_list(lib, config, opts, args): print_(item.path) else: print_(item.evaluate_template(template, lib)) - if verbose: print is_match(query,item, True)[1] + if verbose: print is_match(query,item, verbose=True)[1] fuzzy_cmd = Subcommand('fuzzy', help='list items using fuzzy matching') From 5a2719711e8b3f1c7656c3c922c434d43c89db7e Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Mon, 18 Jun 2012 17:28:10 -0400 Subject: [PATCH 03/21] Rename Fuzzy plugin class name. remove useless conversion of an iterator to a list the plugin class was called Random (because of copy paste) --- beetsplug/fuzzy_list.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/beetsplug/fuzzy_list.py b/beetsplug/fuzzy_list.py index adbeb9447..e124058f8 100644 --- a/beetsplug/fuzzy_list.py +++ b/beetsplug/fuzzy_list.py @@ -48,9 +48,9 @@ def fuzzy_list(lib, config, opts, args): template = Template(fmt) if opts.album: - objs = list(lib.albums()) + objs = lib.albums() else: - objs = list(lib.items()) + objs = lib.items() # matches = [i for i in objs if is_match(query, i)] @@ -83,6 +83,8 @@ fuzzy_cmd.parser.add_option('-v', '--verbose', action='store_true', help='output scores for matches') fuzzy_cmd.func = fuzzy_list -class Random(BeetsPlugin): + + +class Fuzzy(BeetsPlugin): def commands(self): return [fuzzy_cmd] From 325009edf249814832bbba47e772b11b9bb9213e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 9 Sep 2012 23:30:45 -0700 Subject: [PATCH 04/21] Travis: only test master branch --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 70bf2bef0..ebb7162fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,3 +7,6 @@ install: - pip install pylast flask --use-mirrors - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2 --use-mirrors; fi" script: nosetests +branches: + only: + - master From a9fc483a36cd231653098c2483977a099e8425e1 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 11 Sep 2012 17:32:43 -0700 Subject: [PATCH 05/21] importfeed: fix "link" mode w/ unicode paths --- beetsplug/importfeeds.py | 12 ++++++------ docs/changelog.rst | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index 78c77052a..0f9b89a07 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -21,7 +21,7 @@ import re from beets import ui from beets.plugins import BeetsPlugin -from beets.util import normpath +from beets.util import normpath, syspath, bytestring_path M3U_DEFAULT_NAME = 'imported.m3u' @@ -36,9 +36,9 @@ class ImportFeedsPlugin(BeetsPlugin): _feeds_dir = ui.config_val(config, 'importfeeds', 'feeds_dir', None) if _feeds_dir: - _feeds_dir = os.path.expanduser(_feeds_dir) + _feeds_dir = os.path.expanduser(bytestring_path(_feeds_dir)) if not os.path.exists(_feeds_dir): - os.makedirs(_feeds_dir) + os.makedirs(syspath(_feeds_dir)) def _get_feeds_dir(lib): """Given a Library object, return the path to the feeds directory to be @@ -49,8 +49,8 @@ def _get_feeds_dir(lib): dirpath = lib.directory # Ensure directory exists. - if not os.path.exists(dirpath): - os.makedirs(dirpath) + if not os.path.exists(syspath(dirpath)): + os.makedirs(syspath(dirpath)) return dirpath def _build_m3u_filename(basename): @@ -65,7 +65,7 @@ def _build_m3u_filename(basename): def _write_m3u(m3u_path, items_paths): """Append relative paths to items into m3u file. """ - with open(m3u_path, 'a') as f: + with open(syspath(m3u_path), 'a') as f: for path in items_paths: f.write(path + '\n') diff --git a/docs/changelog.rst b/docs/changelog.rst index 03aab8744..20d7a9877 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,6 +20,8 @@ Changelog `Tomahawk resolver`_). * :doc:`/plugins/web`: Files now download with a reasonable filename rather than just being called "file" (thanks to Zach Denton). +* :doc:`/plugins/importfeeds`: Fix error in symlink mode with non-ASCII + filenames. * Add the track mapping dictionary to the ``album_distance`` plugin function. * Fix an assertion failure when the MusicBrainz main database and search server disagree. From 749b19955ee524601180e0bf05ef5a618df54ca6 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 12 Sep 2012 17:57:37 -0700 Subject: [PATCH 06/21] bpd: use pygst.require() before importing gst A user reports that this fixes BPD on OS X. --- beetsplug/bpd/gstplayer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beetsplug/bpd/gstplayer.py b/beetsplug/bpd/gstplayer.py index b100f33bc..4c67713dd 100644 --- a/beetsplug/bpd/gstplayer.py +++ b/beetsplug/bpd/gstplayer.py @@ -17,7 +17,6 @@ music player. """ from __future__ import print_function -import gst import sys import time import gobject @@ -26,6 +25,10 @@ import os import copy import urllib +import pygst +pygst.require('0.10') +import gst + class GstPlayer(object): """A music player abstracting GStreamer's Playbin element. From 49a6b993d36369997d7c7360b234745f60cc8953 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Sat, 15 Sep 2012 12:00:33 +0200 Subject: [PATCH 07/21] lastgenre: add existing genre to the list of suggested tags to canonicalize --- beetsplug/lastgenre/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 62632d32e..4d5cec51a 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -167,16 +167,21 @@ class LastGenrePlugin(plugins.BeetsPlugin): fallback_str = ui.config_val(config, 'lastgenre', 'fallback_str', None) def imported(self, config, task): + tags = [] if task.is_album: album = config.lib.get_album(task.album_id) lastfm_obj = LASTFM.get_album(album.albumartist, album.album) + if album.genre: + tags.append(album.genre) else: item = task.item lastfm_obj = LASTFM.get_track(item.artist, item.title) + if item.genre: + tags.append(item.genre) - tags = _tags_for(lastfm_obj) + tags.extend(_tags_for(lastfm_obj)) genre = _tags_to_genre(tags) - + if not genre and fallback_str != None: genre = fallback_str log.debug(u'no last.fm genre found: fallback to %s' % genre) From 16aa842ccf041b94a04109c3f96b09fa80387818 Mon Sep 17 00:00:00 2001 From: Blemjhoo Tezoulbr Date: Sun, 16 Sep 2012 04:42:39 +0300 Subject: [PATCH 08/21] plugin the: version 1.0 --- beetsplug/the.py | 124 +++++++++++++++++++++++++++++++++++++++++ docs/plugins/index.rst | 2 + docs/plugins/the.rst | 47 ++++++++++++++++ test/test_the.py | 52 +++++++++++++++++ 4 files changed, 225 insertions(+) create mode 100644 beetsplug/the.py create mode 100644 docs/plugins/the.rst create mode 100644 test/test_the.py diff --git a/beetsplug/the.py b/beetsplug/the.py new file mode 100644 index 000000000..f8c8dd6de --- /dev/null +++ b/beetsplug/the.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python2 +# +# This is a plugin for beets music organizer. +# Copyright (c) 2012 Blemjhoo Tezoulbr +# Licensed under the same terms as beets itself. +# + +"""Moves patterns in path formats (suitable for moving articles).""" + +from __future__ import print_function +import sys +import re +from beets.plugins import BeetsPlugin +from beets import ui + + +__author__ = 'baobab@heresiarch.info' +__version__ = '1.0' + +PATTERN_THE = u'^[the]{3}\s' +PATTERN_A = u'^[a][n]?\s' +FORMAT = u'{0}, {1}' + +the_options = { + 'debug': False, + 'the': True, + 'a': True, + 'format': FORMAT, + 'strip': False, + 'silent': False, + 'patterns': [PATTERN_THE, PATTERN_A], +} + + +class ThePlugin(BeetsPlugin): + + def configure(self, config): + if not config.has_section('the'): + print('[the] plugin is not configured, using defaults', + file=sys.stderr) + return + self.in_config = True + the_options['debug'] = ui.config_val(config, 'the', 'debug', False, + bool) + the_options['the'] = ui.config_val(config, 'the', 'the', True, bool) + the_options['a'] = ui.config_val(config, 'the', 'a', True, bool) + the_options['format'] = ui.config_val(config, 'the', 'format', + FORMAT) + the_options['strip'] = ui.config_val(config, 'the', 'strip', False, + bool) + the_options['silent'] = ui.config_val(config, 'the', 'silent', False, + bool) + the_options['patterns'] = ui.config_val(config, 'the', 'patterns', + '').split() + for p in the_options['patterns']: + if p: + try: + re.compile(p) + except re.error: + print(u'[the] invalid pattern: {}'.format(p), + file=sys.stderr) + else: + if not (p.startswith('^') or p.endswith('$')): + if not the_options['silent']: + print(u'[the] warning: pattern \"{}\" will not ' + 'match string start/end'.format(p), + file=sys.stderr) + if the_options['a']: + the_options['patterns'] = [PATTERN_A] + the_options['patterns'] + if the_options['the']: + the_options['patterns'] = [PATTERN_THE] + the_options['patterns'] + if not the_options['patterns'] and not the_options['silent']: + print('[the] no patterns defined!') + if the_options['debug']: + print(u'[the] patterns: {}' + .format(' '.join(the_options['patterns'])), file=sys.stderr) + + +def unthe(text, pattern, strip=False): + """Moves pattern in the path format string or strips it + + text -- text to handle + pattern -- regexp pattern (case ignore is already on) + strip -- if True, pattern will be removed + + """ + if text: + r = re.compile(pattern, flags=re.IGNORECASE) + try: + t = r.findall(text)[0] + except IndexError: + return text + else: + r = re.sub(r, '', text).strip() + if strip: + return r + else: + return the_options['format'].format(r, t.strip()).strip() + else: + return u'' + + +@ThePlugin.template_func('the') +def func_the(text): + """Provides beets template function %the""" + if not the_options['patterns']: + return text + if text: + for p in the_options['patterns']: + r = unthe(text, p, the_options['strip']) + if r != text: + break + if the_options['debug']: + print(u'[the] \"{}\" -> \"{}\"'.format(text, r), file=sys.stderr) + return r + else: + return u'' + + +# simple tests +if __name__ == '__main__': + print(unthe('The The', PATTERN_THE)) + print(unthe('An Apple', PATTERN_A)) + print(unthe('A Girl', PATTERN_A, strip=True)) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index a9edd7dee..b4a6d364f 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -50,6 +50,7 @@ disabled by default, but you can turn them on as described above. rdm mbcollection importfeeds + the Autotagger Extensions '''''''''''''''''''''' @@ -72,6 +73,7 @@ Path Formats * :doc:`inline`: Use Python snippets to customize path format strings. * :doc:`rewrite`: Substitute values in path formats. +* :doc:`the`: Moves patterns in path formats (suitable for moving articles). Interoperability '''''''''''''''' diff --git a/docs/plugins/the.rst b/docs/plugins/the.rst new file mode 100644 index 000000000..c137a1216 --- /dev/null +++ b/docs/plugins/the.rst @@ -0,0 +1,47 @@ +The Plugin +========== + +The ``the`` plugin allows you to move patterns in path formats. It's suitable, +for example, for moving articles from string start to the end. This is useful +for quick search on filesystems and generally looks good. Plugin DOES NOT +change tags. By default plugin supports English "the, a, an", but custom +regexp patterns can be added by user. How it works:: + + The Something -> Something, The + A Band -> Band, A + An Orchestra -> Orchestra, An + +To use plugin, enable it by including ``the`` into ``plugins`` line of +your beets config:: + + [beets] + plugins = the + +Plugin provides template function %the, so you can use it on $albumartist or $artist:: + + [paths] + default: %the{$albumartist}/($year) $album/$track $title + +Default options are acceptable (moves all English articles to the end), but you +can add plugin section into config file:: + + [the] + # handle The, default is on + the=yes + # handle A/An, default is on + a=yes + # format string, {0} - part w/o article, {1} - article + # spaces already trimmed from ends of both parts + # default is '{0}, {1}' + format={0}, {1} + # strip instead of moving to the end, default is off + strip=no + # do not print warnings, default is off + silent=no + # custom regexp patterns, separated by space + patterns= + +Custom patterns are usual regular expressions. Ignore case is turned on, but ^ is not added +automatically, so be careful. Actually, you can swap arguments in format option and write +regexp to match end of the string, so things will be moved from the end of the string to +start. diff --git a/test/test_the.py b/test/test_the.py new file mode 100644 index 000000000..efdd81d9e --- /dev/null +++ b/test/test_the.py @@ -0,0 +1,52 @@ +"""Tests for the 'the' plugin""" + +from _common import unittest +from beetsplug import the + + +class ThePluginTest(unittest.TestCase): + + + def test_unthe_with_default_patterns(self): + self.assertEqual(the.unthe('', the.PATTERN_THE), '') + self.assertEqual(the.unthe('The Something', the.PATTERN_THE), + 'Something, The') + self.assertEqual(the.unthe('The The', the.PATTERN_THE), 'The, The') + self.assertEqual(the.unthe('The The', the.PATTERN_THE), 'The, The') + self.assertEqual(the.unthe('The The X', the.PATTERN_THE), + u'The X, The') + self.assertEqual(the.unthe('the The', the.PATTERN_THE), 'The, the') + self.assertEqual(the.unthe('Protected The', the.PATTERN_THE), + 'Protected The') + self.assertEqual(the.unthe('A Boy', the.PATTERN_A), 'Boy, A') + self.assertEqual(the.unthe('a girl', the.PATTERN_A), 'girl, a') + self.assertEqual(the.unthe('An Apple', the.PATTERN_A), 'Apple, An') + self.assertEqual(the.unthe('An A Thing', the.PATTERN_A), 'A Thing, An') + self.assertEqual(the.unthe('the An Arse', the.PATTERN_A), + 'the An Arse') + self.assertEqual(the.unthe('The Something', the.PATTERN_THE, + strip=True), 'Something') + self.assertEqual(the.unthe('An A', the.PATTERN_A, strip=True), 'A') + + def test_template_function_with_defaults(self): + the.the_options['patterns'] = [the.PATTERN_THE, the.PATTERN_A] + the.the_options['format'] = the.FORMAT + self.assertEqual(the.func_the('The The'), 'The, The') + self.assertEqual(the.func_the('An A'), 'A, An') + + def test_custom_pattern(self): + the.the_options['patterns'] = [ u'^test\s'] + the.the_options['format'] = the.FORMAT + self.assertEqual(the.func_the('test passed'), 'passed, test') + + def test_custom_format(self): + the.the_options['patterns'] = [the.PATTERN_THE, the.PATTERN_A] + the.the_options['format'] = '{1} ({0})' + self.assertEqual(the.func_the('The A'), 'The (A)') + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == '__main__': + unittest.main(defaultTest='suite') From ef248576f06b69b62b1cadbf782bf3ebf41cd953 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 17 Sep 2012 10:24:34 -0700 Subject: [PATCH 09/21] bpd: fix crash w/ unicode extensions (GC-439) --- beets/library.py | 3 +++ docs/changelog.rst | 2 ++ test/test_db.py | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/beets/library.py b/beets/library.py index ef97161a6..8e6f75c1c 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1135,6 +1135,9 @@ class Library(BaseLibrary): # Preserve extension. _, extension = pathmod.splitext(item.path) + if fragment: + # Outputting Unicode. + extension = extension.decode('utf8', 'ignore') subpath += extension.lower() if fragment: diff --git a/docs/changelog.rst b/docs/changelog.rst index 20d7a9877..00b409baa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,8 @@ Changelog disagree. * Fix a bug that caused the :doc:`/plugins/lastgenre` and other plugins not to modify files' tags even when they successfully change the database. +* Fix a VFS bug leading to a crash in the :doc:`/plugins/bpd` when files had + non-ASCII extensions. .. _Tomahawk resolver: http://beets.radbox.org/blog/tomahawk-resolver.html diff --git a/test/test_db.py b/test/test_db.py index 49a1dbef5..fc597b6cd 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -442,6 +442,12 @@ class DestinationTest(unittest.TestCase): finally: sys.getfilesystemencoding = oldfunc + def test_unicode_extension_in_fragment(self): + self.lib.path_formats = [('default', u'foo')] + self.i.path = util.bytestring_path(u'bar.caf\xe9') + dest = self.lib.destination(self.i, platform='linux2', fragment=True) + self.assertEqual(dest, u'foo.caf\xe9') + class PathFormattingMixin(object): """Utilities for testing path formatting.""" def _setf(self, fmt): From c056467c5d019722ee46315db040d21f55962324 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 17 Sep 2012 10:36:48 -0700 Subject: [PATCH 10/21] friendly error message in Item.write (GC-437) --- beets/library.py | 8 +++++++- beets/util/__init__.py | 2 +- docs/changelog.rst | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/beets/library.py b/beets/library.py index 8e6f75c1c..b5e60d657 100644 --- a/beets/library.py +++ b/beets/library.py @@ -23,6 +23,7 @@ import shlex import unicodedata import threading import contextlib +import traceback from collections import defaultdict from unidecode import unidecode from beets.mediafile import MediaFile @@ -283,7 +284,12 @@ class Item(object): f = MediaFile(syspath(self.path)) for key in ITEM_KEYS_WRITABLE: setattr(f, key, getattr(self, key)) - f.save() + + try: + f.save() + except (OSError, IOError) as exc: + raise util.FilesystemError(exc, 'write', (self.path,), + traceback.format_exc()) # The file has a new mtime. self.mtime = self.current_mtime() diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 6c29c7c0e..e311692a9 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -93,7 +93,7 @@ class FilesystemError(HumanReadableException): clause = 'while {0} {1} to {2}'.format( self._gerund(), repr(self.paths[0]), repr(self.paths[1]) ) - elif self.verb in ('delete',): + elif self.verb in ('delete', 'write'): clause = 'while {0} {1}'.format( self._gerund(), repr(self.paths[0]) ) diff --git a/docs/changelog.rst b/docs/changelog.rst index 00b409baa..9b71d2d1a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,7 @@ Changelog modify files' tags even when they successfully change the database. * Fix a VFS bug leading to a crash in the :doc:`/plugins/bpd` when files had non-ASCII extensions. +* Add a human-readable error message when writing files' tags fails. .. _Tomahawk resolver: http://beets.radbox.org/blog/tomahawk-resolver.html From 201571bf93bab204b4b7e5350b5dd479cee95c7d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 17 Sep 2012 11:11:22 -0700 Subject: [PATCH 11/21] changelog note &c. for "the" plugin (GH-47) --- beetsplug/the.py | 49 ++++++++++++++++++++++++++-------------------- docs/changelog.rst | 2 ++ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/beetsplug/the.py b/beetsplug/the.py index f8c8dd6de..df2673793 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -1,9 +1,16 @@ -#!/usr/bin/env python2 +# This file is part of beets. +# Copyright 2012, Blemjhoo Tezoulbr . # -# This is a plugin for beets music organizer. -# Copyright (c) 2012 Blemjhoo Tezoulbr -# Licensed under the same terms as beets itself. +# 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. """Moves patterns in path formats (suitable for moving articles).""" @@ -26,32 +33,32 @@ the_options = { 'the': True, 'a': True, 'format': FORMAT, - 'strip': False, + 'strip': False, 'silent': False, - 'patterns': [PATTERN_THE, PATTERN_A], + 'patterns': [PATTERN_THE, PATTERN_A], } class ThePlugin(BeetsPlugin): - + def configure(self, config): if not config.has_section('the'): print('[the] plugin is not configured, using defaults', file=sys.stderr) return self.in_config = True - the_options['debug'] = ui.config_val(config, 'the', 'debug', False, + the_options['debug'] = ui.config_val(config, 'the', 'debug', False, bool) the_options['the'] = ui.config_val(config, 'the', 'the', True, bool) the_options['a'] = ui.config_val(config, 'the', 'a', True, bool) the_options['format'] = ui.config_val(config, 'the', 'format', FORMAT) - the_options['strip'] = ui.config_val(config, 'the', 'strip', False, + the_options['strip'] = ui.config_val(config, 'the', 'strip', False, bool) - the_options['silent'] = ui.config_val(config, 'the', 'silent', False, + the_options['silent'] = ui.config_val(config, 'the', 'silent', False, bool) - the_options['patterns'] = ui.config_val(config, 'the', 'patterns', - '').split() + the_options['patterns'] = ui.config_val(config, 'the', 'patterns', + '').split() for p in the_options['patterns']: if p: try: @@ -63,18 +70,18 @@ class ThePlugin(BeetsPlugin): if not (p.startswith('^') or p.endswith('$')): if not the_options['silent']: print(u'[the] warning: pattern \"{}\" will not ' - 'match string start/end'.format(p), + 'match string start/end'.format(p), file=sys.stderr) if the_options['a']: the_options['patterns'] = [PATTERN_A] + the_options['patterns'] if the_options['the']: the_options['patterns'] = [PATTERN_THE] + the_options['patterns'] if not the_options['patterns'] and not the_options['silent']: - print('[the] no patterns defined!') + print('[the] no patterns defined!') if the_options['debug']: print(u'[the] patterns: {}' .format(' '.join(the_options['patterns'])), file=sys.stderr) - + def unthe(text, pattern, strip=False): """Moves pattern in the path format string or strips it @@ -82,9 +89,9 @@ def unthe(text, pattern, strip=False): text -- text to handle pattern -- regexp pattern (case ignore is already on) strip -- if True, pattern will be removed - - """ - if text: + + """ + if text: r = re.compile(pattern, flags=re.IGNORECASE) try: t = r.findall(text)[0] @@ -95,10 +102,10 @@ def unthe(text, pattern, strip=False): if strip: return r else: - return the_options['format'].format(r, t.strip()).strip() + return the_options['format'].format(r, t.strip()).strip() else: return u'' - + @ThePlugin.template_func('the') def func_the(text): @@ -119,6 +126,6 @@ def func_the(text): # simple tests if __name__ == '__main__': - print(unthe('The The', PATTERN_THE)) + print(unthe('The The', PATTERN_THE)) print(unthe('An Apple', PATTERN_A)) print(unthe('A Girl', PATTERN_A, strip=True)) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9b71d2d1a..967955a03 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,8 @@ Changelog 1.0b16 (in development) ----------------------- +* New plugin: :doc:`/plugins/the` adds a template function that helps format + text for nicely-sorted directory listings. Thanks to Blemjhoo Tezoulbr. * :doc:`/plugins/scrub`: Scrubbing now removes *all* types of tags from a file rather than just one. For example, if your FLAC file has both ordinary FLAC tags and ID3 tags, the ID3 tags are now also removed. From ca237ce3e74e24c9e9e8429c568926bfd63c986c Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Mon, 17 Sep 2012 22:10:36 -0400 Subject: [PATCH 12/21] make fuzzy_search case insensitive and add a threshold option renamed fuzzy_list.py to fuzzy_search.py --- beetsplug/{fuzzy_list.py => fuzzy_search.py} | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) rename beetsplug/{fuzzy_list.py => fuzzy_search.py} (78%) diff --git a/beetsplug/fuzzy_list.py b/beetsplug/fuzzy_search.py similarity index 78% rename from beetsplug/fuzzy_list.py rename to beetsplug/fuzzy_search.py index e124058f8..064125320 100644 --- a/beetsplug/fuzzy_list.py +++ b/beetsplug/fuzzy_search.py @@ -12,25 +12,26 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Get a random song or album from the library. +"""Like beet list, but with fuzzy matching """ from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs, print_ from beets.util.functemplate import Template -import random import difflib +# THRESHOLD = 0.7 + def fuzzy_score(query, item): return difflib.SequenceMatcher(a=query, b=item).quick_ratio() -def is_match(query, item, album=False, verbose=False): +def is_match(query, item, album=False, verbose=False, threshold=0.7): query = ' '.join(query) if album: values = [item.albumartist, item.album] else: values = [item.artist, item.album, item.title] - s = max(fuzzy_score(query, i) for i in values) - if s > 0.7: return (True, s) if verbose else True + s = max(fuzzy_score(query.lower(), i.lower()) for i in values) + if s > threshold: return (True, s) if verbose else True else: return (False, s) if verbose else False def fuzzy_list(lib, config, opts, args): @@ -38,6 +39,7 @@ def fuzzy_list(lib, config, opts, args): path = opts.path fmt = opts.format verbose = opts.verbose + threshold = float(opts.threshold) if fmt is None: # If no specific template is supplied, use a default @@ -52,19 +54,18 @@ def fuzzy_list(lib, config, opts, args): else: objs = lib.items() - # matches = [i for i in objs if is_match(query, i)] - if opts.album: for album in objs: - if is_match(query, album, album=True): + if is_match(query, album, album=True, threshold=threshold): if path: print_(album.item_dir()) else: print_(album.evaluate_template(template)) - if verbose: print is_match(query,album, album=True, verbose=True)[1] + if verbose: print is_match(query, album, + album=True, verbose=True)[1] else: for item in objs: - if is_match(query, item): + if is_match(query, item, threshold=threshold): if path: print_(item.path) else: @@ -81,6 +82,9 @@ fuzzy_cmd.parser.add_option('-f', '--format', action='store', help='print with custom format', default=None) fuzzy_cmd.parser.add_option('-v', '--verbose', action='store_true', help='output scores for matches') +fuzzy_cmd.parser.add_option('-t', '--threshold', action='store', + help='return result with a fuzzy score above threshold. (default is 0.7)', + default=0.7) fuzzy_cmd.func = fuzzy_list From dfca295e31990403e3b205680b4c8676edeaa772 Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Mon, 17 Sep 2012 22:23:15 -0400 Subject: [PATCH 13/21] pep8ify fuzzy_search --- beetsplug/fuzzy_search.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/beetsplug/fuzzy_search.py b/beetsplug/fuzzy_search.py index 064125320..4e93a4ee3 100644 --- a/beetsplug/fuzzy_search.py +++ b/beetsplug/fuzzy_search.py @@ -21,18 +21,25 @@ import difflib # THRESHOLD = 0.7 + def fuzzy_score(query, item): return difflib.SequenceMatcher(a=query, b=item).quick_ratio() + def is_match(query, item, album=False, verbose=False, threshold=0.7): query = ' '.join(query) - if album: values = [item.albumartist, item.album] - else: values = [item.artist, item.album, item.title] + if album: + values = [item.albumartist, item.album] + else: + values = [item.artist, item.album, item.title] + + s = max(fuzzy_score(query.lower(), i.lower()) for i in values) + if s >= threshold: + return (True, s) if verbose else True + else: + return (False, s) if verbose else False - s = max(fuzzy_score(query.lower(), i.lower()) for i in values) - if s > threshold: return (True, s) if verbose else True - else: return (False, s) if verbose else False def fuzzy_list(lib, config, opts, args): query = decargs(args) @@ -61,8 +68,8 @@ def fuzzy_list(lib, config, opts, args): print_(album.item_dir()) else: print_(album.evaluate_template(template)) - if verbose: print is_match(query, album, - album=True, verbose=True)[1] + if verbose: + print is_match(query, album, album=True, verbose=True)[1] else: for item in objs: if is_match(query, item, threshold=threshold): @@ -70,7 +77,8 @@ def fuzzy_list(lib, config, opts, args): print_(item.path) else: print_(item.evaluate_template(template, lib)) - if verbose: print is_match(query,item, verbose=True)[1] + if verbose: + print is_match(query, item, verbose=True)[1] fuzzy_cmd = Subcommand('fuzzy', help='list items using fuzzy matching') @@ -83,12 +91,11 @@ fuzzy_cmd.parser.add_option('-f', '--format', action='store', fuzzy_cmd.parser.add_option('-v', '--verbose', action='store_true', help='output scores for matches') fuzzy_cmd.parser.add_option('-t', '--threshold', action='store', - help='return result with a fuzzy score above threshold. (default is 0.7)', - default=0.7) + help='return result with a fuzzy score above threshold. \ + (default is 0.7)', default=0.7) fuzzy_cmd.func = fuzzy_list - class Fuzzy(BeetsPlugin): def commands(self): return [fuzzy_cmd] From a49dcb81c67861956e44cc82f575f20efe4679d1 Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Mon, 17 Sep 2012 22:41:02 -0400 Subject: [PATCH 14/21] add documentation for fuzzy_search --- docs/plugins/fuzzy_search.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/plugins/fuzzy_search.rst diff --git a/docs/plugins/fuzzy_search.rst b/docs/plugins/fuzzy_search.rst new file mode 100644 index 000000000..e0ecffc7f --- /dev/null +++ b/docs/plugins/fuzzy_search.rst @@ -0,0 +1,20 @@ +Fuzzy Search Plugin +============= + +The ``fuzzy_search`` plugin provides a command that search your library using +fuzzy pattern matching. This can be useful if you want to find a track with complicated characters in the title. + +First, enable the plugin named ``fuzzy_search`` (see :doc:`/plugins/index`). +You'll then be able to use the ``beet fuzzy`` command:: + + $ beet fuzzy Vareoldur + Sigur Rós - Valtari - Varðeldur + +The command has several options that resemble those for the ``beet list`` +command (see :doc:`/reference/cli`). To choose an album instead of a single +track, use ``-a``; to print paths to items instead of metadata, use ``-p``; and +to use a custom format for printing, use ``-f FORMAT``. + +The ``-t NUMBER`` option lets you specify how precise the fuzzy match has to be +(default is 0.7). To make a fuzzier search, try ``beet fuzzy -t 0.5 Varoeldur``. +A value of ``1`` will show only perfect matches and a value of ``0`` will match everything. From e092af2b2fdf482bca2a4380f8dd421cf2fc1978 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 18 Sep 2012 10:33:58 -0700 Subject: [PATCH 15/21] changelog note: fuzzy_search plugin --- docs/changelog.rst | 3 +++ docs/plugins/fuzzy_search.rst | 2 +- docs/plugins/index.rst | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 967955a03..11ed71673 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,9 @@ Changelog 1.0b16 (in development) ----------------------- +* New plugin: :doc:`/plugins/fuzzy_search` lets you find albums and tracks using + fuzzy string matching so you don't have to type (or even remember) their exact + names. Thanks to Philippe Mongeau. * New plugin: :doc:`/plugins/the` adds a template function that helps format text for nicely-sorted directory listings. Thanks to Blemjhoo Tezoulbr. * :doc:`/plugins/scrub`: Scrubbing now removes *all* types of tags from a file diff --git a/docs/plugins/fuzzy_search.rst b/docs/plugins/fuzzy_search.rst index e0ecffc7f..386fab892 100644 --- a/docs/plugins/fuzzy_search.rst +++ b/docs/plugins/fuzzy_search.rst @@ -1,5 +1,5 @@ Fuzzy Search Plugin -============= +=================== The ``fuzzy_search`` plugin provides a command that search your library using fuzzy pattern matching. This can be useful if you want to find a track with complicated characters in the title. diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index b4a6d364f..a5d9ddf3c 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -51,6 +51,7 @@ disabled by default, but you can turn them on as described above. mbcollection importfeeds the + fuzzy_search Autotagger Extensions '''''''''''''''''''''' @@ -87,6 +88,7 @@ Miscellaneous * :doc:`web`: An experimental Web-based GUI for beets. * :doc:`rdm`: Randomly choose albums and tracks from your library. +* :doc:`fuzzy_search`: Search albums and tracks with fuzzy string matching. * :doc:`mbcollection`: Maintain your MusicBrainz collection list. * :doc:`bpd`: A music player for your beets library that emulates `MPD`_ and is compatible with `MPD clients`_. From 1e48317e515dacffabd493cea4edf4d1dbbe2514 Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Fri, 21 Sep 2012 19:57:47 -0400 Subject: [PATCH 16/21] fuzzy_search: simplifiy some nested code --- beetsplug/fuzzy_search.py | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/beetsplug/fuzzy_search.py b/beetsplug/fuzzy_search.py index 4e93a4ee3..263502306 100644 --- a/beetsplug/fuzzy_search.py +++ b/beetsplug/fuzzy_search.py @@ -35,17 +35,15 @@ def is_match(query, item, album=False, verbose=False, threshold=0.7): values = [item.artist, item.album, item.title] s = max(fuzzy_score(query.lower(), i.lower()) for i in values) - if s >= threshold: - return (True, s) if verbose else True + if verbose: + return (s >= threshold, s) else: - return (False, s) if verbose else False + return s >= threshold def fuzzy_list(lib, config, opts, args): query = decargs(args) - path = opts.path fmt = opts.format - verbose = opts.verbose threshold = float(opts.threshold) if fmt is None: @@ -61,24 +59,18 @@ def fuzzy_list(lib, config, opts, args): else: objs = lib.items() - if opts.album: - for album in objs: - if is_match(query, album, album=True, threshold=threshold): - if path: - print_(album.item_dir()) - else: - print_(album.evaluate_template(template)) - if verbose: - print is_match(query, album, album=True, verbose=True)[1] - else: - for item in objs: - if is_match(query, item, threshold=threshold): - if path: - print_(item.path) - else: - print_(item.evaluate_template(template, lib)) - if verbose: - print is_match(query, item, verbose=True)[1] + items = filter(lambda i: is_match(query, i, album=opts.album, + threshold=threshold), objs) + for i in items: + if opts.path: + print_(i.item_dir() if opts.album else i.path) + elif opts.album: + print_(i.evaluate_template(template)) + else: + print_(i.evaluate_template(template, lib)) + if opts.verbose: + print(is_match(query, i, album=opts.album, verbose=True)[1]) + fuzzy_cmd = Subcommand('fuzzy', help='list items using fuzzy matching') From aff36fa694e24e8d27d96420bdf1044156559a28 Mon Sep 17 00:00:00 2001 From: Blemjhoo Tezoulbr Date: Sat, 22 Sep 2012 15:50:33 +0300 Subject: [PATCH 17/21] zero plugin, version 0.9 --- beetsplug/zero.py | 127 ++++++++++++++++++++++++++++++++++++++++++ docs/plugins/zero.rst | 27 +++++++++ test/test_zero.py | 50 +++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 beetsplug/zero.py create mode 100644 docs/plugins/zero.rst create mode 100644 test/test_zero.py diff --git a/beetsplug/zero.py b/beetsplug/zero.py new file mode 100644 index 000000000..331089681 --- /dev/null +++ b/beetsplug/zero.py @@ -0,0 +1,127 @@ +# This file is part of beets. +# Copyright 2012, Blemjhoo Tezoulbr . +# +# 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. + +""" Clears tag fields in media files.""" + +from __future__ import print_function +import sys +import re +from beets.plugins import BeetsPlugin +from beets import ui +from beets.library import ITEM_KEYS +from beets.importer import action + + +__author__ = 'baobab@heresiarch.info' +__version__ = '0.9' + + +class ZeroPlugin(BeetsPlugin): + + _instance = None + + debug = False + fields = [] + patterns = {} + warned = False + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(ZeroPlugin, + cls).__new__(cls, *args, **kwargs) + return cls._instance + + def __str__(self): + return ('[zero]\n debug = {}\n fields = {}\n patterns = {}\n' + ' warned = {}'.format(self.debug, self.fields, self.patterns, + self.warned)) + + def dbg(self, *args): + """Prints message to stderr.""" + if self.debug: + print('[zero]', *args, file=sys.stderr) + + def configure(self, config): + if not config.has_section('zero'): + self.dbg('plugin is not configured') + return + self.debug = ui.config_val(config, 'zero', 'debug', True, bool) + for f in ui.config_val(config, 'zero', 'fields', '').split(): + if f not in ITEM_KEYS: + self.dbg('invalid field \"{}\" (try \'beet fields\')') + else: + self.fields.append(f) + p = ui.config_val(config, 'zero', f, '').split() + if p: + self.patterns[f] = p + else: + self.patterns[f] = ['.'] + if self.debug: + print(self, file=sys.stderr) + + def import_task_choice_event(self, task, config): + """Listen for import_task_choice event.""" + if self.debug: + self.dbg('listen: import_task_choice') + if task.choice_flag == action.ASIS and not self.warned: + self.dbg('cannot zero in \"as-is\" mode') + self.warned = True + # TODO request write in as-is mode + + @classmethod + def match_patterns(cls, field, patterns): + """Check if field (as string) is matching any of the patterns in + the list. + """ + for p in patterns: + if re.findall(p, unicode(field), flags=re.IGNORECASE): + return True + return False + + def write_event(self, item): + """Listen for write event.""" + if self.debug: + self.dbg('listen: write') + if not self.fields: + self.dbg('no fields, nothing to do') + return + for fn in self.fields: + try: + fval = getattr(item, fn) + except AttributeError: + self.dbg('? no such field: {}'.format(fn)) + else: + if not self.match_patterns(fval, self.patterns[fn]): + self.dbg('\"{}\" ({}) is not match any of: {}' + .format(fval, fn, ' '.join(self.patterns[fn]))) + continue + self.dbg('\"{}\" ({}) match: {}' + .format(fval, fn, ' '.join(self.patterns[fn]))) + setattr(item, fn, type(fval)()) + self.dbg('{}={}'.format(fn, getattr(item, fn))) + + +@ZeroPlugin.listen('import_task_choice') +def zero_choice(task, config): + ZeroPlugin().import_task_choice_event(task, config) + +@ZeroPlugin.listen('write') +def zero_write(item): + ZeroPlugin().write_event(item) + + +# simple test +if __name__ == '__main__': + print(ZeroPlugin().match_patterns('test', ['[0-9]'])) + print(ZeroPlugin().match_patterns('test', ['.'])) diff --git a/docs/plugins/zero.rst b/docs/plugins/zero.rst new file mode 100644 index 000000000..c69d7399d --- /dev/null +++ b/docs/plugins/zero.rst @@ -0,0 +1,27 @@ +Zero Plugin +=========== + +The ``zero`` plugin allows you to null fields before writing tags to files. +Fields can be nulled unconditionally or by pattern match. For example, it can +be used to strip useless comments like "ripped by" etc or any other stuff you +hate. Library is not modified. + +To use plugin, enable it by including ``zero`` into ``plugins`` line of +your beets config:: + + [beets] + plugins = zero + +You need to configure plugin before use, so add following section into config +file and adjust it to your needs:: + + [zero] + # list of fields to null, you can get full list by running 'beet fields' + fields=month day genre comments + # custom regexp patterns for each field, separated by space + # if custom pattern is not defined, field will be nulled unconditionally + comments=EAC LAME from.+collection ripped\sby + genre=rnb power\smetal + + Note: for now plugin will not zero fields in 'as-is' mode. + \ No newline at end of file diff --git a/test/test_zero.py b/test/test_zero.py new file mode 100644 index 000000000..01fa46bd0 --- /dev/null +++ b/test/test_zero.py @@ -0,0 +1,50 @@ +"""Tests for the 'zero' plugin""" + +from _common import unittest +from beets.library import Item +from beetsplug.zero import ZeroPlugin + + +class ThePluginTest(unittest.TestCase): + + def test_singleton(self): + z1 = ZeroPlugin() + z2 = ZeroPlugin() + self.assertTrue(z1 is z2) + + def test_no_patterns(self): + v = {'comments' : 'test comment', + 'day' : 13, + 'month' : 3, + 'year' : 2012} + i=Item(v) + z = ZeroPlugin() + z.debug = False + z.fields = ['comments', 'month', 'day'] + z.patterns = {'comments': ['.'], + 'month': ['.'], + 'day': ['.']} + z.write_event(i) + self.assertEqual(i.comments, '') + self.assertEqual(i.day, 0) + self.assertEqual(i.month, 0) + self.assertEqual(i.year, 2012) + + def test_patterns(self): + v = {'comments' : 'from lame collection, ripped by eac', + 'year' : 2012} + i=Item(v) + z = ZeroPlugin() + z.debug = False + z.fields = ['comments', 'year'] + z.patterns = {'comments': 'eac lame'.split(), + 'year': '2098 2099'.split()} + z.write_event(i) + self.assertEqual(i.comments, '') + self.assertEqual(i.year, 2012) + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == '__main__': + unittest.main(defaultTest='suite') From edcacde9bada25932583631017a68397735e2b4b Mon Sep 17 00:00:00 2001 From: Blemjhoo Tezoulbr Date: Sat, 22 Sep 2012 19:06:38 +0300 Subject: [PATCH 18/21] zero plugin ref included in plugins doc index --- docs/plugins/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index a5d9ddf3c..757d8a3ac 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -52,6 +52,7 @@ disabled by default, but you can turn them on as described above. importfeeds the fuzzy_search + zero Autotagger Extensions '''''''''''''''''''''' @@ -68,6 +69,7 @@ Metadata * :doc:`embedart`: Embed album art images into files' metadata. * :doc:`replaygain`: Calculate volume normalization for players that support it. * :doc:`scrub`: Clean extraneous metadata from music files. +* :doc:`zero`: Nullify fields by pattern or unconditionally. Path Formats '''''''''''' From d6f2bf20f41404c30f52d9714aac297738e0d5f3 Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Sat, 22 Sep 2012 17:35:39 -0400 Subject: [PATCH 19/21] fuzzy_search: enable setting threshold value from the config file --- beetsplug/fuzzy_search.py | 15 +++++++++++++-- docs/plugins/fuzzy_search.rst | 5 +++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/beetsplug/fuzzy_search.py b/beetsplug/fuzzy_search.py index 263502306..dfdb4b296 100644 --- a/beetsplug/fuzzy_search.py +++ b/beetsplug/fuzzy_search.py @@ -14,11 +14,13 @@ """Like beet list, but with fuzzy matching """ +import beets from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs, print_ from beets.util.functemplate import Template import difflib + # THRESHOLD = 0.7 @@ -44,7 +46,10 @@ def is_match(query, item, album=False, verbose=False, threshold=0.7): def fuzzy_list(lib, config, opts, args): query = decargs(args) fmt = opts.format - threshold = float(opts.threshold) + if opts.threshold is not None: + threshold = float(opts.threshold) + else: + threshold = float(conf['threshold']) if fmt is None: # If no specific template is supplied, use a default @@ -84,10 +89,16 @@ fuzzy_cmd.parser.add_option('-v', '--verbose', action='store_true', help='output scores for matches') fuzzy_cmd.parser.add_option('-t', '--threshold', action='store', help='return result with a fuzzy score above threshold. \ - (default is 0.7)', default=0.7) + (default is 0.7)', default=None) fuzzy_cmd.func = fuzzy_list +conf = {} + class Fuzzy(BeetsPlugin): def commands(self): return [fuzzy_cmd] + + def configure(self, config): + conf['threshold'] = beets.ui.config_val(config, 'fuzzy', + 'threshold', 0.7) diff --git a/docs/plugins/fuzzy_search.rst b/docs/plugins/fuzzy_search.rst index 386fab892..9c9720af9 100644 --- a/docs/plugins/fuzzy_search.rst +++ b/docs/plugins/fuzzy_search.rst @@ -18,3 +18,8 @@ to use a custom format for printing, use ``-f FORMAT``. The ``-t NUMBER`` option lets you specify how precise the fuzzy match has to be (default is 0.7). To make a fuzzier search, try ``beet fuzzy -t 0.5 Varoeldur``. A value of ``1`` will show only perfect matches and a value of ``0`` will match everything. + +The default threshold can also be set from the config file.:: + + [fuzzy] + threshold: 0.8 From 4aac7f5324e90e4964c5b4a4813a2fab5d1d7ec8 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 25 Sep 2012 14:15:33 -0700 Subject: [PATCH 20/21] zero: changelog note & doc improvements --- docs/changelog.rst | 2 ++ docs/plugins/zero.rst | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 11ed71673..ceec1f286 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,8 @@ Changelog names. Thanks to Philippe Mongeau. * New plugin: :doc:`/plugins/the` adds a template function that helps format text for nicely-sorted directory listings. Thanks to Blemjhoo Tezoulbr. +* New plugin: :doc:`/plugins/zero` filters out undesirable fields before they + are written to your tags. Thanks again to Blemjhoo Tezoulbr. * :doc:`/plugins/scrub`: Scrubbing now removes *all* types of tags from a file rather than just one. For example, if your FLAC file has both ordinary FLAC tags and ID3 tags, the ID3 tags are now also removed. diff --git a/docs/plugins/zero.rst b/docs/plugins/zero.rst index c69d7399d..572f90f7a 100644 --- a/docs/plugins/zero.rst +++ b/docs/plugins/zero.rst @@ -12,16 +12,18 @@ your beets config:: [beets] plugins = zero -You need to configure plugin before use, so add following section into config -file and adjust it to your needs:: +To configure the plugin, use a ``[zero]`` section in your configuration file. +Set ``fields`` to the (whitespace-separated) list of fields to null. You can get +the list of available fields by running ``beet fields``. To conditionally filter +a field, use ``field=regexp regexp`` to specify regular expressions. + +For example:: [zero] - # list of fields to null, you can get full list by running 'beet fields' fields=month day genre comments - # custom regexp patterns for each field, separated by space - # if custom pattern is not defined, field will be nulled unconditionally + # Custom regexp patterns for each field, separated by spaces: comments=EAC LAME from.+collection ripped\sby genre=rnb power\smetal - Note: for now plugin will not zero fields in 'as-is' mode. - \ No newline at end of file +If custom pattern is not defined, field will be nulled unconditionally. Note +that the plugin currently does not zero fields when importing "as-is". From 2793af7d7013f41a98a61e0bd5b134d69a50ffba Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 25 Sep 2012 15:13:33 -0700 Subject: [PATCH 21/21] zero, the: Python 2.6-compatible format strings --- beetsplug/the.py | 8 ++++---- beetsplug/zero.py | 18 ++++++++++-------- test/test_zero.py | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/beetsplug/the.py b/beetsplug/the.py index df2673793..ab3e9f0a4 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -64,12 +64,12 @@ class ThePlugin(BeetsPlugin): try: re.compile(p) except re.error: - print(u'[the] invalid pattern: {}'.format(p), + print(u'[the] invalid pattern: {0}'.format(p), file=sys.stderr) else: if not (p.startswith('^') or p.endswith('$')): if not the_options['silent']: - print(u'[the] warning: pattern \"{}\" will not ' + print(u'[the] warning: pattern \"{0}\" will not ' 'match string start/end'.format(p), file=sys.stderr) if the_options['a']: @@ -79,7 +79,7 @@ class ThePlugin(BeetsPlugin): if not the_options['patterns'] and not the_options['silent']: print('[the] no patterns defined!') if the_options['debug']: - print(u'[the] patterns: {}' + print(u'[the] patterns: {0}' .format(' '.join(the_options['patterns'])), file=sys.stderr) @@ -118,7 +118,7 @@ def func_the(text): if r != text: break if the_options['debug']: - print(u'[the] \"{}\" -> \"{}\"'.format(text, r), file=sys.stderr) + print(u'[the] \"{0}\" -> \"{1}\"'.format(text, r), file=sys.stderr) return r else: return u'' diff --git a/beetsplug/zero.py b/beetsplug/zero.py index 331089681..01e0cd102 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -43,9 +43,9 @@ class ZeroPlugin(BeetsPlugin): return cls._instance def __str__(self): - return ('[zero]\n debug = {}\n fields = {}\n patterns = {}\n' - ' warned = {}'.format(self.debug, self.fields, self.patterns, - self.warned)) + return ('[zero]\n debug = {0}\n fields = {1}\n patterns = {2}\n' + ' warned = {3}'.format(self.debug, self.fields, self.patterns, + self.warned)) def dbg(self, *args): """Prints message to stderr.""" @@ -59,7 +59,9 @@ class ZeroPlugin(BeetsPlugin): self.debug = ui.config_val(config, 'zero', 'debug', True, bool) for f in ui.config_val(config, 'zero', 'fields', '').split(): if f not in ITEM_KEYS: - self.dbg('invalid field \"{}\" (try \'beet fields\')') + self.dbg( + 'invalid field \"{0}\" (try \'beet fields\')'.format(f) + ) else: self.fields.append(f) p = ui.config_val(config, 'zero', f, '').split() @@ -100,16 +102,16 @@ class ZeroPlugin(BeetsPlugin): try: fval = getattr(item, fn) except AttributeError: - self.dbg('? no such field: {}'.format(fn)) + self.dbg('? no such field: {0}'.format(fn)) else: if not self.match_patterns(fval, self.patterns[fn]): - self.dbg('\"{}\" ({}) is not match any of: {}' + self.dbg('\"{0}\" ({1}) is not match any of: {2}' .format(fval, fn, ' '.join(self.patterns[fn]))) continue - self.dbg('\"{}\" ({}) match: {}' + self.dbg('\"{0}\" ({1}) match: {2}' .format(fval, fn, ' '.join(self.patterns[fn]))) setattr(item, fn, type(fval)()) - self.dbg('{}={}'.format(fn, getattr(item, fn))) + self.dbg('{0}={1}'.format(fn, getattr(item, fn))) @ZeroPlugin.listen('import_task_choice') diff --git a/test/test_zero.py b/test/test_zero.py index 01fa46bd0..7732ca4e9 100644 --- a/test/test_zero.py +++ b/test/test_zero.py @@ -5,7 +5,7 @@ from beets.library import Item from beetsplug.zero import ZeroPlugin -class ThePluginTest(unittest.TestCase): +class ZeroPluginTest(unittest.TestCase): def test_singleton(self): z1 = ZeroPlugin()