diff --git a/beets/library.py b/beets/library.py index 3a6829176..fbaf6262e 100644 --- a/beets/library.py +++ b/beets/library.py @@ -470,6 +470,21 @@ class Item(LibModel): log.error(exc) return False + def try_sync(self, write=None): + """Synchronizes the current state with the database and the + media file tags. + + If `write` is `None` or `True` the method tries to write the + tags to `self.path`. If `write` is `False` it does not write + tags. Otherwise it interprets `write` as a path and tries to + write the tags to that file. + """ + if write is True: + write = None + if write is not False: + self.try_write(path=write) + self.store() + # Files themselves. def move_file(self, dest, copy=False): @@ -882,6 +897,18 @@ class Album(LibModel): item[key] = value item.store() + def try_sync(self, write=True): + """Synchronizes the current state with the database, propagates + it to the items and synchronizes them with the database and + their files. + + The `write` parameter is a boolean indicating whether to write + tags to the item files. + """ + self.store() + for item in self.items(): + item.try_sync(bool(write)) + # Query construction helper. diff --git a/beets/ui/commands.py b/beets/ui/commands.py index adf0070a0..e854fd4e4 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -20,7 +20,6 @@ from __future__ import print_function import logging import os import time -import itertools import codecs import platform import re @@ -1298,7 +1297,7 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm): if not ui.input_yn('Really modify%s (Y/n)?' % extra): return - # Apply changes to database. + # Apply changes to database and files with lib.transaction(): for obj in changed: if move: @@ -1307,16 +1306,7 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm): log.debug('moving object %s' % cur_path) obj.move() - obj.store() - - # Apply tags if requested. - if write: - if album: - changed_items = itertools.chain(*(a.items() for a in changed)) - else: - changed_items = changed - for item in changed_items: - item.try_write() + obj.try_sync(write) def modify_parse_args(args): @@ -1457,7 +1447,7 @@ def write_items(lib, query, pretend, force): changed = ui.show_model_changes(item, clean_item, library.Item._media_fields, force) if (changed or force) and not pretend: - item.try_write() + item.try_sync() def write_func(lib, opts, args): diff --git a/test/test_ui.py b/test/test_ui.py index 76abc20bf..4ef5a9fcf 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -145,7 +145,8 @@ class ModifyTest(unittest.TestCase, TestHelper): def setUp(self): self.setup_beets() - self.add_album_fixture() + self.album = self.add_album_fixture() + [self.item] = self.album.items() def tearDown(self): self.teardown_beets() @@ -183,6 +184,22 @@ class ModifyTest(unittest.TestCase, TestHelper): item = self.lib.items().get() self.assertNotIn('newTitle', item.path) + def test_update_mtime(self): + item = self.item + old_mtime = item.mtime + + self.modify("title=newTitle") + item.load() + self.assertNotEqual(old_mtime, item.mtime) + self.assertEqual(item.current_mtime(), item.mtime) + + def test_reset_mtime_with_no_write(self): + item = self.item + + self.modify("--nowrite", "title=newTitle") + item.load() + self.assertEqual(0, item.mtime) + # Album Tests def test_modify_album(self): @@ -275,6 +292,30 @@ class ModifyTest(unittest.TestCase, TestHelper): self.assertEqual(mods, {"title": "newTitle"}) +class WriteTest(unittest.TestCase, TestHelper): + + def setUp(self): + self.setup_beets() + + def tearDown(self): + self.teardown_beets() + + def write_cmd(self, *args): + ui._raw_main(['write'] + list(args), self.lib) + + def test_update_mtime(self): + item = self.add_item_fixture() + item['title'] = 'a new title' + item.store() + + item = self.lib.items().get() + self.assertEqual(item.mtime, 0) + + self.write_cmd() + item = self.lib.items().get() + self.assertEqual(item.mtime, item.current_mtime()) + + class MoveTest(_common.TestCase): def setUp(self): super(MoveTest, self).setUp()