Skip to content

Commit 3be5936

Browse files
committedMay 25, 2015
add merging functionality to duplicates plugin
This patch depends on %aunique not being present in config.paths for behavior as intended. The logic surrounding moving/copying a track from a duplicated album to the original one could probably be extracted out and put into library.Album. This is based on the ordering imposed by the tiebreaking facility introduced in 6be98b0. Once a natural duplicate ordering is in place, then the strategies for merging are: Items: Iterate through each available field: Iterate through each duplicate: If current duplicate has a field not set in the original, set it there Break Albums: Iterate through each duplicate: Iterate through each item in duplicate: If current item is not present in original, copy it there Continue
1 parent 26380b2 commit 3be5936

File tree

1 file changed

+70
-9
lines changed

1 file changed

+70
-9
lines changed
 

‎beetsplug/duplicates.py

+70-9
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(self):
4242
'format': '',
4343
'full': False,
4444
'keys': [],
45+
'merge': False,
4546
'move': '',
4647
'path': False,
4748
'tiebreak': {},
@@ -81,6 +82,10 @@ def __init__(self):
8182
callback=vararg_callback,
8283
help='report duplicates based on keys')
8384

85+
self._command.parser.add_option('-M', '--merge', dest='merge',
86+
action='store_true',
87+
help='merge duplicate items')
88+
8489
self._command.parser.add_option('-m', '--move', dest='move',
8590
action='store', metavar='DEST',
8691
help='move items to dest')
@@ -108,6 +113,7 @@ def _dup(lib, opts, args):
108113
fmt = self.config['format'].get(str)
109114
full = self.config['full'].get(bool)
110115
keys = self.config['keys'].get(list)
116+
merge = self.config['merge'].get(bool)
111117
move = self.config['move'].get(str)
112118
path = self.config['path'].get(bool)
113119
tiebreak = self.config['tiebreak'].get(dict)
@@ -143,10 +149,11 @@ def _dup(lib, opts, args):
143149
keys=keys,
144150
full=full,
145151
strict=strict,
146-
tiebreak=tiebreak):
152+
tiebreak=tiebreak,
153+
merge=merge):
147154
if obj_id: # Skip empty IDs.
148155
for o in objs:
149-
self._process_item(o, lib,
156+
self._process_item(o,
150157
copy=copy,
151158
move=move,
152159
delete=delete,
@@ -156,10 +163,11 @@ def _dup(lib, opts, args):
156163
self._command.func = _dup
157164
return [self._command]
158165

159-
def _process_item(self, item, lib, copy=False, move=False, delete=False,
166+
def _process_item(self, item, copy=False, move=False, delete=False,
160167
tag=False, fmt=''):
161-
"""Process Item `item` in `lib`.
168+
"""Process Item `item`.
162169
"""
170+
print_(format(item, fmt))
163171
if copy:
164172
item.move(basedir=copy, copy=True)
165173
item.store()
@@ -175,7 +183,6 @@ def _process_item(self, item, lib, copy=False, move=False, delete=False,
175183
raise UserError('%s: can\'t parse k=v tag: %s' % (PLUGIN, tag))
176184
setattr(item, k, v)
177185
item.store()
178-
print_(format(item, fmt))
179186

180187
def _checksum(self, item, prog):
181188
"""Run external `prog` on file path associated with `item`, cache
@@ -249,12 +256,66 @@ def _order(self, objs, tiebreak=None):
249256

250257
return sorted(objs, key=key, reverse=True)
251258

252-
def _duplicates(self, objs, keys, full, strict, tiebreak):
259+
def _merge_items(self, objs):
260+
"""Merge Item objs by copying missing fields from items in the tail to
261+
the head item.
262+
263+
Return same number of items, with the head item modified.
264+
"""
265+
fields = [f for sublist in Item.get_fields() for f in sublist]
266+
for f in fields:
267+
for o in objs[1:]:
268+
if getattr(objs[0], f, None) in (None, ''):
269+
value = getattr(o, f, None)
270+
if value:
271+
self._log.debug(u'key {0} on item {1} is null '
272+
'or empty: setting from item {2}',
273+
f, displayable_path(objs[0].path),
274+
displayable_path(o.path))
275+
setattr(objs[0], f, value)
276+
objs[0].store()
277+
break
278+
return objs
279+
280+
def _merge_albums(self, objs):
281+
"""Merge Album objs by copying missing items from albums in the tail
282+
to the head album.
283+
284+
Return same number of albums, with the head album modified."""
285+
ids = [i.mb_trackid for i in objs[0].items()]
286+
for o in objs[1:]:
287+
for i in o.items():
288+
if i.mb_trackid not in ids:
289+
missing = Item.from_path(i.path)
290+
missing.album_id = objs[0].id
291+
missing.add(i._db)
292+
self._log.debug(u'item {0} missing from album {1}:'
293+
' merging from {2} into {3}',
294+
missing,
295+
objs[0],
296+
displayable_path(o.path),
297+
displayable_path(missing.destination()))
298+
missing.move(copy=True)
299+
return objs
300+
301+
def _merge(self, objs):
302+
"""Merge duplicate items. See ``_merge_items`` and ``_merge_albums``
303+
for the relevant strategies.
304+
"""
305+
kind = Item if all(isinstance(o, Item) for o in objs) else Album
306+
if kind is Item:
307+
objs = self._merge_items(objs)
308+
else:
309+
objs = self._merge_albums(objs)
310+
return objs
311+
312+
def _duplicates(self, objs, keys, full, strict, tiebreak, merge):
253313
"""Generate triples of keys, duplicate counts, and constituent objects.
254314
"""
255315
offset = 0 if full else 1
256316
for k, objs in self._group_by(objs, keys, strict).iteritems():
257317
if len(objs) > 1:
258-
yield (k,
259-
len(objs) - offset,
260-
self._order(objs, tiebreak)[offset:])
318+
objs = self._order(objs, tiebreak)
319+
if merge:
320+
objs = self._merge(objs)
321+
yield (k, len(objs) - offset, objs[offset:])

0 commit comments

Comments
 (0)
Please sign in to comment.