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')