diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py old mode 100644 new mode 100755 index daa636d24..d01e8a5c3 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -342,15 +342,19 @@ class Model(object): # Database interaction (CRUD methods). - def store(self): + def store(self, fields=None): """Save the object's metadata into the library database. + :param fields: the fields to be stored. If not specified, all fields + will be. """ + if fields is None: + fields = self._fields self._check_db() # Build assignments for query. assignments = [] subvars = [] - for key in self._fields: + for key in fields: if key != 'id' and key in self._dirty: self._dirty.remove(key) assignments.append(key + '=?') diff --git a/beets/library.py b/beets/library.py old mode 100644 new mode 100755 index 59784e808..e80c4da72 --- a/beets/library.py +++ b/beets/library.py @@ -323,8 +323,8 @@ class LibModel(dbcore.Model): funcs.update(plugins.template_funcs()) return funcs - def store(self): - super(LibModel, self).store() + def store(self, fields=None): + super(LibModel, self).store(fields) plugins.send('database_change', lib=self._db, model=self) def remove(self): @@ -729,7 +729,8 @@ class Item(LibModel): self._db._memotable = {} - def move(self, copy=False, link=False, basedir=None, with_album=True): + def move(self, copy=False, link=False, basedir=None, with_album=True, + store=True): """Move the item to its designated location within the library directory (provided by destination()). Subdirectories are created as needed. If the operation succeeds, the item's path @@ -745,10 +746,11 @@ class Item(LibModel): move its art. (This can be disabled by passing with_album=False.) - The item is stored to the database if it is in the database, so - any dirty fields prior to the move() call will be written as a - side effect. You probably want to call save() to commit the DB - transaction. + By default, the item is stored to the database if it is in the + database, so any dirty fields prior to the move() call will be written + as a side effect. You probably want to call save() to commit the DB + transaction. If `store` is true however, the item won't be stored, and + you'll have to manually store it after invoking this method. """ self._check_db() dest = self.destination(basedir=basedir) @@ -759,14 +761,16 @@ class Item(LibModel): # Perform the move and store the change. old_path = self.path self.move_file(dest, copy, link) - self.store() + if store: + self.store() # If this item is in an album, move its art. if with_album: album = self.get_album() if album: album.move_art(copy) - album.store() + if store: + album.store() # Prune vacated directory. if not copy: @@ -1000,26 +1004,31 @@ class Album(LibModel): util.prune_dirs(os.path.dirname(old_art), self._db.directory) - def move(self, copy=False, link=False, basedir=None): + def move(self, copy=False, link=False, basedir=None, store=True): """Moves (or copies) all items to their destination. Any album art moves along with them. basedir overrides the library base - directory for the destination. The album is stored to the - database, persisting any modifications to its metadata. + directory for the destination. By default, the album is stored to the + database, persisting any modifications to its metadata. If `store` is + true however, the album is not stored automatically, and you'll have + to manually store it after invoking this method. """ basedir = basedir or self._db.directory # Ensure new metadata is available to items for destination # computation. - self.store() + if store: + self.store() # Move items. items = list(self.items()) for item in items: - item.move(copy, link, basedir=basedir, with_album=False) + item.move(copy, link, basedir=basedir, with_album=False, + store=store) # Move art. self.move_art(copy, link) - self.store() + if store: + self.store() def item_dir(self): """Returns the directory containing the album's first item, @@ -1108,9 +1117,11 @@ class Album(LibModel): plugins.send('art_set', album=self) - def store(self): + def store(self, fields=None): """Update the database with the album information. The album's tracks are also updated. + :param fields: The fields to be stored. If not specified, all fields + will be. """ # Get modified track fields. track_updates = {} @@ -1119,7 +1130,7 @@ class Album(LibModel): track_updates[key] = self[key] with self._db.transaction(): - super(Album, self).store() + super(Album, self).store(fields) if track_updates: for item in self.items(): for key, value in track_updates.items(): diff --git a/beets/ui/commands.py b/beets/ui/commands.py old mode 100644 new mode 100755 index 2f323b604..c02f6accf --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1081,11 +1081,18 @@ default_commands.append(list_cmd) # update: Update library contents according to on-disk tags. -def update_items(lib, query, album, move, pretend): +def update_items(lib, query, album, move, pretend, fields): """For all the items matched by the query, update the library to reflect the item's embedded tags. + :param fields: The fields to be stored. If not specified, all fields will + be. """ with lib.transaction(): + if move and fields is not None and 'path' not in fields: + # Special case: if an item needs to be moved, the path field has to + # updated; otherwise the new path will not be reflected in the + # database. + fields.append('path') items, _ = _do_query(lib, query, album) # Walk through the items and pick up their changes. @@ -1124,24 +1131,25 @@ def update_items(lib, query, album, move, pretend): item._dirty.discard(u'albumartist') # Check for and display changes. - changed = ui.show_model_changes(item, - fields=library.Item._media_fields) + changed = ui.show_model_changes( + item, + fields=fields or library.Item._media_fields) # Save changes. if not pretend: if changed: # Move the item if it's in the library. if move and lib.directory in ancestry(item.path): - item.move() + item.move(store=False) - item.store() + item.store(fields=fields) affected_albums.add(item.album_id) else: # The file's mtime was different, but there were no # changes to the metadata. Store the new mtime, # which is set in the call to read(), so we don't # check this again in the future. - item.store() + item.store(fields=fields) # Skip album changes while pretending. if pretend: @@ -1160,17 +1168,24 @@ def update_items(lib, query, album, move, pretend): # Update album structure to reflect an item in it. for key in library.Album.item_keys: album[key] = first_item[key] - album.store() + album.store(fields=fields) # Move album art (and any inconsistent items). if move and lib.directory in ancestry(first_item.path): log.debug(u'moving album {0}', album_id) - album.move() + + # Manually moving and storing the album. + items = list(album.items()) + for item in items: + item.move(store=False) + item.store(fields=fields) + album.move(store=False) + album.store(fields=fields) def update_func(lib, opts, args): update_items(lib, decargs(args), opts.album, ui.should_move(opts.move), - opts.pretend) + opts.pretend, opts.fields) update_cmd = ui.Subcommand( @@ -1190,6 +1205,10 @@ update_cmd.parser.add_option( u'-p', u'--pretend', action='store_true', help=u"show all changes but do nothing" ) +update_cmd.parser.add_option( + u'-F', u'--field', default=None, action='append', dest='fields', + help=u'list of fields to update' +) update_cmd.func = update_func default_commands.append(update_cmd) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 93b9b6253..b2683cd86 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -272,7 +272,7 @@ update `````` :: - beet update [-aM] QUERY + beet update [-F] FIELD [-aM] QUERY Update the library (and, optionally, move files) to reflect out-of-band metadata changes and file deletions. @@ -288,6 +288,11 @@ To perform a "dry run" of an update, just use the ``-p`` (for "pretend") flag. This will show you all the proposed changes but won't actually change anything on disk. +By default, all the changed metadata will be populated back to the database. +If you only want certain fields to be written, specify them with the ```-F``` +flags (which can be used multiple times). For the list of supported fields, +please see ```beet fields```. + When an updated track is part of an album, the album-level fields of *all* tracks from the album are also updated. (Specifically, the command copies album-level data from the first track on the album and applies it to the @@ -318,7 +323,7 @@ You can think of this command as the opposite of :ref:`update-cmd`. The ``-p`` option previews metadata changes without actually applying them. -The ``-f`` option forces a write to the file, even if the file tags match the database. This is useful for making sure that enabled plugins that run on write (e.g., the Scrub and Zero plugins) are run on the file. +The ``-f`` option forces a write to the file, even if the file tags match the database. This is useful for making sure that enabled plugins that run on write (e.g., the Scrub and Zero plugins) are run on the file. diff --git a/test/test_ui.py b/test/test_ui.py index 31f3f37da..f1c4c0fc1 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -499,12 +499,14 @@ class UpdateTest(_common.TestCase): self.album.store() os.remove(artfile) - def _update(self, query=(), album=False, move=False, reset_mtime=True): + def _update(self, query=(), album=False, move=False, reset_mtime=True, + fields=None): self.io.addinput('y') if reset_mtime: self.i.mtime = 0 self.i.store() - commands.update_items(self.lib, query, album, move, False) + commands.update_items(self.lib, query, album, move, False, + fields=fields) def test_delete_removes_item(self): self.assertTrue(list(self.lib.items())) @@ -549,6 +551,26 @@ class UpdateTest(_common.TestCase): item = self.lib.items().get() self.assertTrue(b'differentTitle' not in item.path) + def test_selective_modified_metadata_moved(self): + mf = MediaFile(self.i.path) + mf.title = u'differentTitle' + mf.genre = u'differentGenre' + mf.save() + self._update(move=True, fields=['title']) + item = self.lib.items().get() + self.assertTrue(b'differentTitle' in item.path) + self.assertNotEqual(item.genre, u'differentGenre') + + def test_selective_modified_metadata_not_moved(self): + mf = MediaFile(self.i.path) + mf.title = u'differentTitle' + mf.genre = u'differentGenre' + mf.save() + self._update(move=False, fields=['title']) + item = self.lib.items().get() + self.assertTrue(b'differentTitle' not in item.path) + self.assertNotEqual(item.genre, u'differentGenre') + def test_modified_album_metadata_moved(self): mf = MediaFile(self.i.path) mf.album = u'differentAlbum' @@ -566,6 +588,26 @@ class UpdateTest(_common.TestCase): album = self.lib.albums()[0] self.assertNotEqual(artpath, album.artpath) + def test_selective_modified_album_metadata_moved(self): + mf = MediaFile(self.i.path) + mf.album = u'differentAlbum' + mf.genre = u'differentGenre' + mf.save() + self._update(move=True, fields=['album']) + item = self.lib.items().get() + self.assertTrue(b'differentAlbum' in item.path) + self.assertNotEqual(item.genre, u'differentGenre') + + def test_selective_modified_album_metadata_not_moved(self): + mf = MediaFile(self.i.path) + mf.album = u'differentAlbum' + mf.genre = u'differentGenre' + mf.save() + self._update(move=True, fields=['genre']) + item = self.lib.items().get() + self.assertTrue(b'differentAlbum' not in item.path) + self.assertEqual(item.genre, u'differentGenre') + def test_mtime_match_skips_update(self): mf = MediaFile(self.i.path) mf.title = u'differentTitle'