diff --git a/beets/library.py b/beets/library.py index f053728b1..3a6829176 100644 --- a/beets/library.py +++ b/beets/library.py @@ -62,6 +62,8 @@ class PathQuery(dbcore.FieldQuery): class DateType(types.Type): + # TODO representation should be `datetime` object + # TODO distinguish beetween date and time types sql = u'REAL' query = dbcore.query.DateQuery null = 0.0 diff --git a/beetsplug/types.py b/beetsplug/types.py new file mode 100644 index 000000000..68aea35c7 --- /dev/null +++ b/beetsplug/types.py @@ -0,0 +1,42 @@ +# This file is part of beets. +# Copyright 2014, Thomas Scholtes. +# +# 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 beets.plugins import BeetsPlugin +from beets.dbcore import types +from beets.util.confit import ConfigValueError +from beets import library + + +class TypesPlugin(BeetsPlugin): + + @property + def item_types(self): + if not self.config.exists(): + return {} + + mytypes = {} + for key, value in self.config.items(): + if value.get() == 'int': + mytypes[key] = types.INTEGER + elif value.get() == 'float': + mytypes[key] = types.FLOAT + elif value.get() == 'bool': + mytypes[key] = types.BOOLEAN + elif value.get() == 'date': + mytypes[key] = library.DateType() + else: + raise ConfigValueError( + u"unknown type '{0}' for the '{1}' field" + .format(value, key)) + return mytypes diff --git a/test/test_types_plugin.py b/test/test_types_plugin.py new file mode 100644 index 000000000..c7f1f4383 --- /dev/null +++ b/test/test_types_plugin.py @@ -0,0 +1,126 @@ +# This file is part of beets. +# Copyright 2014, Thomas Scholtes. +# +# 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. + +import time +from datetime import datetime + +from _common import unittest +from helper import TestHelper + +from beets.util.confit import ConfigValueError + + +class TypesPluginTest(unittest.TestCase, TestHelper): + + def setUp(self): + self.setup_beets() + self.load_plugins('types') + + def tearDown(self): + self.unload_plugins() + self.teardown_beets() + + def test_integer_modify_and_query(self): + self.config['types'] = {'myint': 'int'} + item = self.add_item(artist='aaa') + + # Do not match unset values + out = self.list('myint:1..3') + self.assertEqual('', out) + + self.modify('myint=2') + item.load() + self.assertEqual(item['myint'], 2) + + # Match in range + out = self.list('myint:1..3') + self.assertIn('aaa', out) + + def test_float_modify_and_query(self): + self.config['types'] = {'myfloat': 'float'} + item = self.add_item(artist='aaa') + + self.modify('myfloat=-9.1') + item.load() + self.assertEqual(item['myfloat'], -9.1) + + # Match in range + out = self.list('myfloat:-10..0') + self.assertIn('aaa', out) + + def test_bool_modify_and_query(self): + self.config['types'] = {'mybool': 'bool'} + true = self.add_item(artist='true') + false = self.add_item(artist='false') + self.add_item(artist='unset') + + # Set true + self.modify('mybool=1', 'artist:true') + true.load() + self.assertEqual(true['mybool'], True) + + # Set false + self.modify('mybool=false', 'artist:false') + false.load() + self.assertEqual(false['mybool'], False) + + # Query bools + out = self.list('mybool:true', '$artist $mybool') + self.assertEqual('true True', out) + + out = self.list('mybool:false', '$artist $mybool') + # TODO this should not match the `unset` item + # self.assertEqual('false False', out) + + out = self.list('mybool:', '$artist $mybool') + self.assertIn('unset $mybool', out) + + def test_date_modify_and_query(self): + self.config['types'] = {'mydate': 'date'} + # FIXME parsing should also work with default time format + self.config['time_format'] = '%Y-%m-%d' + old = self.add_item(artist='prince') + new = self.add_item(artist='britney') + + self.modify('mydate=1999-01-01', 'artist:prince') + old.load() + self.assertEqual(old['mydate'], mktime(1999, 01, 01)) + + self.modify('mydate=1999-12-30', 'artist:britney') + new.load() + self.assertEqual(new['mydate'], mktime(1999, 12, 30)) + + # Match in range + out = self.list('mydate:..1999-07', '$artist $mydate') + self.assertEqual('prince 1999-01-01', out) + + # FIXME + self.skipTest('there is a timezone issue here') + out = self.list('mydate:1999-12-30', '$artist $mydate') + self.assertEqual('britney 1999-12-30', out) + + def test_unknown_type_error(self): + self.config['types'] = {'flex': 'unkown type'} + with self.assertRaises(ConfigValueError): + self.run_command('ls') + + def modify(self, *args): + return self.run_with_output('modify', '--yes', '--nowrite', *args) + + def list(self, query, fmt='$artist - $album - $title'): + return self.run_with_output('ls', '-f', fmt, query).strip() + + +def mktime(*args): + return time.mktime(datetime(*args).timetuple())