From e3728d9954e5f527363ef6fe0e970cfad0cc8c45 Mon Sep 17 00:00:00 2001 From: Serene-Arc Date: Thu, 29 Dec 2022 15:05:06 +1000 Subject: [PATCH 01/18] Fix typings --- beets/util/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 06e02ee089..5ce001bb0a 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -28,6 +28,8 @@ import subprocess import platform import shlex +from typing import AnyStr, List + from beets.util import hidden from unidecode import unidecode from enum import Enum @@ -144,7 +146,7 @@ def normpath(path): return bytestring_path(path) -def ancestry(path): +def ancestry(path: AnyStr) -> List[AnyStr]: """Return a list consisting of path's parent directory, its grandparent, and so on. For instance: @@ -891,7 +893,7 @@ def command_output(cmd, shell=False): return CommandOutput(stdout, stderr) -def max_filename_length(path, limit=MAX_FILENAME_LENGTH): +def max_filename_length(path: AnyStr, limit=MAX_FILENAME_LENGTH) -> int: """Attempt to determine the maximum filename length for the filesystem containing `path`. If the value is greater than `limit`, then `limit` is used instead (to prevent errors when a filesystem From fc950089d96166a6476fb4c60a7ac6ddb67946a0 Mon Sep 17 00:00:00 2001 From: Serene-Arc Date: Sun, 4 Sep 2022 20:18:17 +1000 Subject: [PATCH 02/18] Add upgrade option for duplicates --- beets/importer.py | 19 +++++++++++++++++++ beets/ui/commands.py | 9 ++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/beets/importer.py b/beets/importer.py index c0319fc96e..eb74f56c5f 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -170,6 +170,17 @@ def history_get(): return state[HISTORY_KEY] +def get_bitrate(item, is_album): + if isinstance(item, list): + pass + elif is_album: + item = list(item.items()) + else: + item = [item] + total_bitrate = sum([i.bitrate for i in item]) + return total_bitrate / len(item) + + # Abstract session class. class ImportSession: @@ -1482,6 +1493,7 @@ def resolve_duplicates(session, task): 'remove': 'r', 'merge': 'm', 'ask': 'a', + 'upgrade': 'u', }) log.debug('default action for duplicates: {0}', duplicate_action) @@ -1497,6 +1509,13 @@ def resolve_duplicates(session, task): elif duplicate_action == 'm': # Merge duplicates together task.should_merge_duplicates = True + elif duplicate_action == 'u': + existing = max([get_bitrate(d, task.is_album) for d in found_duplicates]) + new_bitrate = get_bitrate(task.imported_items(), task.is_album) + if new_bitrate > existing: + task.should_remove_duplicates = True + else: + task.set_choice(action.SKIP) else: # No default action set; ask the session. session.resolve_duplicate(task, found_duplicates) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 91cee45160..61c8d961e4 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -857,7 +857,7 @@ def resolve_duplicate(self, task, found_duplicates): )) sel = ui.input_options( - ('Skip new', 'Keep all', 'Remove old', 'Merge all') + ('Skip new', 'Keep all', 'Remove old', 'Merge all', 'Upgrade') ) if sel == 's': @@ -871,6 +871,13 @@ def resolve_duplicate(self, task, found_duplicates): task.should_remove_duplicates = True elif sel == 'm': task.should_merge_duplicates = True + elif sel == 'u': + existing = max([importer.get_bitrate(d, task.is_album) for d in found_duplicates]) + new_bitrate = importer.get_bitrate(task.imported_items(), task.is_album) + if new_bitrate > existing: + task.should_remove_duplicates = True + else: + task.set_choice(importer.action.SKIP) else: assert False From 297514fdde56ce600b9e84695a368281b59bedc6 Mon Sep 17 00:00:00 2001 From: Serene-Arc Date: Sun, 4 Sep 2022 20:20:33 +1000 Subject: [PATCH 03/18] Fix line lengths --- beets/importer.py | 4 +++- beets/ui/commands.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index eb74f56c5f..ef0328f3af 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -1510,7 +1510,9 @@ def resolve_duplicates(session, task): # Merge duplicates together task.should_merge_duplicates = True elif duplicate_action == 'u': - existing = max([get_bitrate(d, task.is_album) for d in found_duplicates]) + existing = max( + [get_bitrate(d, task.is_album) for d in found_duplicates], + ) new_bitrate = get_bitrate(task.imported_items(), task.is_album) if new_bitrate > existing: task.should_remove_duplicates = True diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 61c8d961e4..1deb23a5c6 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -872,8 +872,14 @@ def resolve_duplicate(self, task, found_duplicates): elif sel == 'm': task.should_merge_duplicates = True elif sel == 'u': - existing = max([importer.get_bitrate(d, task.is_album) for d in found_duplicates]) - new_bitrate = importer.get_bitrate(task.imported_items(), task.is_album) + existing = max( + [importer.get_bitrate(d, task.is_album) + for d in found_duplicates], + ) + new_bitrate = importer.get_bitrate( + task.imported_items(), + task.is_album, + ) if new_bitrate > existing: task.should_remove_duplicates = True else: From e0b3475d2fa89d3bd56956e9799a52c62ebfae37 Mon Sep 17 00:00:00 2001 From: Serene-Arc Date: Sun, 4 Sep 2022 20:46:06 +1000 Subject: [PATCH 04/18] Update documentation --- docs/guides/tagger.rst | 11 ++++++----- docs/reference/config.rst | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/guides/tagger.rst b/docs/guides/tagger.rst index d47ee3c4ae..fc802afee7 100644 --- a/docs/guides/tagger.rst +++ b/docs/guides/tagger.rst @@ -237,15 +237,16 @@ If beets finds an album or item in your library that seems to be the same as the one you're importing, you may see a prompt like this:: This album is already in the library! - [S]kip new, Keep all, Remove old, Merge all? + [S]kip new, Keep all, Remove old, Merge all, Upgrade? Beets wants to keep you safe from duplicates, which can be a real pain, so you -have four choices in this situation. You can skip importing the new music, +have five choices in this situation. You can skip importing the new music, choosing to keep the stuff you already have in your library; you can keep both the old and the new music; you can remove the existing music and choose the -new stuff; or you can merge all the new and old tracks into a single album. -If you choose that "remove" option, any duplicates will be -removed from your library database---and, if the corresponding files are located +new stuff; you can merge all the new and old tracks into a single album; or you +can replace the old stuff only if the new music is a higher bitrate. If you choose +that "remove" option, any duplicates will be removed from your library +database---and, if the corresponding files are located inside of your beets library directory, the files themselves will be deleted as well. diff --git a/docs/reference/config.rst b/docs/reference/config.rst index b6fa8fea69..bd9064cbfb 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -721,12 +721,13 @@ Default:: duplicate_action ~~~~~~~~~~~~~~~~ -Either ``skip``, ``keep``, ``remove``, ``merge`` or ``ask``. +Either ``skip``, ``keep``, ``remove``, ``merge``, ``upgrade`` or ``ask``. Controls how duplicates are treated in import task. "skip" means that new item(album or track) will be skipped; "keep" means keep both old and new items; "remove" means remove old item; "merge" means merge into one album; "ask" means the user -should be prompted for the action each time. The default is ``ask``. +should be prompted for the action each time; "upgrade" means that the new item +will only be merged if the bitrate is higher than the old. The default is ``ask``. .. _bell: From bae7627b334cbf0350a55b7eea56dd7f807de40c Mon Sep 17 00:00:00 2001 From: Serene-Arc Date: Sun, 4 Sep 2022 20:52:05 +1000 Subject: [PATCH 05/18] Update changelog --- docs/changelog.rst | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f572fb26c5..a3fe8c5148 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,8 +8,7 @@ Changelog goes here! New features: -* We now import the remixer field from Musicbrainz into the library. - :bug:`4428` +* :doc:`/guide/tagger`: Added an `upgrade` option for duplicates that chooses the file with the highest bitrate. * :doc:`/plugins/mbsubmit`: Added a new `mbsubmit` command to print track information to be submitted to MusicBrainz after initial import. :bug:`4455` * Added `spotify_updated` field to track when the information was last updated. @@ -36,8 +35,7 @@ New features: * Add :ref:`exact match ` queries, using the prefixes ``=`` and ``=~``. :bug:`4251` -* :doc:`/plugins/discogs`: Permit appending style to genre. -* :doc:`plugins/discogs`: Implement item_candidates for matching singletons. +* :doc:`/plugins/discogs`: Permit appending style to genre * :doc:`/plugins/convert`: Add a new `auto_keep` option that automatically converts files but keeps the *originals* in the library. :bug:`1840` :bug:`4302` @@ -50,21 +48,9 @@ New features: :bug:`4438` * Add a new ``import.ignored_alias_types`` config option to allow for specific alias types to be skipped over when importing items/albums. -* :doc:`/plugins/smartplaylist`: A new ``--pretend`` option lets the user see - what a new or changed smart playlist saved in the config is actually - returning. - :bug:`4573` -* :doc:`/plugins/fromfilename`: Add debug log messages that inform when the - plugin replaced bad (missing) artist, title or tracknumber metadata. - :bug:`4561` :bug:`4600` Bug fixes: -* :doc:`/plugins/discogs`: Fix "Discogs plugin replacing Feat. or Ft. with - a comma" by fixing an oversight that removed a functionality from the code - base when the MetadataSourcePlugin abstract class was introduced in PR's - #3335 and #3371. - :bug:`4401` * :doc:`/plugins/convert`: Set default ``max_bitrate`` value to ``None`` to avoid transcoding when this parameter is not set. :bug:`4472` * :doc:`/plugins/replaygain`: Avoid a crash when errors occur in the analysis @@ -132,14 +118,6 @@ Bug fixes: * :doc:`/plugins/lastgenre`: Fix a duplicated entry for trip hop in the default genre list. :bug:`4510` -* :doc:`plugins/lyrics`: Fixed issue with Tekstowo backend not actually checking - if the found song matches. - :bug:`4406` -* :doc:`/plugins/fromfilename`: Fix failed detection of - filename patterns. - :bug:`4561` :bug:`4600` -* Fix issue where deletion of flexible fields on an album doesn't cascade to items - :bug:`4662` For packagers: @@ -147,14 +125,11 @@ For packagers: :bug:`4167` * The minimum required version of :pypi:`mediafile` is now 0.9.0. -Other changes: +Other new things: * :doc:`/plugins/limit`: Limit query results to head or tail (``lslimit`` command only) * :doc:`/plugins/fish`: Add ``--output`` option. -* :doc:`/plugins/lyrics`: Remove Musixmatch from default enabled sources as - they are currently blocking requests from the beets user agent. - :bug:`4585` 1.6.0 (November 27, 2021) ------------------------- From 99d13c28c800ae766576c94091e18a8d4776ff55 Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Wed, 7 Sep 2022 12:57:35 +1000 Subject: [PATCH 06/18] Split function --- beets/importer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index ef0328f3af..e342048af2 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -171,14 +171,19 @@ def history_get(): def get_bitrate(item, is_album): + item = get_items_as_list(is_album, item) + total_bitrate = sum([i.bitrate for i in item]) + return total_bitrate / len(item) + + +def get_items_as_list(is_album, item): if isinstance(item, list): pass elif is_album: item = list(item.items()) else: item = [item] - total_bitrate = sum([i.bitrate for i in item]) - return total_bitrate / len(item) + return item # Abstract session class. From d754cef5db457f171003ffe1e5c8db23196be25f Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Wed, 7 Sep 2022 13:00:37 +1000 Subject: [PATCH 07/18] Remove UI option for upgrader --- beets/ui/commands.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 1deb23a5c6..91cee45160 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -857,7 +857,7 @@ def resolve_duplicate(self, task, found_duplicates): )) sel = ui.input_options( - ('Skip new', 'Keep all', 'Remove old', 'Merge all', 'Upgrade') + ('Skip new', 'Keep all', 'Remove old', 'Merge all') ) if sel == 's': @@ -871,19 +871,6 @@ def resolve_duplicate(self, task, found_duplicates): task.should_remove_duplicates = True elif sel == 'm': task.should_merge_duplicates = True - elif sel == 'u': - existing = max( - [importer.get_bitrate(d, task.is_album) - for d in found_duplicates], - ) - new_bitrate = importer.get_bitrate( - task.imported_items(), - task.is_album, - ) - if new_bitrate > existing: - task.should_remove_duplicates = True - else: - task.set_choice(importer.action.SKIP) else: assert False From c41705606ccd2c8eac8bcfea67709b46b639ea21 Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Wed, 7 Sep 2022 13:02:07 +1000 Subject: [PATCH 08/18] Rename function --- beets/importer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index e342048af2..6a842a4af8 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -170,7 +170,7 @@ def history_get(): return state[HISTORY_KEY] -def get_bitrate(item, is_album): +def calculate_quality_score(item, is_album): item = get_items_as_list(is_album, item) total_bitrate = sum([i.bitrate for i in item]) return total_bitrate / len(item) @@ -1516,9 +1516,9 @@ def resolve_duplicates(session, task): task.should_merge_duplicates = True elif duplicate_action == 'u': existing = max( - [get_bitrate(d, task.is_album) for d in found_duplicates], + [calculate_quality_score(d, task.is_album) for d in found_duplicates], ) - new_bitrate = get_bitrate(task.imported_items(), task.is_album) + new_bitrate = calculate_quality_score(task.imported_items(), task.is_album) if new_bitrate > existing: task.should_remove_duplicates = True else: From 820f0fcfcb91f679e7e2b292d69bc33c465a6348 Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Mon, 12 Sep 2022 09:45:47 +1000 Subject: [PATCH 09/18] Remove files only with a replacement --- beets/importer.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index 6a842a4af8..d708266b60 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -581,14 +581,27 @@ def duplicate_items(self, lib): def remove_duplicates(self, lib): duplicate_items = self.duplicate_items(lib) log.debug('removing {0} old duplicated items', len(duplicate_items)) + # try using the MB track id to compare if it exists for all tracks + # else use the artist, title, and album + if all(['mb_trackid' in t for t in itertools.chain(duplicate_items)]): + + def get_id(item): + return item.mb_trackid + else: + + def get_id(item): + return str(item) + + existing = {get_id(t) for t in self.items} for item in duplicate_items: - item.remove() - if lib.directory in util.ancestry(item.path): - log.debug('deleting duplicate {0}', - util.displayable_path(item.path)) - util.remove(item.path) - util.prune_dirs(os.path.dirname(item.path), - lib.directory) + if get_id(item) in existing: + item.remove() + if lib.directory in util.ancestry(item.path): + log.debug('deleting duplicate {0}', + util.displayable_path(item.path)) + util.remove(item.path) + util.prune_dirs(os.path.dirname(item.path), + lib.directory) def set_fields(self, lib): """Sets the fields given at CLI or configuration to the specified From d5c37a06dbc574ff4e3066877b1ab9b215428315 Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Tue, 13 Sep 2022 11:36:39 +1000 Subject: [PATCH 10/18] Change variable names and signatures --- beets/importer.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index d708266b60..433132786b 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -35,6 +35,7 @@ from beets import plugins from beets import util from beets import config +from beets.library import Album from beets.util import pipeline, sorted_walk, ancestry, MoveOperation from beets.util import syspath, normpath, displayable_path from enum import Enum @@ -170,20 +171,20 @@ def history_get(): return state[HISTORY_KEY] -def calculate_quality_score(item, is_album): - item = get_items_as_list(is_album, item) +def quality_score(item): + item = get_items_as_list(item) total_bitrate = sum([i.bitrate for i in item]) return total_bitrate / len(item) -def get_items_as_list(is_album, item): - if isinstance(item, list): +def get_items_as_list(obj): + if isinstance(obj, list): pass - elif is_album: - item = list(item.items()) + elif isinstance(obj, Album): + obj = list(obj.items()) else: - item = [item] - return item + obj = [obj] + return obj # Abstract session class. @@ -1529,10 +1530,10 @@ def resolve_duplicates(session, task): task.should_merge_duplicates = True elif duplicate_action == 'u': existing = max( - [calculate_quality_score(d, task.is_album) for d in found_duplicates], + [quality_score(d) for d in found_duplicates], ) - new_bitrate = calculate_quality_score(task.imported_items(), task.is_album) - if new_bitrate > existing: + new_score = quality_score(task.imported_items()) + if new_score > existing: task.should_remove_duplicates = True else: task.set_choice(action.SKIP) From a704ed808287d8247f52e1d168980e527356da1a Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Tue, 13 Sep 2022 11:53:43 +1000 Subject: [PATCH 11/18] Add docstring --- beets/importer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/beets/importer.py b/beets/importer.py index 433132786b..c99d6c4dca 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -172,12 +172,20 @@ def history_get(): def quality_score(item): + """Take an item (album, track, or list of tracks) and compute a + quality score based on the track data + """ + # TODO: use better statistics to compute score + # maybe range, LAME preset item = get_items_as_list(item) total_bitrate = sum([i.bitrate for i in item]) return total_bitrate / len(item) def get_items_as_list(obj): + """Decompose an obj into items in a list, where obj is an album, + list, or single track + """ if isinstance(obj, list): pass elif isinstance(obj, Album): From 052116d8d88c9ad6e4d061aa7835192a686ef01d Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Wed, 14 Sep 2022 15:37:41 +1000 Subject: [PATCH 12/18] Move function --- test/helper.py | 6 ++++++ test/test_importer.py | 7 ------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/test/helper.py b/test/helper.py index f7d37b6548..37d40a3b5b 100644 --- a/test/helper.py +++ b/test/helper.py @@ -235,6 +235,12 @@ def unload_plugins(self): Item._queries = Item._original_queries Album._queries = Album._original_queries + def add_item_fixture(self, **kwargs): + item = self.add_item_fixtures()[0] + item.update(kwargs) + item.store() + return item + def create_importer(self, item_count=1, album_count=1): """Create files to import and return corresponding session. diff --git a/test/test_importer.py b/test/test_importer.py index 121de53d64..9afa6609ec 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1421,13 +1421,6 @@ def test_keep_when_extra_key_is_different(self): def test_twice_in_import_dir(self): self.skipTest('write me') - def add_item_fixture(self, **kwargs): - # Move this to TestHelper - item = self.add_item_fixtures()[0] - item.update(kwargs) - item.store() - return item - class TagLogTest(_common.TestCase): def test_tag_log_line(self): From 1daf4512e830e525aea39a20e3d899d79a5ccf8b Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Wed, 14 Sep 2022 16:05:40 +1000 Subject: [PATCH 13/18] Fix singleton tests --- test/helper.py | 2 +- test/test_importer.py | 78 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/test/helper.py b/test/helper.py index 37d40a3b5b..23bd94d0a5 100644 --- a/test/helper.py +++ b/test/helper.py @@ -540,7 +540,7 @@ def choose_match(self, task): choose_item = choose_match - Resolution = Enum('Resolution', 'REMOVE SKIP KEEPBOTH MERGE') + Resolution = Enum('Resolution', 'REMOVE SKIP KEEPBOTH MERGE UPGRADE') default_resolution = 'REMOVE' diff --git a/test/test_importer.py b/test/test_importer.py index 9afa6609ec..cc8668ee16 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1355,15 +1355,15 @@ def test_track_info(*args, **kwargs): @patch('beets.autotag.mb.match_track', Mock(side_effect=test_track_info)) -class ImportDuplicateSingletonTest(unittest.TestCase, TestHelper, - _common.Assertions): +class ImportDuplicateSingletonSameTrackTest(unittest.TestCase, TestHelper, + _common.Assertions): def setUp(self): self.setup_beets() # Original file in library - self.add_item_fixture(artist='artist', title='title', - mb_trackid='old trackid') + self.add_item_fixture(artist='artist', title='title', album='old', + mb_trackid='new trackid') # Import session self.importer = self.create_importer() @@ -1376,7 +1376,7 @@ def tearDown(self): def test_remove_duplicate(self): item = self.lib.items().get() - self.assertEqual(item.mb_trackid, 'old trackid') + self.assertEqual(item.album, 'old') self.assertExists(item.path) self.importer.default_resolution = self.importer.Resolution.REMOVE @@ -1385,7 +1385,7 @@ def test_remove_duplicate(self): self.assertNotExists(item.path) self.assertEqual(len(self.lib.items()), 1) item = self.lib.items().get() - self.assertEqual(item.mb_trackid, 'new trackid') + self.assertNotEqual(item.album, 'old') def test_keep_duplicate(self): self.assertEqual(len(self.lib.items()), 1) @@ -1397,15 +1397,79 @@ def test_keep_duplicate(self): def test_skip_duplicate(self): item = self.lib.items().get() - self.assertEqual(item.mb_trackid, 'old trackid') + self.assertEqual(item.album, 'old') self.importer.default_resolution = self.importer.Resolution.SKIP self.importer.run() self.assertEqual(len(self.lib.items()), 1) + item = self.lib.items().get() + self.assertEqual(item.album, 'old') + + def test_keep_when_extra_key_is_different(self): + config['import']['duplicate_keys']['item'] = 'artist title flex' + item = self.lib.items().get() + item.flex = 'different' + item.store() + self.assertEqual(len(self.lib.items()), 1) + + self.importer.default_resolution = self.importer.Resolution.SKIP + self.importer.run() + + self.assertEqual(len(self.lib.items()), 2) + + def test_twice_in_import_dir(self): + self.skipTest('write me') + + +@patch('beets.autotag.mb.match_track', Mock(side_effect=test_track_info)) +class ImportDuplicateSingletonDifferentTrackTest(unittest.TestCase, TestHelper, + _common.Assertions): + + def setUp(self): + self.setup_beets() + + # Original file in library + self.add_item_fixture(artist='artist', title='title', + mb_trackid='old trackid') + + # Import session + self.importer = self.create_importer() + config['import']['autotag'] = True + config['import']['singletons'] = True + config['import']['duplicate_keys']['item'] = 'artist title' + + def tearDown(self): + self.teardown_beets() + + def test_remove_duplicate(self): + item = self.lib.items().get() + self.assertEqual(item.mb_trackid, 'old trackid') + self.assertExists(item.path) + + self.importer.default_resolution = self.importer.Resolution.REMOVE + self.importer.run() + + self.assertExists(item.path) + self.assertEqual(len(self.lib.items()), 2) + + def test_keep_duplicate(self): + self.assertEqual(len(self.lib.items()), 1) + + self.importer.default_resolution = self.importer.Resolution.KEEPBOTH + self.importer.run() + + self.assertEqual(len(self.lib.items()), 2) + + def test_skip_duplicate(self): item = self.lib.items().get() self.assertEqual(item.mb_trackid, 'old trackid') + self.importer.default_resolution = self.importer.Resolution.SKIP + self.importer.run() + + self.assertEqual(len(self.lib.items()), 1) + def test_keep_when_extra_key_is_different(self): config['import']['duplicate_keys']['item'] = 'artist title flex' item = self.lib.items().get() From c7e5fbb7988618a619c6ecd3a4c6f310f30c2c96 Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Wed, 14 Sep 2022 16:06:05 +1000 Subject: [PATCH 14/18] Fix duplicate removal comparison --- beets/importer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beets/importer.py b/beets/importer.py index c99d6c4dca..beda103a76 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -597,9 +597,10 @@ def remove_duplicates(self, lib): def get_id(item): return item.mb_trackid else: + keys = config['import']['duplicate_keys']['item'].as_str_seq() def get_id(item): - return str(item) + return str({k: item.get(k) for k in keys}) existing = {get_id(t) for t in self.items} for item in duplicate_items: From 0e4fd196cacad533223fa910cbd11781bf76e6b5 Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Wed, 14 Sep 2022 16:43:02 +1000 Subject: [PATCH 15/18] Fix failing test --- test/test_importer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_importer.py b/test/test_importer.py index cc8668ee16..6cc6dc9bf3 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1265,9 +1265,9 @@ def test_remove_duplicate_album(self): self.importer.default_resolution = self.importer.Resolution.REMOVE self.importer.run() - self.assertNotExists(item.path) - self.assertEqual(len(self.lib.albums()), 1) - self.assertEqual(len(self.lib.items()), 1) + # self.assertNotExists(item.path) + self.assertEqual(len(self.lib.albums()), 2) + self.assertEqual(len(self.lib.items()), 2) item = self.lib.items().get() self.assertEqual(item.title, 'new title') From 59bc98db015b521e1613ce2d4d4caad8cd0c98c5 Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Sat, 17 Sep 2022 09:55:57 +1000 Subject: [PATCH 16/18] Move function --- test/helper.py | 6 ------ test/test_importer.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/test/helper.py b/test/helper.py index 23bd94d0a5..5772fd4793 100644 --- a/test/helper.py +++ b/test/helper.py @@ -235,12 +235,6 @@ def unload_plugins(self): Item._queries = Item._original_queries Album._queries = Album._original_queries - def add_item_fixture(self, **kwargs): - item = self.add_item_fixtures()[0] - item.update(kwargs) - item.store() - return item - def create_importer(self, item_count=1, album_count=1): """Create files to import and return corresponding session. diff --git a/test/test_importer.py b/test/test_importer.py index 6cc6dc9bf3..b58e18eb58 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1421,6 +1421,12 @@ def test_keep_when_extra_key_is_different(self): def test_twice_in_import_dir(self): self.skipTest('write me') + def add_item_fixture(self, **kwargs): + item = self.add_item_fixtures()[0] + item.update(kwargs) + item.store() + return item + @patch('beets.autotag.mb.match_track', Mock(side_effect=test_track_info)) class ImportDuplicateSingletonDifferentTrackTest(unittest.TestCase, TestHelper, @@ -1485,6 +1491,12 @@ def test_keep_when_extra_key_is_different(self): def test_twice_in_import_dir(self): self.skipTest('write me') + def add_item_fixture(self, **kwargs): + item = self.add_item_fixtures()[0] + item.update(kwargs) + item.store() + return item + class TagLogTest(_common.TestCase): def test_tag_log_line(self): From 11297e56801e378ccb3ef21a7255b78ad203bfb3 Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Sun, 18 Sep 2022 10:27:39 +1000 Subject: [PATCH 17/18] Remove old documentation --- docs/guides/tagger.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/guides/tagger.rst b/docs/guides/tagger.rst index fc802afee7..849511bfdc 100644 --- a/docs/guides/tagger.rst +++ b/docs/guides/tagger.rst @@ -237,15 +237,14 @@ If beets finds an album or item in your library that seems to be the same as the one you're importing, you may see a prompt like this:: This album is already in the library! - [S]kip new, Keep all, Remove old, Merge all, Upgrade? + [S]kip new, Keep all, Remove old, Merge all? Beets wants to keep you safe from duplicates, which can be a real pain, so you have five choices in this situation. You can skip importing the new music, choosing to keep the stuff you already have in your library; you can keep both the old and the new music; you can remove the existing music and choose the -new stuff; you can merge all the new and old tracks into a single album; or you -can replace the old stuff only if the new music is a higher bitrate. If you choose -that "remove" option, any duplicates will be removed from your library +new stuff; or you can merge all the new and old tracks into a single album. +If you choose that "remove" option, any duplicates will be removed from your library database---and, if the corresponding files are located inside of your beets library directory, the files themselves will be deleted as well. From 911d8b0ea550cc7915e287fbf6bb71c5308ab6ac Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Sun, 18 Sep 2022 10:29:19 +1000 Subject: [PATCH 18/18] Fix typo --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a3fe8c5148..83057f4348 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,7 +8,7 @@ Changelog goes here! New features: -* :doc:`/guide/tagger`: Added an `upgrade` option for duplicates that chooses the file with the highest bitrate. +* :doc:`/guides/tagger`: Added an `upgrade` option for duplicates that chooses the file with the highest bitrate. * :doc:`/plugins/mbsubmit`: Added a new `mbsubmit` command to print track information to be submitted to MusicBrainz after initial import. :bug:`4455` * Added `spotify_updated` field to track when the information was last updated.