We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
duplicates: Do not report duplicates when only `mb_albumid` is duplicated
```
$ beet duplicates -F
/home/simon/Música/2 Unlimited/Get Ready/03 - The Magic Friend.mp3
/home/simon/Música/2 Unlimited/Get Ready/04 - Contrast.mp3
/home/simon/Música/2 Unlimited/Get Ready/05 - Desire.mp3
/home/simon/Música/2 Unlimited/Get Ready/06 - Pacific Walk.mp3
/home/simon/Música/2 Unlimited/Get Ready/07 - Workaholic.mp3
/home/simon/Música/2 Unlimited/Get Ready/08 - Rougher Than The Average.mp3
/home/simon/Música/2 Unlimited/Get Ready/09 - Delight.mp3
/home/simon/Música/2 Unlimited/Get Ready/10 - Eternally Yours.mp3
/home/simon/Música/2 Unlimited/Get Ready/11 - Twilight Zone (Rave Version Edit).mp3
/home/simon/Música/2 Unlimited/Get Ready/12 - Get Ready For This (Vocal Version).mp3
/home/simon/Música/2 Unlimited/Get Ready/13 - The Magic Friend (Instrumental).mp3
/home/simon/Música/2 Unlimited/Get Ready/14 - Workaholic (Instrumental).mp3
```

```
$ ll Música/2\ Unlimited/Get\ Ready/
total 83M
-rw-rw-r-- 1 simon simon 7,5M oct 10 16:00 03 - The Magic Friend.mp3
-rw-rw-r-- 1 simon simon 6,4M oct 10 16:00 04 - Contrast.mp3
-rw-rw-r-- 1 simon simon 7,9M oct 10 16:00 05 - Desire.mp3
-rw-rw-r-- 1 simon simon 5,7M oct 10 16:00 06 - Pacific Walk.mp3
-rw-rw-r-- 1 simon simon 7,2M oct 10 16:00 07 - Workaholic.mp3
-rw-rw-r-- 1 simon simon 7,6M oct 10 16:00 08 - Rougher Than The Average.mp3
-rw-rw-r-- 1 simon simon 6,5M oct 10 16:00 09 - Delight.mp3
-rw-rw-r-- 1 simon simon 7,6M oct 10 16:00 10 - Eternally Yours.mp3
-rw-rw-r-- 1 simon simon 7,1M oct 10 16:00 11 - Twilight Zone (Rave Version Edit).mp3
-rw-rw-r-- 1 simon simon 6,4M oct 10 16:00 12 - Get Ready For This (Vocal Version).mp3
-rw-rw-r-- 1 simon simon 5,9M oct 10 16:00 13 - The Magic Friend (Instrumental).mp3
-rw-rw-r-- 1 simon simon 7,2M oct 10 16:00 14 - Workaholic (Instrumental).mp3
-rw-rw-r-- 1 simon simon  30K oct 10 16:00 cover.jpg
```

What is the duplicate of that?


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `beetsplug/duplicates.py`
Content:
```
1 # This file is part of beets.
2 # Copyright 2015, Pedro Silva.
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish,
8 # distribute, sublicense, and/or sell copies of the Software, and to
9 # permit persons to whom the Software is furnished to do so, subject to
10 # the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be
13 # included in all copies or substantial portions of the Software.
14 
15 """List duplicate tracks or albums.
16 """
17 from __future__ import (division, absolute_import, print_function,
18                         unicode_literals)
19 
20 import shlex
21 
22 from beets.plugins import BeetsPlugin
23 from beets.ui import decargs, print_, vararg_callback, Subcommand, UserError
24 from beets.util import command_output, displayable_path, subprocess
25 
26 PLUGIN = 'duplicates'
27 
28 
29 def _process_item(item, lib, copy=False, move=False, delete=False,
30                   tag=False, fmt=''):
31     """Process Item `item` in `lib`.
32     """
33     if copy:
34         item.move(basedir=copy, copy=True)
35         item.store()
36     if move:
37         item.move(basedir=move, copy=False)
38         item.store()
39     if delete:
40         item.remove(delete=True)
41     if tag:
42         try:
43             k, v = tag.split('=')
44         except:
45             raise UserError('%s: can\'t parse k=v tag: %s' % (PLUGIN, tag))
46         setattr(k, v)
47         item.store()
48     print_(format(item, fmt))
49 
50 
51 def _checksum(item, prog, log):
52     """Run external `prog` on file path associated with `item`, cache
53     output as flexattr on a key that is the name of the program, and
54     return the key, checksum tuple.
55     """
56     args = [p.format(file=item.path) for p in shlex.split(prog)]
57     key = args[0]
58     checksum = getattr(item, key, False)
59     if not checksum:
60         log.debug(u'{0}: key {1} on item {2} not cached: computing checksum',
61                   PLUGIN, key, displayable_path(item.path))
62         try:
63             checksum = command_output(args)
64             setattr(item, key, checksum)
65             item.store()
66             log.debug(u'{0}: computed checksum for {1} using {2}',
67                       PLUGIN, item.title, key)
68         except subprocess.CalledProcessError as e:
69             log.debug(u'{0}: failed to checksum {1}: {2}',
70                       PLUGIN, displayable_path(item.path), e)
71     else:
72         log.debug(u'{0}: key {1} on item {2} cached: not computing checksum',
73                   PLUGIN, key, displayable_path(item.path))
74     return key, checksum
75 
76 
77 def _group_by(objs, keys, log):
78     """Return a dictionary with keys arbitrary concatenations of attributes and
79     values lists of objects (Albums or Items) with those keys.
80     """
81     import collections
82     counts = collections.defaultdict(list)
83     for obj in objs:
84         values = [getattr(obj, k, None) for k in keys]
85         values = [v for v in values if v not in (None, '')]
86         if values:
87             key = '\001'.join(values)
88             counts[key].append(obj)
89         else:
90             log.debug(u'{0}: all keys {1} on item {2} are null: skipping',
91                       PLUGIN, keys, displayable_path(obj.path))
92 
93     return counts
94 
95 
96 def _duplicates(objs, keys, full, log):
97     """Generate triples of keys, duplicate counts, and constituent objects.
98     """
99     offset = 0 if full else 1
100     for k, objs in _group_by(objs, keys, log).iteritems():
101         if len(objs) > 1:
102             yield (k, len(objs) - offset, objs[offset:])
103 
104 
105 class DuplicatesPlugin(BeetsPlugin):
106     """List duplicate tracks or albums
107     """
108     def __init__(self):
109         super(DuplicatesPlugin, self).__init__()
110 
111         self.config.add({
112             'format': '',
113             'count': False,
114             'album': False,
115             'full': False,
116             'path': False,
117             'keys': ['mb_trackid', 'mb_albumid'],
118             'checksum': None,
119             'copy': False,
120             'move': False,
121             'delete': False,
122             'tag': False,
123         })
124 
125         self._command = Subcommand('duplicates',
126                                    help=__doc__,
127                                    aliases=['dup'])
128         self._command.parser.add_option('-c', '--count', dest='count',
129                                         action='store_true',
130                                         help='show duplicate counts')
131 
132         self._command.parser.add_option('-C', '--checksum', dest='checksum',
133                                         action='store', metavar='PROG',
134                                         help='report duplicates based on'
135                                         ' arbitrary command')
136 
137         self._command.parser.add_option('-d', '--delete', dest='delete',
138                                         action='store_true',
139                                         help='delete items from library and '
140                                         'disk')
141 
142         self._command.parser.add_option('-F', '--full', dest='full',
143                                         action='store_true',
144                                         help='show all versions of duplicate'
145                                         ' tracks or albums')
146 
147         self._command.parser.add_option('-k', '--keys', dest='keys',
148                                         action='callback', metavar='KEY1 KEY2',
149                                         callback=vararg_callback,
150                                         help='report duplicates based on keys')
151 
152         self._command.parser.add_option('-m', '--move', dest='move',
153                                         action='store', metavar='DEST',
154                                         help='move items to dest')
155 
156         self._command.parser.add_option('-o', '--copy', dest='copy',
157                                         action='store', metavar='DEST',
158                                         help='copy items to dest')
159 
160         self._command.parser.add_option('-t', '--tag', dest='tag',
161                                         action='store',
162                                         help='tag matched items with \'k=v\''
163                                         ' attribute')
164         self._command.parser.add_all_common_options()
165 
166     def commands(self):
167 
168         def _dup(lib, opts, args):
169             self.config.set_args(opts)
170             fmt = self.config['format'].get()
171             album = self.config['album'].get(bool)
172             full = self.config['full'].get(bool)
173             keys = self.config['keys'].get()
174             checksum = self.config['checksum'].get()
175             copy = self.config['copy'].get()
176             move = self.config['move'].get()
177             delete = self.config['delete'].get(bool)
178             tag = self.config['tag'].get()
179 
180             if album:
181                 keys = ['mb_albumid']
182                 items = lib.albums(decargs(args))
183             else:
184                 items = lib.items(decargs(args))
185 
186             if self.config['path']:
187                 fmt = '$path'
188 
189             # Default format string for count mode.
190             if self.config['count'] and not fmt:
191                 if album:
192                     fmt = '$albumartist - $album'
193                 else:
194                     fmt = '$albumartist - $album - $title'
195                 fmt += ': {0}'
196 
197             if checksum:
198                 if not isinstance(checksum, basestring):
199                     raise UserError(
200                         'duplicates: "checksum" option must be a command'
201                     )
202                 for i in items:
203                     k, _ = self._checksum(i, checksum, self._log)
204                 keys = [k]
205 
206             for obj_id, obj_count, objs in _duplicates(items,
207                                                        keys=keys,
208                                                        full=full,
209                                                        log=self._log):
210                 if obj_id:  # Skip empty IDs.
211                     for o in objs:
212                         _process_item(o, lib,
213                                       copy=copy,
214                                       move=move,
215                                       delete=delete,
216                                       tag=tag,
217                                       fmt=fmt.format(obj_count))
218 
219         self._command.func = _dup
220         return [self._command]
221
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py
--- a/beetsplug/duplicates.py
+++ b/beetsplug/duplicates.py
@@ -74,30 +74,37 @@
     return key, checksum
 
 
-def _group_by(objs, keys, log):
+def _group_by(objs, keys, strict, log):
     """Return a dictionary with keys arbitrary concatenations of attributes and
     values lists of objects (Albums or Items) with those keys.
+
+    If strict, all attributes must be defined for a duplicate match.
     """
     import collections
     counts = collections.defaultdict(list)
     for obj in objs:
         values = [getattr(obj, k, None) for k in keys]
         values = [v for v in values if v not in (None, '')]
-        if values:
+        if strict and len(values) < len(keys):
+            log.debug(u'{0}: some keys {1} on item {2} are null or empty: '
+                      'skipping',
+                      PLUGIN, keys, displayable_path(obj.path))
+        elif (not strict and not len(values)):
+            log.debug(u'{0}: all keys {1} on item {2} are null or empty: '
+                      'skipping',
+                      PLUGIN, keys, displayable_path(obj.path))
+        else:
             key = '\001'.join(values)
             counts[key].append(obj)
-        else:
-            log.debug(u'{0}: all keys {1} on item {2} are null: skipping',
-                      PLUGIN, keys, displayable_path(obj.path))
 
     return counts
 
 
-def _duplicates(objs, keys, full, log):
+def _duplicates(objs, keys, full, strict, log):
     """Generate triples of keys, duplicate counts, and constituent objects.
     """
     offset = 0 if full else 1
-    for k, objs in _group_by(objs, keys, log).iteritems():
+    for k, objs in _group_by(objs, keys, strict, log).iteritems():
         if len(objs) > 1:
             yield (k, len(objs) - offset, objs[offset:])
 
@@ -113,6 +120,7 @@
             'count': False,
             'album': False,
             'full': False,
+            'strict': False,
             'path': False,
             'keys': ['mb_trackid', 'mb_albumid'],
             'checksum': None,
@@ -144,6 +152,11 @@
                                         help='show all versions of duplicate'
                                         ' tracks or albums')
 
+        self._command.parser.add_option('-s', '--strict', dest='strict',
+                                        action='store_true',
+                                        help='report duplicates only if all'
+                                        ' attributes are set')
+
         self._command.parser.add_option('-k', '--keys', dest='keys',
                                         action='callback', metavar='KEY1 KEY2',
                                         callback=vararg_callback,
@@ -170,6 +183,7 @@
             fmt = self.config['format'].get()
             album = self.config['album'].get(bool)
             full = self.config['full'].get(bool)
+            strict = self.config['strict'].get(bool)
             keys = self.config['keys'].get()
             checksum = self.config['checksum'].get()
             copy = self.config['copy'].get()
@@ -206,6 +220,7 @@
             for obj_id, obj_count, objs in _duplicates(items,
                                                        keys=keys,
                                                        full=full,
+                                                       strict=strict,
                                                        log=self._log):
                 if obj_id:  # Skip empty IDs.
                     for o in objs:

{"golden_diff": "diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py\n--- a/beetsplug/duplicates.py\n+++ b/beetsplug/duplicates.py\n@@ -74,30 +74,37 @@\n     return key, checksum\n \n \n-def _group_by(objs, keys, log):\n+def _group_by(objs, keys, strict, log):\n     \"\"\"Return a dictionary with keys arbitrary concatenations of attributes and\n     values lists of objects (Albums or Items) with those keys.\n+\n+    If strict, all attributes must be defined for a duplicate match.\n     \"\"\"\n     import collections\n     counts = collections.defaultdict(list)\n     for obj in objs:\n         values = [getattr(obj, k, None) for k in keys]\n         values = [v for v in values if v not in (None, '')]\n-        if values:\n+        if strict and len(values) < len(keys):\n+            log.debug(u'{0}: some keys {1} on item {2} are null or empty: '\n+                      'skipping',\n+                      PLUGIN, keys, displayable_path(obj.path))\n+        elif (not strict and not len(values)):\n+            log.debug(u'{0}: all keys {1} on item {2} are null or empty: '\n+                      'skipping',\n+                      PLUGIN, keys, displayable_path(obj.path))\n+        else:\n             key = '\\001'.join(values)\n             counts[key].append(obj)\n-        else:\n-            log.debug(u'{0}: all keys {1} on item {2} are null: skipping',\n-                      PLUGIN, keys, displayable_path(obj.path))\n \n     return counts\n \n \n-def _duplicates(objs, keys, full, log):\n+def _duplicates(objs, keys, full, strict, log):\n     \"\"\"Generate triples of keys, duplicate counts, and constituent objects.\n     \"\"\"\n     offset = 0 if full else 1\n-    for k, objs in _group_by(objs, keys, log).iteritems():\n+    for k, objs in _group_by(objs, keys, strict, log).iteritems():\n         if len(objs) > 1:\n             yield (k, len(objs) - offset, objs[offset:])\n \n@@ -113,6 +120,7 @@\n             'count': False,\n             'album': False,\n             'full': False,\n+            'strict': False,\n             'path': False,\n             'keys': ['mb_trackid', 'mb_albumid'],\n             'checksum': None,\n@@ -144,6 +152,11 @@\n                                         help='show all versions of duplicate'\n                                         ' tracks or albums')\n \n+        self._command.parser.add_option('-s', '--strict', dest='strict',\n+                                        action='store_true',\n+                                        help='report duplicates only if all'\n+                                        ' attributes are set')\n+\n         self._command.parser.add_option('-k', '--keys', dest='keys',\n                                         action='callback', metavar='KEY1 KEY2',\n                                         callback=vararg_callback,\n@@ -170,6 +183,7 @@\n             fmt = self.config['format'].get()\n             album = self.config['album'].get(bool)\n             full = self.config['full'].get(bool)\n+            strict = self.config['strict'].get(bool)\n             keys = self.config['keys'].get()\n             checksum = self.config['checksum'].get()\n             copy = self.config['copy'].get()\n@@ -206,6 +220,7 @@\n             for obj_id, obj_count, objs in _duplicates(items,\n                                                        keys=keys,\n                                                        full=full,\n+                                                       strict=strict,\n                                                        log=self._log):\n                 if obj_id:  # Skip empty IDs.\n                     for o in objs:\n", "issue": "duplicates: Do not report duplicates when only `mb_albumid` is duplicated\n```\n$ beet duplicates -F\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/03 - The Magic Friend.mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/04 - Contrast.mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/05 - Desire.mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/06 - Pacific Walk.mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/07 - Workaholic.mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/08 - Rougher Than The Average.mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/09 - Delight.mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/10 - Eternally Yours.mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/11 - Twilight Zone (Rave Version Edit).mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/12 - Get Ready For This (Vocal Version).mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/13 - The Magic Friend (Instrumental).mp3\n/home/simon/M\u00fasica/2 Unlimited/Get Ready/14 - Workaholic (Instrumental).mp3\n```\n\n```\n$ ll M\u00fasica/2\\ Unlimited/Get\\ Ready/\ntotal 83M\n-rw-rw-r-- 1 simon simon 7,5M oct 10 16:00 03 - The Magic Friend.mp3\n-rw-rw-r-- 1 simon simon 6,4M oct 10 16:00 04 - Contrast.mp3\n-rw-rw-r-- 1 simon simon 7,9M oct 10 16:00 05 - Desire.mp3\n-rw-rw-r-- 1 simon simon 5,7M oct 10 16:00 06 - Pacific Walk.mp3\n-rw-rw-r-- 1 simon simon 7,2M oct 10 16:00 07 - Workaholic.mp3\n-rw-rw-r-- 1 simon simon 7,6M oct 10 16:00 08 - Rougher Than The Average.mp3\n-rw-rw-r-- 1 simon simon 6,5M oct 10 16:00 09 - Delight.mp3\n-rw-rw-r-- 1 simon simon 7,6M oct 10 16:00 10 - Eternally Yours.mp3\n-rw-rw-r-- 1 simon simon 7,1M oct 10 16:00 11 - Twilight Zone (Rave Version Edit).mp3\n-rw-rw-r-- 1 simon simon 6,4M oct 10 16:00 12 - Get Ready For This (Vocal Version).mp3\n-rw-rw-r-- 1 simon simon 5,9M oct 10 16:00 13 - The Magic Friend (Instrumental).mp3\n-rw-rw-r-- 1 simon simon 7,2M oct 10 16:00 14 - Workaholic (Instrumental).mp3\n-rw-rw-r-- 1 simon simon  30K oct 10 16:00 cover.jpg\n```\n\nWhat is the duplicate of that?\n\n", "before_files": [{"content": "# This file is part of beets.\n# Copyright 2015, Pedro Silva.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"List duplicate tracks or albums.\n\"\"\"\nfrom __future__ import (division, absolute_import, print_function,\n                        unicode_literals)\n\nimport shlex\n\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import decargs, print_, vararg_callback, Subcommand, UserError\nfrom beets.util import command_output, displayable_path, subprocess\n\nPLUGIN = 'duplicates'\n\n\ndef _process_item(item, lib, copy=False, move=False, delete=False,\n                  tag=False, fmt=''):\n    \"\"\"Process Item `item` in `lib`.\n    \"\"\"\n    if copy:\n        item.move(basedir=copy, copy=True)\n        item.store()\n    if move:\n        item.move(basedir=move, copy=False)\n        item.store()\n    if delete:\n        item.remove(delete=True)\n    if tag:\n        try:\n            k, v = tag.split('=')\n        except:\n            raise UserError('%s: can\\'t parse k=v tag: %s' % (PLUGIN, tag))\n        setattr(k, v)\n        item.store()\n    print_(format(item, fmt))\n\n\ndef _checksum(item, prog, log):\n    \"\"\"Run external `prog` on file path associated with `item`, cache\n    output as flexattr on a key that is the name of the program, and\n    return the key, checksum tuple.\n    \"\"\"\n    args = [p.format(file=item.path) for p in shlex.split(prog)]\n    key = args[0]\n    checksum = getattr(item, key, False)\n    if not checksum:\n        log.debug(u'{0}: key {1} on item {2} not cached: computing checksum',\n                  PLUGIN, key, displayable_path(item.path))\n        try:\n            checksum = command_output(args)\n            setattr(item, key, checksum)\n            item.store()\n            log.debug(u'{0}: computed checksum for {1} using {2}',\n                      PLUGIN, item.title, key)\n        except subprocess.CalledProcessError as e:\n            log.debug(u'{0}: failed to checksum {1}: {2}',\n                      PLUGIN, displayable_path(item.path), e)\n    else:\n        log.debug(u'{0}: key {1} on item {2} cached: not computing checksum',\n                  PLUGIN, key, displayable_path(item.path))\n    return key, checksum\n\n\ndef _group_by(objs, keys, log):\n    \"\"\"Return a dictionary with keys arbitrary concatenations of attributes and\n    values lists of objects (Albums or Items) with those keys.\n    \"\"\"\n    import collections\n    counts = collections.defaultdict(list)\n    for obj in objs:\n        values = [getattr(obj, k, None) for k in keys]\n        values = [v for v in values if v not in (None, '')]\n        if values:\n            key = '\\001'.join(values)\n            counts[key].append(obj)\n        else:\n            log.debug(u'{0}: all keys {1} on item {2} are null: skipping',\n                      PLUGIN, keys, displayable_path(obj.path))\n\n    return counts\n\n\ndef _duplicates(objs, keys, full, log):\n    \"\"\"Generate triples of keys, duplicate counts, and constituent objects.\n    \"\"\"\n    offset = 0 if full else 1\n    for k, objs in _group_by(objs, keys, log).iteritems():\n        if len(objs) > 1:\n            yield (k, len(objs) - offset, objs[offset:])\n\n\nclass DuplicatesPlugin(BeetsPlugin):\n    \"\"\"List duplicate tracks or albums\n    \"\"\"\n    def __init__(self):\n        super(DuplicatesPlugin, self).__init__()\n\n        self.config.add({\n            'format': '',\n            'count': False,\n            'album': False,\n            'full': False,\n            'path': False,\n            'keys': ['mb_trackid', 'mb_albumid'],\n            'checksum': None,\n            'copy': False,\n            'move': False,\n            'delete': False,\n            'tag': False,\n        })\n\n        self._command = Subcommand('duplicates',\n                                   help=__doc__,\n                                   aliases=['dup'])\n        self._command.parser.add_option('-c', '--count', dest='count',\n                                        action='store_true',\n                                        help='show duplicate counts')\n\n        self._command.parser.add_option('-C', '--checksum', dest='checksum',\n                                        action='store', metavar='PROG',\n                                        help='report duplicates based on'\n                                        ' arbitrary command')\n\n        self._command.parser.add_option('-d', '--delete', dest='delete',\n                                        action='store_true',\n                                        help='delete items from library and '\n                                        'disk')\n\n        self._command.parser.add_option('-F', '--full', dest='full',\n                                        action='store_true',\n                                        help='show all versions of duplicate'\n                                        ' tracks or albums')\n\n        self._command.parser.add_option('-k', '--keys', dest='keys',\n                                        action='callback', metavar='KEY1 KEY2',\n                                        callback=vararg_callback,\n                                        help='report duplicates based on keys')\n\n        self._command.parser.add_option('-m', '--move', dest='move',\n                                        action='store', metavar='DEST',\n                                        help='move items to dest')\n\n        self._command.parser.add_option('-o', '--copy', dest='copy',\n                                        action='store', metavar='DEST',\n                                        help='copy items to dest')\n\n        self._command.parser.add_option('-t', '--tag', dest='tag',\n                                        action='store',\n                                        help='tag matched items with \\'k=v\\''\n                                        ' attribute')\n        self._command.parser.add_all_common_options()\n\n    def commands(self):\n\n        def _dup(lib, opts, args):\n            self.config.set_args(opts)\n            fmt = self.config['format'].get()\n            album = self.config['album'].get(bool)\n            full = self.config['full'].get(bool)\n            keys = self.config['keys'].get()\n            checksum = self.config['checksum'].get()\n            copy = self.config['copy'].get()\n            move = self.config['move'].get()\n            delete = self.config['delete'].get(bool)\n            tag = self.config['tag'].get()\n\n            if album:\n                keys = ['mb_albumid']\n                items = lib.albums(decargs(args))\n            else:\n                items = lib.items(decargs(args))\n\n            if self.config['path']:\n                fmt = '$path'\n\n            # Default format string for count mode.\n            if self.config['count'] and not fmt:\n                if album:\n                    fmt = '$albumartist - $album'\n                else:\n                    fmt = '$albumartist - $album - $title'\n                fmt += ': {0}'\n\n            if checksum:\n                if not isinstance(checksum, basestring):\n                    raise UserError(\n                        'duplicates: \"checksum\" option must be a command'\n                    )\n                for i in items:\n                    k, _ = self._checksum(i, checksum, self._log)\n                keys = [k]\n\n            for obj_id, obj_count, objs in _duplicates(items,\n                                                       keys=keys,\n                                                       full=full,\n                                                       log=self._log):\n                if obj_id:  # Skip empty IDs.\n                    for o in objs:\n                        _process_item(o, lib,\n                                      copy=copy,\n                                      move=move,\n                                      delete=delete,\n                                      tag=tag,\n                                      fmt=fmt.format(obj_count))\n\n        self._command.func = _dup\n        return [self._command]\n", "path": "beetsplug/duplicates.py"}], "after_files": [{"content": "# This file is part of beets.\n# Copyright 2015, Pedro Silva.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"List duplicate tracks or albums.\n\"\"\"\nfrom __future__ import (division, absolute_import, print_function,\n                        unicode_literals)\n\nimport shlex\n\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import decargs, print_, vararg_callback, Subcommand, UserError\nfrom beets.util import command_output, displayable_path, subprocess\n\nPLUGIN = 'duplicates'\n\n\ndef _process_item(item, lib, copy=False, move=False, delete=False,\n                  tag=False, fmt=''):\n    \"\"\"Process Item `item` in `lib`.\n    \"\"\"\n    if copy:\n        item.move(basedir=copy, copy=True)\n        item.store()\n    if move:\n        item.move(basedir=move, copy=False)\n        item.store()\n    if delete:\n        item.remove(delete=True)\n    if tag:\n        try:\n            k, v = tag.split('=')\n        except:\n            raise UserError('%s: can\\'t parse k=v tag: %s' % (PLUGIN, tag))\n        setattr(k, v)\n        item.store()\n    print_(format(item, fmt))\n\n\ndef _checksum(item, prog, log):\n    \"\"\"Run external `prog` on file path associated with `item`, cache\n    output as flexattr on a key that is the name of the program, and\n    return the key, checksum tuple.\n    \"\"\"\n    args = [p.format(file=item.path) for p in shlex.split(prog)]\n    key = args[0]\n    checksum = getattr(item, key, False)\n    if not checksum:\n        log.debug(u'{0}: key {1} on item {2} not cached: computing checksum',\n                  PLUGIN, key, displayable_path(item.path))\n        try:\n            checksum = command_output(args)\n            setattr(item, key, checksum)\n            item.store()\n            log.debug(u'{0}: computed checksum for {1} using {2}',\n                      PLUGIN, item.title, key)\n        except subprocess.CalledProcessError as e:\n            log.debug(u'{0}: failed to checksum {1}: {2}',\n                      PLUGIN, displayable_path(item.path), e)\n    else:\n        log.debug(u'{0}: key {1} on item {2} cached: not computing checksum',\n                  PLUGIN, key, displayable_path(item.path))\n    return key, checksum\n\n\ndef _group_by(objs, keys, strict, log):\n    \"\"\"Return a dictionary with keys arbitrary concatenations of attributes and\n    values lists of objects (Albums or Items) with those keys.\n\n    If strict, all attributes must be defined for a duplicate match.\n    \"\"\"\n    import collections\n    counts = collections.defaultdict(list)\n    for obj in objs:\n        values = [getattr(obj, k, None) for k in keys]\n        values = [v for v in values if v not in (None, '')]\n        if strict and len(values) < len(keys):\n            log.debug(u'{0}: some keys {1} on item {2} are null or empty: '\n                      'skipping',\n                      PLUGIN, keys, displayable_path(obj.path))\n        elif (not strict and not len(values)):\n            log.debug(u'{0}: all keys {1} on item {2} are null or empty: '\n                      'skipping',\n                      PLUGIN, keys, displayable_path(obj.path))\n        else:\n            key = '\\001'.join(values)\n            counts[key].append(obj)\n\n    return counts\n\n\ndef _duplicates(objs, keys, full, strict, log):\n    \"\"\"Generate triples of keys, duplicate counts, and constituent objects.\n    \"\"\"\n    offset = 0 if full else 1\n    for k, objs in _group_by(objs, keys, strict, log).iteritems():\n        if len(objs) > 1:\n            yield (k, len(objs) - offset, objs[offset:])\n\n\nclass DuplicatesPlugin(BeetsPlugin):\n    \"\"\"List duplicate tracks or albums\n    \"\"\"\n    def __init__(self):\n        super(DuplicatesPlugin, self).__init__()\n\n        self.config.add({\n            'format': '',\n            'count': False,\n            'album': False,\n            'full': False,\n            'strict': False,\n            'path': False,\n            'keys': ['mb_trackid', 'mb_albumid'],\n            'checksum': None,\n            'copy': False,\n            'move': False,\n            'delete': False,\n            'tag': False,\n        })\n\n        self._command = Subcommand('duplicates',\n                                   help=__doc__,\n                                   aliases=['dup'])\n        self._command.parser.add_option('-c', '--count', dest='count',\n                                        action='store_true',\n                                        help='show duplicate counts')\n\n        self._command.parser.add_option('-C', '--checksum', dest='checksum',\n                                        action='store', metavar='PROG',\n                                        help='report duplicates based on'\n                                        ' arbitrary command')\n\n        self._command.parser.add_option('-d', '--delete', dest='delete',\n                                        action='store_true',\n                                        help='delete items from library and '\n                                        'disk')\n\n        self._command.parser.add_option('-F', '--full', dest='full',\n                                        action='store_true',\n                                        help='show all versions of duplicate'\n                                        ' tracks or albums')\n\n        self._command.parser.add_option('-s', '--strict', dest='strict',\n                                        action='store_true',\n                                        help='report duplicates only if all'\n                                        ' attributes are set')\n\n        self._command.parser.add_option('-k', '--keys', dest='keys',\n                                        action='callback', metavar='KEY1 KEY2',\n                                        callback=vararg_callback,\n                                        help='report duplicates based on keys')\n\n        self._command.parser.add_option('-m', '--move', dest='move',\n                                        action='store', metavar='DEST',\n                                        help='move items to dest')\n\n        self._command.parser.add_option('-o', '--copy', dest='copy',\n                                        action='store', metavar='DEST',\n                                        help='copy items to dest')\n\n        self._command.parser.add_option('-t', '--tag', dest='tag',\n                                        action='store',\n                                        help='tag matched items with \\'k=v\\''\n                                        ' attribute')\n        self._command.parser.add_all_common_options()\n\n    def commands(self):\n\n        def _dup(lib, opts, args):\n            self.config.set_args(opts)\n            fmt = self.config['format'].get()\n            album = self.config['album'].get(bool)\n            full = self.config['full'].get(bool)\n            strict = self.config['strict'].get(bool)\n            keys = self.config['keys'].get()\n            checksum = self.config['checksum'].get()\n            copy = self.config['copy'].get()\n            move = self.config['move'].get()\n            delete = self.config['delete'].get(bool)\n            tag = self.config['tag'].get()\n\n            if album:\n                keys = ['mb_albumid']\n                items = lib.albums(decargs(args))\n            else:\n                items = lib.items(decargs(args))\n\n            if self.config['path']:\n                fmt = '$path'\n\n            # Default format string for count mode.\n            if self.config['count'] and not fmt:\n                if album:\n                    fmt = '$albumartist - $album'\n                else:\n                    fmt = '$albumartist - $album - $title'\n                fmt += ': {0}'\n\n            if checksum:\n                if not isinstance(checksum, basestring):\n                    raise UserError(\n                        'duplicates: \"checksum\" option must be a command'\n                    )\n                for i in items:\n                    k, _ = self._checksum(i, checksum, self._log)\n                keys = [k]\n\n            for obj_id, obj_count, objs in _duplicates(items,\n                                                       keys=keys,\n                                                       full=full,\n                                                       strict=strict,\n                                                       log=self._log):\n                if obj_id:  # Skip empty IDs.\n                    for o in objs:\n                        _process_item(o, lib,\n                                      copy=copy,\n                                      move=move,\n                                      delete=delete,\n                                      tag=tag,\n                                      fmt=fmt.format(obj_count))\n\n        self._command.func = _dup\n        return [self._command]\n", "path": "beetsplug/duplicates.py"}]}