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 diff --git a/beets/library.py b/beets/library.py index ef97161a6..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() @@ -1135,6 +1141,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/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/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. diff --git a/beetsplug/fuzzy_search.py b/beetsplug/fuzzy_search.py new file mode 100644 index 000000000..dfdb4b296 --- /dev/null +++ b/beetsplug/fuzzy_search.py @@ -0,0 +1,104 @@ +# 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. + +"""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 + + +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] + + s = max(fuzzy_score(query.lower(), i.lower()) for i in values) + if verbose: + return (s >= threshold, s) + else: + return s >= threshold + + +def fuzzy_list(lib, config, opts, args): + query = decargs(args) + fmt = opts.format + 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 + if opts.album: + fmt = u'$albumartist - $album' + else: + fmt = u'$artist - $album - $title' + template = Template(fmt) + + if opts.album: + objs = lib.albums() + else: + objs = lib.items() + + 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') +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.parser.add_option('-t', '--threshold', action='store', + help='return result with a fuzzy score above threshold. \ + (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/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/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) diff --git a/beetsplug/the.py b/beetsplug/the.py new file mode 100644 index 000000000..ab3e9f0a4 --- /dev/null +++ b/beetsplug/the.py @@ -0,0 +1,131 @@ +# 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. + +"""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: {0}'.format(p), + file=sys.stderr) + else: + if not (p.startswith('^') or p.endswith('$')): + if not the_options['silent']: + print(u'[the] warning: pattern \"{0}\" 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: {0}' + .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] \"{0}\" -> \"{1}\"'.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/beetsplug/zero.py b/beetsplug/zero.py new file mode 100644 index 000000000..01e0cd102 --- /dev/null +++ b/beetsplug/zero.py @@ -0,0 +1,129 @@ +# 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 = {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.""" + 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 \"{0}\" (try \'beet fields\')'.format(f) + ) + 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: {0}'.format(fn)) + else: + if not self.match_patterns(fval, self.patterns[fn]): + self.dbg('\"{0}\" ({1}) is not match any of: {2}' + .format(fval, fn, ' '.join(self.patterns[fn]))) + continue + self.dbg('\"{0}\" ({1}) match: {2}' + .format(fval, fn, ' '.join(self.patterns[fn]))) + setattr(item, fn, type(fval)()) + self.dbg('{0}={1}'.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/changelog.rst b/docs/changelog.rst index 03aab8744..ceec1f286 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,13 @@ 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. +* 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. @@ -20,11 +27,16 @@ 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. * 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. +* Add a human-readable error message when writing files' tags fails. .. _Tomahawk resolver: http://beets.radbox.org/blog/tomahawk-resolver.html diff --git a/docs/plugins/fuzzy_search.rst b/docs/plugins/fuzzy_search.rst new file mode 100644 index 000000000..9c9720af9 --- /dev/null +++ b/docs/plugins/fuzzy_search.rst @@ -0,0 +1,25 @@ +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. + +The default threshold can also be set from the config file.:: + + [fuzzy] + threshold: 0.8 diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index a9edd7dee..757d8a3ac 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -50,6 +50,9 @@ disabled by default, but you can turn them on as described above. rdm mbcollection importfeeds + the + fuzzy_search + zero Autotagger Extensions '''''''''''''''''''''' @@ -66,12 +69,14 @@ 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 '''''''''''' * :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 '''''''''''''''' @@ -85,6 +90,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`_. 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/docs/plugins/zero.rst b/docs/plugins/zero.rst new file mode 100644 index 000000000..572f90f7a --- /dev/null +++ b/docs/plugins/zero.rst @@ -0,0 +1,29 @@ +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 + +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] + fields=month day genre comments + # Custom regexp patterns for each field, separated by spaces: + comments=EAC LAME from.+collection ripped\sby + genre=rnb power\smetal + +If custom pattern is not defined, field will be nulled unconditionally. Note +that the plugin currently does not zero fields when importing "as-is". 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): 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') diff --git a/test/test_zero.py b/test/test_zero.py new file mode 100644 index 000000000..7732ca4e9 --- /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 ZeroPluginTest(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')