mirror of
https://github.com/beetbox/beets.git
synced 2025-12-26 18:43:38 +01:00
Merge pull request #2231 from dangmai/selective-update
Selective field updates
This commit is contained in:
commit
dab54043a1
5 changed files with 113 additions and 32 deletions
8
beets/dbcore/db.py
Normal file → Executable file
8
beets/dbcore/db.py
Normal file → Executable file
|
|
@ -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 + '=?')
|
||||
|
|
|
|||
45
beets/library.py
Normal file → Executable file
45
beets/library.py
Normal file → Executable file
|
|
@ -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():
|
||||
|
|
|
|||
37
beets/ui/commands.py
Normal file → Executable file
37
beets/ui/commands.py
Normal file → Executable file
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in a new issue