From 95f38dbe5263bc7fa07116747f49cb8515677449 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 23 Oct 2011 14:12:13 -0700 Subject: [PATCH] "info dictionaries" replaced with AlbumInfo and TrackInfo --- beets/autotag/__init__.py | 67 +++++++------- beets/autotag/art.py | 6 +- beets/autotag/hooks.py | 8 +- beets/autotag/match.py | 42 ++++----- beets/autotag/mb.py | 92 +++++++++---------- beets/importer.py | 8 +- beets/plugins.py | 8 +- beets/ui/commands.py | 22 ++--- test/test_art.py | 9 +- test/test_autotag.py | 187 +++++++++++++++++++------------------- test/test_importer.py | 32 ++++--- test/test_mb.py | 68 +++++++------- 12 files changed, 275 insertions(+), 274 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 9f76155be..e05c16df5 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -22,6 +22,7 @@ from beets.util import sorted_walk # Parts of external interface. from .hooks import AlbumInfo, TrackInfo +from .match import AutotagError from .match import tag_item, tag_album from .match import RECOMMEND_STRONG, RECOMMEND_MEDIUM, RECOMMEND_NONE from .match import STRONG_REC_THRESH, MEDIUM_REC_THRESH, REC_GAP_THRESH @@ -55,54 +56,54 @@ def albums_in_dir(path): if items: yield root, items -def apply_item_metadata(item, track_data): - """Set an item's metadata from its matched info dictionary. +def apply_item_metadata(item, track_info): + """Set an item's metadata from its matched TrackInfo object. """ - item.artist = track_data['artist'] - item.title = track_data['title'] - item.mb_trackid = track_data['id'] - if 'artist_id' in track_data: - item.mb_artistid = track_data['artist_id'] + item.artist = track_info.artist + item.title = track_info.title + item.mb_trackid = track_info.track_id + if track_info.artist_id: + item.mb_artistid = track_info.artist_id # At the moment, the other metadata is left intact (including album # and track number). Perhaps these should be emptied? -def apply_metadata(items, info): - """Set the items' metadata to match the data given in info. The - list of items must be ordered. +def apply_metadata(items, album_info): + """Set the items' metadata to match an AlbumInfo object. The list + of items must be ordered. """ - for index, (item, track_data) in enumerate(zip(items, info['tracks'])): + for index, (item, track_info) in enumerate(zip(items, album_info.tracks)): # Album, artist, track count. - if 'artist' in track_data: - item.artist = track_data['artist'] + if track_info.artist: + item.artist = track_info.artist else: - item.artist = info['artist'] - item.albumartist = info['artist'] - item.album = info['album'] + item.artist = album_info.artist + item.albumartist = album_info.artist + item.album = album_info.album item.tracktotal = len(items) # Release date. - if 'year' in info: - item.year = info['year'] - if 'month' in info: - item.month = info['month'] - if 'day' in info: - item.day = info['day'] + if album_info.year: + item.year = album_info.year + if album_info.month: + item.month = album_info.month + if album_info.day: + item.day = album_info.day # Title and track index. - item.title = track_data['title'] + item.title = track_info.title item.track = index + 1 # MusicBrainz IDs. - item.mb_trackid = track_data['id'] - item.mb_albumid = info['album_id'] - if 'artist_id' in track_data: - item.mb_artistid = track_data['artist_id'] + item.mb_trackid = track_info.track_id + item.mb_albumid = album_info.album_id + if track_info.artist_id: + item.mb_artistid = track_info.artist_id else: - item.mb_artistid = info['artist_id'] - item.mb_albumartistid = info['artist_id'] - item.albumtype = info['albumtype'] - if 'label' in info: - item.label = info['label'] + item.mb_artistid = album_info.artist_id + item.mb_albumartistid = album_info.artist_id + item.albumtype = album_info.albumtype + if album_info.label: + item.label = album_info.label # Compilation flag. - item.comp = info['va'] + item.comp = album_info.va diff --git a/beets/autotag/art.py b/beets/autotag/art.py index 551ce8793..768704b3d 100644 --- a/beets/autotag/art.py +++ b/beets/autotag/art.py @@ -89,9 +89,9 @@ def art_for_album(album, path): if out: return out - if album['asin']: - log.debug('Fetching album art for ASIN %s.' % album['asin']) - return art_for_asin(album['asin']) + if album.asin: + log.debug('Fetching album art for ASIN %s.' % album.asin) + return art_for_asin(album.asin) else: log.debug('No ASIN available: no art found.') return None diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 4918ac85f..f587b35e6 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -15,7 +15,7 @@ """Glue between metadata sources and the matching logic.""" from beets import plugins -from . import mb +from beets.autotag import mb # Classes used to represent candidate options. @@ -40,7 +40,8 @@ class AlbumInfo(object): optional and may be None. """ def __init__(self, album, album_id, artist, artist_id, tracks, asin=None, - albumtype=None, va=False, year=None, month=None, day=None): + albumtype=None, va=False, year=None, month=None, day=None, + label=None): self.album = album self.album_id = album_id self.artist = artist @@ -52,6 +53,7 @@ class AlbumInfo(object): self.year = year self.month = month self.day = day + self.label = label class TrackInfo(object): """Describes a canonical track present on a release. Appears as part @@ -71,7 +73,7 @@ class TrackInfo(object): self.title = title self.track_id = track_id self.artist = artist - self.artist_id = artist + self.artist_id = artist_id self.length = length diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 2aa5cbd24..435e6fb03 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -192,10 +192,10 @@ def order_items(items, trackinfo): ordered_items[canon_idx] = items[cur_idx] return ordered_items -def track_distance(item, track_data, track_index=None, incl_artist=False): +def track_distance(item, track_info, track_index=None, incl_artist=False): """Determines the significance of a track metadata change. Returns a float in [0.0,1.0]. `track_index` is the track number of the - `track_data` metadata set. If `track_index` is provided and + `track_info` metadata set. If `track_index` is provided and item.track is set, then these indices are used as a component of the distance calculation. `incl_artist` indicates that a distance component should be included for the track artist (i.e., for @@ -205,26 +205,26 @@ def track_distance(item, track_data, track_index=None, incl_artist=False): dist, dist_max = 0.0, 0.0 # Check track length. - if 'length' not in track_data: + if not track_info.length: # If there's no length to check, assume the worst. dist += TRACK_LENGTH_WEIGHT else: - diff = abs(item.length - track_data['length']) + diff = abs(item.length - track_info.length) diff = max(diff - TRACK_LENGTH_GRACE, 0.0) diff = min(diff, TRACK_LENGTH_MAX) dist += (diff / TRACK_LENGTH_MAX) * TRACK_LENGTH_WEIGHT dist_max += TRACK_LENGTH_WEIGHT # Track title. - dist += string_dist(item.title, track_data['title']) * TRACK_TITLE_WEIGHT + dist += string_dist(item.title, track_info.title) * TRACK_TITLE_WEIGHT dist_max += TRACK_TITLE_WEIGHT # Track artist, if included. # Attention: MB DB does not have artist info for all compilations, # so only check artist distance if there is actually an artist in # the MB track data. - if incl_artist and 'artist' in track_data: - dist += string_dist(item.artist, track_data['artist']) * \ + if incl_artist and track_info.artist: + dist += string_dist(item.artist, track_info.artist) * \ TRACK_ARTIST_WEIGHT dist_max += TRACK_ARTIST_WEIGHT @@ -236,18 +236,18 @@ def track_distance(item, track_data, track_index=None, incl_artist=False): # MusicBrainz track ID. if item.mb_trackid: - if item.mb_trackid != track_data['id']: + if item.mb_trackid != track_info.track_id: dist += TRACK_ID_WEIGHT dist_max += TRACK_ID_WEIGHT # Plugin distances. - plugin_d, plugin_dm = plugins.track_distance(item, track_data) + plugin_d, plugin_dm = plugins.track_distance(item, track_info) dist += plugin_d dist_max += plugin_dm return dist / dist_max -def distance(items, info): +def distance(items, album_info): """Determines how "significant" an album metadata change would be. Returns a float in [0.0,1.0]. The list of items must be ordered. """ @@ -261,20 +261,20 @@ def distance(items, info): dist_max = 0.0 # Artist/album metadata. - if not info['va']: - dist += string_dist(cur_artist, info['artist']) * ARTIST_WEIGHT + if not album_info.va: + dist += string_dist(cur_artist, album_info.artist) * ARTIST_WEIGHT dist_max += ARTIST_WEIGHT - dist += string_dist(cur_album, info['album']) * ALBUM_WEIGHT + dist += string_dist(cur_album, album_info.album) * ALBUM_WEIGHT dist_max += ALBUM_WEIGHT # Track distances. - for i, (item, track_data) in enumerate(zip(items, info['tracks'])): - dist += track_distance(item, track_data, i+1, info['va']) * \ + for i, (item, track_info) in enumerate(zip(items, album_info.tracks)): + dist += track_distance(item, track_info, i+1, album_info.va) * \ TRACK_WEIGHT dist_max += TRACK_WEIGHT # Plugin distances. - plugin_d, plugin_dm = plugins.album_distance(items, info) + plugin_d, plugin_dm = plugins.album_distance(items, album_info) dist += plugin_d dist_max += plugin_dm @@ -340,20 +340,20 @@ def validate_candidate(items, tuple_dict, info): the track count, ordering the items, checking for duplicates, and calculating the distance. """ - log.debug('Candidate: %s - %s' % (info['artist'], info['album'])) + log.debug('Candidate: %s - %s' % (info.artist, info.album)) # Don't duplicate. - if info['album_id'] in tuple_dict: + if info.album_id in tuple_dict: log.debug('Duplicate.') return # Make sure the album has the correct number of tracks. - if len(items) != len(info['tracks']): + if len(items) != len(info.tracks): log.debug('Track count mismatch.') return # Put items in order. - ordered = order_items(items, info['tracks']) + ordered = order_items(items, info.tracks) if not ordered: log.debug('Not orderable.') return @@ -362,7 +362,7 @@ def validate_candidate(items, tuple_dict, info): dist = distance(ordered, info) log.debug('Success. Distance: %f' % dist) - tuple_dict[info['album_id']] = dist, ordered, info + tuple_dict[info.album_id] = dist, ordered, info def tag_album(items, timid=False, search_artist=None, search_album=None, search_id=None): diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 1aa3d8a6f..98682d1f1 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -30,6 +30,8 @@ from musicbrainz2.model import Release from threading import Lock from musicbrainz2.model import VARIOUS_ARTISTS_ID +import beets.autotag.hooks + SEARCH_LIMIT = 5 VARIOUS_ARTISTS_ID = VARIOUS_ARTISTS_ID.rsplit('/', 1)[1] @@ -115,7 +117,7 @@ def _query_wrap(fun, *args, **kwargs): def get_releases(**params): """Given a list of parameters to ReleaseFilter, executes the - query and yields release dicts (complete with tracks). + query and yields AlbumInfo objects. """ # Replace special cases. if 'artistName' in params: @@ -135,7 +137,7 @@ def get_releases(**params): for result in results: release = result.release tracks, _ = release_info(release.id) - yield release_dict(release, tracks) + yield album_info(release, tracks) def release_info(release_id): """Given a MusicBrainz release ID, fetch a list of tracks on the @@ -173,9 +175,9 @@ def _lucene_query(criteria): return u' '.join(query_parts) def find_releases(criteria, limit=SEARCH_LIMIT): - """Get a list of release dictionaries from the MusicBrainz - database that match `criteria`. The latter is a dictionary whose - keys are MusicBrainz field names and whose values are search terms + """Get a list of AlbumInfo objects from the MusicBrainz database + that match `criteria`. The latter is a dictionary whose keys are + MusicBrainz field names and whose values are search terms for those fields. The field names are from MusicBrainz's Lucene query syntax, which @@ -196,7 +198,7 @@ def find_releases(criteria, limit=SEARCH_LIMIT): return get_releases(limit=limit, query=query) def find_tracks(criteria, limit=SEARCH_LIMIT): - """Get a sequence of track dictionaries from MusicBrainz that match + """Get a sequence of TrackInfo objects from MusicBrainz that match `criteria`, a search term dictionary similar to the one passed to `find_releases`. """ @@ -210,43 +212,44 @@ def find_tracks(criteria, limit=SEARCH_LIMIT): results = () for result in results: track = result.track - yield track_dict(track) + yield track_info(track) -def track_dict(track): - """Produces a dictionary summarizing a MusicBrainz `Track` object. +def track_info(track): + """Translates a MusicBrainz ``Track`` object into a beets + ``TrackInfo`` object. """ - t = {'title': track.title, - 'id': track.id.rsplit('/', 1)[1]} + info = beets.autotag.hooks.TrackInfo(track.title, + track.id.rsplit('/', 1)[1]) if track.artist is not None: # Track artists will only be present for releases with # multiple artists. - t['artist'] = track.artist.name - t['artist_id'] = track.artist.id.rsplit('/', 1)[1] + info.artist = track.artist.name + info.artist_id = track.artist.id.rsplit('/', 1)[1] if track.duration is not None: # Duration not always present. - t['length'] = track.duration/(1000.0) - return t + info.length = track.duration/(1000.0) + return info -def release_dict(release, tracks=None): - """Takes a MusicBrainz `Release` object and returns a dictionary - containing the interesting data about that release. A list of - `Track` objects may also be provided as `tracks`; they are then - included in the resulting dictionary. +def album_info(release, tracks): + """Takes a MusicBrainz ``Release`` object and returns a beets + AlbumInfo object containing the interesting data about that release. + ``tracks`` is a list of ``Track`` objects that make up the album. """ # Basic info. - out = {'album': release.title, - 'album_id': release.id.rsplit('/', 1)[1], - 'artist': release.artist.name, - 'artist_id': release.artist.id.rsplit('/', 1)[1], - 'asin': release.asin, - 'albumtype': '', - } - out['va'] = out['artist_id'] == VARIOUS_ARTISTS_ID + info = beets.autotag.hooks.AlbumInfo( + release.title, + release.id.rsplit('/', 1)[1], + release.artist.name, + release.artist.id.rsplit('/', 1)[1], + [track_info(track) for track in tracks], + release.asin + ) + info.va = info.artist_id == VARIOUS_ARTISTS_ID # Release type not always populated. for releasetype in release.types: if releasetype in RELEASE_TYPES: - out['albumtype'] = releasetype.split('#')[1].lower() + info.albumtype = releasetype.split('#')[1].lower() break # Release date and label. @@ -255,7 +258,7 @@ def release_dict(release, tracks=None): except: # The python-musicbrainz2 module has a bug that will raise an # exception when there is no release date to be found. In this - # case, we just skip adding a release date to the dict. + # case, we just skip adding a release date to the result. pass else: if event: @@ -265,25 +268,20 @@ def release_dict(release, tracks=None): date_parts = date_str.split('-') for key in ('year', 'month', 'day'): if date_parts: - out[key] = int(date_parts.pop(0)) + setattr(info, key, int(date_parts.pop(0))) # Label name. label = event.getLabel() if label: name = label.getName() if name and name != '[no label]': - out['label'] = name + info.label = name - # Tracks. - if tracks is not None: - out['tracks'] = map(track_dict, tracks) - - return out + return info def match_album(artist, album, tracks=None, limit=SEARCH_LIMIT): """Searches for a single album ("release" in MusicBrainz parlance) - and returns an iterator over dictionaries of information (as - returned by `release_dict`). + and returns an iterator over AlbumInfo objects. The query consists of an artist name, an album name, and, optionally, a number of tracks on the album. @@ -302,8 +300,8 @@ def match_album(artist, album, tracks=None, limit=SEARCH_LIMIT): return find_releases(criteria, limit) def match_track(artist, title): - """Searches for a single track and returns an iterable of track - info dictionaries (as returned by `track_dict`). + """Searches for a single track and returns an iterable of TrackInfo + objects. """ return find_tracks({ 'artist': artist, @@ -311,8 +309,8 @@ def match_track(artist, title): }) def album_for_id(albumid): - """Fetches an album by its MusicBrainz ID and returns an - information dictionary. If no match is found, returns None. + """Fetches an album by its MusicBrainz ID and returns an AlbumInfo + object or None if the album is not found. """ query = mbws.Query() try: @@ -322,11 +320,11 @@ def album_for_id(albumid): except (mbws.ResourceNotFoundError, mbws.RequestError), exc: log.debug('Album ID match failed: ' + str(exc)) return None - return release_dict(album, album.tracks) + return album_info(album, album.tracks) def track_for_id(trackid): - """Fetches a track by its MusicBrainz ID. Returns a track info - dictionary or None if no track is found. + """Fetches a track by its MusicBrainz ID. Returns a TrackInfo object + or None if no track is found. """ query = mbws.Query() try: @@ -336,4 +334,4 @@ def track_for_id(trackid): except (mbws.ResourceNotFoundError, mbws.RequestError), exc: log.debug('Track ID match failed: ' + str(exc)) return None - return track_dict(track) + return track_info(track) diff --git a/beets/importer.py b/beets/importer.py index e938bdcff..9ce9e07a8 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -91,8 +91,8 @@ def _duplicate_check(lib, task, recent=None): artist = task.cur_artist album = task.cur_album elif task.choice_flag is action.APPLY: - artist = task.info['artist'] - album = task.info['album'] + artist = task.info.artist + album = task.info.album else: return False @@ -125,8 +125,8 @@ def _item_duplicate_check(lib, task, recent=None): artist = task.item.artist title = task.item.title elif task.choice_flag is action.APPLY: - artist = task.info['artist'] - title = task.info['title'] + artist = task.info.artist + title = task.info.title else: return False diff --git a/beets/plugins.py b/beets/plugins.py index bcb241024..fffec7642 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -55,14 +55,14 @@ class BeetsPlugin(object): return 0.0, 0.0 def candidates(self, items): - """Should return a sequence of MusicBrainz info dictionaries - that match the album whose items are provided. + """Should return a sequence of AlbumInfo objects that match the + album whose items are provided. """ return () def item_candidates(self, item): - """Should return a sequence of MusicBrainz track info - dictionaries that match the item provided. + """Should return a sequence of TrackInfo objects that match the + item provided. """ return () diff --git a/beets/ui/commands.py b/beets/ui/commands.py index ac13c39c2..cf8ea9a9b 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -130,10 +130,10 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True): print_(' (unknown album)') # Identify the album in question. - if cur_artist != info['artist'] or \ - (cur_album != info['album'] and info['album'] != VARIOUS_ARTISTS): - artist_l, artist_r = cur_artist or '', info['artist'] - album_l, album_r = cur_album or '', info['album'] + if cur_artist != info.artist or \ + (cur_album != info.album and info.album != VARIOUS_ARTISTS): + artist_l, artist_r = cur_artist or '', info.artist + album_l, album_r = cur_album or '', info.album if artist_r == VARIOUS_ARTISTS: # Hide artists for VA releases. artist_l, artist_r = u'', u'' @@ -147,17 +147,17 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True): print_("To:") show_album(artist_r, album_r) else: - print_("Tagging: %s - %s" % (info['artist'], info['album'])) + print_("Tagging: %s - %s" % (info.artist, info.album)) # Distance/similarity. print_('(Similarity: %s)' % dist_string(dist, color)) # Tracks. - for i, (item, track_data) in enumerate(zip(items, info['tracks'])): + for i, (item, track_info) in enumerate(zip(items, info.tracks)): cur_track = str(item.track) new_track = str(i+1) cur_title = item.title - new_title = track_data['title'] + new_title = track_info.title # Possibly colorize changes. if color: @@ -183,8 +183,8 @@ def show_item_change(item, info, dist, color): """Print out the change that would occur by tagging `item` with the metadata from `info`. """ - cur_artist, new_artist = item.artist, info['artist'] - cur_title, new_title = item.title, info['title'] + cur_artist, new_artist = item.artist, info.artist + cur_title, new_title = item.title, info.title if cur_artist != new_artist or cur_title != new_title: if color: @@ -228,7 +228,7 @@ def choose_candidate(candidates, singleton, rec, color, timid, Returns the result of the choice, which may SKIP, ASIS, TRACKS, or MANUAL or a candidate. For albums, a candidate is a `(info, items)` - pair; for items, it is just an `info` dictionary. + pair; for items, it is just a TrackInfo object. """ # Sanity check. if singleton: @@ -462,7 +462,7 @@ def choose_match(task, config): def choose_item(task, config): """Ask the user for a choice about tagging a single item. Returns - either an action constant or a track info dictionary. + either an action constant or a TrackInfo object. """ print_() print_(task.item.path) diff --git a/test/test_art.py b/test/test_art.py index f8bb82b68..9833d24dc 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -18,6 +18,7 @@ import unittest import _common from beets.autotag import art +from beets.autotag import AlbumInfo import os import shutil @@ -76,25 +77,25 @@ class CombinedTest(unittest.TestCase): def test_main_interface_returns_amazon_art(self): art.urllib.urlretrieve = MockUrlRetrieve('anotherpath', 'image/jpeg') - album = {'asin': 'xxxx'} + album = AlbumInfo(None, None, None, None, None, asin='xxxx') artpath = art.art_for_album(album, None) self.assertEqual(artpath, 'anotherpath') def test_main_interface_returns_none_for_missing_asin_and_path(self): - album = {'asin': None} + album = AlbumInfo(None, None, None, None, None, asin=None) artpath = art.art_for_album(album, None) self.assertEqual(artpath, None) def test_main_interface_gives_precedence_to_fs_art(self): _common.touch(os.path.join(self.dpath, 'a.jpg')) art.urllib.urlretrieve = MockUrlRetrieve('anotherpath', 'image/jpeg') - album = {'asin': 'xxxx'} + album = AlbumInfo(None, None, None, None, None, asin='xxxx') artpath = art.art_for_album(album, self.dpath) self.assertEqual(artpath, os.path.join(self.dpath, 'a.jpg')) def test_main_interface_falls_back_to_amazon(self): art.urllib.urlretrieve = MockUrlRetrieve('anotherpath', 'image/jpeg') - album = {'asin': 'xxxx'} + album = AlbumInfo(None, None, None, None, None, asin='xxxx') artpath = art.art_for_album(album, self.dpath) self.assertEqual(artpath, 'anotherpath') diff --git a/test/test_autotag.py b/test/test_autotag.py index 37f43d338..e2f71d94a 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -18,12 +18,14 @@ import unittest import os import shutil import re +import copy import _common from beets import autotag from beets.autotag import match from beets.library import Item from beets.util import plurality +from beets.autotag import AlbumInfo, TrackInfo class PluralityTest(unittest.TestCase): def test_plurality_consensus(self): @@ -73,12 +75,9 @@ class AlbumDistanceTest(unittest.TestCase): def trackinfo(self): ti = [] - ti.append({'title': 'one', 'artist': 'some artist', - 'track': 1, 'length': 1}) - ti.append({'title': 'two', 'artist': 'some artist', - 'track': 2, 'length': 1}) - ti.append({'title': 'three', 'artist': 'some artist', - 'track': 3, 'length': 1}) + ti.append(TrackInfo('one', None, 'some artist', length=1)) + ti.append(TrackInfo('two', None, 'some artist', length=1)) + ti.append(TrackInfo('three', None, 'some artist', length=1)) return ti def test_identical_albums(self): @@ -86,12 +85,13 @@ class AlbumDistanceTest(unittest.TestCase): items.append(self.item('one', 1)) items.append(self.item('two', 2)) items.append(self.item('three', 3)) - info = { - 'artist': 'some artist', - 'album': 'some album', - 'tracks': self.trackinfo(), - 'va': False, - } + info = AlbumInfo( + artist = 'some artist', + album = 'some album', + tracks = self.trackinfo(), + va = False, + album_id = None, artist_id = None, + ) self.assertEqual(match.distance(items, info), 0) def test_global_artists_differ(self): @@ -99,12 +99,13 @@ class AlbumDistanceTest(unittest.TestCase): items.append(self.item('one', 1)) items.append(self.item('two', 2)) items.append(self.item('three', 3)) - info = { - 'artist': 'someone else', - 'album': 'some album', - 'tracks': self.trackinfo(), - 'va': False, - } + info = AlbumInfo( + artist = 'someone else', + album = 'some album', + tracks = self.trackinfo(), + va = False, + album_id = None, artist_id = None, + ) self.assertNotEqual(match.distance(items, info), 0) def test_comp_track_artists_match(self): @@ -112,12 +113,13 @@ class AlbumDistanceTest(unittest.TestCase): items.append(self.item('one', 1)) items.append(self.item('two', 2)) items.append(self.item('three', 3)) - info = { - 'artist': 'should be ignored', - 'album': 'some album', - 'tracks': self.trackinfo(), - 'va': True, - } + info = AlbumInfo( + artist = 'should be ignored', + album = 'some album', + tracks = self.trackinfo(), + va = True, + album_id = None, artist_id = None, + ) self.assertEqual(match.distance(items, info), 0) def test_comp_no_track_artists(self): @@ -126,15 +128,16 @@ class AlbumDistanceTest(unittest.TestCase): items.append(self.item('one', 1)) items.append(self.item('two', 2)) items.append(self.item('three', 3)) - info = { - 'artist': 'should be ignored', - 'album': 'some album', - 'tracks': self.trackinfo(), - 'va': True, - } - del info['tracks'][0]['artist'] - del info['tracks'][1]['artist'] - del info['tracks'][2]['artist'] + info = AlbumInfo( + artist = 'should be ignored', + album = 'some album', + tracks = self.trackinfo(), + va = True, + album_id = None, artist_id = None, + ) + info.tracks[0].artist = None + info.tracks[1].artist = None + info.tracks[2].artist = None self.assertEqual(match.distance(items, info), 0) def test_comp_track_artists_do_not_match(self): @@ -142,12 +145,13 @@ class AlbumDistanceTest(unittest.TestCase): items.append(self.item('one', 1)) items.append(self.item('two', 2, 'someone else')) items.append(self.item('three', 3)) - info = { - 'artist': 'some artist', - 'album': 'some album', - 'tracks': self.trackinfo(), - 'va': True, - } + info = AlbumInfo( + artist = 'some artist', + album = 'some album', + tracks = self.trackinfo(), + va = True, + album_id = None, artist_id = None, + ) self.assertNotEqual(match.distance(items, info), 0) def _mkmp3(path): @@ -206,9 +210,9 @@ class OrderingTest(unittest.TestCase): items.append(self.item('three', 2)) items.append(self.item('two', 3)) trackinfo = [] - trackinfo.append({'title': 'one', 'track': 1}) - trackinfo.append({'title': 'two', 'track': 2}) - trackinfo.append({'title': 'three', 'track': 3}) + trackinfo.append(TrackInfo('one', None)) + trackinfo.append(TrackInfo('two', None)) + trackinfo.append(TrackInfo('three', None)) ordered = match.order_items(items, trackinfo) self.assertEqual(ordered[0].title, 'one') self.assertEqual(ordered[1].title, 'two') @@ -220,9 +224,9 @@ class OrderingTest(unittest.TestCase): items.append(self.item('three', 1)) items.append(self.item('two', 1)) trackinfo = [] - trackinfo.append({'title': 'one', 'track': 1}) - trackinfo.append({'title': 'two', 'track': 2}) - trackinfo.append({'title': 'three', 'track': 3}) + trackinfo.append(TrackInfo('one', None)) + trackinfo.append(TrackInfo('two', None)) + trackinfo.append(TrackInfo('three', None)) ordered = match.order_items(items, trackinfo) self.assertEqual(ordered[0].title, 'one') self.assertEqual(ordered[1].title, 'two') @@ -233,7 +237,7 @@ class OrderingTest(unittest.TestCase): items.append(self.item('one', 1)) items.append(self.item('two', 2)) trackinfo = [] - trackinfo.append({'title': 'one', 'track': 1}) + trackinfo.append(TrackInfo('one', None)) ordered = match.order_items(items, trackinfo) self.assertEqual(ordered, None) @@ -263,10 +267,7 @@ class OrderingTest(unittest.TestCase): items.append(item(12, 186.45916150485752)) def info(title, length): - return { - 'title': title, - 'length': length, - } + return TrackInfo(title, None, length=length) trackinfo = [] trackinfo.append(info('Alone', 238.893)) trackinfo.append(info('The Woman in You', 341.44)) @@ -291,23 +292,19 @@ class ApplyTest(unittest.TestCase): self.items.append(Item({})) self.items.append(Item({})) trackinfo = [] - trackinfo.append({ - 'title': 'oneNew', - 'id': 'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', - }) - trackinfo.append({ - 'title': 'twoNew', - 'id': '40130ed1-a27c-42fd-a328-1ebefb6caef4', - }) - self.info = { - 'tracks': trackinfo, - 'artist': 'artistNew', - 'album': 'albumNew', - 'album_id': '7edb51cb-77d6-4416-a23c-3a8c2994a2c7', - 'artist_id': 'a6623d39-2d8e-4f70-8242-0a9553b91e50', - 'albumtype': 'album', - 'va': False, - } + trackinfo.append(TrackInfo('oneNew', + 'dfa939ec-118c-4d0f-84a0-60f3d1e6522c')) + trackinfo.append(TrackInfo('twoNew', + '40130ed1-a27c-42fd-a328-1ebefb6caef4')) + self.info = AlbumInfo( + tracks = trackinfo, + artist = 'artistNew', + album = 'albumNew', + album_id = '7edb51cb-77d6-4416-a23c-3a8c2994a2c7', + artist_id = 'a6623d39-2d8e-4f70-8242-0a9553b91e50', + albumtype = 'album', + va = False, + ) def test_titles_applied(self): autotag.apply_metadata(self.items, self.info) @@ -352,17 +349,15 @@ class ApplyTest(unittest.TestCase): self.assertEqual(self.items[1].albumtype, 'album') def test_album_artist_overrides_empty_track_artist(self): - my_info = dict(self.info) - my_info['tracks'] = [dict(t) for t in self.info['tracks']] + my_info = copy.deepcopy(self.info) autotag.apply_metadata(self.items, my_info) self.assertEqual(self.items[0].artist, 'artistNew') self.assertEqual(self.items[0].artist, 'artistNew') def test_album_artist_overriden_by_nonempty_track_artist(self): - my_info = dict(self.info) - my_info['tracks'] = [dict(t) for t in self.info['tracks']] - my_info['tracks'][0]['artist'] = 'artist1!' - my_info['tracks'][1]['artist'] = 'artist2!' + my_info = copy.deepcopy(self.info) + my_info.tracks[0].artist = 'artist1!' + my_info.tracks[1].artist = 'artist2!' autotag.apply_metadata(self.items, my_info) self.assertEqual(self.items[0].artist, 'artist1!') self.assertEqual(self.items[1].artist, 'artist2!') @@ -373,27 +368,27 @@ class ApplyCompilationTest(unittest.TestCase): self.items.append(Item({})) self.items.append(Item({})) trackinfo = [] - trackinfo.append({ - 'title': 'oneNew', - 'id': 'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', - 'artist': 'artistOneNew', - 'artist_id': 'a05686fc-9db2-4c23-b99e-77f5db3e5282', - }) - trackinfo.append({ - 'title': 'twoNew', - 'id': '40130ed1-a27c-42fd-a328-1ebefb6caef4', - 'artist': 'artistTwoNew', - 'artist_id': '80b3cf5e-18fe-4c59-98c7-e5bb87210710', - }) - self.info = { - 'tracks': trackinfo, - 'artist': 'variousNew', - 'album': 'albumNew', - 'album_id': '3b69ea40-39b8-487f-8818-04b6eff8c21a', - 'artist_id': '89ad4ac3-39f7-470e-963a-56509c546377', - 'albumtype': 'compilation', - 'va': False, - } + trackinfo.append(TrackInfo( + 'oneNew', + 'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + 'artistOneNew', + 'a05686fc-9db2-4c23-b99e-77f5db3e5282', + )) + trackinfo.append(TrackInfo( + 'twoNew', + '40130ed1-a27c-42fd-a328-1ebefb6caef4', + 'artistTwoNew', + '80b3cf5e-18fe-4c59-98c7-e5bb87210710', + )) + self.info = AlbumInfo( + tracks = trackinfo, + artist = 'variousNew', + album = 'albumNew', + album_id = '3b69ea40-39b8-487f-8818-04b6eff8c21a', + artist_id = '89ad4ac3-39f7-470e-963a-56509c546377', + albumtype = 'compilation', + va = False, + ) def test_album_and_track_artists_separate(self): autotag.apply_metadata(self.items, self.info) @@ -419,8 +414,8 @@ class ApplyCompilationTest(unittest.TestCase): self.assertFalse(self.items[1].comp) def test_va_flag_sets_comp(self): - va_info = dict(self.info) # make a copy - va_info['va'] = True + va_info = copy.deepcopy(self.info) + va_info.va = True autotag.apply_metadata(self.items, va_info) self.assertTrue(self.items[0].comp) self.assertTrue(self.items[1].comp) diff --git a/test/test_importer.py b/test/test_importer.py index 4607f1087..22809c43f 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -22,6 +22,7 @@ import _common from beets import library from beets import importer from beets import mediafile +from beets.autotag import AlbumInfo, TrackInfo TEST_TITLES = ('The Opener', 'The Second Track', 'The Last Track') class NonAutotaggedImportTest(unittest.TestCase): @@ -173,17 +174,17 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): self.i = library.Item.from_path(self.srcpath) self.i.comp = False - trackinfo = {'title': 'one', 'artist': 'some artist', - 'track': 1, 'length': 1, 'id': 'trackid'} - self.info = { - 'artist': 'some artist', - 'album': 'some album', - 'tracks': [trackinfo], - 'va': False, - 'album_id': 'albumid', - 'artist_id': 'artistid', - 'albumtype': 'soundtrack', - } + trackinfo = TrackInfo('one', 'trackid', 'some artist', + 'artistid', 1) + self.info = AlbumInfo( + artist = 'some artist', + album = 'some album', + tracks = [trackinfo], + va = False, + album_id = 'albumid', + artist_id = 'artistid', + albumtype = 'soundtrack', + ) def tearDown(self): shutil.rmtree(self.libdir) @@ -227,7 +228,7 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): coro.next() # Prime coroutine. task = importer.ImportTask.item_task(self.i) - task.set_choice(self.info['tracks'][0]) + task.set_choice(self.info.tracks[0]) coro.send(task) self.assertExists( @@ -559,7 +560,10 @@ class DuplicateCheckTest(unittest.TestCase): if asis: task.set_choice(importer.action.ASIS) else: - task.set_choice(({'artist': artist, 'album': album}, [item])) + task.set_choice(( + AlbumInfo(album, None, artist, None, None), + [item] + )) return task def _item_task(self, asis, artist=None, title=None, existing=False): @@ -576,7 +580,7 @@ class DuplicateCheckTest(unittest.TestCase): item.title = title task.set_choice(importer.action.ASIS) else: - task.set_choice({'artist': artist, 'title': title}) + task.set_choice(TrackInfo(title, None, artist)) return task def test_duplicate_album_apply(self): diff --git a/test/test_mb.py b/test/test_mb.py index 30e0c8aea..1769ccb86 100644 --- a/test/test_mb.py +++ b/test/test_mb.py @@ -100,7 +100,7 @@ class MBQueryErrorTest(unittest.TestCase): with self.assertRaises(mb.ServerBusyError): mb._query_wrap(raise_func(exc)) -class MBReleaseDictTest(unittest.TestCase): +class MBAlbumInfoTest(unittest.TestCase): def _make_release(self, date_str='2009'): release = musicbrainz2.model.Release() release.title = 'ALBUM TITLE' @@ -128,68 +128,68 @@ class MBReleaseDictTest(unittest.TestCase): def test_parse_release_with_year(self): release = self._make_release('1984') - d = mb.release_dict(release) - self.assertEqual(d['album'], 'ALBUM TITLE') - self.assertEqual(d['album_id'], 'ALBUM ID') - self.assertEqual(d['artist'], 'ARTIST NAME') - self.assertEqual(d['artist_id'], 'ARTIST ID') - self.assertEqual(d['year'], 1984) + d = mb.album_info(release, []) + self.assertEqual(d.album, 'ALBUM TITLE') + self.assertEqual(d.album_id, 'ALBUM ID') + self.assertEqual(d.artist, 'ARTIST NAME') + self.assertEqual(d.artist_id, 'ARTIST ID') + self.assertEqual(d.year, 1984) def test_parse_release_type(self): release = self._make_release('1984') - d = mb.release_dict(release) - self.assertEqual(d['albumtype'], 'album') + d = mb.album_info(release, []) + self.assertEqual(d.albumtype, 'album') def test_parse_release_full_date(self): release = self._make_release('1987-03-31') - d = mb.release_dict(release) - self.assertEqual(d['year'], 1987) - self.assertEqual(d['month'], 3) - self.assertEqual(d['day'], 31) + d = mb.album_info(release, []) + self.assertEqual(d.year, 1987) + self.assertEqual(d.month, 3) + self.assertEqual(d.day, 31) def test_parse_tracks(self): release = self._make_release() tracks = [self._make_track('TITLE ONE', 'dom/ID ONE', 100.0 * 1000.0), self._make_track('TITLE TWO', 'dom/ID TWO', 200.0 * 1000.0)] - d = mb.release_dict(release, tracks) - t = d['tracks'] + d = mb.album_info(release, tracks) + t = d.tracks self.assertEqual(len(t), 2) - self.assertEqual(t[0]['title'], 'TITLE ONE') - self.assertEqual(t[0]['id'], 'ID ONE') - self.assertEqual(t[0]['length'], 100.0) - self.assertEqual(t[1]['title'], 'TITLE TWO') - self.assertEqual(t[1]['id'], 'ID TWO') - self.assertEqual(t[1]['length'], 200.0) + self.assertEqual(t[0].title, 'TITLE ONE') + self.assertEqual(t[0].track_id, 'ID ONE') + self.assertEqual(t[0].length, 100.0) + self.assertEqual(t[1].title, 'TITLE TWO') + self.assertEqual(t[1].track_id, 'ID TWO') + self.assertEqual(t[1].length, 200.0) def test_parse_release_year_month_only(self): release = self._make_release('1987-03') - d = mb.release_dict(release) - self.assertEqual(d['year'], 1987) - self.assertEqual(d['month'], 3) + d = mb.album_info(release, []) + self.assertEqual(d.year, 1987) + self.assertEqual(d.month, 3) def test_no_durations(self): release = self._make_release() tracks = [self._make_track('TITLE', 'dom/ID', None)] - d = mb.release_dict(release, tracks) - self.assertFalse('length' in d['tracks'][0]) + d = mb.album_info(release, tracks) + self.assertEqual(d.tracks[0].length, None) def test_no_release_date(self): release = self._make_release(None) - d = mb.release_dict(release) - self.assertFalse('year' in d) - self.assertFalse('month' in d) - self.assertFalse('day' in d) + d = mb.album_info(release, []) + self.assertFalse(d.year) + self.assertFalse(d.month) + self.assertFalse(d.day) def test_various_artists_defaults_false(self): release = self._make_release(None) - d = mb.release_dict(release) - self.assertFalse(d['va']) + d = mb.album_info(release, []) + self.assertFalse(d.va) def test_detect_various_artists(self): release = self._make_release(None) release.artist.id = musicbrainz2.model.VARIOUS_ARTISTS_ID - d = mb.release_dict(release) - self.assertTrue(d['va']) + d = mb.album_info(release, []) + self.assertTrue(d.va) class QuerySanitationTest(unittest.TestCase): def test_special_char_escaped(self):