diff --git a/beets/library.py b/beets/library.py index 4962a44ad..47c42f398 100644 --- a/beets/library.py +++ b/beets/library.py @@ -24,6 +24,7 @@ import unicodedata import threading import contextlib import traceback +import time from collections import defaultdict from unidecode import unidecode from beets.mediafile import MediaFile @@ -36,6 +37,10 @@ import beets MAX_FILENAME_LENGTH = 200 +# This is the default format when printing the import time +# of an object. This needs to be a format accepted by time.strftime() +ITIME_FORMAT = '%Y-%m-%d %H:%M:%S' + # Fields in the "items" database table; all the metadata available for # items in the library. These are used directly in SQL; they are # vulnerable to injection if accessible to the user. @@ -105,6 +110,7 @@ ITEM_FIELDS = [ ('bitdepth', 'int', False, True), ('channels', 'int', False, True), ('mtime', 'int', False, False), + ('itime', 'datetime', False, False), ] ITEM_KEYS_WRITABLE = [f[0] for f in ITEM_FIELDS if f[3] and f[2]] ITEM_KEYS_META = [f[0] for f in ITEM_FIELDS if f[3]] @@ -142,10 +148,12 @@ ALBUM_FIELDS = [ ('media', 'text', True), ('albumdisambig', 'text', True), ('rg_album_gain', 'real', True), - ('rg_album_peak', 'real', True), + ('rg_album_peak', 'real', True),® ('original_year', 'int', True), ('original_month', 'int', True), ('original_day', 'int', True), + + ('itime', 'datetime', False), ] ALBUM_KEYS = [f[0] for f in ALBUM_FIELDS] ALBUM_KEYS_ITEM = [f[0] for f in ALBUM_FIELDS if f[2]] @@ -394,6 +402,9 @@ class Item(object): if not sanitize: mapping['path'] = displayable_path(self.path) + # Convert the import time to human readable + mapping['itime'] = time.strftime(ITIME_FORMAT, time.localtime(getattr(self, 'itime'))) + # Use the album artist if the track artist is not set and # vice-versa. if not mapping['artist']: @@ -1289,6 +1300,7 @@ class Library(BaseLibrary): # Item manipulation. def add(self, item, copy=False): + item.itime = time.time() item.library = self if copy: self.move(item, copy=True) @@ -1498,15 +1510,21 @@ class Library(BaseLibrary): from its items. The items are added to the database if they don't yet have an ID. Returns an Album object. """ + album_keys = ALBUM_KEYS_ITEM + ['itime'] + # Set the metadata from the first item. - item_values = dict( + album_values = dict( (key, getattr(items[0], key)) for key in ALBUM_KEYS_ITEM) + # Manually set the date when the album was added, + # because the items don't yet have these + album_values['itime'] = time.time() + with self.transaction() as tx: sql = 'INSERT INTO albums (%s) VALUES (%s)' % \ - (', '.join(ALBUM_KEYS_ITEM), - ', '.join(['?'] * len(ALBUM_KEYS_ITEM))) - subvals = [item_values[key] for key in ALBUM_KEYS_ITEM] + (', '.join(album_keys), + ', '.join(['?'] * len(album_keys))) + subvals = [album_values[key] for key in album_keys] album_id = tx.mutate(sql, subvals) # Add the items to the library. @@ -1520,8 +1538,8 @@ class Library(BaseLibrary): # Construct the new Album object. record = {} for key in ALBUM_KEYS: - if key in ALBUM_KEYS_ITEM: - record[key] = item_values[key] + if key in album_keys: + record[key] = album_values[key] else: # Non-item fields default to None. record[key] = None @@ -1730,6 +1748,9 @@ class Album(BaseAlbum): mapping['artpath'] = displayable_path(mapping['artpath']) mapping['path'] = displayable_path(self.item_dir()) + # Convert the import time to human readable format + mapping['itime'] = time.strftime(ITIME_FORMAT, time.localtime(mapping['itime'])) + # Get template functions. funcs = DefaultTemplateFunctions().functions() funcs.update(plugins.template_funcs()) @@ -1818,6 +1839,12 @@ class DefaultTemplateFunctions(object): """ return unidecode(s) + @staticmethod + def tmpl_format(s, format): + """Format the import time to any format according to time.strfime() + """ + return time.strftime(format, time.strptime(s, ITIME_FORMAT)) + def tmpl_aunique(self, keys=None, disam=None): """Generate a string that is guaranteed to be unique among all albums in the library who share the same set of keys. A fields diff --git a/beets/ui/commands.py b/beets/ui/commands.py index be5cd811c..6a2669f49 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -993,6 +993,7 @@ CONSTRUCTOR_MAPPING = { 'int': int, 'bool': util.str2bool, 'real': float, + 'datetime': lambda v: int(time.mktime(time.strptime(v, library.ITIME_FORMAT))), } # Convert a string (from user input) to the correct Python type @@ -1013,7 +1014,7 @@ def _convert_type(key, value, album=False): def modify_items(lib, mods, query, write, move, album, confirm): """Modifies matching items according to key=value assignments.""" # Parse key=value specifications into a dictionary. - allowed_keys = library.ALBUM_KEYS if album else library.ITEM_KEYS_WRITABLE + allowed_keys = library.ALBUM_KEYS if album else library.ITEM_KEYS_WRITABLE + ['itime'] fsets = {} for mod in mods: key, value = mod.split('=', 1) diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index 13661d9d8..2474c97d5 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -56,7 +56,7 @@ track's artists. These functions are built in to beets: -* ``%lower{text}``: Convert ``text`` to lowercase. +* ``%lower{text}``: Convert ``text`` to lowercase. * ``%upper{text}``: Convert ``text`` to UPPERCASE. * ``%title{text}``: Convert ``text`` to Title Case. * ``%left{text,n}``: Return the first ``n`` characters of ``text``. @@ -70,8 +70,12 @@ These functions are built in to beets: `unidecode module`_. * ``%aunique{identifiers,disambiguators}``: Provides a unique string to disambiguate similar albums in the database. See :ref:`aunique`, below. +* ``%format{date_time,format}``: Return the date and time in any format accepted + by the `time.strfime() method`_. Should probably be used together with the + ``itime`` field (import time). .. _unidecode module: http://pypi.python.org/pypi/Unidecode +.. _time.strftime() method: http://docs.python.org/2/library/time.html#time.strftime Plugins can extend beets with more template functions (see :ref:`writing-plugins`). diff --git a/test/test_db.py b/test/test_db.py index 4ee6ea419..5099cb83e 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -967,6 +967,21 @@ class MtimeTest(unittest.TestCase): self.i.read() self.assertGreaterEqual(self.i.mtime, self._mtime()) +class ImportTimeTest(unittest.TestCase): + def setUp(self): + self.lib = beets.library.Library(':memory:') + + def test_itime_for_album(self): + self.track = item() + self.album = self.lib.add_album((self.track,)) + self.assertGreater(self.album.itime, 0) + self.assertGreater(self.track.itime, 0) + + def test_atime_for_singleton(self): + self.singleton = item() + self.lib.add(self.singleton) + self.assertGreater(self.singleton.itime, 0) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__)