diff --git a/beets/library.py b/beets/library.py index 46d2f416c..405b546e2 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1369,22 +1369,27 @@ class Album(LibModel): plugins.send('art_set', album=self) - def store(self, fields=None): + def store(self, fields=None, inherit=True): """Update the database with the album information. - The album's tracks are also updated. - `fields` represents the fields to be stored. If not specified, all fields will be. + + The album's tracks are also updated when the `inherit` flag is enabled. + This applies to fixed attributes as well as flexible ones. The `id` + attribute of the album will never be inherited. """ # Get modified track fields. track_updates = {} track_deletes = set() for key in self._dirty: - if key in self.item_keys: - track_updates[key] = self[key] - elif key not in self: - track_deletes.add(key) + if inherit: + if key in self.item_keys: # is a fixed attribute + track_updates[key] = self[key] + elif key not in self: # is a fixed or a flexible attribute + track_deletes.add(key) + elif key != 'id': # is a flexible attribute + track_updates[key] = self[key] with self._db.transaction(): super().store(fields) @@ -1400,7 +1405,7 @@ class Album(LibModel): del item[key] item.store() - def try_sync(self, write, move): + def try_sync(self, write, move, inherit=True): """Synchronize the album and its items with the database. Optionally, also write any new tags into the files and update their paths. @@ -1409,7 +1414,7 @@ class Album(LibModel): `move` controls whether files (both audio and album art) are moved. """ - self.store() + self.store(inherit=inherit) for item in self.items(): item.try_sync(write, move) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index abe368bb5..f5b92ada1 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1498,7 +1498,7 @@ default_commands.append(version_cmd) # modify: Declaratively change metadata. -def modify_items(lib, mods, dels, query, write, move, album, confirm): +def modify_items(lib, mods, dels, query, write, move, album, confirm, inherit): """Modifies matching items according to user-specified assignments and deletions. @@ -1551,7 +1551,7 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm): # Apply changes to database and files with lib.transaction(): for obj in changed: - obj.try_sync(write, move) + obj.try_sync(write, move, inherit) def print_and_modify(obj, mods, dels): @@ -1594,7 +1594,8 @@ def modify_func(lib, opts, args): if not mods and not dels: raise ui.UserError('no modifications specified') modify_items(lib, mods, dels, query, ui.should_write(opts.write), - ui.should_move(opts.move), opts.album, not opts.yes) + ui.should_move(opts.move), opts.album, not opts.yes, + opts.inherit) modify_cmd = ui.Subcommand( @@ -1622,6 +1623,10 @@ modify_cmd.parser.add_option( '-y', '--yes', action='store_true', help='skip confirmation' ) +modify_cmd.parser.add_option( + '-I', '--noinherit', action='store_false', dest='inherit', default=True, + help="when modifying albums, don't also change item data" +) modify_cmd.func = modify_func default_commands.append(modify_cmd) diff --git a/beetsplug/ipfs.py b/beetsplug/ipfs.py index 5794143bd..11f131418 100644 --- a/beetsplug/ipfs.py +++ b/beetsplug/ipfs.py @@ -296,4 +296,4 @@ class IPFSPlugin(BeetsPlugin): self._log.info("Adding '{0}' to temporary library", album) new_album = tmplib.add_album(items) new_album.ipfs = album.ipfs - new_album.store() + new_album.store(inherit=False) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7fdb9ed22..760241217 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -214,6 +214,11 @@ Bug fixes: the highest number of likes * :doc:`/plugins/lyrics`: Fix a crash with the Google backend when processing some web pages. :bug:`4875` +* Modifying flexible attributes of albums now cascade to the individual album + tracks, similar to how fixed album attributes have been cascading to tracks + already. A new option ``--noinherit/-I`` to :ref:`modify ` + allows changing this behaviour. + :bug:`4822` For packagers: diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index da119d0f8..9306397a9 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -257,7 +257,7 @@ modify `````` :: - beet modify [-MWay] [-f FORMAT] QUERY [FIELD=VALUE...] [FIELD!...] + beet modify [-IMWay] [-f FORMAT] QUERY [FIELD=VALUE...] [FIELD!...] Change the metadata for items or albums in the database. @@ -274,13 +274,17 @@ name into the artist field for all your tracks, and ``beet modify title='$track $title'`` will add track numbers to their title metadata. -The ``-a`` switch also operates on albums in addition to the individual tracks. +The ``-a`` option changes to querying album fields instead of track fields and +also enables to operate on albums in addition to the individual tracks. Without this flag, the command will only change *track-level* data, even if all the tracks belong to the same album. If you want to change an *album-level* field, such as ``year`` or ``albumartist``, you'll want to use the ``-a`` flag to avoid a confusing situation where the data for individual tracks conflicts with the data for the whole album. +Modifications issued using ``-a`` by default cascade to individual tracks. To +prevent this behavior, use ``-I``/``--noinherit``. + Items will automatically be moved around when necessary if they're in your library directory, but you can disable that with ``-M``. Tags will be written to the files according to the settings you have for imports, but these can be diff --git a/test/test_ipfs.py b/test/test_ipfs.py index 8f72f5132..593a01b8f 100644 --- a/test/test_ipfs.py +++ b/test/test_ipfs.py @@ -87,7 +87,7 @@ class IPFSPluginTest(unittest.TestCase, TestHelper): album = self.lib.add_album(items) album.ipfs = "QmfM9ic5LJj7V6ecozFx1MkSoaaiq3PXfhJoFvyqzpLXSf" - album.store() + album.store(inherit=False) return album diff --git a/test/test_library.py b/test/test_library.py index 0e9637635..269771575 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -1022,10 +1022,17 @@ class AlbumInfoTest(_common.TestCase): self.assertEqual(i.albumartist, 'myNewArtist') self.assertNotEqual(i.artist, 'myNewArtist') + def test_albuminfo_change_artist_does_change_items(self): + ai = self.lib.get_album(self.i) + ai.artist = 'myNewArtist' + ai.store(inherit=True) + i = self.lib.items()[0] + self.assertEqual(i.artist, 'myNewArtist') + def test_albuminfo_change_artist_does_not_change_items(self): ai = self.lib.get_album(self.i) ai.artist = 'myNewArtist' - ai.store() + ai.store(inherit=False) i = self.lib.items()[0] self.assertNotEqual(i.artist, 'myNewArtist')