From 0bf4222947a32baa4c94ef6cd6e18aad9ba7241c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stig=20Inge=20Lea=20Bj=C3=B8rnsen?= Date: Mon, 12 May 2014 15:41:23 +0200 Subject: [PATCH 1/5] Add importmtimes plugin for preserving file mtimes during copy imports. File modification times are stored as item.mtime, item.added and album.added. --- beetsplug/importmtimes.py | 77 +++++++++++++++++++++++++++++++++++ docs/plugins/importmtimes.rst | 24 +++++++++++ docs/plugins/index.rst | 3 ++ 3 files changed, 104 insertions(+) create mode 100644 beetsplug/importmtimes.py create mode 100644 docs/plugins/importmtimes.rst diff --git a/beetsplug/importmtimes.py b/beetsplug/importmtimes.py new file mode 100644 index 000000000..b5e447f87 --- /dev/null +++ b/beetsplug/importmtimes.py @@ -0,0 +1,77 @@ +"""Preserve file modification times during copy imports. + +File modification times are also stored in the `added` field. +""" + +from __future__ import unicode_literals, absolute_import, print_function + +import logging, os + +from beets import config +from beets import util +from beets.plugins import BeetsPlugin + +log = logging.getLogger('beets') + +class ImportMtimesPlugin(BeetsPlugin): + pass + +@ImportMtimesPlugin.listen('import_task_start') +def check_config(task, session): + if not config['import']['copy']: + raise ValueError('The importmtimes plugin can only be used for copy' + ' imports') + +def copy_mtime(source_path, dest_path): + """Copy the file mtime from the source path to the destination path. + """ + source_stat = os.stat(util.syspath(source_path)) + dest_stat = os.stat(util.syspath(dest_path)) + os.utime(util.syspath(dest_path), + (dest_stat.st_atime, source_stat.st_mtime)) + +# key: item path in the library +# value: file outside the library from which the item was imported +item_source_paths = dict() + +def preserve_mtime(item): + """Preserve the file modification time of an imported item by copying the + mtime from the file that the item is copied from. + """ + source_path = item_source_paths.get(item.path) + if source_path is None: + log.warn("No import source path found for item " + + util.displayable_path(item.path)) + return + + copy_mtime(source_path, item.path) + item.mtime = os.path.getmtime(util.syspath(item.path)) + del item_source_paths[item.path] + +@ImportMtimesPlugin.listen('item_copied') +def record_import_source_path(item, source, destination): + """Record which file an imported item is copied from. + """ + if (source == destination): + return + + item_source_paths[destination] = source + log.debug('Recorded item source path "%s" <- "%s"', + util.displayable_path(destination), + util.displayable_path(source)) + +@ImportMtimesPlugin.listen('album_imported') +def update_album_times(lib, album): + for item in album.items(): + preserve_mtime(item) + item.store() + + item_mtimes = (item.mtime for item in album.items() if item.mtime > 0) + album.added = min(item_mtimes) + album.store() + +@ImportMtimesPlugin.listen('item_imported') +def update_item_times(lib, item): + preserve_mtime(item) + item.added = item.mtime + item.store() diff --git a/docs/plugins/importmtimes.rst b/docs/plugins/importmtimes.rst new file mode 100644 index 000000000..bff43ed17 --- /dev/null +++ b/docs/plugins/importmtimes.rst @@ -0,0 +1,24 @@ +ImportMtimes Plugin +=================== + +The ``importmtimes`` plugin is useful when an existing collection is imported +and the time when albums and items were added should be preserved. + +The :abbr:`mtime (modification time)` of files that are imported into the +library are assumed to represent the time when the items were originally +added. + +File modification times are imported as follows: + +* For all items, ``item.mtime`` is set to the mtime of the file + from which the item is imported from. +* For singleton items with no album, ``item.added`` is set to ``item.mtime`` + during import. +* For items that are part of an album, ``album.added`` and ``item.added`` is + set to the oldest mtime of the album items. +* The mtime of an album's directory is ignored. + +There are no configuration options for this plugin. + +This plugin can only be used if Beets is :doc:`configured ` to copy +files on import. \ No newline at end of file diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index eab7a69b9..dfa75498d 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -59,6 +59,7 @@ by typing ``beet version``. ftintitle keyfinder bucket + importmtimes Autotagger Extensions --------------------- @@ -91,6 +92,8 @@ Metadata statistics (last_played, play_count, skip_count, rating). * :doc:`keyfinder`: Use the `KeyFinder`_ program to detect the musical key from the audio. +* :doc:`importmtimes`: Preserve file modification times and use them as values + for the `added` and `mtime` fields in the database. .. _Acoustic Attributes: http://developer.echonest.com/acoustic-attributes.html .. _the Echo Nest: http://www.echonest.com From 94aad7e3093f4fa6449ca2e02dce48e819f3f1ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stig=20Inge=20Lea=20Bj=C3=B8rnsen?= Date: Mon, 12 May 2014 18:00:27 +0200 Subject: [PATCH 2/5] Record mtimes instead of pre-import paths. This adds support for move imports. Item and file mtime preservation is now configurable and disabled by default. --- beetsplug/importmtimes.py | 82 ++++++++++++++++++++--------------- docs/plugins/importmtimes.rst | 29 ++++++++----- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/beetsplug/importmtimes.py b/beetsplug/importmtimes.py index b5e447f87..5921beaf1 100644 --- a/beetsplug/importmtimes.py +++ b/beetsplug/importmtimes.py @@ -1,6 +1,5 @@ -"""Preserve file modification times during copy imports. - -File modification times are also stored in the `added` field. +"""Populate an items `added` and `mtime` field by using the file modification +time (mtime) of the item's source file before import. """ from __future__ import unicode_literals, absolute_import, print_function @@ -14,64 +13,77 @@ from beets.plugins import BeetsPlugin log = logging.getLogger('beets') class ImportMtimesPlugin(BeetsPlugin): - pass + def __init__(self): + super(ImportMtimesPlugin, self).__init__() + self.config.add({ + 'preserve_mtimes': False, + }) @ImportMtimesPlugin.listen('import_task_start') def check_config(task, session): - if not config['import']['copy']: - raise ValueError('The importmtimes plugin can only be used for copy' - ' imports') + config['importmtimes']['preserve_mtimes'].get(bool) -def copy_mtime(source_path, dest_path): - """Copy the file mtime from the source path to the destination path. +def write_file_mtime(path, mtime): + """Write the given mtime to the destination path. """ - source_stat = os.stat(util.syspath(source_path)) - dest_stat = os.stat(util.syspath(dest_path)) - os.utime(util.syspath(dest_path), - (dest_stat.st_atime, source_stat.st_mtime)) + stat = os.stat(util.syspath(path)) + os.utime(util.syspath(path), + (stat.st_atime, mtime)) # key: item path in the library -# value: file outside the library from which the item was imported -item_source_paths = dict() +# value: the file mtime of the file the item was imported from +item_mtime = dict() -def preserve_mtime(item): - """Preserve the file modification time of an imported item by copying the - mtime from the file that the item is copied from. +def write_item_mtime(item, mtime): + """Write the given mtime to an item's `mtime` field and to the mtime of the + item's file. """ - source_path = item_source_paths.get(item.path) - if source_path is None: - log.warn("No import source path found for item " + if mtime is None: + log.warn("No mtime to be preserved for item " + util.displayable_path(item.path)) return - copy_mtime(source_path, item.path) - item.mtime = os.path.getmtime(util.syspath(item.path)) - del item_source_paths[item.path] + # The file's mtime on disk must be in sync with the item's mtime + write_file_mtime(util.syspath(item.path), mtime) + item.mtime = mtime +@ImportMtimesPlugin.listen('before_item_moved') @ImportMtimesPlugin.listen('item_copied') -def record_import_source_path(item, source, destination): - """Record which file an imported item is copied from. +def record_import_mtime(item, source, destination): + """Record the file mtime of an item's path before import. """ if (source == destination): + # Re-import of an existing library item? return - item_source_paths[destination] = source - log.debug('Recorded item source path "%s" <- "%s"', + mtime = os.stat(util.syspath(source)).st_mtime + item_mtime[destination] = mtime + log.debug('Recorded mtime %s for item "%s" imported from "%s"', + mtime, util.displayable_path(destination), util.displayable_path(source)) @ImportMtimesPlugin.listen('album_imported') def update_album_times(lib, album): + album_mtimes = [] for item in album.items(): - preserve_mtime(item) - item.store() + mtime = item_mtime[item.path] + if mtime is not None: + album_mtimes.append(mtime) + if config['importmtimes']['preserve_mtimes'].get(bool): + write_item_mtime(item, mtime) + item.store() + del item_mtime[item.path] - item_mtimes = (item.mtime for item in album.items() if item.mtime > 0) - album.added = min(item_mtimes) + album.added = min(album_mtimes) album.store() @ImportMtimesPlugin.listen('item_imported') def update_item_times(lib, item): - preserve_mtime(item) - item.added = item.mtime - item.store() + mtime = item_mtime[item.path] + if mtime is not None: + item.added = mtime + if config['importmtimes']['preserve_mtimes'].get(bool): + write_item_mtime(item, mtime) + item.store() + del item_mtime[item.path] diff --git a/docs/plugins/importmtimes.rst b/docs/plugins/importmtimes.rst index bff43ed17..2e1acad30 100644 --- a/docs/plugins/importmtimes.rst +++ b/docs/plugins/importmtimes.rst @@ -8,17 +8,26 @@ The :abbr:`mtime (modification time)` of files that are imported into the library are assumed to represent the time when the items were originally added. -File modification times are imported as follows: +The ``item.added`` field is populated as follows: -* For all items, ``item.mtime`` is set to the mtime of the file - from which the item is imported from. -* For singleton items with no album, ``item.added`` is set to ``item.mtime`` - during import. +* For singleton items with no album, ``item.added`` is set to the item's file + mtime before it was imported. * For items that are part of an album, ``album.added`` and ``item.added`` is - set to the oldest mtime of the album items. -* The mtime of an album's directory is ignored. + set to the oldest mtime of the files in the album before they were imported. + The mtime of album directories are ignored. -There are no configuration options for this plugin. +This plugin can optionally be configured to also preserve mtimes:: -This plugin can only be used if Beets is :doc:`configured ` to copy -files on import. \ No newline at end of file + importmtimes: + preserve_mtimes: yes # default: no + +File modification times are preserved as follows: + +* For all items: + + * ``item.mtime`` is set to the mtime of the file + from which the item is imported from. + * The mtime of the file ``item.path`` is set to ``item.mtime``. + +Note that albums doesn't have an mtime field. The mtime of album +directories are not preserved. From f8abd50090e5ffcd28e6342d5310ccd29df1f430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stig=20Inge=20Lea=20Bj=C3=B8rnsen?= Date: Mon, 12 May 2014 21:06:25 +0200 Subject: [PATCH 3/5] Only delete album item mtimes that are in the dictionary. --- beetsplug/importmtimes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/importmtimes.py b/beetsplug/importmtimes.py index 5921beaf1..2e572a8b3 100644 --- a/beetsplug/importmtimes.py +++ b/beetsplug/importmtimes.py @@ -73,7 +73,7 @@ def update_album_times(lib, album): if config['importmtimes']['preserve_mtimes'].get(bool): write_item_mtime(item, mtime) item.store() - del item_mtime[item.path] + del item_mtime[item.path] album.added = min(album_mtimes) album.store() From 34c256925e4322f0b13f19f5ab02657fcaae0bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stig=20Inge=20Lea=20Bj=C3=B8rnsen?= Date: Mon, 12 May 2014 22:31:26 +0200 Subject: [PATCH 4/5] Fix style errors reported by Flake8. --- beetsplug/importmtimes.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/beetsplug/importmtimes.py b/beetsplug/importmtimes.py index 2e572a8b3..590646e85 100644 --- a/beetsplug/importmtimes.py +++ b/beetsplug/importmtimes.py @@ -4,7 +4,8 @@ time (mtime) of the item's source file before import. from __future__ import unicode_literals, absolute_import, print_function -import logging, os +import logging +import os from beets import config from beets import util @@ -12,6 +13,7 @@ from beets.plugins import BeetsPlugin log = logging.getLogger('beets') + class ImportMtimesPlugin(BeetsPlugin): def __init__(self): super(ImportMtimesPlugin, self).__init__() @@ -19,10 +21,12 @@ class ImportMtimesPlugin(BeetsPlugin): 'preserve_mtimes': False, }) + @ImportMtimesPlugin.listen('import_task_start') def check_config(task, session): config['importmtimes']['preserve_mtimes'].get(bool) + def write_file_mtime(path, mtime): """Write the given mtime to the destination path. """ @@ -34,6 +38,7 @@ def write_file_mtime(path, mtime): # value: the file mtime of the file the item was imported from item_mtime = dict() + def write_item_mtime(item, mtime): """Write the given mtime to an item's `mtime` field and to the mtime of the item's file. @@ -47,6 +52,7 @@ def write_item_mtime(item, mtime): write_file_mtime(util.syspath(item.path), mtime) item.mtime = mtime + @ImportMtimesPlugin.listen('before_item_moved') @ImportMtimesPlugin.listen('item_copied') def record_import_mtime(item, source, destination): @@ -63,6 +69,7 @@ def record_import_mtime(item, source, destination): util.displayable_path(destination), util.displayable_path(source)) + @ImportMtimesPlugin.listen('album_imported') def update_album_times(lib, album): album_mtimes = [] @@ -78,6 +85,7 @@ def update_album_times(lib, album): album.added = min(album_mtimes) album.store() + @ImportMtimesPlugin.listen('item_imported') def update_item_times(lib, item): mtime = item_mtime[item.path] From e800c9cc62143c157959f2c1450149f26c471911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stig=20Inge=20Lea=20Bj=C3=B8rnsen?= Date: Tue, 13 May 2014 23:52:57 +0200 Subject: [PATCH 5/5] Plugin rename importmtimes -> importadded. This includes some minor changes to the documentation. --- beetsplug/{importmtimes.py => importadded.py} | 20 +++++++++---------- .../{importmtimes.rst => importadded.rst} | 12 +++++------ docs/plugins/index.rst | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) rename beetsplug/{importmtimes.py => importadded.py} (82%) rename docs/plugins/{importmtimes.rst => importadded.rst} (78%) diff --git a/beetsplug/importmtimes.py b/beetsplug/importadded.py similarity index 82% rename from beetsplug/importmtimes.py rename to beetsplug/importadded.py index 590646e85..539853859 100644 --- a/beetsplug/importmtimes.py +++ b/beetsplug/importadded.py @@ -14,17 +14,17 @@ from beets.plugins import BeetsPlugin log = logging.getLogger('beets') -class ImportMtimesPlugin(BeetsPlugin): +class ImportAddedPlugin(BeetsPlugin): def __init__(self): - super(ImportMtimesPlugin, self).__init__() + super(ImportAddedPlugin, self).__init__() self.config.add({ 'preserve_mtimes': False, }) -@ImportMtimesPlugin.listen('import_task_start') +@ImportAddedPlugin.listen('import_task_start') def check_config(task, session): - config['importmtimes']['preserve_mtimes'].get(bool) + config['importadded']['preserve_mtimes'].get(bool) def write_file_mtime(path, mtime): @@ -53,8 +53,8 @@ def write_item_mtime(item, mtime): item.mtime = mtime -@ImportMtimesPlugin.listen('before_item_moved') -@ImportMtimesPlugin.listen('item_copied') +@ImportAddedPlugin.listen('before_item_moved') +@ImportAddedPlugin.listen('item_copied') def record_import_mtime(item, source, destination): """Record the file mtime of an item's path before import. """ @@ -70,14 +70,14 @@ def record_import_mtime(item, source, destination): util.displayable_path(source)) -@ImportMtimesPlugin.listen('album_imported') +@ImportAddedPlugin.listen('album_imported') def update_album_times(lib, album): album_mtimes = [] for item in album.items(): mtime = item_mtime[item.path] if mtime is not None: album_mtimes.append(mtime) - if config['importmtimes']['preserve_mtimes'].get(bool): + if config['importadded']['preserve_mtimes'].get(bool): write_item_mtime(item, mtime) item.store() del item_mtime[item.path] @@ -86,12 +86,12 @@ def update_album_times(lib, album): album.store() -@ImportMtimesPlugin.listen('item_imported') +@ImportAddedPlugin.listen('item_imported') def update_item_times(lib, item): mtime = item_mtime[item.path] if mtime is not None: item.added = mtime - if config['importmtimes']['preserve_mtimes'].get(bool): + if config['importadded']['preserve_mtimes'].get(bool): write_item_mtime(item, mtime) item.store() del item_mtime[item.path] diff --git a/docs/plugins/importmtimes.rst b/docs/plugins/importadded.rst similarity index 78% rename from docs/plugins/importmtimes.rst rename to docs/plugins/importadded.rst index 2e1acad30..5da919325 100644 --- a/docs/plugins/importmtimes.rst +++ b/docs/plugins/importadded.rst @@ -1,7 +1,7 @@ -ImportMtimes Plugin -=================== +ImportAdded Plugin +================== -The ``importmtimes`` plugin is useful when an existing collection is imported +The ``importadded`` plugin is useful when an existing collection is imported and the time when albums and items were added should be preserved. The :abbr:`mtime (modification time)` of files that are imported into the @@ -18,7 +18,7 @@ The ``item.added`` field is populated as follows: This plugin can optionally be configured to also preserve mtimes:: - importmtimes: + importadded: preserve_mtimes: yes # default: no File modification times are preserved as follows: @@ -29,5 +29,5 @@ File modification times are preserved as follows: from which the item is imported from. * The mtime of the file ``item.path`` is set to ``item.mtime``. -Note that albums doesn't have an mtime field. The mtime of album -directories are not preserved. +Note that there is no ``album.mtime`` field in the database and that the mtime +of album directories on disk aren't preserved. diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index dfa75498d..7dc0fe799 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -59,7 +59,7 @@ by typing ``beet version``. ftintitle keyfinder bucket - importmtimes + importadded Autotagger Extensions --------------------- @@ -92,8 +92,8 @@ Metadata statistics (last_played, play_count, skip_count, rating). * :doc:`keyfinder`: Use the `KeyFinder`_ program to detect the musical key from the audio. -* :doc:`importmtimes`: Preserve file modification times and use them as values - for the `added` and `mtime` fields in the database. +* :doc:`importadded`: Use file modification times for guessing the value for + the `added` field in the database. .. _Acoustic Attributes: http://developer.echonest.com/acoustic-attributes.html .. _the Echo Nest: http://www.echonest.com