Repository: beetbox/beets

Issue:
AssertionError in summarize_items() after crashed import
Steps to reproduce
1) launch an import session with a faulty listener on `database_change`
2) import an album
3) let it crash on `database_change` because of the faulty listener (while an empty album is being inserted)
4) launch a new import session
5) get the following traceback:

```
This album is already in the library!
Traceback (most recent call last):
  File "./beet", line 20, in <module>
    beets.ui.main()
  File "/home/bru/code/beets/beets/ui/__init__.py", line 1081, in main
    _raw_main(args)
  File "/home/bru/code/beets/beets/ui/__init__.py", line 1071, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/home/bru/code/beets/beets/ui/commands.py", line 876, in import_func
    import_files(lib, paths, query)
  File "/home/bru/code/beets/beets/ui/commands.py", line 853, in import_files
    session.run()
  File "/home/bru/code/beets/beets/importer.py", line 316, in run
    pl.run_parallel(QUEUE_SIZE)
  File "/home/bru/code/beets/beets/util/pipeline.py", line 301, in run
    out = self.coro.send(msg)
  File "/home/bru/code/beets/beets/util/pipeline.py", line 160, in coro
    task = func(*(args + (task,)))
  File "/home/bru/code/beets/beets/importer.py", line 1287, in user_query
    resolve_duplicates(session, task)
  File "/home/bru/code/beets/beets/importer.py", line 1299, in resolve_duplicates
    session.resolve_duplicate(task, found_duplicates)
  File "/home/bru/code/beets/beets/ui/commands.py", line 791, in resolve_duplicate
    not task.is_album,
  File "/home/bru/code/beets/beets/ui/commands.py", line 429, in summarize_items
    assert items, "summarizing zero items"
AssertionError: summarizing zero items
```

Previous album has no item, so the assertion fails.

AssertionError in summarize_items() after crashed import
Steps to reproduce
1) launch an import session with a faulty listener on `database_change`
2) import an album
3) let it crash on `database_change` because of the faulty listener (while an empty album is being inserted)
4) launch a new import session
5) get the following traceback:

```
This album is already in the library!
Traceback (most recent call last):
  File "./beet", line 20, in <module>
    beets.ui.main()
  File "/home/bru/code/beets/beets/ui/__init__.py", line 1081, in main
    _raw_main(args)
  File "/home/bru/code/beets/beets/ui/__init__.py", line 1071, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/home/bru/code/beets/beets/ui/commands.py", line 876, in import_func
    import_files(lib, paths, query)
  File "/home/bru/code/beets/beets/ui/commands.py", line 853, in import_files
    session.run()
  File "/home/bru/code/beets/beets/importer.py", line 316, in run
    pl.run_parallel(QUEUE_SIZE)
  File "/home/bru/code/beets/beets/util/pipeline.py", line 301, in run
    out = self.coro.send(msg)
  File "/home/bru/code/beets/beets/util/pipeline.py", line 160, in coro
    task = func(*(args + (task,)))
  File "/home/bru/code/beets/beets/importer.py", line 1287, in user_query
    resolve_duplicates(session, task)
  File "/home/bru/code/beets/beets/importer.py", line 1299, in resolve_duplicates
    session.resolve_duplicate(task, found_duplicates)
  File "/home/bru/code/beets/beets/ui/commands.py", line 791, in resolve_duplicate
    not task.is_album,
  File "/home/bru/code/beets/beets/ui/commands.py", line 429, in summarize_items
    assert items, "summarizing zero items"
AssertionError: summarizing zero items
```

Previous album has no item, so the assertion fails.



Discussion:

Resolving patch:
diff --git a/beets/ui/commands.py b/beets/ui/commands.py
--- a/beets/ui/commands.py
+++ b/beets/ui/commands.py
@@ -426,8 +426,6 @@ def summarize_items(items, singleton):
     this is an album or single-item import (if the latter, them `items`
     should only have one element).
     """
-    assert items, "summarizing zero items"
-
     summary_parts = []
     if not singleton:
         summary_parts.append("{0} items".format(len(items)))
@@ -439,16 +437,18 @@ def summarize_items(items, singleton):
         # A single format.
         summary_parts.append(items[0].format)
     else:
-        # Enumerate all the formats.
-        for fmt, count in format_counts.iteritems():
+        # Enumerate all the formats by decreasing frequencies:
+        for fmt, count in sorted(format_counts.items(),
+                                 key=lambda (f, c): (-c, f)):
             summary_parts.append('{0} {1}'.format(fmt, count))
 
-    average_bitrate = sum([item.bitrate for item in items]) / len(items)
-    total_duration = sum([item.length for item in items])
-    total_filesize = sum([item.filesize for item in items])
-    summary_parts.append('{0}kbps'.format(int(average_bitrate / 1000)))
-    summary_parts.append(ui.human_seconds_short(total_duration))
-    summary_parts.append(ui.human_bytes(total_filesize))
+    if items:
+        average_bitrate = sum([item.bitrate for item in items]) / len(items)
+        total_duration = sum([item.length for item in items])
+        total_filesize = sum([item.filesize for item in items])
+        summary_parts.append('{0}kbps'.format(int(average_bitrate / 1000)))
+        summary_parts.append(ui.human_seconds_short(total_duration))
+        summary_parts.append(ui.human_bytes(total_filesize))
 
     return ', '.join(summary_parts)
 
@@ -778,6 +778,16 @@ def resolve_duplicate(self, task, found_duplicates):
         log.warn(u"This {0} is already in the library!",
                  ("album" if task.is_album else "item"))
 
+        # skip empty albums (coming from a previous failed import session)
+        if task.is_album:
+            real_duplicates = filter(len, found_duplicates)
+            if not real_duplicates:
+                log.info("All duplicates are empty, we ignore them")
+                task.should_remove_duplicates = True
+                return
+        else:
+            real_duplicates = found_duplicates
+
         if config['import']['quiet']:
             # In quiet mode, don't prompt -- just skip.
             log.info(u'Skipping.')
@@ -785,11 +795,17 @@ def resolve_duplicate(self, task, found_duplicates):
         else:
             # Print some detail about the existing and new items so the
             # user can make an informed decision.
-            for duplicate in found_duplicates:
+            for duplicate in real_duplicates:
                 print("Old: " + summarize_items(
                     list(duplicate.items()) if task.is_album else [duplicate],
                     not task.is_album,
                 ))
+
+            if real_duplicates != found_duplicates:  # there's empty albums
+                count = len(found_duplicates) - len(real_duplicates)
+                print("Old: {0} empty album{1}".format(
+                    count, "s" if count > 1 else ""))
+
             print("New: " + summarize_items(
                 task.imported_items(),
                 not task.is_album,


Test patch:
diff --git a/test/test_ui.py b/test/test_ui.py
--- a/test/test_ui.py
+++ b/test/test_ui.py
@@ -22,7 +22,9 @@
 import re
 import subprocess
 import platform
+from copy import deepcopy
 
+from mock import patch
 from test import _common
 from test._common import unittest
 from test.helper import capture_stdout, has_program, TestHelper, control_stdin
@@ -972,6 +974,43 @@ def test_item_data_change_title_missing_with_unicode_filename(self):
                         u'caf.mp3 ->' in msg)
 
 
+class SummarizeItemsTest(_common.TestCase):
+    def setUp(self):
+        super(SummarizeItemsTest, self).setUp()
+        item = library.Item()
+        item.bitrate = 4321
+        item.length = 10 * 60 + 54
+        item.format = "F"
+        self.item = item
+        fsize_mock = patch('beets.library.Item.try_filesize').start()
+        fsize_mock.return_value = 987
+
+    def test_summarize_item(self):
+        summary = commands.summarize_items([], True)
+        self.assertEqual(summary, "")
+
+        summary = commands.summarize_items([self.item], True)
+        self.assertEqual(summary, "F, 4kbps, 10:54, 987.0 B")
+
+    def test_summarize_items(self):
+        summary = commands.summarize_items([], False)
+        self.assertEqual(summary, "0 items")
+
+        summary = commands.summarize_items([self.item], False)
+        self.assertEqual(summary, "1 items, F, 4kbps, 10:54, 987.0 B")
+
+        i2 = deepcopy(self.item)
+        summary = commands.summarize_items([self.item, i2], False)
+        self.assertEqual(summary, "2 items, F, 4kbps, 21:48, 1.9 KB")
+
+        i2.format = "G"
+        summary = commands.summarize_items([self.item, i2], False)
+        self.assertEqual(summary, "2 items, F 1, G 1, 4kbps, 21:48, 1.9 KB")
+
+        summary = commands.summarize_items([self.item, i2, i2], False)
+        self.assertEqual(summary, "3 items, G 2, F 1, 4kbps, 32:42, 2.9 KB")
+
+
 class PathFormatTest(_common.TestCase):
     def test_custom_paths_prepend(self):
         default_formats = ui.get_path_formats()