From af347d957e97b2957621305c2127af1a6047c948 Mon Sep 17 00:00:00 2001 From: Howard Jones Date: Sun, 17 Aug 2014 18:53:02 +0100 Subject: [PATCH 1/5] Basic summary information for duplicates Added a one-line summary of each album (in lib, and import target) so you can easily tell if (for example) you are about to overwrite your FLAC copy with a low-bitrate mp3 from somewhere else. --- beets/importer.py | 3 +++ beets/ui/commands.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/beets/importer.py b/beets/importer.py index 060a188c9..b0f56a497 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -355,6 +355,8 @@ class ImportTask(object): self.should_remove_duplicates = False self.is_album = True + self.found_duplicates = None + def set_null_candidates(self): """Set the candidates to indicate no album match was found. """ @@ -534,6 +536,7 @@ class ImportTask(object): album_paths = set(i.path for i in album.items()) if album_paths != task_paths: duplicates.append(album) + self.found_duplicates = duplicates return duplicates def infer_album_fields(self): diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 14f2bf0a4..39cf2a6c7 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -24,6 +24,7 @@ import itertools import codecs import platform import re +import collections import beets from beets import ui @@ -741,6 +742,22 @@ class TerminalImportSession(importer.ImportSession): assert isinstance(choice, autotag.TrackMatch) return choice + def summarize_items(self,items): + summary_text = "" + summary_text += "%d items. " % len(items) + + format_counts = collections.Counter( [item[1] for item in items] ) + + for format, count in format_counts.iteritems(): + summary_text += '{count} {format}. '.format(format=format, count=count) + + average_bitrate = sum([item[2] for item in items]) / len(items) + total_duration = sum([item[3] for item in items]) + summary_text += '{bitrate} average bitrate. '.format(bitrate=average_bitrate) + summary_text += '{length}s total length. '.format(length=int(total_duration)) + + return summary_text + def resolve_duplicate(self, task): """Decide what to do when a new album or item seems similar to one that's already in the library. @@ -753,6 +770,14 @@ class TerminalImportSession(importer.ImportSession): log.info('Skipping.') sel = 's' else: + # print some detail about the existing and new items so it can be an informed decision + + for duplicate in task.found_duplicates: + old_items = [(item.path, item.format, item.bitrate, item.length) for item in duplicate.items()] + print("OLD: " + self.summarize_items(old_items)) + new_items = [(item.path, item.format, item.bitrate, item.length) for item in task.items] + print("NEW: " + self.summarize_items(new_items)) + sel = ui.input_options( ('Skip new', 'Keep both', 'Remove old') ) From 4bf07aa8a6d2bf78d18557ebe76bd9d6af0589ae Mon Sep 17 00:00:00 2001 From: Howard Jones Date: Sun, 17 Aug 2014 19:11:01 +0100 Subject: [PATCH 2/5] Changed to not require collections collections library is only available in Py 2.7+ --- beets/ui/commands.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 39cf2a6c7..0e2a15ae2 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -24,7 +24,6 @@ import itertools import codecs import platform import re -import collections import beets from beets import ui @@ -745,9 +744,10 @@ class TerminalImportSession(importer.ImportSession): def summarize_items(self,items): summary_text = "" summary_text += "%d items. " % len(items) - - format_counts = collections.Counter( [item[1] for item in items] ) - + format_counts = {} + for item in items: + format_counts[item[1]] = format_counts.get(item[1],0) + 1; + for format, count in format_counts.iteritems(): summary_text += '{count} {format}. '.format(format=format, count=count) From a15cf2b0015e25e30e291f3a7e2287c96305d492 Mon Sep 17 00:00:00 2001 From: Howard Jones Date: Mon, 18 Aug 2014 08:57:59 +0100 Subject: [PATCH 3/5] Moved summarize_items to utils. Cleaned up output Output now uses K for bitrate. summarize function now in util --- beets/ui/commands.py | 22 ++-------------------- beets/util/__init__.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 0e2a15ae2..e0af65e7e 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -741,23 +741,6 @@ class TerminalImportSession(importer.ImportSession): assert isinstance(choice, autotag.TrackMatch) return choice - def summarize_items(self,items): - summary_text = "" - summary_text += "%d items. " % len(items) - format_counts = {} - for item in items: - format_counts[item[1]] = format_counts.get(item[1],0) + 1; - - for format, count in format_counts.iteritems(): - summary_text += '{count} {format}. '.format(format=format, count=count) - - average_bitrate = sum([item[2] for item in items]) / len(items) - total_duration = sum([item[3] for item in items]) - summary_text += '{bitrate} average bitrate. '.format(bitrate=average_bitrate) - summary_text += '{length}s total length. '.format(length=int(total_duration)) - - return summary_text - def resolve_duplicate(self, task): """Decide what to do when a new album or item seems similar to one that's already in the library. @@ -771,12 +754,11 @@ class TerminalImportSession(importer.ImportSession): sel = 's' else: # print some detail about the existing and new items so it can be an informed decision - for duplicate in task.found_duplicates: old_items = [(item.path, item.format, item.bitrate, item.length) for item in duplicate.items()] - print("OLD: " + self.summarize_items(old_items)) + print("OLD: " + util.summarize_items(old_items)) new_items = [(item.path, item.format, item.bitrate, item.length) for item in task.items] - print("NEW: " + self.summarize_items(new_items)) + print("NEW: " + util.summarize_items(new_items)) sel = ui.input_options( ('Skip new', 'Keep both', 'Remove old') diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 428de312a..c66831c30 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -658,3 +658,30 @@ def max_filename_length(path, limit=MAX_FILENAME_LENGTH): return min(res[9], limit) else: return limit + +def summarize_items(items): + """Produces a brief summary line for manually resolving duplicates during import. + Accepts a list of tuples, one per item containing: + (path, format, bitrate, duration) + """ + + summary_text = "" + summary_text += "%d items. " % len(items) + format_counts = {} + for item in items: + format_counts[item[1]] = format_counts.get(item[1],0) + 1; + + for format, count in format_counts.iteritems(): + summary_text += '{count} {format}. '.format(format=format, count=count) + + average_bitrate = sum([item[2] for item in items]) / len(items) + total_duration = sum([item[3] for item in items]) + summary_text += '{bitrate}K average bitrate. '.format(bitrate=int(average_bitrate/1000)) + summary_text += '{length}s total length. '.format(length=int(total_duration)) + + return summary_text + + + + + From 3ec7426a35591b08c5a173b99a5ef603febbf348 Mon Sep 17 00:00:00 2001 From: Howard Jones Date: Mon, 18 Aug 2014 10:35:01 +0100 Subject: [PATCH 4/5] Pass found duplicates between task and session --- beets/importer.py | 10 ++++------ beets/ui/commands.py | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index b0f56a497..76c90dbea 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -250,7 +250,7 @@ class ImportSession(object): def choose_match(self, task): raise NotImplementedError - def resolve_duplicate(self, task): + def resolve_duplicate(self, task, found_duplicates): raise NotImplementedError def choose_item(self, task): @@ -355,8 +355,6 @@ class ImportTask(object): self.should_remove_duplicates = False self.is_album = True - self.found_duplicates = None - def set_null_candidates(self): """Set the candidates to indicate no album match was found. """ @@ -536,7 +534,6 @@ class ImportTask(object): album_paths = set(i.path for i in album.items()) if album_paths != task_paths: duplicates.append(album) - self.found_duplicates = duplicates return duplicates def infer_album_fields(self): @@ -1059,8 +1056,9 @@ def resolve_duplicates(session, task): """ if task.choice_flag in (action.ASIS, action.APPLY): ident = task.chosen_ident() - if ident in session.seen_idents or task.find_duplicates(session.lib): - session.resolve_duplicate(task) + found_duplicates = task.find_duplicates(session.lib) + if ident in session.seen_idents or found_duplicates: + session.resolve_duplicate(task, found_duplicates) session.log_choice(task, True) session.seen_idents.add(ident) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index e0af65e7e..3eec70fd7 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -741,7 +741,7 @@ class TerminalImportSession(importer.ImportSession): assert isinstance(choice, autotag.TrackMatch) return choice - def resolve_duplicate(self, task): + def resolve_duplicate(self, task, found_duplicates): """Decide what to do when a new album or item seems similar to one that's already in the library. """ @@ -754,7 +754,7 @@ class TerminalImportSession(importer.ImportSession): sel = 's' else: # print some detail about the existing and new items so it can be an informed decision - for duplicate in task.found_duplicates: + for duplicate in found_duplicates: old_items = [(item.path, item.format, item.bitrate, item.length) for item in duplicate.items()] print("OLD: " + util.summarize_items(old_items)) new_items = [(item.path, item.format, item.bitrate, item.length) for item in task.items] From 4aa783f09d08a42944988c98b651b7d2260d7de5 Mon Sep 17 00:00:00 2001 From: Howard Jones Date: Mon, 18 Aug 2014 12:21:45 +0100 Subject: [PATCH 5/5] Final reshuffle to put summarize_items in beets.ui Also improved duration display. --- beets/ui/__init__.py | 26 ++++++++++++++++++++++++++ beets/ui/commands.py | 4 ++-- beets/util/__init__.py | 28 ---------------------------- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 1e64cd89c..993964291 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -989,3 +989,29 @@ def main(args=None): except KeyboardInterrupt: # Silently ignore ^C except in verbose mode. log.debug(traceback.format_exc()) + + + +def summarize_items(items): + """Produces a brief summary line for manually resolving duplicates during import. + Accepts a list of tuples, one per item containing: + (path, format, bitrate, duration) + """ + + summary_text = "" + summary_text += "%d items. " % len(items) + format_counts = {} + for item in items: + format_counts[item[1]] = format_counts.get(item[1],0) + 1; + + for format, count in format_counts.iteritems(): + summary_text += '{count} {format}. '.format(format=format, count=count) + + average_bitrate = sum([item[2] for item in items]) / len(items) + total_duration = sum([item[3] for item in items]) + summary_text += '{bitrate}kbps average bitrate. '.format(bitrate=int(average_bitrate/1000)) + summary_text += '{length} total length. '.format(length=human_seconds_short(total_duration)) + + return summary_text + + diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 3eec70fd7..0cc3bcf9d 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -756,9 +756,9 @@ class TerminalImportSession(importer.ImportSession): # print some detail about the existing and new items so it can be an informed decision for duplicate in found_duplicates: old_items = [(item.path, item.format, item.bitrate, item.length) for item in duplicate.items()] - print("OLD: " + util.summarize_items(old_items)) + print("OLD: " + ui.summarize_items(old_items)) new_items = [(item.path, item.format, item.bitrate, item.length) for item in task.items] - print("NEW: " + util.summarize_items(new_items)) + print("NEW: " + ui.summarize_items(new_items)) sel = ui.input_options( ('Skip new', 'Keep both', 'Remove old') diff --git a/beets/util/__init__.py b/beets/util/__init__.py index c66831c30..ec3a3c1bc 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -25,7 +25,6 @@ import traceback import subprocess import platform - MAX_FILENAME_LENGTH = 200 WINDOWS_MAGIC_PREFIX = u'\\\\?\\' @@ -658,30 +657,3 @@ def max_filename_length(path, limit=MAX_FILENAME_LENGTH): return min(res[9], limit) else: return limit - -def summarize_items(items): - """Produces a brief summary line for manually resolving duplicates during import. - Accepts a list of tuples, one per item containing: - (path, format, bitrate, duration) - """ - - summary_text = "" - summary_text += "%d items. " % len(items) - format_counts = {} - for item in items: - format_counts[item[1]] = format_counts.get(item[1],0) + 1; - - for format, count in format_counts.iteritems(): - summary_text += '{count} {format}. '.format(format=format, count=count) - - average_bitrate = sum([item[2] for item in items]) / len(items) - total_duration = sum([item[3] for item in items]) - summary_text += '{bitrate}K average bitrate. '.format(bitrate=int(average_bitrate/1000)) - summary_text += '{length}s total length. '.format(length=int(total_duration)) - - return summary_text - - - - -