diff -Nru beets-1.5.0/beets/art.py beets-1.6.0/beets/art.py --- beets-1.5.0/beets/art.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beets/art.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -17,7 +16,6 @@ music and items' embedded album art. """ -from __future__ import division, absolute_import, print_function import subprocess import platform @@ -43,7 +41,7 @@ try: mf = mediafile.MediaFile(syspath(item.path)) except mediafile.UnreadableFileError as exc: - log.warning(u'Could not extract art from {0}: {1}', + log.warning('Could not extract art from {0}: {1}', displayable_path(item.path), exc) return @@ -58,20 +56,20 @@ # Conditions and filters. if compare_threshold: if not check_art_similarity(log, item, imagepath, compare_threshold): - log.info(u'Image not similar; skipping.') + log.info('Image not similar; skipping.') return if ifempty and get_art(log, item): - log.info(u'media file already contained art') + log.info('media file already contained art') return if maxwidth and not as_album: imagepath = resize_image(log, imagepath, maxwidth, quality) # Get the `Image` object from the file. try: - log.debug(u'embedding {0}', displayable_path(imagepath)) + log.debug('embedding {0}', displayable_path(imagepath)) image = mediafile_image(imagepath, maxwidth) - except IOError as exc: - log.warning(u'could not read image file: {0}', exc) + except OSError as exc: + log.warning('could not read image file: {0}', exc) return # Make sure the image kind is safe (some formats only support PNG @@ -90,16 +88,16 @@ """ imagepath = album.artpath if not imagepath: - log.info(u'No album art present for {0}', album) + log.info('No album art present for {0}', album) return if not os.path.isfile(syspath(imagepath)): - log.info(u'Album art not found at {0} for {1}', + log.info('Album art not found at {0} for {1}', displayable_path(imagepath), album) return if maxwidth: imagepath = resize_image(log, imagepath, maxwidth, quality) - log.info(u'Embedding album art into {0}', album) + log.info('Embedding album art into {0}', album) for item in album.items(): embed_item(log, item, imagepath, maxwidth, None, compare_threshold, @@ -110,7 +108,7 @@ """Returns path to an image resized to maxwidth and encoded with the specified quality level. """ - log.debug(u'Resizing album art to {0} pixels wide and encoding at quality \ + log.debug('Resizing album art to {0} pixels wide and encoding at quality \ level {1}', maxwidth, quality) imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath), quality=quality) @@ -135,7 +133,7 @@ syspath(art, prefix=False), '-colorspace', 'gray', 'MIFF:-'] compare_cmd = ['compare', '-metric', 'PHASH', '-', 'null:'] - log.debug(u'comparing images with pipeline {} | {}', + log.debug('comparing images with pipeline {} | {}', convert_cmd, compare_cmd) convert_proc = subprocess.Popen( convert_cmd, @@ -159,7 +157,7 @@ convert_proc.wait() if convert_proc.returncode: log.debug( - u'ImageMagick convert failed with status {}: {!r}', + 'ImageMagick convert failed with status {}: {!r}', convert_proc.returncode, convert_stderr, ) @@ -169,7 +167,7 @@ stdout, stderr = compare_proc.communicate() if compare_proc.returncode: if compare_proc.returncode != 1: - log.debug(u'ImageMagick compare failed: {0}, {1}', + log.debug('ImageMagick compare failed: {0}, {1}', displayable_path(imagepath), displayable_path(art)) return @@ -180,10 +178,10 @@ try: phash_diff = float(out_str) except ValueError: - log.debug(u'IM output is not a number: {0!r}', out_str) + log.debug('IM output is not a number: {0!r}', out_str) return - log.debug(u'ImageMagick compare score: {0}', phash_diff) + log.debug('ImageMagick compare score: {0}', phash_diff) return phash_diff <= compare_threshold return True @@ -193,18 +191,18 @@ art = get_art(log, item) outpath = bytestring_path(outpath) if not art: - log.info(u'No album art present in {0}, skipping.', item) + log.info('No album art present in {0}, skipping.', item) return # Add an extension to the filename. ext = mediafile.image_extension(art) if not ext: - log.warning(u'Unknown image type in {0}.', + log.warning('Unknown image type in {0}.', displayable_path(item.path)) return outpath += bytestring_path('.' + ext) - log.info(u'Extracting album art from: {0} to: {1}', + log.info('Extracting album art from: {0} to: {1}', item, displayable_path(outpath)) with open(syspath(outpath), 'wb') as f: f.write(art) @@ -220,7 +218,7 @@ def clear(log, lib, query): items = lib.items(query) - log.info(u'Clearing album art from {0} items', len(items)) + log.info('Clearing album art from {0} items', len(items)) for item in items: - log.debug(u'Clearing art for {0}', item) + log.debug('Clearing art for {0}', item) item.try_write(tags={'images': None}) diff -Nru beets-1.5.0/beets/autotag/hooks.py beets-1.6.0/beets/autotag/hooks.py --- beets-1.5.0/beets/autotag/hooks.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beets/autotag/hooks.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,7 +13,6 @@ # included in all copies or substantial portions of the Software. """Glue between metadata sources and the matching logic.""" -from __future__ import division, absolute_import, print_function from collections import namedtuple from functools import total_ordering @@ -27,7 +25,6 @@ from beets.autotag import mb from jellyfish import levenshtein_distance from unidecode import unidecode -import six log = logging.getLogger('beets') @@ -70,6 +67,7 @@ ``mediums`` along with the fields up through ``tracks`` are required. The others are optional and may be None. """ + def __init__(self, tracks, album=None, album_id=None, artist=None, artist_id=None, asin=None, albumtype=None, va=False, year=None, month=None, day=None, label=None, mediums=None, @@ -155,6 +153,7 @@ may be None. The indices ``index``, ``medium``, and ``medium_index`` are all 1-based. """ + def __init__(self, title=None, track_id=None, release_track_id=None, artist=None, artist_id=None, length=None, index=None, medium=None, medium_index=None, medium_total=None, @@ -236,8 +235,8 @@ transliteration/lowering to ASCII characters. Normalized by string length. """ - assert isinstance(str1, six.text_type) - assert isinstance(str2, six.text_type) + assert isinstance(str1, str) + assert isinstance(str2, str) str1 = as_string(unidecode(str1)) str2 = as_string(unidecode(str2)) str1 = re.sub(r'[^a-z0-9]', '', str1.lower()) @@ -265,9 +264,9 @@ # "something, the". for word in SD_END_WORDS: if str1.endswith(', %s' % word): - str1 = '%s %s' % (word, str1[:-len(word) - 2]) + str1 = '{} {}'.format(word, str1[:-len(word) - 2]) if str2.endswith(', %s' % word): - str2 = '%s %s' % (word, str2[:-len(word) - 2]) + str2 = '{} {}'.format(word, str2[:-len(word) - 2]) # Perform a couple of basic normalizing substitutions. for pat, repl in SD_REPLACE: @@ -305,11 +304,12 @@ return base_dist + penalty -class LazyClassProperty(object): +class LazyClassProperty: """A decorator implementing a read-only property that is *lazy* in the sense that the getter is only invoked once. Subsequent accesses through *any* instance use the cached result. """ + def __init__(self, getter): self.getter = getter self.computed = False @@ -322,12 +322,12 @@ @total_ordering -@six.python_2_unicode_compatible -class Distance(object): +class Distance: """Keeps track of multiple distance penalties. Provides a single weighted distance for all penalties as well as a weighted distance for each individual penalty. """ + def __init__(self): self._penalties = {} @@ -410,7 +410,7 @@ return other - self.distance def __str__(self): - return "{0:.2f}".format(self.distance) + return f"{self.distance:.2f}" # Behave like a dict. @@ -437,7 +437,7 @@ """ if not isinstance(dist, Distance): raise ValueError( - u'`dist` must be a Distance object, not {0}'.format(type(dist)) + '`dist` must be a Distance object, not {}'.format(type(dist)) ) for key, penalties in dist._penalties.items(): self._penalties.setdefault(key, []).extend(penalties) @@ -461,7 +461,7 @@ """ if not 0.0 <= dist <= 1.0: raise ValueError( - u'`dist` must be between 0.0 and 1.0, not {0}'.format(dist) + f'`dist` must be between 0.0 and 1.0, not {dist}' ) self._penalties.setdefault(key, []).append(dist) @@ -557,7 +557,7 @@ try: album = mb.album_for_id(release_id) if album: - plugins.send(u'albuminfo_received', info=album) + plugins.send('albuminfo_received', info=album) return album except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -570,7 +570,7 @@ try: track = mb.track_for_id(recording_id) if track: - plugins.send(u'trackinfo_received', info=track) + plugins.send('trackinfo_received', info=track) return track except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -583,7 +583,7 @@ yield a for a in plugins.album_for_id(album_id): if a: - plugins.send(u'albuminfo_received', info=a) + plugins.send('albuminfo_received', info=a) yield a @@ -594,11 +594,11 @@ yield t for t in plugins.track_for_id(track_id): if t: - plugins.send(u'trackinfo_received', info=t) + plugins.send('trackinfo_received', info=t) yield t -@plugins.notify_info_yielded(u'albuminfo_received') +@plugins.notify_info_yielded('albuminfo_received') def album_candidates(items, artist, album, va_likely, extra_tags): """Search for album matches. ``items`` is a list of Item objects that make up the album. ``artist`` and ``album`` are the respective @@ -612,28 +612,25 @@ # Base candidates if we have album and artist to match. if artist and album: try: - for candidate in mb.match_album(artist, album, len(items), - extra_tags): - yield candidate + yield from mb.match_album(artist, album, len(items), + extra_tags) except mb.MusicBrainzAPIError as exc: exc.log(log) # Also add VA matches from MusicBrainz where appropriate. if va_likely and album: try: - for candidate in mb.match_album(None, album, len(items), - extra_tags): - yield candidate + yield from mb.match_album(None, album, len(items), + extra_tags) except mb.MusicBrainzAPIError as exc: exc.log(log) # Candidates from plugins. - for candidate in plugins.candidates(items, artist, album, va_likely, - extra_tags): - yield candidate + yield from plugins.candidates(items, artist, album, va_likely, + extra_tags) -@plugins.notify_info_yielded(u'trackinfo_received') +@plugins.notify_info_yielded('trackinfo_received') def item_candidates(item, artist, title): """Search for item matches. ``item`` is the Item to be matched. ``artist`` and ``title`` are strings and either reflect the item or @@ -643,11 +640,9 @@ # MusicBrainz candidates. if artist and title: try: - for candidate in mb.match_track(artist, title): - yield candidate + yield from mb.match_track(artist, title) except mb.MusicBrainzAPIError as exc: exc.log(log) # Plugin candidates. - for candidate in plugins.item_candidates(item, artist, title): - yield candidate + yield from plugins.item_candidates(item, artist, title) diff -Nru beets-1.5.0/beets/autotag/__init__.py beets-1.6.0/beets/autotag/__init__.py --- beets-1.5.0/beets/autotag/__init__.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beets/autotag/__init__.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Facilities for automatically determining files' correct metadata. """ -from __future__ import division, absolute_import, print_function from beets import logging from beets import config diff -Nru beets-1.5.0/beets/autotag/match.py beets-1.6.0/beets/autotag/match.py --- beets-1.5.0/beets/autotag/match.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beets/autotag/match.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -17,7 +16,6 @@ releases and tracks. """ -from __future__ import division, absolute_import, print_function import datetime import re @@ -35,7 +33,7 @@ # album level to determine whether a given release is likely a VA # release and also on the track level to to remove the penalty for # differing artists. -VA_ARTISTS = (u'', u'various artists', u'various', u'va', u'unknown') +VA_ARTISTS = ('', 'various artists', 'various', 'va', 'unknown') # Global logger. log = logging.getLogger('beets') @@ -108,7 +106,7 @@ log.debug('...done.') # Produce the output matching. - mapping = dict((items[i], tracks[j]) for (i, j) in matching) + mapping = {items[i]: tracks[j] for (i, j) in matching} extra_items = list(set(items) - set(mapping.keys())) extra_items.sort(key=lambda i: (i.disc, i.track, i.title)) extra_tracks = list(set(tracks) - set(mapping.values())) @@ -276,16 +274,16 @@ try: first = next(albumids) except StopIteration: - log.debug(u'No album ID found.') + log.debug('No album ID found.') return None # Is there a consensus on the MB album ID? for other in albumids: if other != first: - log.debug(u'No album ID consensus.') + log.debug('No album ID consensus.') return None # If all album IDs are equal, look up the album. - log.debug(u'Searching for discovered album ID: {0}', first) + log.debug('Searching for discovered album ID: {0}', first) return hooks.album_for_mbid(first) @@ -351,23 +349,23 @@ checking the track count, ordering the items, checking for duplicates, and calculating the distance. """ - log.debug(u'Candidate: {0} - {1} ({2})', + log.debug('Candidate: {0} - {1} ({2})', info.artist, info.album, info.album_id) # Discard albums with zero tracks. if not info.tracks: - log.debug(u'No tracks.') + log.debug('No tracks.') return # Don't duplicate. if info.album_id in results: - log.debug(u'Duplicate.') + log.debug('Duplicate.') return # Discard matches without required tags. for req_tag in config['match']['required'].as_str_seq(): if getattr(info, req_tag) is None: - log.debug(u'Ignored. Missing required tag: {0}', req_tag) + log.debug('Ignored. Missing required tag: {0}', req_tag) return # Find mapping between the items and the track info. @@ -380,10 +378,10 @@ penalties = [key for key, _ in dist] for penalty in config['match']['ignored'].as_str_seq(): if penalty in penalties: - log.debug(u'Ignored. Penalty: {0}', penalty) + log.debug('Ignored. Penalty: {0}', penalty) return - log.debug(u'Success. Distance: {0}', dist) + log.debug('Success. Distance: {0}', dist) results[info.album_id] = hooks.AlbumMatch(dist, info, mapping, extra_items, extra_tracks) @@ -411,7 +409,7 @@ likelies, consensus = current_metadata(items) cur_artist = likelies['artist'] cur_album = likelies['album'] - log.debug(u'Tagging {0} - {1}', cur_artist, cur_album) + log.debug('Tagging {0} - {1}', cur_artist, cur_album) # The output result (distance, AlbumInfo) tuples (keyed by MB album # ID). @@ -420,7 +418,7 @@ # Search by explicit ID. if search_ids: for search_id in search_ids: - log.debug(u'Searching for album ID: {0}', search_id) + log.debug('Searching for album ID: {0}', search_id) for id_candidate in hooks.albums_for_id(search_id): _add_candidate(items, candidates, id_candidate) @@ -431,13 +429,13 @@ if id_info: _add_candidate(items, candidates, id_info) rec = _recommendation(list(candidates.values())) - log.debug(u'Album ID match recommendation is {0}', rec) + log.debug('Album ID match recommendation is {0}', rec) if candidates and not config['import']['timid']: # If we have a very good MBID match, return immediately. # Otherwise, this match will compete against metadata-based # matches. if rec == Recommendation.strong: - log.debug(u'ID match.') + log.debug('ID match.') return cur_artist, cur_album, \ Proposal(list(candidates.values()), rec) @@ -445,19 +443,19 @@ if not (search_artist and search_album): # No explicit search terms -- use current metadata. search_artist, search_album = cur_artist, cur_album - log.debug(u'Search terms: {0} - {1}', search_artist, search_album) + log.debug('Search terms: {0} - {1}', search_artist, search_album) extra_tags = None if config['musicbrainz']['extra_tags']: tag_list = config['musicbrainz']['extra_tags'].get() extra_tags = {k: v for (k, v) in likelies.items() if k in tag_list} - log.debug(u'Additional search terms: {0}', extra_tags) + log.debug('Additional search terms: {0}', extra_tags) # Is this album likely to be a "various artist" release? va_likely = ((not consensus['artist']) or (search_artist.lower() in VA_ARTISTS) or any(item.comp for item in items)) - log.debug(u'Album might be VA: {0}', va_likely) + log.debug('Album might be VA: {0}', va_likely) # Get the results from the data sources. for matched_candidate in hooks.album_candidates(items, @@ -467,7 +465,7 @@ extra_tags): _add_candidate(items, candidates, matched_candidate) - log.debug(u'Evaluating {0} candidates.', len(candidates)) + log.debug('Evaluating {0} candidates.', len(candidates)) # Sort and get the recommendation. candidates = _sort_candidates(candidates.values()) rec = _recommendation(candidates) @@ -492,7 +490,7 @@ trackids = search_ids or [t for t in [item.mb_trackid] if t] if trackids: for trackid in trackids: - log.debug(u'Searching for track ID: {0}', trackid) + log.debug('Searching for track ID: {0}', trackid) for track_info in hooks.tracks_for_id(trackid): dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = \ @@ -501,7 +499,7 @@ rec = _recommendation(_sort_candidates(candidates.values())) if rec == Recommendation.strong and \ not config['import']['timid']: - log.debug(u'Track ID match.') + log.debug('Track ID match.') return Proposal(_sort_candidates(candidates.values()), rec) # If we're searching by ID, don't proceed. @@ -514,7 +512,7 @@ # Search terms. if not (search_artist and search_title): search_artist, search_title = item.artist, item.title - log.debug(u'Item search terms: {0} - {1}', search_artist, search_title) + log.debug('Item search terms: {0} - {1}', search_artist, search_title) # Get and evaluate candidate metadata. for track_info in hooks.item_candidates(item, search_artist, search_title): @@ -522,7 +520,7 @@ candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info) # Sort by distance and return with recommendation. - log.debug(u'Found {0} candidates.', len(candidates)) + log.debug('Found {0} candidates.', len(candidates)) candidates = _sort_candidates(candidates.values()) rec = _recommendation(candidates) return Proposal(candidates, rec) diff -Nru beets-1.5.0/beets/autotag/mb.py beets-1.6.0/beets/autotag/mb.py --- beets-1.5.0/beets/autotag/mb.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beets/autotag/mb.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,12 +14,10 @@ """Searches for albums in the MusicBrainz database. """ -from __future__ import division, absolute_import, print_function import musicbrainzngs import re import traceback -from six.moves.urllib.parse import urljoin from beets import logging from beets import plugins @@ -28,14 +25,12 @@ import beets from beets import util from beets import config -import six +from collections import Counter +from urllib.parse import urljoin VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' -if util.SNI_SUPPORTED: - BASE_URL = 'https://musicbrainz.org/' -else: - BASE_URL = 'http://musicbrainz.org/' +BASE_URL = 'https://musicbrainz.org/' SKIPPED_TRACKS = ['[data track]'] @@ -55,17 +50,19 @@ """An error while talking to MusicBrainz. The `query` field is the parameter to the action and may have any type. """ + def __init__(self, reason, verb, query, tb=None): self.query = query if isinstance(reason, musicbrainzngs.WebServiceError): - reason = u'MusicBrainz not reachable' - super(MusicBrainzAPIError, self).__init__(reason, verb, tb) + reason = 'MusicBrainz not reachable' + super().__init__(reason, verb, tb) def get_message(self): - return u'{0} in {1} with query {2}'.format( + return '{} in {} with query {}'.format( self._reasonstr(), self.verb, repr(self.query) ) + log = logging.getLogger('beets') RELEASE_INCLUDES = ['artists', 'media', 'recordings', 'release-groups', @@ -159,7 +156,7 @@ artist_sort_parts = [] artist_credit_parts = [] for el in credit: - if isinstance(el, six.string_types): + if isinstance(el, str): # Join phrase. artist_parts.append(el) artist_credit_parts.append(el) @@ -212,7 +209,7 @@ medium=medium, medium_index=medium_index, medium_total=medium_total, - data_source=u'MusicBrainz', + data_source='MusicBrainz', data_url=track_url(recording['id']), ) @@ -255,10 +252,10 @@ composer_sort.append( artist_relation['artist']['sort-name']) if lyricist: - info.lyricist = u', '.join(lyricist) + info.lyricist = ', '.join(lyricist) if composer: - info.composer = u', '.join(composer) - info.composer_sort = u', '.join(composer_sort) + info.composer = ', '.join(composer) + info.composer_sort = ', '.join(composer_sort) arranger = [] for artist_relation in recording.get('artist-relation-list', ()): @@ -267,7 +264,7 @@ if type == 'arranger': arranger.append(artist_relation['artist']['name']) if arranger: - info.arranger = u', '.join(arranger) + info.arranger = ', '.join(arranger) # Supplementary fields provided by plugins extra_trackdatas = plugins.send('mb_track_extract', data=recording) @@ -312,14 +309,14 @@ # when the release has more than 500 tracks. So we use browse_recordings # on chunks of tracks to recover the same information in this case. if ntracks > BROWSE_MAXTRACKS: - log.debug(u'Album {} has too many tracks', release['id']) + log.debug('Album {} has too many tracks', release['id']) recording_list = [] for i in range(0, ntracks, BROWSE_CHUNKSIZE): - log.debug(u'Retrieving tracks starting at {}', i) + log.debug('Retrieving tracks starting at {}', i) recording_list.extend(musicbrainzngs.browse_recordings( - release=release['id'], limit=BROWSE_CHUNKSIZE, - includes=BROWSE_INCLUDES, - offset=i)['recording-list']) + release=release['id'], limit=BROWSE_CHUNKSIZE, + includes=BROWSE_INCLUDES, + offset=i)['recording-list']) track_map = {r['id']: r for r in recording_list} for medium in release['medium-list']: for recording in medium['track-list']: @@ -392,7 +389,7 @@ mediums=len(release['medium-list']), artist_sort=artist_sort_name, artist_credit=artist_credit_name, - data_source=u'MusicBrainz', + data_source='MusicBrainz', data_url=album_url(release['id']), ) info.va = info.artist_id == VARIOUS_ARTISTS_ID @@ -416,18 +413,17 @@ if reltype: info.albumtype = reltype.lower() - # Log the new-style "primary" and "secondary" release types. - # Eventually, we'd like to actually store this data, but we just log - # it for now to help understand the differences. + # Set the new-style "primary" and "secondary" release types. + albumtypes = [] if 'primary-type' in release['release-group']: rel_primarytype = release['release-group']['primary-type'] if rel_primarytype: - log.debug('primary MB release type: ' + rel_primarytype.lower()) + albumtypes.append(rel_primarytype.lower()) if 'secondary-type-list' in release['release-group']: if release['release-group']['secondary-type-list']: - log.debug('secondary MB release type(s): ' + ', '.join( - [secondarytype.lower() for secondarytype in - release['release-group']['secondary-type-list']])) + for sec_type in release['release-group']['secondary-type-list']: + albumtypes.append(sec_type.lower()) + info.albumtypes = '; '.join(albumtypes) # Release events. info.country, release_date = _preferred_release_event(release) @@ -458,9 +454,17 @@ first_medium = release['medium-list'][0] info.media = first_medium.get('format') - genres = release.get('genre-list') - if config['musicbrainz']['genres'] and genres: - info.genre = ';'.join(g['name'] for g in genres) + if config['musicbrainz']['genres']: + sources = [ + release['release-group'].get('genre-list', []), + release.get('genre-list', []), + ] + genres = Counter() + for source in sources: + for genreitem in source: + genres[genreitem['name']] += int(genreitem['count']) + info.genre = '; '.join(g[0] for g in sorted(genres.items(), + key=lambda g: -g[1])) extra_albumdatas = plugins.send('mb_album_extract', data=release) for extra_albumdata in extra_albumdatas: @@ -486,15 +490,15 @@ # Various Artists search. criteria['arid'] = VARIOUS_ARTISTS_ID if tracks is not None: - criteria['tracks'] = six.text_type(tracks) + criteria['tracks'] = str(tracks) # Additional search cues from existing metadata. if extra_tags: for tag in extra_tags: key = FIELDS_TO_MB_KEYS[tag] - value = six.text_type(extra_tags.get(tag, '')).lower().strip() + value = str(extra_tags.get(tag, '')).lower().strip() if key == 'catno': - value = value.replace(u' ', '') + value = value.replace(' ', '') if value: criteria[key] = value @@ -503,7 +507,7 @@ return try: - log.debug(u'Searching for MusicBrainz releases with: {!r}', criteria) + log.debug('Searching for MusicBrainz releases with: {!r}', criteria) res = musicbrainzngs.search_releases( limit=config['musicbrainz']['searchlimit'].get(int), **criteria) except musicbrainzngs.MusicBrainzError as exc: @@ -544,7 +548,7 @@ no ID can be found, return None. """ # Find the first thing that looks like a UUID/MBID. - match = re.search(u'[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}', s) + match = re.search('[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}', s) if match: return match.group() @@ -554,19 +558,19 @@ object or None if the album is not found. May raise a MusicBrainzAPIError. """ - log.debug(u'Requesting MusicBrainz release {}', releaseid) + log.debug('Requesting MusicBrainz release {}', releaseid) albumid = _parse_id(releaseid) if not albumid: - log.debug(u'Invalid MBID ({0}).', releaseid) + log.debug('Invalid MBID ({0}).', releaseid) return try: res = musicbrainzngs.get_release_by_id(albumid, RELEASE_INCLUDES) except musicbrainzngs.ResponseError: - log.debug(u'Album ID match failed.') + log.debug('Album ID match failed.') return None except musicbrainzngs.MusicBrainzError as exc: - raise MusicBrainzAPIError(exc, u'get release by ID', albumid, + raise MusicBrainzAPIError(exc, 'get release by ID', albumid, traceback.format_exc()) return album_info(res['release']) @@ -577,14 +581,14 @@ """ trackid = _parse_id(releaseid) if not trackid: - log.debug(u'Invalid MBID ({0}).', releaseid) + log.debug('Invalid MBID ({0}).', releaseid) return try: res = musicbrainzngs.get_recording_by_id(trackid, TRACK_INCLUDES) except musicbrainzngs.ResponseError: - log.debug(u'Track ID match failed.') + log.debug('Track ID match failed.') return None except musicbrainzngs.MusicBrainzError as exc: - raise MusicBrainzAPIError(exc, u'get recording by ID', trackid, + raise MusicBrainzAPIError(exc, 'get recording by ID', trackid, traceback.format_exc()) return track_info(res['recording']) diff -Nru beets-1.5.0/beets/dbcore/db.py beets-1.6.0/beets/dbcore/db.py --- beets-1.5.0/beets/dbcore/db.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beets/dbcore/db.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """The central Model and Database constructs for DBCore. """ -from __future__ import division, absolute_import, print_function import time import os @@ -30,11 +28,7 @@ from beets.util import py3_path from beets.dbcore import types from .query import MatchQuery, NullSort, TrueQuery -import six -if six.PY2: - from collections import Mapping -else: - from collections.abc import Mapping +from collections.abc import Mapping class DBAccessError(Exception): @@ -86,7 +80,7 @@ def get(self, key, default=None): if default is None: default = self.model._type(key).format(None) - return super(FormattedMapping, self).get(key, default) + return super().get(key, default) def _get_formatted(self, model, key): value = model._type(key).format(model.get(key)) @@ -107,7 +101,7 @@ return value -class LazyConvertDict(object): +class LazyConvertDict: """Lazily convert types for attributes fetched from the database """ @@ -203,7 +197,7 @@ # Abstract base for model classes. -class Model(object): +class Model: """An abstract object representing an object in the database. Model objects act like dictionaries (i.e., they allow subscript access like ``obj['field']``). The same field set is available via attribute @@ -317,9 +311,9 @@ return obj def __repr__(self): - return '{0}({1})'.format( + return '{}({})'.format( type(self).__name__, - ', '.join('{0}={1!r}'.format(k, v) for k, v in dict(self).items()), + ', '.join(f'{k}={v!r}' for k, v in dict(self).items()), ) def clear_dirty(self): @@ -337,10 +331,10 @@ """ if not self._db: raise ValueError( - u'{0} has no database'.format(type(self).__name__) + '{} has no database'.format(type(self).__name__) ) if need_id and not self.id: - raise ValueError(u'{0} has no id'.format(type(self).__name__)) + raise ValueError('{} has no id'.format(type(self).__name__)) def copy(self): """Create a copy of the model object. @@ -431,9 +425,9 @@ elif key in self._fields: # Fixed setattr(self, key, self._type(key).null) elif key in self._getters(): # Computed. - raise KeyError(u'computed field {0} cannot be deleted'.format(key)) + raise KeyError(f'computed field {key} cannot be deleted') else: - raise KeyError(u'no such field {0}'.format(key)) + raise KeyError(f'no such field {key}') def keys(self, computed=False): """Get a list of available field names for this object. The @@ -483,22 +477,22 @@ def __getattr__(self, key): if key.startswith('_'): - raise AttributeError(u'model has no attribute {0!r}'.format(key)) + raise AttributeError(f'model has no attribute {key!r}') else: try: return self[key] except KeyError: - raise AttributeError(u'no such field {0!r}'.format(key)) + raise AttributeError(f'no such field {key!r}') def __setattr__(self, key, value): if key.startswith('_'): - super(Model, self).__setattr__(key, value) + super().__setattr__(key, value) else: self[key] = value def __delattr__(self, key): if key.startswith('_'): - super(Model, self).__delattr__(key) + super().__delattr__(key) else: del self[key] @@ -527,7 +521,7 @@ with self._db.transaction() as tx: # Main table update. if assignments: - query = 'UPDATE {0} SET {1} WHERE id=?'.format( + query = 'UPDATE {} SET {} WHERE id=?'.format( self._table, assignments ) subvars.append(self.id) @@ -538,7 +532,7 @@ if key in self._dirty: self._dirty.remove(key) tx.mutate( - 'INSERT INTO {0} ' + 'INSERT INTO {} ' '(entity_id, key, value) ' 'VALUES (?, ?, ?);'.format(self._flex_table), (self.id, key, value), @@ -547,7 +541,7 @@ # Deleted flexible attributes. for key in self._dirty: tx.mutate( - 'DELETE FROM {0} ' + 'DELETE FROM {} ' 'WHERE entity_id=? AND key=?'.format(self._flex_table), (self.id, key) ) @@ -565,7 +559,7 @@ # Exit early return stored_obj = self._db._get(type(self), self.id) - assert stored_obj is not None, u"object {0} not in DB".format(self.id) + assert stored_obj is not None, f"object {self.id} not in DB" self._values_fixed = LazyConvertDict(self) self._values_flex = LazyConvertDict(self) self.update(dict(stored_obj)) @@ -577,11 +571,11 @@ self._check_db() with self._db.transaction() as tx: tx.mutate( - 'DELETE FROM {0} WHERE id=?'.format(self._table), + f'DELETE FROM {self._table} WHERE id=?', (self.id,) ) tx.mutate( - 'DELETE FROM {0} WHERE entity_id=?'.format(self._flex_table), + f'DELETE FROM {self._flex_table} WHERE entity_id=?', (self.id,) ) @@ -599,7 +593,7 @@ with self._db.transaction() as tx: new_id = tx.mutate( - 'INSERT INTO {0} DEFAULT VALUES'.format(self._table) + f'INSERT INTO {self._table} DEFAULT VALUES' ) self.id = new_id self.added = time.time() @@ -626,7 +620,7 @@ separators will be added to the template. """ # Perform substitution. - if isinstance(template, six.string_types): + if isinstance(template, str): template = functemplate.template(template) return template.substitute(self.formatted(for_path=for_path), self._template_funcs()) @@ -637,8 +631,8 @@ def _parse(cls, key, string): """Parse a string as a value for the given key. """ - if not isinstance(string, six.string_types): - raise TypeError(u"_parse() argument must be a string") + if not isinstance(string, str): + raise TypeError("_parse() argument must be a string") return cls._type(key).parse(string) @@ -650,10 +644,11 @@ # Database controller and supporting interfaces. -class Results(object): +class Results: """An item query result set. Iterating over the collection lazily constructs LibModel objects that reflect database rows. """ + def __init__(self, model_class, rows, db, flex_rows, query=None, sort=None): """Create a result set that will construct objects of type @@ -751,8 +746,8 @@ """ Create a Model object for the given row """ cols = dict(row) - values = dict((k, v) for (k, v) in cols.items() - if not k[:4] == 'flex') + values = {k: v for (k, v) in cols.items() + if not k[:4] == 'flex'} # Construct the Python object obj = self.model_class._awaken(self.db, values, flex_values) @@ -801,7 +796,7 @@ next(it) return next(it) except StopIteration: - raise IndexError(u'result index {0} out of range'.format(n)) + raise IndexError(f'result index {n} out of range') def get(self): """Return the first matching object, or None if no objects @@ -814,7 +809,7 @@ return None -class Transaction(object): +class Transaction: """A context manager for safe, concurrent access to the database. All SQL commands should be executed through a transaction. """ @@ -889,7 +884,7 @@ self.db._connection().executescript(statements) -class Database(object): +class Database: """A container for Model objects that wraps an SQLite database as the backend. """ @@ -1001,7 +996,7 @@ """Load an SQLite extension into all open connections.""" if not self.supports_extensions: raise ValueError( - 'this sqlite3 installation does not support extensions') + 'this sqlite3 installation does not support extensions') self._extensions.append(path) @@ -1018,7 +1013,7 @@ # Get current schema. with self.transaction() as tx: rows = tx.query('PRAGMA table_info(%s)' % table) - current_fields = set([row[1] for row in rows]) + current_fields = {row[1] for row in rows} field_names = set(fields.keys()) if current_fields.issuperset(field_names): @@ -1029,9 +1024,9 @@ # No table exists. columns = [] for name, typ in fields.items(): - columns.append('{0} {1}'.format(name, typ.sql)) - setup_sql = 'CREATE TABLE {0} ({1});\n'.format(table, - ', '.join(columns)) + columns.append(f'{name} {typ.sql}') + setup_sql = 'CREATE TABLE {} ({});\n'.format(table, + ', '.join(columns)) else: # Table exists does not match the field set. @@ -1039,7 +1034,7 @@ for name, typ in fields.items(): if name in current_fields: continue - setup_sql += 'ALTER TABLE {0} ADD COLUMN {1} {2};\n'.format( + setup_sql += 'ALTER TABLE {} ADD COLUMN {} {};\n'.format( table, name, typ.sql ) @@ -1075,23 +1070,23 @@ where, subvals = query.clause() order_by = sort.order_clause() - sql = ("SELECT * FROM {0} WHERE {1} {2}").format( + sql = ("SELECT * FROM {} WHERE {} {}").format( model_cls._table, where or '1', - "ORDER BY {0}".format(order_by) if order_by else '', + f"ORDER BY {order_by}" if order_by else '', ) # Fetch flexible attributes for items matching the main query. # Doing the per-item filtering in python is faster than issuing # one query per item to sqlite. flex_sql = (""" - SELECT * FROM {0} WHERE entity_id IN - (SELECT id FROM {1} WHERE {2}); + SELECT * FROM {} WHERE entity_id IN + (SELECT id FROM {} WHERE {}); """.format( - model_cls._flex_table, - model_cls._table, - where or '1', - ) + model_cls._flex_table, + model_cls._table, + where or '1', + ) ) with self.transaction() as tx: diff -Nru beets-1.5.0/beets/dbcore/__init__.py beets-1.6.0/beets/dbcore/__init__.py --- beets-1.5.0/beets/dbcore/__init__.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/dbcore/__init__.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """DBCore is an abstract database package that forms the basis for beets' Library. """ -from __future__ import division, absolute_import, print_function from .db import Model, Database from .query import Query, FieldQuery, MatchQuery, AndQuery, OrQuery diff -Nru beets-1.5.0/beets/dbcore/queryparse.py beets-1.6.0/beets/dbcore/queryparse.py --- beets-1.5.0/beets/dbcore/queryparse.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/dbcore/queryparse.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """Parsing of strings into DBCore queries. """ -from __future__ import division, absolute_import, print_function import re import itertools @@ -226,8 +224,8 @@ # Split up query in to comma-separated subqueries, each representing # an AndQuery, which need to be joined together in one OrQuery subquery_parts = [] - for part in parts + [u',']: - if part.endswith(u','): + for part in parts + [',']: + if part.endswith(','): # Ensure we can catch "foo, bar" as well as "foo , bar" last_subquery_part = part[:-1] if last_subquery_part: @@ -241,8 +239,8 @@ else: # Sort parts (1) end in + or -, (2) don't have a field, and # (3) consist of more than just the + or -. - if part.endswith((u'+', u'-')) \ - and u':' not in part \ + if part.endswith(('+', '-')) \ + and ':' not in part \ and len(part) > 1: sort_parts.append(part) else: diff -Nru beets-1.5.0/beets/dbcore/query.py beets-1.6.0/beets/dbcore/query.py --- beets-1.5.0/beets/dbcore/query.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beets/dbcore/query.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """The Query type hierarchy for DBCore. """ -from __future__ import division, absolute_import, print_function import re from operator import mul @@ -23,10 +21,6 @@ from datetime import datetime, timedelta import unicodedata from functools import reduce -import six - -if not six.PY2: - buffer = memoryview # sqlite won't accept memoryview in python 2 class ParsingError(ValueError): @@ -44,8 +38,8 @@ def __init__(self, query, explanation): if isinstance(query, list): query = " ".join(query) - message = u"'{0}': {1}".format(query, explanation) - super(InvalidQueryError, self).__init__(message) + message = f"'{query}': {explanation}" + super().__init__(message) class InvalidQueryArgumentValueError(ParsingError): @@ -56,13 +50,13 @@ """ def __init__(self, what, expected, detail=None): - message = u"'{0}' is not {1}".format(what, expected) + message = f"'{what}' is not {expected}" if detail: - message = u"{0}: {1}".format(message, detail) - super(InvalidQueryArgumentValueError, self).__init__(message) + message = f"{message}: {detail}" + super().__init__(message) -class Query(object): +class Query: """An abstract class representing a query into the item database. """ @@ -82,7 +76,7 @@ raise NotImplementedError def __repr__(self): - return "{0.__class__.__name__}()".format(self) + return f"{self.__class__.__name__}()" def __eq__(self, other): return type(self) == type(other) @@ -129,7 +123,7 @@ "{0.fast})".format(self)) def __eq__(self, other): - return super(FieldQuery, self).__eq__(other) and \ + return super().__eq__(other) and \ self.field == other.field and self.pattern == other.pattern def __hash__(self): @@ -151,7 +145,7 @@ """A query that checks whether a field is null.""" def __init__(self, field, fast=True): - super(NoneQuery, self).__init__(field, None, fast) + super().__init__(field, None, fast) def col_clause(self): return self.field + " IS NULL", () @@ -210,14 +204,14 @@ """ def __init__(self, field, pattern, fast=True): - super(RegexpQuery, self).__init__(field, pattern, fast) + super().__init__(field, pattern, fast) pattern = self._normalize(pattern) try: self.pattern = re.compile(self.pattern) except re.error as exc: # Invalid regular expression. raise InvalidQueryArgumentValueError(pattern, - u"a regular expression", + "a regular expression", format(exc)) @staticmethod @@ -238,8 +232,8 @@ """ def __init__(self, field, pattern, fast=True): - super(BooleanQuery, self).__init__(field, pattern, fast) - if isinstance(pattern, six.string_types): + super().__init__(field, pattern, fast) + if isinstance(pattern, str): self.pattern = util.str2bool(pattern) self.pattern = int(self.pattern) @@ -252,16 +246,16 @@ """ def __init__(self, field, pattern): - super(BytesQuery, self).__init__(field, pattern) + super().__init__(field, pattern) # Use a buffer/memoryview representation of the pattern for SQLite # matching. This instructs SQLite to treat the blob as binary # rather than encoded Unicode. - if isinstance(self.pattern, (six.text_type, bytes)): - if isinstance(self.pattern, six.text_type): + if isinstance(self.pattern, (str, bytes)): + if isinstance(self.pattern, str): self.pattern = self.pattern.encode('utf-8') - self.buf_pattern = buffer(self.pattern) - elif isinstance(self.pattern, buffer): + self.buf_pattern = memoryview(self.pattern) + elif isinstance(self.pattern, memoryview): self.buf_pattern = self.pattern self.pattern = bytes(self.pattern) @@ -293,10 +287,10 @@ try: return float(s) except ValueError: - raise InvalidQueryArgumentValueError(s, u"an int or a float") + raise InvalidQueryArgumentValueError(s, "an int or a float") def __init__(self, field, pattern, fast=True): - super(NumericQuery, self).__init__(field, pattern, fast) + super().__init__(field, pattern, fast) parts = pattern.split('..', 1) if len(parts) == 1: @@ -314,7 +308,7 @@ if self.field not in item: return False value = item[self.field] - if isinstance(value, six.string_types): + if isinstance(value, str): value = self._convert(value) if self.point is not None: @@ -331,14 +325,14 @@ return self.field + '=?', (self.point,) else: if self.rangemin is not None and self.rangemax is not None: - return (u'{0} >= ? AND {0} <= ?'.format(self.field), + return ('{0} >= ? AND {0} <= ?'.format(self.field), (self.rangemin, self.rangemax)) elif self.rangemin is not None: - return u'{0} >= ?'.format(self.field), (self.rangemin,) + return f'{self.field} >= ?', (self.rangemin,) elif self.rangemax is not None: - return u'{0} <= ?'.format(self.field), (self.rangemax,) + return f'{self.field} <= ?', (self.rangemax,) else: - return u'1', () + return '1', () class CollectionQuery(Query): @@ -383,7 +377,7 @@ return "{0.__class__.__name__}({0.subqueries!r})".format(self) def __eq__(self, other): - return super(CollectionQuery, self).__eq__(other) and \ + return super().__eq__(other) and \ self.subqueries == other.subqueries def __hash__(self): @@ -407,7 +401,7 @@ subqueries = [] for field in self.fields: subqueries.append(cls(field, pattern, True)) - super(AnyFieldQuery, self).__init__(subqueries) + super().__init__(subqueries) def clause(self): return self.clause_with_joiner('or') @@ -423,7 +417,7 @@ "{0.query_class.__name__})".format(self)) def __eq__(self, other): - return super(AnyFieldQuery, self).__eq__(other) and \ + return super().__eq__(other) and \ self.query_class == other.query_class def __hash__(self): @@ -449,7 +443,7 @@ return self.clause_with_joiner('and') def match(self, item): - return all([q.match(item) for q in self.subqueries]) + return all(q.match(item) for q in self.subqueries) class OrQuery(MutableCollectionQuery): @@ -459,7 +453,7 @@ return self.clause_with_joiner('or') def match(self, item): - return any([q.match(item) for q in self.subqueries]) + return any(q.match(item) for q in self.subqueries) class NotQuery(Query): @@ -473,7 +467,7 @@ def clause(self): clause, subvals = self.subquery.clause() if clause: - return 'not ({0})'.format(clause), subvals + return f'not ({clause})', subvals else: # If there is no clause, there is nothing to negate. All the logic # is handled by match() for slow queries. @@ -486,7 +480,7 @@ return "{0.__class__.__name__}({0.subquery!r})".format(self) def __eq__(self, other): - return super(NotQuery, self).__eq__(other) and \ + return super().__eq__(other) and \ self.subquery == other.subquery def __hash__(self): @@ -542,7 +536,7 @@ return (start, end) -class Period(object): +class Period: """A period of time given by a date, time and precision. Example: 2014-01-01 10:50:30 with precision 'month' represents all @@ -568,7 +562,7 @@ or "second"). """ if precision not in Period.precisions: - raise ValueError(u'Invalid precision {0}'.format(precision)) + raise ValueError(f'Invalid precision {precision}') self.date = date self.precision = precision @@ -649,10 +643,10 @@ elif 'second' == precision: return date + timedelta(seconds=1) else: - raise ValueError(u'unhandled precision {0}'.format(precision)) + raise ValueError(f'unhandled precision {precision}') -class DateInterval(object): +class DateInterval: """A closed-open interval of dates. A left endpoint of None means since the beginning of time. @@ -661,7 +655,7 @@ def __init__(self, start, end): if start is not None and end is not None and not start < end: - raise ValueError(u"start date {0} is not before end date {1}" + raise ValueError("start date {} is not before end date {}" .format(start, end)) self.start = start self.end = end @@ -682,7 +676,7 @@ return True def __str__(self): - return '[{0}, {1})'.format(self.start, self.end) + return f'[{self.start}, {self.end})' class DateQuery(FieldQuery): @@ -696,7 +690,7 @@ """ def __init__(self, field, pattern, fast=True): - super(DateQuery, self).__init__(field, pattern, fast) + super().__init__(field, pattern, fast) start, end = _parse_periods(pattern) self.interval = DateInterval.from_periods(start, end) @@ -755,12 +749,12 @@ except ValueError: raise InvalidQueryArgumentValueError( s, - u"a M:SS string or a float") + "a M:SS string or a float") # Sorting. -class Sort(object): +class Sort: """An abstract class representing a sort operation for a query into the item database. """ @@ -847,13 +841,13 @@ return items def __repr__(self): - return 'MultipleSort({!r})'.format(self.sorts) + return f'MultipleSort({self.sorts!r})' def __hash__(self): return hash(tuple(self.sorts)) def __eq__(self, other): - return super(MultipleSort, self).__eq__(other) and \ + return super().__eq__(other) and \ self.sorts == other.sorts @@ -874,14 +868,14 @@ def key(item): field_val = item.get(self.field, '') - if self.case_insensitive and isinstance(field_val, six.text_type): + if self.case_insensitive and isinstance(field_val, str): field_val = field_val.lower() return field_val return sorted(objs, key=key, reverse=not self.ascending) def __repr__(self): - return '<{0}: {1}{2}>'.format( + return '<{}: {}{}>'.format( type(self).__name__, self.field, '+' if self.ascending else '-', @@ -891,7 +885,7 @@ return hash((self.field, self.ascending)) def __eq__(self, other): - return super(FieldSort, self).__eq__(other) and \ + return super().__eq__(other) and \ self.field == other.field and \ self.ascending == other.ascending @@ -909,7 +903,7 @@ 'ELSE {0} END)'.format(self.field) else: field = self.field - return "{0} {1}".format(field, order) + return f"{field} {order}" class SlowFieldSort(FieldSort): diff -Nru beets-1.5.0/beets/dbcore/types.py beets-1.6.0/beets/dbcore/types.py --- beets-1.5.0/beets/dbcore/types.py 2020-12-15 12:48:01.000000000 +0000 +++ beets-1.6.0/beets/dbcore/types.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,25 +14,20 @@ """Representation of type information for DBCore model fields. """ -from __future__ import division, absolute_import, print_function from . import query from beets.util import str2bool -import six - -if not six.PY2: - buffer = memoryview # sqlite won't accept memoryview in python 2 # Abstract base. -class Type(object): +class Type: """An object encapsulating the type of a model field. Includes information about how to store, query, format, and parse a given field. """ - sql = u'TEXT' + sql = 'TEXT' """The SQLite column type for the value. """ @@ -41,7 +35,7 @@ """The `Query` subclass to be used when querying the field. """ - model_type = six.text_type + model_type = str """The Python type that is used to represent the value in the model. The model is guaranteed to return a value of this type if the field @@ -63,11 +57,11 @@ value = self.null # `self.null` might be `None` if value is None: - value = u'' + value = '' if isinstance(value, bytes): value = value.decode('utf-8', 'ignore') - return six.text_type(value) + return str(value) def parse(self, string): """Parse a (possibly human-written) string and return the @@ -101,12 +95,12 @@ https://docs.python.org/2/library/sqlite3.html#sqlite-and-python-types Flexible fields have the type affinity `TEXT`. This means the - `sql_value` is either a `buffer`/`memoryview` or a `unicode` object` + `sql_value` is either a `memoryview` or a `unicode` object` and the method must handle these in addition. """ - if isinstance(sql_value, buffer): + if isinstance(sql_value, memoryview): sql_value = bytes(sql_value).decode('utf-8', 'ignore') - if isinstance(sql_value, six.text_type): + if isinstance(sql_value, str): return self.parse(sql_value) else: return self.normalize(sql_value) @@ -127,7 +121,7 @@ class Integer(Type): """A basic integer type. """ - sql = u'INTEGER' + sql = 'INTEGER' query = query.NumericQuery model_type = int @@ -148,7 +142,7 @@ self.digits = digits def format(self, value): - return u'{0:0{1}d}'.format(value or 0, self.digits) + return '{0:0{1}d}'.format(value or 0, self.digits) class NullPaddedInt(PaddedInt): @@ -161,12 +155,12 @@ """An integer whose formatting operation scales the number by a constant and adds a suffix. Good for units with large magnitudes. """ - def __init__(self, unit, suffix=u''): + def __init__(self, unit, suffix=''): self.unit = unit self.suffix = suffix def format(self, value): - return u'{0}{1}'.format((value or 0) // self.unit, self.suffix) + return '{}{}'.format((value or 0) // self.unit, self.suffix) class Id(Integer): @@ -177,14 +171,14 @@ def __init__(self, primary=True): if primary: - self.sql = u'INTEGER PRIMARY KEY' + self.sql = 'INTEGER PRIMARY KEY' class Float(Type): """A basic floating-point type. The `digits` parameter specifies how many decimal places to use in the human-readable representation. """ - sql = u'REAL' + sql = 'REAL' query = query.NumericQuery model_type = float @@ -192,7 +186,7 @@ self.digits = digits def format(self, value): - return u'{0:.{1}f}'.format(value or 0, self.digits) + return '{0:.{1}f}'.format(value or 0, self.digits) class NullFloat(Float): @@ -204,7 +198,7 @@ class String(Type): """A Unicode string type. """ - sql = u'TEXT' + sql = 'TEXT' query = query.SubstringQuery def normalize(self, value): @@ -217,12 +211,12 @@ class Boolean(Type): """A boolean type. """ - sql = u'INTEGER' + sql = 'INTEGER' query = query.BooleanQuery model_type = bool def format(self, value): - return six.text_type(bool(value)) + return str(bool(value)) def parse(self, string): return str2bool(string) diff -Nru beets-1.5.0/beets/importer.py beets-1.6.0/beets/importer.py --- beets-1.5.0/beets/importer.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beets/importer.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function """Provides the basic, interface-agnostic workflow for importing and autotagging music files. @@ -75,7 +73,7 @@ # unpickling, including ImportError. We use a catch-all # exception to avoid enumerating them all (the docs don't even have a # full list!). - log.debug(u'state file could not be read: {0}', exc) + log.debug('state file could not be read: {0}', exc) return {} @@ -84,8 +82,8 @@ try: with open(config['statefile'].as_filename(), 'wb') as f: pickle.dump(state, f) - except IOError as exc: - log.error(u'state file could not be written: {0}', exc) + except OSError as exc: + log.error('state file could not be written: {0}', exc) # Utilities for reading and writing the beets progress file, which @@ -174,10 +172,11 @@ # Abstract session class. -class ImportSession(object): +class ImportSession: """Controls an import action. Subclasses should implement methods to communicate with the user or otherwise make decisions. """ + def __init__(self, lib, loghandler, paths, query): """Create a session. `lib` is a Library object. `loghandler` is a logging.Handler. Either `paths` or `query` is non-null and indicates @@ -258,7 +257,7 @@ """Log a message about a given album to the importer log. The status should reflect the reason the album couldn't be tagged. """ - self.logger.info(u'{0} {1}', status, displayable_path(paths)) + self.logger.info('{0} {1}', status, displayable_path(paths)) def log_choice(self, task, duplicate=False): """Logs the task's current choice if it should be logged. If @@ -269,17 +268,17 @@ if duplicate: # Duplicate: log all three choices (skip, keep both, and trump). if task.should_remove_duplicates: - self.tag_log(u'duplicate-replace', paths) + self.tag_log('duplicate-replace', paths) elif task.choice_flag in (action.ASIS, action.APPLY): - self.tag_log(u'duplicate-keep', paths) + self.tag_log('duplicate-keep', paths) elif task.choice_flag is (action.SKIP): - self.tag_log(u'duplicate-skip', paths) + self.tag_log('duplicate-skip', paths) else: # Non-duplicate: log "skip" and "asis" choices. if task.choice_flag is action.ASIS: - self.tag_log(u'asis', paths) + self.tag_log('asis', paths) elif task.choice_flag is action.SKIP: - self.tag_log(u'skip', paths) + self.tag_log('skip', paths) def should_resume(self, path): raise NotImplementedError @@ -296,7 +295,7 @@ def run(self): """Run the import task. """ - self.logger.info(u'import started {0}', time.asctime()) + self.logger.info('import started {0}', time.asctime()) self.set_config(config['import']) # Set up the pipeline. @@ -380,8 +379,8 @@ """Mark paths and directories as merged for future reimport tasks. """ self._merged_items.update(paths) - dirs = set([os.path.dirname(path) if os.path.isfile(path) else path - for path in paths]) + dirs = {os.path.dirname(path) if os.path.isfile(path) else path + for path in paths} self._merged_dirs.update(dirs) def is_resuming(self, toppath): @@ -401,7 +400,7 @@ # Either accept immediately or prompt for input to decide. if self.want_resume is True or \ self.should_resume(toppath): - log.warning(u'Resuming interrupted import of {0}', + log.warning('Resuming interrupted import of {0}', util.displayable_path(toppath)) self._is_resuming[toppath] = True else: @@ -411,11 +410,12 @@ # The importer task class. -class BaseImportTask(object): +class BaseImportTask: """An abstract base class for importer tasks. Tasks flow through the importer pipeline. Each stage can update them. """ + def __init__(self, toppath, paths, items): """Create a task. The primary fields that define a task are: @@ -469,8 +469,9 @@ * `finalize()` Update the import progress and cleanup the file system. """ + def __init__(self, toppath, paths, items): - super(ImportTask, self).__init__(toppath, paths, items) + super().__init__(toppath, paths, items) self.choice_flag = None self.cur_album = None self.cur_artist = None @@ -562,11 +563,11 @@ def remove_duplicates(self, lib): duplicate_items = self.duplicate_items(lib) - log.debug(u'removing {0} old duplicated items', len(duplicate_items)) + log.debug('removing {0} old duplicated items', len(duplicate_items)) for item in duplicate_items: item.remove() if lib.directory in util.ancestry(item.path): - log.debug(u'deleting duplicate {0}', + log.debug('deleting duplicate {0}', util.displayable_path(item.path)) util.remove(item.path) util.prune_dirs(os.path.dirname(item.path), @@ -579,7 +580,7 @@ items = self.imported_items() for field, view in config['import']['set_fields'].items(): value = view.get() - log.debug(u'Set field {1}={2} for {0}', + log.debug('Set field {1}={2} for {0}', displayable_path(self.paths), field, value) @@ -673,7 +674,7 @@ return [] duplicates = [] - task_paths = set(i.path for i in self.items if i) + task_paths = {i.path for i in self.items if i} duplicate_query = dbcore.AndQuery(( dbcore.MatchQuery('albumartist', artist), dbcore.MatchQuery('album', album), @@ -683,7 +684,7 @@ # Check whether the album paths are all present in the task # i.e. album is being completely re-imported by the task, # in which case it is not a duplicate (will be replaced). - album_paths = set(i.path for i in album.items()) + album_paths = {i.path for i in album.items()} if not (album_paths <= task_paths): duplicates.append(album) return duplicates @@ -809,8 +810,8 @@ self.album.artpath = replaced_album.artpath self.album.store() log.debug( - u'Reimported album: added {0}, flexible ' - u'attributes {1} from album {2} for {3}', + 'Reimported album: added {0}, flexible ' + 'attributes {1} from album {2} for {3}', self.album.added, replaced_album._values_flex.keys(), replaced_album.id, @@ -823,16 +824,16 @@ if dup_item.added and dup_item.added != item.added: item.added = dup_item.added log.debug( - u'Reimported item added {0} ' - u'from item {1} for {2}', + 'Reimported item added {0} ' + 'from item {1} for {2}', item.added, dup_item.id, displayable_path(item.path) ) item.update(dup_item._values_flex) log.debug( - u'Reimported item flexible attributes {0} ' - u'from item {1} for {2}', + 'Reimported item flexible attributes {0} ' + 'from item {1} for {2}', dup_item._values_flex.keys(), dup_item.id, displayable_path(item.path) @@ -845,10 +846,10 @@ """ for item in self.imported_items(): for dup_item in self.replaced_items[item]: - log.debug(u'Replacing item {0}: {1}', + log.debug('Replacing item {0}: {1}', dup_item.id, displayable_path(item.path)) dup_item.remove() - log.debug(u'{0} of {1} items replaced', + log.debug('{0} of {1} items replaced', sum(bool(l) for l in self.replaced_items.values()), len(self.imported_items())) @@ -886,7 +887,7 @@ """ def __init__(self, toppath, item): - super(SingletonImportTask, self).__init__(toppath, [item.path], [item]) + super().__init__(toppath, [item.path], [item]) self.item = item self.is_album = False self.paths = [item.path] @@ -958,7 +959,7 @@ """ for field, view in config['import']['set_fields'].items(): value = view.get() - log.debug(u'Set field {1}={2} for {0}', + log.debug('Set field {1}={2} for {0}', displayable_path(self.paths), field, value) @@ -979,7 +980,7 @@ """ def __init__(self, toppath, paths): - super(SentinelImportTask, self).__init__(toppath, paths, ()) + super().__init__(toppath, paths, ()) # TODO Remove the remaining attributes eventually self.should_remove_duplicates = False self.is_album = True @@ -1023,7 +1024,7 @@ """ def __init__(self, toppath): - super(ArchiveImportTask, self).__init__(toppath, ()) + super().__init__(toppath, ()) self.extracted = False @classmethod @@ -1073,7 +1074,7 @@ """Removes the temporary directory the archive was extracted to. """ if self.extracted: - log.debug(u'Removing extracted directory: {0}', + log.debug('Removing extracted directory: {0}', displayable_path(self.toppath)) shutil.rmtree(self.toppath) @@ -1095,10 +1096,11 @@ self.toppath = extract_to -class ImportTaskFactory(object): +class ImportTaskFactory: """Generate album and singleton import tasks for all media files indicated by a path. """ + def __init__(self, toppath, session): """Create a new task factory. @@ -1136,14 +1138,12 @@ if self.session.config['singletons']: for path in paths: tasks = self._create(self.singleton(path)) - for task in tasks: - yield task + yield from tasks yield self.sentinel(dirs) else: tasks = self._create(self.album(paths, dirs)) - for task in tasks: - yield task + yield from tasks # Produce the final sentinel for this toppath to indicate that # it is finished. This is usually just a SentinelImportTask, but @@ -1191,7 +1191,7 @@ """Return a `SingletonImportTask` for the music file. """ if self.session.already_imported(self.toppath, [path]): - log.debug(u'Skipping previously-imported path: {0}', + log.debug('Skipping previously-imported path: {0}', displayable_path(path)) self.skipped += 1 return None @@ -1212,10 +1212,10 @@ return None if dirs is None: - dirs = list(set(os.path.dirname(p) for p in paths)) + dirs = list({os.path.dirname(p) for p in paths}) if self.session.already_imported(self.toppath, dirs): - log.debug(u'Skipping previously-imported path: {0}', + log.debug('Skipping previously-imported path: {0}', displayable_path(dirs)) self.skipped += 1 return None @@ -1245,22 +1245,22 @@ if not (self.session.config['move'] or self.session.config['copy']): - log.warning(u"Archive importing requires either " - u"'copy' or 'move' to be enabled.") + log.warning("Archive importing requires either " + "'copy' or 'move' to be enabled.") return - log.debug(u'Extracting archive: {0}', + log.debug('Extracting archive: {0}', displayable_path(self.toppath)) archive_task = ArchiveImportTask(self.toppath) try: archive_task.extract() except Exception as exc: - log.error(u'extraction failed: {0}', exc) + log.error('extraction failed: {0}', exc) return # Now read albums from the extracted directory. self.toppath = archive_task.toppath - log.debug(u'Archive extracted to: {0}', self.toppath) + log.debug('Archive extracted to: {0}', self.toppath) return archive_task def read_item(self, path): @@ -1276,9 +1276,9 @@ # Silently ignore non-music files. pass elif isinstance(exc.reason, mediafile.UnreadableFileError): - log.warning(u'unreadable file: {0}', displayable_path(path)) + log.warning('unreadable file: {0}', displayable_path(path)) else: - log.error(u'error reading {0}: {1}', + log.error('error reading {0}: {1}', displayable_path(path), exc) @@ -1317,17 +1317,16 @@ # Generate tasks. task_factory = ImportTaskFactory(toppath, session) - for t in task_factory.tasks(): - yield t + yield from task_factory.tasks() skipped += task_factory.skipped if not task_factory.imported: - log.warning(u'No files imported from {0}', + log.warning('No files imported from {0}', displayable_path(toppath)) # Show skipped directories (due to incremental/resume). if skipped: - log.info(u'Skipped {0} paths.', skipped) + log.info('Skipped {0} paths.', skipped) def query_tasks(session): @@ -1345,7 +1344,7 @@ else: # Search for albums. for album in session.lib.albums(session.query): - log.debug(u'yielding album {0}: {1} - {2}', + log.debug('yielding album {0}: {1} - {2}', album.id, album.albumartist, album.album) items = list(album.items()) _freshen_items(items) @@ -1368,7 +1367,7 @@ return plugins.send('import_task_start', session=session, task=task) - log.debug(u'Looking up: {0}', displayable_path(task.paths)) + log.debug('Looking up: {0}', displayable_path(task.paths)) # Restrict the initial lookup to IDs specified by the user via the -m # option. Currently all the IDs are passed onto the tasks directly. @@ -1407,8 +1406,7 @@ def emitter(task): for item in task.items: task = SingletonImportTask(task.toppath, item) - for new_task in task.handle_created(session): - yield new_task + yield from task.handle_created(session) yield SentinelImportTask(task.toppath, task.paths) return _extend_pipeline(emitter(task), @@ -1454,30 +1452,30 @@ if task.choice_flag in (action.ASIS, action.APPLY, action.RETAG): found_duplicates = task.find_duplicates(session.lib) if found_duplicates: - log.debug(u'found duplicates: {}'.format( + log.debug('found duplicates: {}'.format( [o.id for o in found_duplicates] )) # Get the default action to follow from config. duplicate_action = config['import']['duplicate_action'].as_choice({ - u'skip': u's', - u'keep': u'k', - u'remove': u'r', - u'merge': u'm', - u'ask': u'a', + 'skip': 's', + 'keep': 'k', + 'remove': 'r', + 'merge': 'm', + 'ask': 'a', }) - log.debug(u'default action for duplicates: {0}', duplicate_action) + log.debug('default action for duplicates: {0}', duplicate_action) - if duplicate_action == u's': + if duplicate_action == 's': # Skip new. task.set_choice(action.SKIP) - elif duplicate_action == u'k': + elif duplicate_action == 'k': # Keep both. Do nothing; leave the choice intact. pass - elif duplicate_action == u'r': + elif duplicate_action == 'r': # Remove old. task.should_remove_duplicates = True - elif duplicate_action == u'm': + elif duplicate_action == 'm': # Merge duplicates together task.should_merge_duplicates = True else: @@ -1497,7 +1495,7 @@ if task.skip: return - log.info(u'{}', displayable_path(task.paths)) + log.info('{}', displayable_path(task.paths)) task.set_choice(action.ASIS) apply_choice(session, task) @@ -1580,11 +1578,11 @@ """A coroutine (pipeline stage) to log each file to be imported. """ if isinstance(task, SingletonImportTask): - log.info(u'Singleton: {0}', displayable_path(task.item['path'])) + log.info('Singleton: {0}', displayable_path(task.item['path'])) elif task.items: - log.info(u'Album: {0}', displayable_path(task.paths[0])) + log.info('Album: {0}', displayable_path(task.paths[0])) for item in task.items: - log.info(u' {0}', displayable_path(item['path'])) + log.info(' {0}', displayable_path(item['path'])) def group_albums(session): diff -Nru beets-1.5.0/beets/__init__.py beets-1.6.0/beets/__init__.py --- beets-1.5.0/beets/__init__.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beets/__init__.py 2021-11-27 16:16:04.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,13 +12,12 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import confuse from sys import stderr -__version__ = u'1.5.0' -__author__ = u'Adrian Sampson ' +__version__ = '1.6.0' +__author__ = 'Adrian Sampson ' class IncludeLazyConfig(confuse.LazyConfig): @@ -27,7 +25,7 @@ YAML files specified in an `include` setting. """ def read(self, user=True, defaults=True): - super(IncludeLazyConfig, self).read(user, defaults) + super().read(user, defaults) try: for view in self['include']: diff -Nru beets-1.5.0/beets/library.py beets-1.6.0/beets/library.py --- beets-1.5.0/beets/library.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beets/library.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,15 +14,14 @@ """The core data store and collection logic for beets. """ -from __future__ import division, absolute_import, print_function import os import sys import unicodedata import time import re -import six import string +import shlex from beets import logging from mediafile import MediaFile, UnreadableFileError @@ -37,13 +35,9 @@ import beets # To use the SQLite "blob" type, it doesn't suffice to provide a byte -# string; SQLite treats that as encoded text. Wrapping it in a `buffer` or a -# `memoryview`, depending on the Python version, tells it that we -# actually mean non-text data. -if six.PY2: - BLOB_TYPE = buffer # noqa: F821 -else: - BLOB_TYPE = memoryview +# string; SQLite treats that as encoded text. Wrapping it in a +# `memoryview` tells it that we actually mean non-text data. +BLOB_TYPE = memoryview log = logging.getLogger('beets') @@ -65,7 +59,7 @@ `case_sensitive` can be a bool or `None`, indicating that the behavior should depend on the filesystem. """ - super(PathQuery, self).__init__(field, pattern, fast) + super().__init__(field, pattern, fast) # By default, the case sensitivity depends on the filesystem # that the query path is located on. @@ -150,7 +144,7 @@ `bytes` objects, in keeping with the Unix filesystem abstraction. """ - sql = u'BLOB' + sql = 'BLOB' query = PathQuery model_type = bytes @@ -174,7 +168,7 @@ return normpath(bytestring_path(string)) def normalize(self, value): - if isinstance(value, six.text_type): + if isinstance(value, str): # Paths stored internally as encoded bytes. return bytestring_path(value) @@ -252,6 +246,7 @@ """Sort by artist (either album artist or track artist), prioritizing the sort field over the raw field. """ + def __init__(self, model_cls, ascending=True, case_insensitive=True): self.album = model_cls is Album self.ascending = ascending @@ -267,12 +262,15 @@ def sort(self, objs): if self.album: - field = lambda a: a.albumartist_sort or a.albumartist + def field(a): + return a.albumartist_sort or a.albumartist else: - field = lambda i: i.artist_sort or i.artist + def field(i): + return i.artist_sort or i.artist if self.case_insensitive: - key = lambda x: field(x).lower() + def key(x): + return field(x).lower() else: key = field return sorted(objs, key=key, reverse=not self.ascending) @@ -283,17 +281,17 @@ # Exceptions. -@six.python_2_unicode_compatible class FileOperationError(Exception): """Indicates an error when interacting with a file on disk. Possibilities include an unsupported media type, a permissions error, and an unhandled Mutagen exception. """ + def __init__(self, path, reason): """Create an exception describing an operation on the file at `path` with the underlying (chained) exception `reason`. """ - super(FileOperationError, self).__init__(path, reason) + super().__init__(path, reason) self.path = path self.reason = reason @@ -301,9 +299,9 @@ """Get a string representing the error. Describes both the underlying reason and the file path in question. """ - return u'{0}: {1}'.format( + return '{}: {}'.format( util.displayable_path(self.path), - six.text_type(self.reason) + str(self.reason) ) # define __str__ as text to avoid infinite loop on super() calls @@ -311,25 +309,24 @@ __str__ = text -@six.python_2_unicode_compatible class ReadError(FileOperationError): """An error while reading a file (i.e. in `Item.read`). """ + def __str__(self): - return u'error reading ' + super(ReadError, self).text() + return 'error reading ' + super().text() -@six.python_2_unicode_compatible class WriteError(FileOperationError): """An error while writing a file (i.e. in `Item.write`). """ + def __str__(self): - return u'error writing ' + super(WriteError, self).text() + return 'error writing ' + super().text() # Item and Album model classes. -@six.python_2_unicode_compatible class LibModel(dbcore.Model): """Shared concrete functionality for Items and Albums. """ @@ -344,21 +341,21 @@ return funcs def store(self, fields=None): - super(LibModel, self).store(fields) + super().store(fields) plugins.send('database_change', lib=self._db, model=self) def remove(self): - super(LibModel, self).remove() + super().remove() plugins.send('database_change', lib=self._db, model=self) def add(self, lib=None): - super(LibModel, self).add(lib) + super().add(lib) plugins.send('database_change', lib=self._db, model=self) def __format__(self, spec): if not spec: spec = beets.config[self._format_config_key].as_str() - assert isinstance(spec, six.text_type) + assert isinstance(spec, str) return self.evaluate_template(spec) def __str__(self): @@ -379,8 +376,8 @@ def __init__(self, item, included_keys=ALL_KEYS, for_path=False): # We treat album and item keys specially here, # so exclude transitive album keys from the model's keys. - super(FormattedItemMapping, self).__init__(item, included_keys=[], - for_path=for_path) + super().__init__(item, included_keys=[], + for_path=for_path) self.included_keys = included_keys if included_keys == self.ALL_KEYS: # Performance note: this triggers a database query. @@ -454,84 +451,85 @@ _table = 'items' _flex_table = 'item_attributes' _fields = { - 'id': types.PRIMARY_ID, - 'path': PathType(), + 'id': types.PRIMARY_ID, + 'path': PathType(), 'album_id': types.FOREIGN_ID, - 'title': types.STRING, - 'artist': types.STRING, - 'artist_sort': types.STRING, - 'artist_credit': types.STRING, - 'album': types.STRING, - 'albumartist': types.STRING, - 'albumartist_sort': types.STRING, - 'albumartist_credit': types.STRING, - 'genre': types.STRING, - 'style': types.STRING, - 'discogs_albumid': types.INTEGER, - 'discogs_artistid': types.INTEGER, - 'discogs_labelid': types.INTEGER, - 'lyricist': types.STRING, - 'composer': types.STRING, - 'composer_sort': types.STRING, - 'work': types.STRING, - 'mb_workid': types.STRING, - 'work_disambig': types.STRING, - 'arranger': types.STRING, - 'grouping': types.STRING, - 'year': types.PaddedInt(4), - 'month': types.PaddedInt(2), - 'day': types.PaddedInt(2), - 'track': types.PaddedInt(2), - 'tracktotal': types.PaddedInt(2), - 'disc': types.PaddedInt(2), - 'disctotal': types.PaddedInt(2), - 'lyrics': types.STRING, - 'comments': types.STRING, - 'bpm': types.INTEGER, - 'comp': types.BOOLEAN, - 'mb_trackid': types.STRING, - 'mb_albumid': types.STRING, - 'mb_artistid': types.STRING, - 'mb_albumartistid': types.STRING, - 'mb_releasetrackid': types.STRING, - 'trackdisambig': types.STRING, - 'albumtype': types.STRING, - 'label': types.STRING, + 'title': types.STRING, + 'artist': types.STRING, + 'artist_sort': types.STRING, + 'artist_credit': types.STRING, + 'album': types.STRING, + 'albumartist': types.STRING, + 'albumartist_sort': types.STRING, + 'albumartist_credit': types.STRING, + 'genre': types.STRING, + 'style': types.STRING, + 'discogs_albumid': types.INTEGER, + 'discogs_artistid': types.INTEGER, + 'discogs_labelid': types.INTEGER, + 'lyricist': types.STRING, + 'composer': types.STRING, + 'composer_sort': types.STRING, + 'work': types.STRING, + 'mb_workid': types.STRING, + 'work_disambig': types.STRING, + 'arranger': types.STRING, + 'grouping': types.STRING, + 'year': types.PaddedInt(4), + 'month': types.PaddedInt(2), + 'day': types.PaddedInt(2), + 'track': types.PaddedInt(2), + 'tracktotal': types.PaddedInt(2), + 'disc': types.PaddedInt(2), + 'disctotal': types.PaddedInt(2), + 'lyrics': types.STRING, + 'comments': types.STRING, + 'bpm': types.INTEGER, + 'comp': types.BOOLEAN, + 'mb_trackid': types.STRING, + 'mb_albumid': types.STRING, + 'mb_artistid': types.STRING, + 'mb_albumartistid': types.STRING, + 'mb_releasetrackid': types.STRING, + 'trackdisambig': types.STRING, + 'albumtype': types.STRING, + 'albumtypes': types.STRING, + 'label': types.STRING, 'acoustid_fingerprint': types.STRING, - 'acoustid_id': types.STRING, - 'mb_releasegroupid': types.STRING, - 'asin': types.STRING, - 'isrc': types.STRING, - 'catalognum': types.STRING, - 'script': types.STRING, - 'language': types.STRING, - 'country': types.STRING, - 'albumstatus': types.STRING, - 'media': types.STRING, - 'albumdisambig': types.STRING, + 'acoustid_id': types.STRING, + 'mb_releasegroupid': types.STRING, + 'asin': types.STRING, + 'isrc': types.STRING, + 'catalognum': types.STRING, + 'script': types.STRING, + 'language': types.STRING, + 'country': types.STRING, + 'albumstatus': types.STRING, + 'media': types.STRING, + 'albumdisambig': types.STRING, 'releasegroupdisambig': types.STRING, - 'disctitle': types.STRING, - 'encoder': types.STRING, - 'rg_track_gain': types.NULL_FLOAT, - 'rg_track_peak': types.NULL_FLOAT, - 'rg_album_gain': types.NULL_FLOAT, - 'rg_album_peak': types.NULL_FLOAT, - 'r128_track_gain': types.NullPaddedInt(6), - 'r128_album_gain': types.NullPaddedInt(6), - 'original_year': types.PaddedInt(4), - 'original_month': types.PaddedInt(2), - 'original_day': types.PaddedInt(2), - 'initial_key': MusicalKey(), - - 'length': DurationType(), - 'bitrate': types.ScaledInt(1000, u'kbps'), - 'format': types.STRING, - 'samplerate': types.ScaledInt(1000, u'kHz'), - 'bitdepth': types.INTEGER, - 'channels': types.INTEGER, - 'mtime': DateType(), - 'added': DateType(), + 'disctitle': types.STRING, + 'encoder': types.STRING, + 'rg_track_gain': types.NULL_FLOAT, + 'rg_track_peak': types.NULL_FLOAT, + 'rg_album_gain': types.NULL_FLOAT, + 'rg_album_peak': types.NULL_FLOAT, + 'r128_track_gain': types.NullPaddedInt(6), + 'r128_album_gain': types.NullPaddedInt(6), + 'original_year': types.PaddedInt(4), + 'original_month': types.PaddedInt(2), + 'original_day': types.PaddedInt(2), + 'initial_key': MusicalKey(), + + 'length': DurationType(), + 'bitrate': types.ScaledInt(1000, 'kbps'), + 'format': types.STRING, + 'samplerate': types.ScaledInt(1000, 'kHz'), + 'bitdepth': types.INTEGER, + 'channels': types.INTEGER, + 'mtime': DateType(), + 'added': DateType(), } _search_fields = ('artist', 'title', 'comments', @@ -609,14 +607,14 @@ """ # Encode unicode paths and read buffers. if key == 'path': - if isinstance(value, six.text_type): + if isinstance(value, str): value = bytestring_path(value) elif isinstance(value, BLOB_TYPE): value = bytes(value) elif key == 'album_id': self._cached_album = None - changed = super(Item, self)._setitem(key, value) + changed = super()._setitem(key, value) if changed and key in MediaFile.fields(): self.mtime = 0 # Reset mtime on dirty. @@ -626,7 +624,7 @@ necessary. Raise a KeyError if the field is not available. """ try: - return super(Item, self).__getitem__(key) + return super().__getitem__(key) except KeyError: if self._cached_album: return self._cached_album[key] @@ -636,9 +634,9 @@ # This must not use `with_album=True`, because that might access # the database. When debugging, that is not guaranteed to succeed, and # can even deadlock due to the database lock. - return '{0}({1})'.format( + return '{}({})'.format( type(self).__name__, - ', '.join('{0}={1!r}'.format(k, self[k]) + ', '.join('{}={!r}'.format(k, self[k]) for k in self.keys(with_album=False)), ) @@ -646,7 +644,7 @@ """Get a list of available field names. `with_album` controls whether the album's fields are included. """ - keys = super(Item, self).keys(computed=computed) + keys = super().keys(computed=computed) if with_album and self._cached_album: keys = set(keys) keys.update(self._cached_album.keys(computed=computed)) @@ -668,7 +666,7 @@ """Set all key/value pairs in the mapping. If mtime is specified, it is not reset (as it might otherwise be). """ - super(Item, self).update(values) + super().update(values) if self.mtime == 0 and 'mtime' in values: self.mtime = values['mtime'] @@ -708,7 +706,7 @@ for key in self._media_fields: value = getattr(mediafile, key) - if isinstance(value, six.integer_types): + if isinstance(value, int): if value.bit_length() > 63: value = 0 self[key] = value @@ -780,7 +778,7 @@ self.write(*args, **kwargs) return True except FileOperationError as exc: - log.error(u"{0}", exc) + log.error("{0}", exc) return False def try_sync(self, write, move, with_album=True): @@ -800,7 +798,7 @@ if move: # Check whether this file is inside the library directory. if self._db and self._db.directory in util.ancestry(self.path): - log.debug(u'moving {0} to synchronize path', + log.debug('moving {0} to synchronize path', util.displayable_path(self.path)) self.move(with_album=with_album) self.store() @@ -863,7 +861,7 @@ try: return os.path.getsize(syspath(self.path)) except (OSError, Exception) as exc: - log.warning(u'could not get filesize: {0}', exc) + log.warning('could not get filesize: {0}', exc) return 0 # Model methods. @@ -873,7 +871,7 @@ removed from disk. If `with_album`, then the item's album (if any) is removed if it the item was the last in the album. """ - super(Item, self).remove() + super().remove() # Remove the album if it is empty. if with_album: @@ -940,7 +938,7 @@ # Templating. def destination(self, fragment=False, basedir=None, platform=None, - path_formats=None): + path_formats=None, replacements=None): """Returns the path in the library directory designated for the item (i.e., where the file ought to be). fragment makes this method return just the path fragment underneath the root library @@ -952,6 +950,8 @@ platform = platform or sys.platform basedir = basedir or self._db.directory path_formats = path_formats or self._db.path_formats + if replacements is None: + replacements = self._db.replacements # Use a path format based on a query, falling back on the # default. @@ -969,7 +969,7 @@ if query == PF_KEY_DEFAULT: break else: - assert False, u"no default path format" + assert False, "no default path format" if isinstance(path_format, Template): subpath_tmpl = path_format else: @@ -996,16 +996,16 @@ maxlen = util.max_filename_length(self._db.directory) subpath, fellback = util.legalize_path( - subpath, self._db.replacements, maxlen, + subpath, replacements, maxlen, os.path.splitext(self.path)[1], fragment ) if fellback: # Print an error message if legalization fell back to # default replacements because of the maximum length. log.warning( - u'Fell back to default replacements when naming ' - u'file {}. Configure replacements to avoid lengthening ' - u'the filename.', + 'Fell back to default replacements when naming ' + 'file {}. Configure replacements to avoid lengthening ' + 'the filename.', subpath ) @@ -1024,49 +1024,50 @@ _flex_table = 'album_attributes' _always_dirty = True _fields = { - 'id': types.PRIMARY_ID, + 'id': types.PRIMARY_ID, 'artpath': PathType(True), - 'added': DateType(), + 'added': DateType(), - 'albumartist': types.STRING, - 'albumartist_sort': types.STRING, - 'albumartist_credit': types.STRING, - 'album': types.STRING, - 'genre': types.STRING, - 'style': types.STRING, - 'discogs_albumid': types.INTEGER, - 'discogs_artistid': types.INTEGER, - 'discogs_labelid': types.INTEGER, - 'year': types.PaddedInt(4), - 'month': types.PaddedInt(2), - 'day': types.PaddedInt(2), - 'disctotal': types.PaddedInt(2), - 'comp': types.BOOLEAN, - 'mb_albumid': types.STRING, - 'mb_albumartistid': types.STRING, - 'albumtype': types.STRING, - 'label': types.STRING, - 'mb_releasegroupid': types.STRING, - 'asin': types.STRING, - 'catalognum': types.STRING, - 'script': types.STRING, - 'language': types.STRING, - 'country': types.STRING, - 'albumstatus': types.STRING, - 'albumdisambig': types.STRING, + 'albumartist': types.STRING, + 'albumartist_sort': types.STRING, + 'albumartist_credit': types.STRING, + 'album': types.STRING, + 'genre': types.STRING, + 'style': types.STRING, + 'discogs_albumid': types.INTEGER, + 'discogs_artistid': types.INTEGER, + 'discogs_labelid': types.INTEGER, + 'year': types.PaddedInt(4), + 'month': types.PaddedInt(2), + 'day': types.PaddedInt(2), + 'disctotal': types.PaddedInt(2), + 'comp': types.BOOLEAN, + 'mb_albumid': types.STRING, + 'mb_albumartistid': types.STRING, + 'albumtype': types.STRING, + 'albumtypes': types.STRING, + 'label': types.STRING, + 'mb_releasegroupid': types.STRING, + 'asin': types.STRING, + 'catalognum': types.STRING, + 'script': types.STRING, + 'language': types.STRING, + 'country': types.STRING, + 'albumstatus': types.STRING, + 'albumdisambig': types.STRING, 'releasegroupdisambig': types.STRING, - 'rg_album_gain': types.NULL_FLOAT, - 'rg_album_peak': types.NULL_FLOAT, - 'r128_album_gain': types.NullPaddedInt(6), - 'original_year': types.PaddedInt(4), - 'original_month': types.PaddedInt(2), - 'original_day': types.PaddedInt(2), + 'rg_album_gain': types.NULL_FLOAT, + 'rg_album_peak': types.NULL_FLOAT, + 'r128_album_gain': types.NullPaddedInt(6), + 'original_year': types.PaddedInt(4), + 'original_month': types.PaddedInt(2), + 'original_day': types.PaddedInt(2), } _search_fields = ('album', 'albumartist', 'genre') _types = { - 'path': PathType(), + 'path': PathType(), 'data_source': types.STRING, } @@ -1094,6 +1095,7 @@ 'mb_albumid', 'mb_albumartistid', 'albumtype', + 'albumtypes', 'label', 'mb_releasegroupid', 'asin', @@ -1138,7 +1140,10 @@ containing the album are also removed (recursively) if empty. Set with_items to False to avoid removing the album's items. """ - super(Album, self).remove() + super().remove() + + # Send a 'album_removed' signal to plugins + plugins.send('album_removed', album=self) # Delete art file. if delete: @@ -1163,7 +1168,7 @@ return if not os.path.exists(old_art): - log.error(u'removing reference to missing album art file {}', + log.error('removing reference to missing album art file {}', util.displayable_path(old_art)) self.artpath = None return @@ -1173,7 +1178,7 @@ return new_art = util.unique_path(new_art) - log.debug(u'moving album art {0} to {1}', + log.debug('moving album art {0} to {1}', util.displayable_path(old_art), util.displayable_path(new_art)) if operation == MoveOperation.MOVE: @@ -1230,7 +1235,7 @@ """ item = self.items().get() if not item: - raise ValueError(u'empty album for album id %d' % self.id) + raise ValueError('empty album for album id %d' % self.id) return os.path.dirname(item.path) def _albumtotal(self): @@ -1327,7 +1332,7 @@ track_updates[key] = self[key] with self._db.transaction(): - super(Album, self).store(fields) + super().store(fields) if track_updates: for item in self.items(): for key, value in track_updates.items(): @@ -1392,10 +1397,10 @@ The string is split into components using shell-like syntax. """ - message = u"Query is not unicode: {0!r}".format(s) - assert isinstance(s, six.text_type), message + message = f"Query is not unicode: {s!r}" + assert isinstance(s, str), message try: - parts = util.shlex_split(s) + parts = shlex.split(s) except ValueError as exc: raise dbcore.InvalidQueryError(s, exc) return parse_query_parts(parts, model_cls) @@ -1408,10 +1413,7 @@ ``-DSQLITE_LIKE_DOESNT_MATCH_BLOBS``. See ``https://github.com/beetbox/beets/issues/2172`` for details. """ - if not six.PY2: - return bytestring.lower() - - return buffer(bytes(bytestring).lower()) # noqa: F821 + return bytestring.lower() # The Library: interface to the database. @@ -1427,7 +1429,7 @@ '$artist/$album/$track $title'),), replacements=None): timeout = beets.config['timeout'].as_number() - super(Library, self).__init__(path, timeout=timeout) + super().__init__(path, timeout=timeout) self.directory = bytestring_path(normpath(directory)) self.path_formats = path_formats @@ -1436,7 +1438,7 @@ self._memotable = {} # Used for template substitution performance. def _create_connection(self): - conn = super(Library, self)._create_connection() + conn = super()._create_connection() conn.create_function('bytelower', 1, _sqlite_bytelower) return conn @@ -1458,10 +1460,10 @@ be empty. """ if not items: - raise ValueError(u'need at least one item') + raise ValueError('need at least one item') # Create the album structure using metadata from the first item. - values = dict((key, items[0][key]) for key in Album.item_keys) + values = {key: items[0][key] for key in Album.item_keys} album = Album(self, **values) # Add the album structure and set the items' album_id fields. @@ -1486,7 +1488,7 @@ # Parse the query, if necessary. try: parsed_sort = None - if isinstance(query, six.string_types): + if isinstance(query, str): query, parsed_sort = parse_query_string(query, model_cls) elif isinstance(query, (list, tuple)): query, parsed_sort = parse_query_parts(query, model_cls) @@ -1498,7 +1500,7 @@ if parsed_sort and not isinstance(parsed_sort, dbcore.query.NullSort): sort = parsed_sort - return super(Library, self)._fetch( + return super()._fetch( model_cls, query, sort ) @@ -1557,7 +1559,7 @@ return int(s.strip()) -class DefaultTemplateFunctions(object): +class DefaultTemplateFunctions: """A container class for the default functions provided to path templates. These functions are contained in an object to provide additional context to the functions -- specifically, the Item being @@ -1609,7 +1611,7 @@ return s[-_int_arg(chars):] @staticmethod - def tmpl_if(condition, trueval, falseval=u''): + def tmpl_if(condition, trueval, falseval=''): """If ``condition`` is nonempty and nonzero, emit ``trueval``; otherwise, emit ``falseval`` (if provided). """ @@ -1651,7 +1653,7 @@ """ # Fast paths: no album, no item or library, or memoized value. if not self.item or not self.lib: - return u'' + return '' if isinstance(self.item, Item): album_id = self.item.album_id @@ -1659,7 +1661,7 @@ album_id = self.item.id if album_id is None: - return u'' + return '' memokey = ('aunique', keys, disam, album_id) memoval = self.lib._memotable.get(memokey) @@ -1678,32 +1680,34 @@ bracket_l = bracket[0] bracket_r = bracket[1] else: - bracket_l = u'' - bracket_r = u'' + bracket_l = '' + bracket_r = '' album = self.lib.get_album(album_id) if not album: # Do nothing for singletons. - self.lib._memotable[memokey] = u'' - return u'' + self.lib._memotable[memokey] = '' + return '' # Find matching albums to disambiguate with. subqueries = [] for key in keys: value = album.get(key, '') - subqueries.append(dbcore.MatchQuery(key, value)) + # Use slow queries for flexible attributes. + fast = key in album.item_keys + subqueries.append(dbcore.MatchQuery(key, value, fast)) albums = self.lib.albums(dbcore.AndQuery(subqueries)) # If there's only one album to matching these details, then do # nothing. if len(albums) == 1: - self.lib._memotable[memokey] = u'' - return u'' + self.lib._memotable[memokey] = '' + return '' # Find the first disambiguator that distinguishes the albums. for disambiguator in disam: # Get the value for each album for the current field. - disam_values = set([a.get(disambiguator, '') for a in albums]) + disam_values = {a.get(disambiguator, '') for a in albums} # If the set of unique values is equal to the number of # albums in the disambiguation set, we're done -- this is @@ -1713,7 +1717,7 @@ else: # No disambiguator distinguished all fields. - res = u' {1}{0}{2}'.format(album.id, bracket_l, bracket_r) + res = f' {bracket_l}{album.id}{bracket_r}' self.lib._memotable[memokey] = res return res @@ -1722,15 +1726,15 @@ # Return empty string if disambiguator is empty. if disam_value: - res = u' {1}{0}{2}'.format(disam_value, bracket_l, bracket_r) + res = f' {bracket_l}{disam_value}{bracket_r}' else: - res = u'' + res = '' self.lib._memotable[memokey] = res return res @staticmethod - def tmpl_first(s, count=1, skip=0, sep=u'; ', join_str=u'; '): + def tmpl_first(s, count=1, skip=0, sep='; ', join_str='; '): """ Gets the item(s) from x to y in a string separated by something and join then with something @@ -1744,7 +1748,7 @@ count = skip + int(count) return join_str.join(s.split(sep)[skip:count]) - def tmpl_ifdef(self, field, trueval=u'', falseval=u''): + def tmpl_ifdef(self, field, trueval='', falseval=''): """ If field exists return trueval or the field (default) otherwise, emit return falseval (if provided). diff -Nru beets-1.5.0/beets/logging.py beets-1.6.0/beets/logging.py --- beets-1.5.0/beets/logging.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/logging.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -21,13 +20,11 @@ {}-style formatting. """ -from __future__ import division, absolute_import, print_function from copy import copy from logging import * # noqa import subprocess import threading -import six def logsafe(val): @@ -43,7 +40,7 @@ example. """ # Already Unicode. - if isinstance(val, six.text_type): + if isinstance(val, str): return val # Bytestring: needs decoding. @@ -57,7 +54,7 @@ # A "problem" object: needs a workaround. elif isinstance(val, subprocess.CalledProcessError): try: - return six.text_type(val) + return str(val) except UnicodeDecodeError: # An object with a broken __unicode__ formatter. Use __str__ # instead. @@ -74,7 +71,7 @@ instead of %-style formatting. """ - class _LogMessage(object): + class _LogMessage: def __init__(self, msg, args, kwargs): self.msg = msg self.args = args @@ -82,22 +79,23 @@ def __str__(self): args = [logsafe(a) for a in self.args] - kwargs = dict((k, logsafe(v)) for (k, v) in self.kwargs.items()) + kwargs = {k: logsafe(v) for (k, v) in self.kwargs.items()} return self.msg.format(*args, **kwargs) def _log(self, level, msg, args, exc_info=None, extra=None, **kwargs): """Log msg.format(*args, **kwargs)""" m = self._LogMessage(msg, args, kwargs) - return super(StrFormatLogger, self)._log(level, m, (), exc_info, extra) + return super()._log(level, m, (), exc_info, extra) class ThreadLocalLevelLogger(Logger): """A version of `Logger` whose level is thread-local instead of shared. """ + def __init__(self, name, level=NOTSET): self._thread_level = threading.local() self.default_level = NOTSET - super(ThreadLocalLevelLogger, self).__init__(name, level) + super().__init__(name, level) @property def level(self): diff -Nru beets-1.5.0/beets/__main__.py beets-1.6.0/beets/__main__.py --- beets-1.5.0/beets/__main__.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/__main__.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2017, Adrian Sampson. # @@ -17,7 +16,6 @@ `python -m beets`. """ -from __future__ import division, absolute_import, print_function import sys from .ui import main diff -Nru beets-1.5.0/beets/mediafile.py beets-1.6.0/beets/mediafile.py --- beets-1.5.0/beets/mediafile.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/mediafile.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import mediafile diff -Nru beets-1.5.0/beets/plugins.py beets-1.6.0/beets/plugins.py --- beets-1.5.0/beets/plugins.py 2021-03-28 18:23:15.000000000 +0000 +++ beets-1.6.0/beets/plugins.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """Support for beets plugins.""" -from __future__ import division, absolute_import, print_function import traceback import re @@ -28,7 +26,6 @@ import beets from beets import logging import mediafile -import six PLUGIN_NAMESPACE = 'beetsplug' @@ -52,26 +49,28 @@ """A logging filter that identifies the plugin that emitted a log message. """ + def __init__(self, plugin): - self.prefix = u'{0}: '.format(plugin.name) + self.prefix = f'{plugin.name}: ' def filter(self, record): if hasattr(record.msg, 'msg') and isinstance(record.msg.msg, - six.string_types): + str): # A _LogMessage from our hacked-up Logging replacement. record.msg.msg = self.prefix + record.msg.msg - elif isinstance(record.msg, six.string_types): + elif isinstance(record.msg, str): record.msg = self.prefix + record.msg return True # Managing the plugins themselves. -class BeetsPlugin(object): +class BeetsPlugin: """The base class for all beets plugins. Plugins provide functionality by defining a subclass of BeetsPlugin and overriding the abstract methods defined here. """ + def __init__(self, name=None): """Perform one-time plugin setup. """ @@ -129,14 +128,7 @@ value after the function returns). Also determines which params may not be sent for backwards-compatibility. """ - if six.PY2: - argspec = inspect.getargspec(func) - func_args = argspec.args - has_varkw = argspec.keywords is not None - else: - argspec = inspect.getfullargspec(func) - func_args = argspec.args - has_varkw = argspec.varkw is not None + argspec = inspect.getfullargspec(func) @wraps(func) def wrapper(*args, **kwargs): @@ -145,9 +137,9 @@ verbosity = beets.config['verbose'].get(int) log_level = max(logging.DEBUG, base_log_level - 10 * verbosity) self._log.setLevel(log_level) - if not has_varkw: - kwargs = dict((k, v) for k, v in kwargs.items() - if k in func_args) + if argspec.varkw is None: + kwargs = {k: v for k, v in kwargs.items() + if k in argspec.args} try: return func(*args, **kwargs) @@ -270,14 +262,14 @@ BeetsPlugin subclasses desired. """ for name in names: - modname = '{0}.{1}'.format(PLUGIN_NAMESPACE, name) + modname = f'{PLUGIN_NAMESPACE}.{name}' try: try: namespace = __import__(modname, None, None) except ImportError as exc: # Again, this is hacky: if exc.args[0].endswith(' ' + name): - log.warning(u'** plugin {0} not found', name) + log.warning('** plugin {0} not found', name) else: raise else: @@ -288,7 +280,7 @@ except Exception: log.warning( - u'** error loading plugin {}:\n{}', + '** error loading plugin {}:\n{}', name, traceback.format_exc(), ) @@ -340,16 +332,16 @@ def types(model_cls): # Gives us `item_types` and `album_types` - attr_name = '{0}_types'.format(model_cls.__name__.lower()) + attr_name = f'{model_cls.__name__.lower()}_types' types = {} for plugin in find_plugins(): plugin_types = getattr(plugin, attr_name, {}) for field in plugin_types: if field in types and plugin_types[field] != types[field]: raise PluginConflictException( - u'Plugin {0} defines flexible field {1} ' - u'which has already been defined with ' - u'another type.'.format(plugin.name, field) + 'Plugin {} defines flexible field {} ' + 'which has already been defined with ' + 'another type.'.format(plugin.name, field) ) types.update(plugin_types) return types @@ -357,7 +349,7 @@ def named_queries(model_cls): # Gather `item_queries` and `album_queries` from the plugins. - attr_name = '{0}_queries'.format(model_cls.__name__.lower()) + attr_name = f'{model_cls.__name__.lower()}_queries' queries = {} for plugin in find_plugins(): plugin_queries = getattr(plugin, attr_name, {}) @@ -389,17 +381,15 @@ """Gets MusicBrainz candidates for an album from each plugin. """ for plugin in find_plugins(): - for candidate in plugin.candidates(items, artist, album, va_likely, - extra_tags): - yield candidate + yield from plugin.candidates(items, artist, album, va_likely, + extra_tags) def item_candidates(item, artist, title): """Gets MusicBrainz candidates for an item from the plugins. """ for plugin in find_plugins(): - for item_candidate in plugin.item_candidates(item, artist, title): - yield item_candidate + yield from plugin.item_candidates(item, artist, title) def album_for_id(album_id): @@ -492,7 +482,7 @@ Return a list of non-None values returned from the handlers. """ - log.debug(u'Sending event: {0}', event) + log.debug('Sending event: {0}', event) results = [] for handler in event_handlers()[event]: result = handler(**arguments) @@ -510,7 +500,7 @@ feat_words = ['ft', 'featuring', 'feat', 'feat.', 'ft.'] if for_artist: feat_words += ['with', 'vs', 'and', 'con', '&'] - return r'(?<=\s)(?:{0})(?=\s)'.format( + return r'(?<=\s)(?:{})(?=\s)'.format( '|'.join(re.escape(x) for x in feat_words) ) @@ -627,10 +617,9 @@ item.store() -@six.add_metaclass(abc.ABCMeta) -class MetadataSourcePlugin(object): +class MetadataSourcePlugin(metaclass=abc.ABCMeta): def __init__(self): - super(MetadataSourcePlugin, self).__init__() + super().__init__() self.config.add({'source_weight': 0.5}) @abc.abstractproperty @@ -712,7 +701,7 @@ :rtype: str """ self._log.debug( - u"Searching {} for {} '{}'", self.data_source, url_type, id_ + "Searching {} for {} '{}'", self.data_source, url_type, id_ ) match = re.search(self.id_regex['pattern'].format(url_type), str(id_)) if match: diff -Nru beets-1.5.0/beets/random.py beets-1.6.0/beets/random.py --- beets-1.5.0/beets/random.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/random.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Philippe Mongeau. # @@ -15,7 +14,6 @@ """Get a random song or album from the library. """ -from __future__ import division, absolute_import, print_function import random from operator import attrgetter diff -Nru beets-1.5.0/beets/ui/commands.py beets-1.6.0/beets/ui/commands.py --- beets-1.5.0/beets/ui/commands.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beets/ui/commands.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -17,7 +16,6 @@ interface. """ -from __future__ import division, absolute_import, print_function import os import re @@ -39,10 +37,10 @@ from beets import library from beets import config from beets import logging -import six + from . import _store_dict -VARIOUS_ARTISTS = u'Various Artists' +VARIOUS_ARTISTS = 'Various Artists' PromptChoice = namedtuple('PromptChoice', ['short', 'long', 'callback']) # Global logger. @@ -74,9 +72,9 @@ items = list(lib.items(query)) if album and not albums: - raise ui.UserError(u'No matching albums found.') + raise ui.UserError('No matching albums found.') elif not album and not items: - raise ui.UserError(u'No matching items found.') + raise ui.UserError('No matching items found.') return items, albums @@ -88,33 +86,34 @@ returned row, with indentation of 2 spaces. """ for row in query: - print_(u' ' * 2 + row['key']) + print_(' ' * 2 + row['key']) def fields_func(lib, opts, args): def _print_rows(names): names.sort() - print_(u' ' + u'\n '.join(names)) + print_(' ' + '\n '.join(names)) - print_(u"Item fields:") + print_("Item fields:") _print_rows(library.Item.all_keys()) - print_(u"Album fields:") + print_("Album fields:") _print_rows(library.Album.all_keys()) with lib.transaction() as tx: # The SQL uses the DISTINCT to get unique values from the query unique_fields = 'SELECT DISTINCT key FROM (%s)' - print_(u"Item flexible attributes:") + print_("Item flexible attributes:") _print_keys(tx.query(unique_fields % library.Item._flex_table)) - print_(u"Album flexible attributes:") + print_("Album flexible attributes:") _print_keys(tx.query(unique_fields % library.Album._flex_table)) + fields_cmd = ui.Subcommand( 'fields', - help=u'show fields available for queries and format strings' + help='show fields available for queries and format strings' ) fields_cmd.func = fields_func default_commands.append(fields_cmd) @@ -125,9 +124,9 @@ class HelpCommand(ui.Subcommand): def __init__(self): - super(HelpCommand, self).__init__( + super().__init__( 'help', aliases=('?',), - help=u'give detailed help on a specific sub-command', + help='give detailed help on a specific sub-command', ) def func(self, lib, opts, args): @@ -135,7 +134,7 @@ cmdname = args[0] helpcommand = self.root_parser._subcommand_for_name(cmdname) if not helpcommand: - raise ui.UserError(u"unknown command '{0}'".format(cmdname)) + raise ui.UserError(f"unknown command '{cmdname}'") helpcommand.print_help() else: self.root_parser.print_help() @@ -160,13 +159,13 @@ if isinstance(info, hooks.AlbumInfo): if info.media: if info.mediums and info.mediums > 1: - disambig.append(u'{0}x{1}'.format( + disambig.append('{}x{}'.format( info.mediums, info.media )) else: disambig.append(info.media) if info.year: - disambig.append(six.text_type(info.year)) + disambig.append(str(info.year)) if info.country: disambig.append(info.country) if info.label: @@ -177,14 +176,14 @@ disambig.append(info.albumdisambig) if disambig: - return u', '.join(disambig) + return ', '.join(disambig) def dist_string(dist): """Formats a distance (a float) as a colorized similarity percentage string. """ - out = u'%.1f%%' % ((1 - dist) * 100) + out = '%.1f%%' % ((1 - dist) * 100) if dist <= config['match']['strong_rec_thresh'].as_number(): out = ui.colorize('text_success', out) elif dist <= config['match']['medium_rec_thresh'].as_number(): @@ -207,7 +206,7 @@ if penalties: if limit and len(penalties) > limit: penalties = penalties[:limit] + ['...'] - return ui.colorize('text_warning', u'(%s)' % ', '.join(penalties)) + return ui.colorize('text_warning', '(%s)' % ', '.join(penalties)) def show_change(cur_artist, cur_album, match): @@ -217,11 +216,11 @@ """ def show_album(artist, album): if artist: - album_description = u' %s - %s' % (artist, album) + album_description = f' {artist} - {album}' elif album: - album_description = u' %s' % album + album_description = ' %s' % album else: - album_description = u' (unknown album)' + album_description = ' (unknown album)' print_(album_description) def format_index(track_info): @@ -239,22 +238,22 @@ mediums = track_info.disctotal if config['per_disc_numbering']: if mediums and mediums > 1: - return u'{0}-{1}'.format(medium, medium_index) + return f'{medium}-{medium_index}' else: - return six.text_type(medium_index if medium_index is not None - else index) + return str(medium_index if medium_index is not None + else index) else: - return six.text_type(index) + return str(index) # Identify the album in question. if cur_artist != match.info.artist or \ (cur_album != match.info.album and match.info.album != VARIOUS_ARTISTS): artist_l, artist_r = cur_artist or '', match.info.artist - album_l, album_r = cur_album or '', match.info.album + album_l, album_r = cur_album or '', match.info.album if artist_r == VARIOUS_ARTISTS: # Hide artists for VA releases. - artist_l, artist_r = u'', u'' + artist_l, artist_r = '', '' if config['artist_credit']: artist_r = match.info.artist_credit @@ -262,21 +261,21 @@ artist_l, artist_r = ui.colordiff(artist_l, artist_r) album_l, album_r = ui.colordiff(album_l, album_r) - print_(u"Correcting tags from:") + print_("Correcting tags from:") show_album(artist_l, album_l) - print_(u"To:") + print_("To:") show_album(artist_r, album_r) else: - print_(u"Tagging:\n {0.artist} - {0.album}".format(match.info)) + print_("Tagging:\n {0.artist} - {0.album}".format(match.info)) # Data URL. if match.info.data_url: - print_(u'URL:\n %s' % match.info.data_url) + print_('URL:\n %s' % match.info.data_url) # Info line. info = [] # Similarity. - info.append(u'(Similarity: %s)' % dist_string(match.distance)) + info.append('(Similarity: %s)' % dist_string(match.distance)) # Penalties. penalties = penalty_string(match.distance) if penalties: @@ -284,7 +283,7 @@ # Disambiguation. disambig = disambig_string(match.info) if disambig: - info.append(ui.colorize('text_highlight_minor', u'(%s)' % disambig)) + info.append(ui.colorize('text_highlight_minor', '(%s)' % disambig)) print_(' '.join(info)) # Tracks. @@ -302,16 +301,16 @@ if medium != track_info.medium or disctitle != track_info.disctitle: media = match.info.media or 'Media' if match.info.mediums > 1 and track_info.disctitle: - lhs = u'%s %s: %s' % (media, track_info.medium, - track_info.disctitle) + lhs = '{} {}: {}'.format(media, track_info.medium, + track_info.disctitle) elif match.info.mediums > 1: - lhs = u'%s %s' % (media, track_info.medium) + lhs = f'{media} {track_info.medium}' elif track_info.disctitle: - lhs = u'%s: %s' % (media, track_info.disctitle) + lhs = f'{media}: {track_info.disctitle}' else: lhs = None if lhs: - lines.append((lhs, u'', 0)) + lines.append((lhs, '', 0)) medium, disctitle = track_info.medium, track_info.disctitle # Titles. @@ -332,7 +331,7 @@ color = 'text_highlight_minor' else: color = 'text_highlight' - templ = ui.colorize(color, u' (#{0})') + templ = ui.colorize(color, ' (#{0})') lhs += templ.format(cur_track) rhs += templ.format(new_track) lhs_width += len(cur_track) + 4 @@ -343,7 +342,7 @@ config['ui']['length_diff_thresh'].as_number(): cur_length = ui.human_seconds_short(item.length) new_length = ui.human_seconds_short(track_info.length) - templ = ui.colorize('text_highlight', u' ({0})') + templ = ui.colorize('text_highlight', ' ({0})') lhs += templ.format(cur_length) rhs += templ.format(new_length) lhs_width += len(cur_length) + 3 @@ -354,9 +353,9 @@ rhs += ' %s' % penalties if lhs != rhs: - lines.append((u' * %s' % lhs, rhs, lhs_width)) + lines.append((' * %s' % lhs, rhs, lhs_width)) elif config['import']['detail']: - lines.append((u' * %s' % lhs, '', lhs_width)) + lines.append((' * %s' % lhs, '', lhs_width)) # Print each track in two columns, or across two lines. col_width = (ui.term_width() - len(''.join([' * ', ' -> ']))) // 2 @@ -366,14 +365,14 @@ if not rhs: print_(lhs) elif max_width > col_width: - print_(u'%s ->\n %s' % (lhs, rhs)) + print_(f'{lhs} ->\n {rhs}') else: pad = max_width - lhs_width - print_(u'%s%s -> %s' % (lhs, ' ' * pad, rhs)) + print_('{}{} -> {}'.format(lhs, ' ' * pad, rhs)) # Missing and unmatched tracks. if match.extra_tracks: - print_(u'Missing tracks ({0}/{1} - {2:.1%}):'.format( + print_('Missing tracks ({}/{} - {:.1%}):'.format( len(match.extra_tracks), len(match.info.tracks), len(match.extra_tracks) / len(match.info.tracks) @@ -381,21 +380,21 @@ pad_width = max(len(track_info.title) for track_info in match.extra_tracks) for track_info in match.extra_tracks: - line = u' ! {0: <{width}} (#{1: >2})'.format(track_info.title, - format_index(track_info), - width=pad_width) + line = ' ! {0: <{width}} (#{1: >2})'.format(track_info.title, + format_index(track_info), + width=pad_width) if track_info.length: - line += u' (%s)' % ui.human_seconds_short(track_info.length) + line += ' (%s)' % ui.human_seconds_short(track_info.length) print_(ui.colorize('text_warning', line)) if match.extra_items: - print_(u'Unmatched tracks ({0}):'.format(len(match.extra_items))) + print_('Unmatched tracks ({}):'.format(len(match.extra_items))) pad_width = max(len(item.title) for item in match.extra_items) for item in match.extra_items: - line = u' ! {0: <{width}} (#{1: >2})'.format(item.title, - format_index(item), - width=pad_width) + line = ' ! {0: <{width}} (#{1: >2})'.format(item.title, + format_index(item), + width=pad_width) if item.length: - line += u' (%s)' % ui.human_seconds_short(item.length) + line += ' (%s)' % ui.human_seconds_short(item.length) print_(ui.colorize('text_warning', line)) @@ -410,22 +409,22 @@ cur_artist, new_artist = ui.colordiff(cur_artist, new_artist) cur_title, new_title = ui.colordiff(cur_title, new_title) - print_(u"Correcting track tags from:") - print_(u" %s - %s" % (cur_artist, cur_title)) - print_(u"To:") - print_(u" %s - %s" % (new_artist, new_title)) + print_("Correcting track tags from:") + print_(f" {cur_artist} - {cur_title}") + print_("To:") + print_(f" {new_artist} - {new_title}") else: - print_(u"Tagging track: %s - %s" % (cur_artist, cur_title)) + print_(f"Tagging track: {cur_artist} - {cur_title}") # Data URL. if match.info.data_url: - print_(u'URL:\n %s' % match.info.data_url) + print_('URL:\n %s' % match.info.data_url) # Info line. info = [] # Similarity. - info.append(u'(Similarity: %s)' % dist_string(match.distance)) + info.append('(Similarity: %s)' % dist_string(match.distance)) # Penalties. penalties = penalty_string(match.distance) if penalties: @@ -433,7 +432,7 @@ # Disambiguation. disambig = disambig_string(match.info) if disambig: - info.append(ui.colorize('text_highlight_minor', u'(%s)' % disambig)) + info.append(ui.colorize('text_highlight_minor', '(%s)' % disambig)) print_(' '.join(info)) @@ -447,7 +446,7 @@ """ summary_parts = [] if not singleton: - summary_parts.append(u"{0} items".format(len(items))) + summary_parts.append("{} items".format(len(items))) format_counts = {} for item in items: @@ -461,21 +460,21 @@ format_counts.items(), key=lambda fmt_and_count: (-fmt_and_count[1], fmt_and_count[0]) ): - summary_parts.append('{0} {1}'.format(fmt, count)) + summary_parts.append(f'{fmt} {count}') 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(u'{0}kbps'.format(int(average_bitrate / 1000))) + summary_parts.append('{}kbps'.format(int(average_bitrate / 1000))) if items[0].format == "FLAC": - sample_bits = u'{}kHz/{} bit'.format( + sample_bits = '{}kHz/{} bit'.format( round(int(items[0].samplerate) / 1000, 1), items[0].bitdepth) summary_parts.append(sample_bits) summary_parts.append(ui.human_seconds_short(total_duration)) summary_parts.append(ui.human_bytes(total_filesize)) - return u', '.join(summary_parts) + return ', '.join(summary_parts) def _summary_judgment(rec): @@ -506,9 +505,9 @@ return None if action == importer.action.SKIP: - print_(u'Skipping.') + print_('Skipping.') elif action == importer.action.ASIS: - print_(u'Importing as-is.') + print_('Importing as-is.') return action @@ -543,12 +542,12 @@ # Zero candidates. if not candidates: if singleton: - print_(u"No matching recordings found.") + print_("No matching recordings found.") else: - print_(u"No matching release found for {0} tracks." + print_("No matching release found for {} tracks." .format(itemcount)) - print_(u'For help, see: ' - u'https://beets.readthedocs.org/en/latest/faq.html#nomatch') + print_('For help, see: ' + 'https://beets.readthedocs.org/en/latest/faq.html#nomatch') sel = ui.input_options(choice_opts) if sel in choice_actions: return choice_actions[sel] @@ -567,22 +566,22 @@ if not bypass_candidates: # Display list of candidates. - print_(u'Finding tags for {0} "{1} - {2}".'.format( - u'track' if singleton else u'album', + print_('Finding tags for {} "{} - {}".'.format( + 'track' if singleton else 'album', item.artist if singleton else cur_artist, item.title if singleton else cur_album, )) - print_(u'Candidates:') + print_('Candidates:') for i, match in enumerate(candidates): # Index, metadata, and distance. line = [ - u'{0}.'.format(i + 1), - u'{0} - {1}'.format( + '{}.'.format(i + 1), + '{} - {}'.format( match.info.artist, match.info.title if singleton else match.info.album, ), - u'({0})'.format(dist_string(match.distance)), + '({})'.format(dist_string(match.distance)), ] # Penalties. @@ -594,14 +593,14 @@ disambig = disambig_string(match.info) if disambig: line.append(ui.colorize('text_highlight_minor', - u'(%s)' % disambig)) + '(%s)' % disambig)) - print_(u' '.join(line)) + print_(' '.join(line)) # Ask the user for a choice. sel = ui.input_options(choice_opts, numrange=(1, len(candidates))) - if sel == u'm': + if sel == 'm': pass elif sel in choice_actions: return choice_actions[sel] @@ -625,19 +624,19 @@ # Ask for confirmation. default = config['import']['default_action'].as_choice({ - u'apply': u'a', - u'skip': u's', - u'asis': u'u', - u'none': None, + 'apply': 'a', + 'skip': 's', + 'asis': 'u', + 'none': None, }) if default is None: require = True # Bell ring when user interaction is needed. if config['import']['bell']: - ui.print_(u'\a', end=u'') - sel = ui.input_options((u'Apply', u'More candidates') + choice_opts, + ui.print_('\a', end='') + sel = ui.input_options(('Apply', 'More candidates') + choice_opts, require=require, default=default) - if sel == u'a': + if sel == 'a': return match elif sel in choice_actions: return choice_actions[sel] @@ -649,8 +648,8 @@ Input either an artist and album (for full albums) or artist and track name (for singletons) for manual search. """ - artist = input_(u'Artist:').strip() - name = input_(u'Album:' if task.is_album else u'Track:').strip() + artist = input_('Artist:').strip() + name = input_('Album:' if task.is_album else 'Track:').strip() if task.is_album: _, _, prop = autotag.tag_album( @@ -666,8 +665,8 @@ Input an ID, either for an album ("release") or a track ("recording"). """ - prompt = u'Enter {0} ID:'.format(u'release' if task.is_album - else u'recording') + prompt = 'Enter {} ID:'.format('release' if task.is_album + else 'recording') search_id = input_(prompt).strip() if task.is_album: @@ -688,6 +687,7 @@ class TerminalImportSession(importer.ImportSession): """An import session that runs in a terminal. """ + def choose_match(self, task): """Given an initial autotagging of items, go through an interactive dance with the user to ask for a choice of metadata. Returns an @@ -695,8 +695,8 @@ """ # Show what we're tagging. print_() - print_(displayable_path(task.paths, u'\n') + - u' ({0} items)'.format(len(task.items))) + print_(displayable_path(task.paths, '\n') + + ' ({} items)'.format(len(task.items))) # Let plugins display info or prompt the user before we go through the # process of selecting candidate. @@ -708,8 +708,8 @@ return actions[0] elif len(actions) > 1: raise plugins.PluginConflictException( - u'Only one handler for `import_task_before_choice` may return ' - u'an action.') + 'Only one handler for `import_task_before_choice` may return ' + 'an action.') # Take immediate action if appropriate. action = _summary_judgment(task.rec) @@ -798,48 +798,48 @@ """Decide what to do when a new album or item seems similar to one that's already in the library. """ - log.warning(u"This {0} is already in the library!", - (u"album" if task.is_album else u"item")) + log.warning("This {0} is already in the library!", + ("album" if task.is_album else "item")) if config['import']['quiet']: # In quiet mode, don't prompt -- just skip. - log.info(u'Skipping.') - sel = u's' + log.info('Skipping.') + sel = 's' else: # Print some detail about the existing and new items so the # user can make an informed decision. for duplicate in found_duplicates: - print_(u"Old: " + summarize_items( + print_("Old: " + summarize_items( list(duplicate.items()) if task.is_album else [duplicate], not task.is_album, )) - print_(u"New: " + summarize_items( + print_("New: " + summarize_items( task.imported_items(), not task.is_album, )) sel = ui.input_options( - (u'Skip new', u'Keep all', u'Remove old', u'Merge all') + ('Skip new', 'Keep all', 'Remove old', 'Merge all') ) - if sel == u's': + if sel == 's': # Skip new. task.set_choice(importer.action.SKIP) - elif sel == u'k': + elif sel == 'k': # Keep both. Do nothing; leave the choice intact. pass - elif sel == u'r': + elif sel == 'r': # Remove old. task.should_remove_duplicates = True - elif sel == u'm': + elif sel == 'm': task.should_merge_duplicates = True else: assert False def should_resume(self, path): - return ui.input_yn(u"Import of the directory:\n{0}\n" - u"was interrupted. Resume (Y/n)?" + return ui.input_yn("Import of the directory:\n{}\n" + "was interrupted. Resume (Y/n)?" .format(displayable_path(path))) def _get_choices(self, task): @@ -860,22 +860,22 @@ """ # Standard, built-in choices. choices = [ - PromptChoice(u's', u'Skip', + PromptChoice('s', 'Skip', lambda s, t: importer.action.SKIP), - PromptChoice(u'u', u'Use as-is', + PromptChoice('u', 'Use as-is', lambda s, t: importer.action.ASIS) ] if task.is_album: choices += [ - PromptChoice(u't', u'as Tracks', + PromptChoice('t', 'as Tracks', lambda s, t: importer.action.TRACKS), - PromptChoice(u'g', u'Group albums', + PromptChoice('g', 'Group albums', lambda s, t: importer.action.ALBUMS), ] choices += [ - PromptChoice(u'e', u'Enter search', manual_search), - PromptChoice(u'i', u'enter Id', manual_id), - PromptChoice(u'b', u'aBort', abort_action), + PromptChoice('e', 'Enter search', manual_search), + PromptChoice('i', 'enter Id', manual_id), + PromptChoice('b', 'aBort', abort_action), ] # Send the before_choose_candidate event and flatten list. @@ -885,7 +885,7 @@ # Add a "dummy" choice for the other baked-in option, for # duplicate checking. all_choices = [ - PromptChoice(u'a', u'Apply', None), + PromptChoice('a', 'Apply', None), ] + choices + extra_choices # Check for conflicts. @@ -898,8 +898,8 @@ # Keep the first of the choices, removing the rest. dup_choices = [c for c in all_choices if c.short == short] for c in dup_choices[1:]: - log.warning(u"Prompt choice '{0}' removed due to conflict " - u"with '{1}' (short letter: '{2}')", + log.warning("Prompt choice '{0}' removed due to conflict " + "with '{1}' (short letter: '{2}')", c.long, dup_choices[0].long, c.short) extra_choices.remove(c) @@ -916,21 +916,21 @@ # Check the user-specified directories. for path in paths: if not os.path.exists(syspath(normpath(path))): - raise ui.UserError(u'no such file or directory: {0}'.format( + raise ui.UserError('no such file or directory: {}'.format( displayable_path(path))) # Check parameter consistency. if config['import']['quiet'] and config['import']['timid']: - raise ui.UserError(u"can't be both quiet and timid") + raise ui.UserError("can't be both quiet and timid") # Open the log. if config['import']['log'].get() is not None: logpath = syspath(config['import']['log'].as_filename()) try: loghandler = logging.FileHandler(logpath) - except IOError: - raise ui.UserError(u"could not open log file for writing: " - u"{0}".format(displayable_path(logpath))) + except OSError: + raise ui.UserError("could not open log file for writing: " + "{}".format(displayable_path(logpath))) else: loghandler = None @@ -961,111 +961,111 @@ query = None paths = args if not paths: - raise ui.UserError(u'no path specified') + raise ui.UserError('no path specified') - # On Python 2, we get filenames as raw bytes, which is what we - # need. On Python 3, we need to undo the "helpful" conversion to - # Unicode strings to get the real bytestring filename. - if not six.PY2: - paths = [p.encode(util.arg_encoding(), 'surrogateescape') - for p in paths] + # On Python 2, we used to get filenames as raw bytes, which is + # what we need. On Python 3, we need to undo the "helpful" + # conversion to Unicode strings to get the real bytestring + # filename. + paths = [p.encode(util.arg_encoding(), 'surrogateescape') + for p in paths] import_files(lib, paths, query) import_cmd = ui.Subcommand( - u'import', help=u'import new music', aliases=(u'imp', u'im') + 'import', help='import new music', aliases=('imp', 'im') ) import_cmd.parser.add_option( - u'-c', u'--copy', action='store_true', default=None, - help=u"copy tracks into library directory (default)" + '-c', '--copy', action='store_true', default=None, + help="copy tracks into library directory (default)" ) import_cmd.parser.add_option( - u'-C', u'--nocopy', action='store_false', dest='copy', - help=u"don't copy tracks (opposite of -c)" + '-C', '--nocopy', action='store_false', dest='copy', + help="don't copy tracks (opposite of -c)" ) import_cmd.parser.add_option( - u'-m', u'--move', action='store_true', dest='move', - help=u"move tracks into the library (overrides -c)" + '-m', '--move', action='store_true', dest='move', + help="move tracks into the library (overrides -c)" ) import_cmd.parser.add_option( - u'-w', u'--write', action='store_true', default=None, - help=u"write new metadata to files' tags (default)" + '-w', '--write', action='store_true', default=None, + help="write new metadata to files' tags (default)" ) import_cmd.parser.add_option( - u'-W', u'--nowrite', action='store_false', dest='write', - help=u"don't write metadata (opposite of -w)" + '-W', '--nowrite', action='store_false', dest='write', + help="don't write metadata (opposite of -w)" ) import_cmd.parser.add_option( - u'-a', u'--autotag', action='store_true', dest='autotag', - help=u"infer tags for imported files (default)" + '-a', '--autotag', action='store_true', dest='autotag', + help="infer tags for imported files (default)" ) import_cmd.parser.add_option( - u'-A', u'--noautotag', action='store_false', dest='autotag', - help=u"don't infer tags for imported files (opposite of -a)" + '-A', '--noautotag', action='store_false', dest='autotag', + help="don't infer tags for imported files (opposite of -a)" ) import_cmd.parser.add_option( - u'-p', u'--resume', action='store_true', default=None, - help=u"resume importing if interrupted" + '-p', '--resume', action='store_true', default=None, + help="resume importing if interrupted" ) import_cmd.parser.add_option( - u'-P', u'--noresume', action='store_false', dest='resume', - help=u"do not try to resume importing" + '-P', '--noresume', action='store_false', dest='resume', + help="do not try to resume importing" ) import_cmd.parser.add_option( - u'-q', u'--quiet', action='store_true', dest='quiet', - help=u"never prompt for input: skip albums instead" + '-q', '--quiet', action='store_true', dest='quiet', + help="never prompt for input: skip albums instead" ) import_cmd.parser.add_option( - u'-l', u'--log', dest='log', - help=u'file to log untaggable albums for later review' + '-l', '--log', dest='log', + help='file to log untaggable albums for later review' ) import_cmd.parser.add_option( - u'-s', u'--singletons', action='store_true', - help=u'import individual tracks instead of full albums' + '-s', '--singletons', action='store_true', + help='import individual tracks instead of full albums' ) import_cmd.parser.add_option( - u'-t', u'--timid', dest='timid', action='store_true', - help=u'always confirm all actions' + '-t', '--timid', dest='timid', action='store_true', + help='always confirm all actions' ) import_cmd.parser.add_option( - u'-L', u'--library', dest='library', action='store_true', - help=u'retag items matching a query' + '-L', '--library', dest='library', action='store_true', + help='retag items matching a query' ) import_cmd.parser.add_option( - u'-i', u'--incremental', dest='incremental', action='store_true', - help=u'skip already-imported directories' + '-i', '--incremental', dest='incremental', action='store_true', + help='skip already-imported directories' ) import_cmd.parser.add_option( - u'-I', u'--noincremental', dest='incremental', action='store_false', - help=u'do not skip already-imported directories' + '-I', '--noincremental', dest='incremental', action='store_false', + help='do not skip already-imported directories' ) import_cmd.parser.add_option( - u'--from-scratch', dest='from_scratch', action='store_true', - help=u'erase existing metadata before applying new metadata' + '--from-scratch', dest='from_scratch', action='store_true', + help='erase existing metadata before applying new metadata' ) import_cmd.parser.add_option( - u'--flat', dest='flat', action='store_true', - help=u'import an entire tree as a single album' + '--flat', dest='flat', action='store_true', + help='import an entire tree as a single album' ) import_cmd.parser.add_option( - u'-g', u'--group-albums', dest='group_albums', action='store_true', - help=u'group tracks in a folder into separate albums' + '-g', '--group-albums', dest='group_albums', action='store_true', + help='group tracks in a folder into separate albums' ) import_cmd.parser.add_option( - u'--pretend', dest='pretend', action='store_true', - help=u'just print the files to import' + '--pretend', dest='pretend', action='store_true', + help='just print the files to import' ) import_cmd.parser.add_option( - u'-S', u'--search-id', dest='search_ids', action='append', + '-S', '--search-id', dest='search_ids', action='append', metavar='ID', - help=u'restrict matching to a specific metadata backend ID' + help='restrict matching to a specific metadata backend ID' ) import_cmd.parser.add_option( - u'--set', dest='set_fields', action='callback', + '--set', dest='set_fields', action='callback', callback=_store_dict, metavar='FIELD=VALUE', - help=u'set the given fields to the supplied values' + help='set the given fields to the supplied values' ) import_cmd.func = import_func default_commands.append(import_cmd) @@ -1073,7 +1073,7 @@ # list: Query and show library contents. -def list_items(lib, query, album, fmt=u''): +def list_items(lib, query, album, fmt=''): """Print out items in lib matching query. If album, then search for albums instead of single items. """ @@ -1089,9 +1089,9 @@ list_items(lib, decargs(args), opts.album) -list_cmd = ui.Subcommand(u'list', help=u'query the library', aliases=(u'ls',)) -list_cmd.parser.usage += u"\n" \ - u'Example: %prog -f \'$album: $title\' artist:beatles' +list_cmd = ui.Subcommand('list', help='query the library', aliases=('ls',)) +list_cmd.parser.usage += "\n" \ + 'Example: %prog -f \'$album: $title\' artist:beatles' list_cmd.parser.add_all_common_options() list_cmd.func = list_func default_commands.append(list_cmd) @@ -1119,7 +1119,7 @@ # Item deleted? if not os.path.exists(syspath(item.path)): ui.print_(format(item)) - ui.print_(ui.colorize('text_error', u' deleted')) + ui.print_(ui.colorize('text_error', ' deleted')) if not pretend: item.remove(True) affected_albums.add(item.album_id) @@ -1127,7 +1127,7 @@ # Did the item change since last checked? if item.current_mtime() <= item.mtime: - log.debug(u'skipping {0} because mtime is up to date ({1})', + log.debug('skipping {0} because mtime is up to date ({1})', displayable_path(item.path), item.mtime) continue @@ -1135,7 +1135,7 @@ try: item.read() except library.ReadError as exc: - log.error(u'error reading {0}: {1}', + log.error('error reading {0}: {1}', displayable_path(item.path), exc) continue @@ -1146,7 +1146,7 @@ old_item = lib.get_item(item.id) if old_item.albumartist == old_item.artist == item.artist: item.albumartist = old_item.albumartist - item._dirty.discard(u'albumartist') + item._dirty.discard('albumartist') # Check for and display changes. changed = ui.show_model_changes( @@ -1179,7 +1179,7 @@ continue album = lib.get_album(album_id) if not album: # Empty albums have already been removed. - log.debug(u'emptied album {0}', album_id) + log.debug('emptied album {0}', album_id) continue first_item = album.items().get() @@ -1190,7 +1190,7 @@ # Move album art (and any inconsistent items). if move and lib.directory in ancestry(first_item.path): - log.debug(u'moving album {0}', album_id) + log.debug('moving album {0}', album_id) # Manually moving and storing the album. items = list(album.items()) @@ -1213,25 +1213,25 @@ update_cmd = ui.Subcommand( - u'update', help=u'update the library', aliases=(u'upd', u'up',) + 'update', help='update the library', aliases=('upd', 'up',) ) update_cmd.parser.add_album_option() update_cmd.parser.add_format_option() update_cmd.parser.add_option( - u'-m', u'--move', action='store_true', dest='move', - help=u"move files in the library directory" + '-m', '--move', action='store_true', dest='move', + help="move files in the library directory" ) update_cmd.parser.add_option( - u'-M', u'--nomove', action='store_false', dest='move', - help=u"don't move files in library" + '-M', '--nomove', action='store_false', dest='move', + help="don't move files in library" ) update_cmd.parser.add_option( - u'-p', u'--pretend', action='store_true', - help=u"show all changes but do nothing" + '-p', '--pretend', action='store_true', + help="show all changes but do nothing" ) update_cmd.parser.add_option( - u'-F', u'--field', default=None, action='append', dest='fields', - help=u'list of fields to update' + '-F', '--field', default=None, action='append', dest='fields', + help='list of fields to update' ) update_cmd.func = update_func default_commands.append(update_cmd) @@ -1250,21 +1250,21 @@ # Confirm file removal if not forcing removal. if not force: # Prepare confirmation with user. - album_str = u" in {} album{}".format( - len(albums), u's' if len(albums) > 1 else u'' - ) if album else "" + album_str = " in {} album{}".format( + len(albums), 's' if len(albums) > 1 else '' + ) if album else "" if delete: - fmt = u'$path - $title' - prompt = u'Really DELETE' - prompt_all = u'Really DELETE {} file{}{}'.format( - len(items), u's' if len(items) > 1 else u'', album_str + fmt = '$path - $title' + prompt = 'Really DELETE' + prompt_all = 'Really DELETE {} file{}{}'.format( + len(items), 's' if len(items) > 1 else '', album_str ) else: - fmt = u'' - prompt = u'Really remove from the library?' - prompt_all = u'Really remove {} item{}{} from the library?'.format( - len(items), u's' if len(items) > 1 else u'', album_str + fmt = '' + prompt = 'Really remove from the library?' + prompt_all = 'Really remove {} item{}{} from the library?'.format( + len(items), 's' if len(items) > 1 else '', album_str ) # Helpers for printing affected items @@ -1300,15 +1300,15 @@ remove_cmd = ui.Subcommand( - u'remove', help=u'remove matching items from the library', aliases=(u'rm',) + 'remove', help='remove matching items from the library', aliases=('rm',) ) remove_cmd.parser.add_option( - u"-d", u"--delete", action="store_true", - help=u"also remove files from disk" + "-d", "--delete", action="store_true", + help="also remove files from disk" ) remove_cmd.parser.add_option( - u"-f", u"--force", action="store_true", - help=u"do not ask when removing items" + "-f", "--force", action="store_true", + help="do not ask when removing items" ) remove_cmd.parser.add_album_option() remove_cmd.func = remove_func @@ -1333,7 +1333,7 @@ try: total_size += os.path.getsize(syspath(item.path)) except OSError as exc: - log.info(u'could not get size of {}: {}', item.path, exc) + log.info('could not get size of {}: {}', item.path, exc) else: total_size += int(item.length * item.bitrate / 8) total_time += item.length @@ -1343,20 +1343,20 @@ if item.album_id: albums.add(item.album_id) - size_str = u'' + ui.human_bytes(total_size) + size_str = '' + ui.human_bytes(total_size) if exact: - size_str += u' ({0} bytes)'.format(total_size) + size_str += f' ({total_size} bytes)' - print_(u"""Tracks: {0} -Total time: {1}{2} -{3}: {4} -Artists: {5} -Albums: {6} -Album artists: {7}""".format( + print_("""Tracks: {} +Total time: {}{} +{}: {} +Artists: {} +Albums: {} +Album artists: {}""".format( total_items, ui.human_seconds(total_time), - u' ({0:.2f} seconds)'.format(total_time) if exact else '', - u'Total size' if exact else u'Approximate total size', + f' ({total_time:.2f} seconds)' if exact else '', + 'Total size' if exact else 'Approximate total size', size_str, len(artists), len(albums), @@ -1369,11 +1369,11 @@ stats_cmd = ui.Subcommand( - u'stats', help=u'show statistics about the library or a query' + 'stats', help='show statistics about the library or a query' ) stats_cmd.parser.add_option( - u'-e', u'--exact', action='store_true', - help=u'exact size and time' + '-e', '--exact', action='store_true', + help='exact size and time' ) stats_cmd.func = stats_func default_commands.append(stats_cmd) @@ -1382,18 +1382,18 @@ # version: Show current beets version. def show_version(lib, opts, args): - print_(u'beets version %s' % beets.__version__) - print_(u'Python version {}'.format(python_version())) + print_('beets version %s' % beets.__version__) + print_(f'Python version {python_version()}') # Show plugins. names = sorted(p.name for p in plugins.find_plugins()) if names: - print_(u'plugins:', ', '.join(names)) + print_('plugins:', ', '.join(names)) else: - print_(u'no plugins loaded') + print_('no plugins loaded') version_cmd = ui.Subcommand( - u'version', help=u'output version information' + 'version', help='output version information' ) version_cmd.func = show_version default_commands.append(version_cmd) @@ -1420,8 +1420,8 @@ # Apply changes *temporarily*, preview them, and collect modified # objects. - print_(u'Modifying {0} {1}s.' - .format(len(objs), u'album' if album else u'item')) + print_('Modifying {} {}s.' + .format(len(objs), 'album' if album else 'item')) changed = [] for obj in objs: if print_and_modify(obj, mods, dels) and obj not in changed: @@ -1429,22 +1429,22 @@ # Still something to do? if not changed: - print_(u'No changes to make.') + print_('No changes to make.') return # Confirm action. if confirm: if write and move: - extra = u', move and write tags' + extra = ', move and write tags' elif write: - extra = u' and write tags' + extra = ' and write tags' elif move: - extra = u' and move' + extra = ' and move' else: - extra = u'' + extra = '' changed = ui.input_select_objects( - u'Really modify%s' % extra, changed, + 'Really modify%s' % extra, changed, lambda o: print_and_modify(o, mods, dels) ) @@ -1492,35 +1492,35 @@ def modify_func(lib, opts, args): query, mods, dels = modify_parse_args(decargs(args)) if not mods and not dels: - raise ui.UserError(u'no modifications specified') + raise ui.UserError('no modifications specified') modify_items(lib, mods, dels, query, ui.should_write(opts.write), ui.should_move(opts.move), opts.album, not opts.yes) modify_cmd = ui.Subcommand( - u'modify', help=u'change metadata fields', aliases=(u'mod',) + 'modify', help='change metadata fields', aliases=('mod',) ) modify_cmd.parser.add_option( - u'-m', u'--move', action='store_true', dest='move', - help=u"move files in the library directory" + '-m', '--move', action='store_true', dest='move', + help="move files in the library directory" ) modify_cmd.parser.add_option( - u'-M', u'--nomove', action='store_false', dest='move', - help=u"don't move files in library" + '-M', '--nomove', action='store_false', dest='move', + help="don't move files in library" ) modify_cmd.parser.add_option( - u'-w', u'--write', action='store_true', default=None, - help=u"write new metadata to files' tags (default)" + '-w', '--write', action='store_true', default=None, + help="write new metadata to files' tags (default)" ) modify_cmd.parser.add_option( - u'-W', u'--nowrite', action='store_false', dest='write', - help=u"don't write metadata (opposite of -w)" + '-W', '--nowrite', action='store_false', dest='write', + help="don't write metadata (opposite of -w)" ) modify_cmd.parser.add_album_option() modify_cmd.parser.add_format_option(target='item') modify_cmd.parser.add_option( - u'-y', u'--yes', action='store_true', - help=u'skip confirmation' + '-y', '--yes', action='store_true', + help='skip confirmation' ) modify_cmd.func = modify_func default_commands.append(modify_cmd) @@ -1539,21 +1539,25 @@ num_objs = len(objs) # Filter out files that don't need to be moved. - isitemmoved = lambda item: item.path != item.destination(basedir=dest) - isalbummoved = lambda album: any(isitemmoved(i) for i in album.items()) + def isitemmoved(item): + return item.path != item.destination(basedir=dest) + + def isalbummoved(album): + return any(isitemmoved(i) for i in album.items()) + objs = [o for o in objs if (isalbummoved if album else isitemmoved)(o)] num_unmoved = num_objs - len(objs) # Report unmoved files that match the query. - unmoved_msg = u'' + unmoved_msg = '' if num_unmoved > 0: - unmoved_msg = u' ({} already in place)'.format(num_unmoved) + unmoved_msg = f' ({num_unmoved} already in place)' copy = copy or export # Exporting always copies. - action = u'Copying' if copy else u'Moving' - act = u'copy' if copy else u'move' - entity = u'album' if album else u'item' - log.info(u'{0} {1} {2}{3}{4}.', action, len(objs), entity, - u's' if len(objs) != 1 else u'', unmoved_msg) + action = 'Copying' if copy else 'Moving' + act = 'copy' if copy else 'move' + entity = 'album' if album else 'item' + log.info('{0} {1} {2}{3}{4}.', action, len(objs), entity, + 's' if len(objs) != 1 else '', unmoved_msg) if not objs: return @@ -1567,12 +1571,12 @@ else: if confirm: objs = ui.input_select_objects( - u'Really %s' % act, objs, + 'Really %s' % act, objs, lambda o: show_path_changes( [(o.path, o.destination(basedir=dest))])) for obj in objs: - log.debug(u'moving: {0}', util.displayable_path(obj.path)) + log.debug('moving: {0}', util.displayable_path(obj.path)) if export: # Copy without affecting the database. @@ -1591,34 +1595,34 @@ if dest is not None: dest = normpath(dest) if not os.path.isdir(dest): - raise ui.UserError(u'no such directory: %s' % dest) + raise ui.UserError('no such directory: %s' % dest) move_items(lib, dest, decargs(args), opts.copy, opts.album, opts.pretend, opts.timid, opts.export) move_cmd = ui.Subcommand( - u'move', help=u'move or copy items', aliases=(u'mv',) + 'move', help='move or copy items', aliases=('mv',) ) move_cmd.parser.add_option( - u'-d', u'--dest', metavar='DIR', dest='dest', - help=u'destination directory' + '-d', '--dest', metavar='DIR', dest='dest', + help='destination directory' ) move_cmd.parser.add_option( - u'-c', u'--copy', default=False, action='store_true', - help=u'copy instead of moving' + '-c', '--copy', default=False, action='store_true', + help='copy instead of moving' ) move_cmd.parser.add_option( - u'-p', u'--pretend', default=False, action='store_true', - help=u'show how files would be moved, but don\'t touch anything' + '-p', '--pretend', default=False, action='store_true', + help='show how files would be moved, but don\'t touch anything' ) move_cmd.parser.add_option( - u'-t', u'--timid', dest='timid', action='store_true', - help=u'always confirm all actions' + '-t', '--timid', dest='timid', action='store_true', + help='always confirm all actions' ) move_cmd.parser.add_option( - u'-e', u'--export', default=False, action='store_true', - help=u'copy without changing the database path' + '-e', '--export', default=False, action='store_true', + help='copy without changing the database path' ) move_cmd.parser.add_album_option() move_cmd.func = move_func @@ -1636,14 +1640,14 @@ for item in items: # Item deleted? if not os.path.exists(syspath(item.path)): - log.info(u'missing file: {0}', util.displayable_path(item.path)) + log.info('missing file: {0}', util.displayable_path(item.path)) continue # Get an Item object reflecting the "clean" (on-disk) state. try: clean_item = library.Item.from_path(item.path) except library.ReadError as exc: - log.error(u'error reading {0}: {1}', + log.error('error reading {0}: {1}', displayable_path(item.path), exc) continue @@ -1660,14 +1664,14 @@ write_items(lib, decargs(args), opts.pretend, opts.force) -write_cmd = ui.Subcommand(u'write', help=u'write tag information to files') +write_cmd = ui.Subcommand('write', help='write tag information to files') write_cmd.parser.add_option( - u'-p', u'--pretend', action='store_true', - help=u"show all changes but do nothing" + '-p', '--pretend', action='store_true', + help="show all changes but do nothing" ) write_cmd.parser.add_option( - u'-f', u'--force', action='store_true', - help=u"write tags even if the existing tags match the database" + '-f', '--force', action='store_true', + help="write tags even if the existing tags match the database" ) write_cmd.func = write_func default_commands.append(write_cmd) @@ -1721,29 +1725,30 @@ open(path, 'w+').close() util.interactive_open([path], editor) except OSError as exc: - message = u"Could not edit configuration: {0}".format(exc) + message = f"Could not edit configuration: {exc}" if not editor: - message += u". Please set the EDITOR environment variable" + message += ". Please set the EDITOR environment variable" raise ui.UserError(message) -config_cmd = ui.Subcommand(u'config', - help=u'show or edit the user configuration') + +config_cmd = ui.Subcommand('config', + help='show or edit the user configuration') config_cmd.parser.add_option( - u'-p', u'--paths', action='store_true', - help=u'show files that configuration was loaded from' + '-p', '--paths', action='store_true', + help='show files that configuration was loaded from' ) config_cmd.parser.add_option( - u'-e', u'--edit', action='store_true', - help=u'edit user configuration with $EDITOR' + '-e', '--edit', action='store_true', + help='edit user configuration with $EDITOR' ) config_cmd.parser.add_option( - u'-d', u'--defaults', action='store_true', - help=u'include the default configuration' + '-d', '--defaults', action='store_true', + help='include the default configuration' ) config_cmd.parser.add_option( - u'-c', u'--clear', action='store_false', + '-c', '--clear', action='store_false', dest='redact', default=True, - help=u'do not redact sensitive fields' + help='do not redact sensitive fields' ) config_cmd.func = config_func default_commands.append(config_cmd) @@ -1753,19 +1758,20 @@ def print_completion(*args): for line in completion_script(default_commands + plugins.commands()): - print_(line, end=u'') + print_(line, end='') if not any(map(os.path.isfile, BASH_COMPLETION_PATHS)): - log.warning(u'Warning: Unable to find the bash-completion package. ' - u'Command line completion might not work.') + log.warning('Warning: Unable to find the bash-completion package. ' + 'Command line completion might not work.') + BASH_COMPLETION_PATHS = map(syspath, [ - u'/etc/bash_completion', - u'/usr/share/bash-completion/bash_completion', - u'/usr/local/share/bash-completion/bash_completion', + '/etc/bash_completion', + '/usr/share/bash-completion/bash_completion', + '/usr/local/share/bash-completion/bash_completion', # SmartOS - u'/opt/local/share/bash-completion/bash_completion', + '/opt/local/share/bash-completion/bash_completion', # Homebrew (before bash-completion2) - u'/usr/local/etc/bash_completion', + '/usr/local/etc/bash_completion', ]) @@ -1776,7 +1782,7 @@ completion data for. """ base_script = os.path.join(os.path.dirname(__file__), 'completion_base.sh') - with open(base_script, 'r') as base_script: + with open(base_script) as base_script: yield util.text_string(base_script.read()) options = {} @@ -1792,12 +1798,12 @@ if re.match(r'^\w+$', alias): aliases[alias] = name - options[name] = {u'flags': [], u'opts': []} + options[name] = {'flags': [], 'opts': []} for opts in cmd.parser._get_all_options()[1:]: if opts.action in ('store_true', 'store_false'): - option_type = u'flags' + option_type = 'flags' else: - option_type = u'opts' + option_type = 'opts' options[name][option_type].extend( opts._short_opts + opts._long_opts @@ -1805,31 +1811,31 @@ # Add global options options['_global'] = { - u'flags': [u'-v', u'--verbose'], - u'opts': - u'-l --library -c --config -d --directory -h --help'.split(u' ') + 'flags': ['-v', '--verbose'], + 'opts': + '-l --library -c --config -d --directory -h --help'.split(' ') } # Add flags common to all commands options['_common'] = { - u'flags': [u'-h', u'--help'] + 'flags': ['-h', '--help'] } # Start generating the script - yield u"_beet() {\n" + yield "_beet() {\n" # Command names - yield u" local commands='%s'\n" % ' '.join(command_names) - yield u"\n" + yield " local commands='%s'\n" % ' '.join(command_names) + yield "\n" # Command aliases - yield u" local aliases='%s'\n" % ' '.join(aliases.keys()) + yield " local aliases='%s'\n" % ' '.join(aliases.keys()) for alias, cmd in aliases.items(): - yield u" local alias__%s=%s\n" % (alias.replace('-', '_'), cmd) - yield u'\n' + yield " local alias__{}={}\n".format(alias.replace('-', '_'), cmd) + yield '\n' # Fields - yield u" fields='%s'\n" % ' '.join( + yield " fields='%s'\n" % ' '.join( set( list(library.Item._fields.keys()) + list(library.Album._fields.keys()) @@ -1840,17 +1846,17 @@ for cmd, opts in options.items(): for option_type, option_list in opts.items(): if option_list: - option_list = u' '.join(option_list) - yield u" local %s__%s='%s'\n" % ( + option_list = ' '.join(option_list) + yield " local {}__{}='{}'\n".format( option_type, cmd.replace('-', '_'), option_list) - yield u' _beet_dispatch\n' - yield u'}\n' + yield ' _beet_dispatch\n' + yield '}\n' completion_cmd = ui.Subcommand( 'completion', - help=u'print shell script that provides command line completion' + help='print shell script that provides command line completion' ) completion_cmd.func = print_completion completion_cmd.hide = True diff -Nru beets-1.5.0/beets/ui/__init__.py beets-1.6.0/beets/ui/__init__.py --- beets-1.5.0/beets/ui/__init__.py 2021-03-28 18:23:15.000000000 +0000 +++ beets-1.6.0/beets/ui/__init__.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -18,7 +17,6 @@ CLI commands are implemented in the ui.commands module. """ -from __future__ import division, absolute_import, print_function import optparse import textwrap @@ -30,7 +28,6 @@ import struct import traceback import os.path -from six.moves import input from beets import logging from beets import library @@ -43,7 +40,6 @@ from beets.dbcore import query as db_query from beets.dbcore import db import confuse -import six # On Windows platforms, use colorama to support "ANSI" terminal colors. if sys.platform == 'win32': @@ -62,8 +58,8 @@ PF_KEY_QUERIES = { - 'comp': u'comp:true', - 'singleton': u'singleton:true', + 'comp': 'comp:true', + 'singleton': 'singleton:true', } @@ -113,10 +109,7 @@ """Given a list of command-line argument bytestrings, attempts to decode them to Unicode strings when running under Python 2. """ - if six.PY2: - return [s.decode(util.arg_encoding()) for s in arglist] - else: - return arglist + return arglist def print_(*strings, **kwargs): @@ -131,30 +124,25 @@ (it defaults to a newline). """ if not strings: - strings = [u''] - assert isinstance(strings[0], six.text_type) + strings = [''] + assert isinstance(strings[0], str) - txt = u' '.join(strings) - txt += kwargs.get('end', u'\n') + txt = ' '.join(strings) + txt += kwargs.get('end', '\n') # Encode the string and write it to stdout. - if six.PY2: - # On Python 2, sys.stdout expects bytes. + # On Python 3, sys.stdout expects text strings and uses the + # exception-throwing encoding error policy. To avoid throwing + # errors and use our configurable encoding override, we use the + # underlying bytes buffer instead. + if hasattr(sys.stdout, 'buffer'): out = txt.encode(_out_encoding(), 'replace') - sys.stdout.write(out) + sys.stdout.buffer.write(out) + sys.stdout.buffer.flush() else: - # On Python 3, sys.stdout expects text strings and uses the - # exception-throwing encoding error policy. To avoid throwing - # errors and use our configurable encoding override, we use the - # underlying bytes buffer instead. - if hasattr(sys.stdout, 'buffer'): - out = txt.encode(_out_encoding(), 'replace') - sys.stdout.buffer.write(out) - sys.stdout.buffer.flush() - else: - # In our test harnesses (e.g., DummyOut), sys.stdout.buffer - # does not exist. We instead just record the text string. - sys.stdout.write(txt) + # In our test harnesses (e.g., DummyOut), sys.stdout.buffer + # does not exist. We instead just record the text string. + sys.stdout.write(txt) # Configuration wrappers. @@ -206,17 +194,14 @@ # use print_() explicitly to display prompts. # https://bugs.python.org/issue1927 if prompt: - print_(prompt, end=u' ') + print_(prompt, end=' ') try: resp = input() except EOFError: - raise UserError(u'stdin stream ended while input required') + raise UserError('stdin stream ended while input required') - if six.PY2: - return resp.decode(_in_encoding(), 'ignore') - else: - return resp + return resp def input_options(options, require=False, prompt=None, fallback_prompt=None, @@ -260,7 +245,7 @@ found_letter = letter break else: - raise ValueError(u'no unambiguous lettering found') + raise ValueError('no unambiguous lettering found') letters[found_letter.lower()] = option index = option.index(found_letter) @@ -268,7 +253,7 @@ # Mark the option's shortcut letter for display. if not require and ( (default is None and not numrange and first) or - (isinstance(default, six.string_types) and + (isinstance(default, str) and found_letter.lower() == default.lower())): # The first option is the default; mark it. show_letter = '[%s]' % found_letter.upper() @@ -304,11 +289,11 @@ prompt_part_lengths = [] if numrange: if isinstance(default, int): - default_name = six.text_type(default) + default_name = str(default) default_name = colorize('action_default', default_name) tmpl = '# selection (default %s)' prompt_parts.append(tmpl % default_name) - prompt_part_lengths.append(len(tmpl % six.text_type(default))) + prompt_part_lengths.append(len(tmpl % str(default))) else: prompt_parts.append('# selection') prompt_part_lengths.append(len(prompt_parts[-1])) @@ -343,9 +328,9 @@ # Make a fallback prompt too. This is displayed if the user enters # something that is not recognized. if not fallback_prompt: - fallback_prompt = u'Enter one of ' + fallback_prompt = 'Enter one of ' if numrange: - fallback_prompt += u'%i-%i, ' % numrange + fallback_prompt += '%i-%i, ' % numrange fallback_prompt += ', '.join(display_letters) + ':' resp = input_(prompt) @@ -384,9 +369,9 @@ "yes" unless `require` is `True`, in which case there is no default. """ sel = input_options( - ('y', 'n'), require, prompt, u'Enter Y or N:' + ('y', 'n'), require, prompt, 'Enter Y or N:' ) - return sel == u'y' + return sel == 'y' def input_select_objects(prompt, objs, rep, prompt_all=None): @@ -400,24 +385,24 @@ objects individually. """ choice = input_options( - (u'y', u'n', u's'), False, - u'%s? (Yes/no/select)' % (prompt_all or prompt)) + ('y', 'n', 's'), False, + '%s? (Yes/no/select)' % (prompt_all or prompt)) print() # Blank line. - if choice == u'y': # Yes. + if choice == 'y': # Yes. return objs - elif choice == u's': # Select. + elif choice == 's': # Select. out = [] for obj in objs: rep(obj) answer = input_options( - ('y', 'n', 'q'), True, u'%s? (yes/no/quit)' % prompt, - u'Enter Y or N:' + ('y', 'n', 'q'), True, '%s? (yes/no/quit)' % prompt, + 'Enter Y or N:' ) - if answer == u'y': + if answer == 'y': out.append(obj) - elif answer == u'q': + elif answer == 'q': return out return out @@ -429,14 +414,14 @@ def human_bytes(size): """Formats size, a number of bytes, in a human-readable way.""" - powers = [u'', u'K', u'M', u'G', u'T', u'P', u'E', u'Z', u'Y', u'H'] + powers = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'H'] unit = 'B' for power in powers: if size < 1024: - return u"%3.1f %s%s" % (size, power, unit) + return f"{size:3.1f} {power}{unit}" size /= 1024.0 - unit = u'iB' - return u"big" + unit = 'iB' + return "big" def human_seconds(interval): @@ -444,13 +429,13 @@ interval using English words. """ units = [ - (1, u'second'), - (60, u'minute'), - (60, u'hour'), - (24, u'day'), - (7, u'week'), - (52, u'year'), - (10, u'decade'), + (1, 'second'), + (60, 'minute'), + (60, 'hour'), + (24, 'day'), + (7, 'week'), + (52, 'year'), + (10, 'decade'), ] for i in range(len(units) - 1): increment, suffix = units[i] @@ -463,7 +448,7 @@ increment, suffix = units[-1] interval /= float(increment) - return u"%3.1f %ss" % (interval, suffix) + return f"{interval:3.1f} {suffix}s" def human_seconds_short(interval): @@ -471,7 +456,7 @@ string. """ interval = int(interval) - return u'%i:%02i' % (interval // 60, interval % 60) + return '%i:%02i' % (interval // 60, interval % 60) # Colorization. @@ -524,7 +509,7 @@ elif color in LIGHT_COLORS: escape = COLOR_ESCAPE + "%i;01m" % (LIGHT_COLORS[color] + 30) else: - raise ValueError(u'no such color %s', color) + raise ValueError('no such color %s', color) return escape + text + RESET_COLOR @@ -537,14 +522,14 @@ global COLORS if not COLORS: - COLORS = dict((name, - config['ui']['colors'][name].as_str()) - for name in COLOR_NAMES) + COLORS = {name: + config['ui']['colors'][name].as_str() + for name in COLOR_NAMES} # In case a 3rd party plugin is still passing the actual color ('red') # instead of the abstract color name ('text_error') color = COLORS.get(color_name) if not color: - log.debug(u'Invalid color_name: {0}', color_name) + log.debug('Invalid color_name: {0}', color_name) color = color_name return _colorize(color, text) @@ -556,11 +541,11 @@ highlighted intelligently to show differences; other values are stringified and highlighted in their entirety. """ - if not isinstance(a, six.string_types) \ - or not isinstance(b, six.string_types): + if not isinstance(a, str) \ + or not isinstance(b, str): # Non-strings: use ordinary equality. - a = six.text_type(a) - b = six.text_type(b) + a = str(a) + b = str(b) if a == b: return a, b else: @@ -598,7 +583,7 @@ else: assert(False) - return u''.join(a_out), u''.join(b_out) + return ''.join(a_out), ''.join(b_out) def colordiff(a, b, highlight='text_highlight'): @@ -608,7 +593,7 @@ if config['ui']['color']: return _colordiff(a, b, highlight) else: - return six.text_type(a), six.text_type(b) + return str(a), str(b) def get_path_formats(subview=None): @@ -633,7 +618,7 @@ replacements.append((re.compile(pattern), repl)) except re.error: raise UserError( - u'malformed regular expression in replace: {0}'.format( + 'malformed regular expression in replace: {}'.format( pattern ) ) @@ -654,7 +639,7 @@ try: buf = fcntl.ioctl(0, termios.TIOCGWINSZ, ' ' * 4) - except IOError: + except OSError: return fallback try: height, width = struct.unpack('hh', buf) @@ -682,18 +667,18 @@ return None # Get formatted values for output. - oldstr = old_fmt.get(field, u'') - newstr = new_fmt.get(field, u'') + oldstr = old_fmt.get(field, '') + newstr = new_fmt.get(field, '') # For strings, highlight changes. For others, colorize the whole # thing. - if isinstance(oldval, six.string_types): + if isinstance(oldval, str): oldstr, newstr = colordiff(oldval, newstr) else: oldstr = colorize('text_error', oldstr) newstr = colorize('text_error', newstr) - return u'{0} -> {1}'.format(oldstr, newstr) + return f'{oldstr} -> {newstr}' def show_model_changes(new, old=None, fields=None, always=False): @@ -723,14 +708,14 @@ # Detect and show difference for this field. line = _field_diff(field, old, old_fmt, new, new_fmt) if line: - changes.append(u' {0}: {1}'.format(field, line)) + changes.append(f' {field}: {line}') # New fields. for field in set(new) - set(old): if fields and field not in fields: continue - changes.append(u' {0}: {1}'.format( + changes.append(' {}: {}'.format( field, colorize('text_highlight', new_fmt[field]) )) @@ -739,7 +724,7 @@ if changes or always: print_(format(old)) if changes: - print_(u'\n'.join(changes)) + print_('\n'.join(changes)) return bool(changes) @@ -772,15 +757,21 @@ if max_width > col_width: # Print every change over two lines for source, dest in zip(sources, destinations): - log.info(u'{0} \n -> {1}', source, dest) + color_source, color_dest = colordiff(source, dest) + print_('{0} \n -> {1}'.format(color_source, color_dest)) else: # Print every change on a single line, and add a header title_pad = max_width - len('Source ') + len(' -> ') - log.info(u'Source {0} Destination', ' ' * title_pad) + print_('Source {0} Destination'.format(' ' * title_pad)) for source, dest in zip(sources, destinations): pad = max_width - len(source) - log.info(u'{0} {1} -> {2}', source, ' ' * pad, dest) + color_source, color_dest = colordiff(source, dest) + print_('{0} {1} -> {2}'.format( + color_source, + ' ' * pad, + color_dest, + )) # Helper functions for option parsing. @@ -808,13 +799,13 @@ raise ValueError except ValueError: raise UserError( - "supplied argument `{0}' is not of the form `key=value'" + "supplied argument `{}' is not of the form `key=value'" .format(value)) option_values[key] = value -class CommonOptionsParser(optparse.OptionParser, object): +class CommonOptionsParser(optparse.OptionParser): """Offers a simple way to add common formatting options. Options available include: @@ -829,8 +820,9 @@ Each method is fully documented in the related method. """ + def __init__(self, *args, **kwargs): - super(CommonOptionsParser, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._album_flags = False # this serves both as an indicator that we offer the feature AND allows # us to check whether it has been specified on the CLI - bypassing the @@ -844,7 +836,7 @@ Sets the album property on the options extracted from the CLI. """ album = optparse.Option(*flags, action='store_true', - help=u'match albums instead of tracks') + help='match albums instead of tracks') self.add_option(album) self._album_flags = set(flags) @@ -862,7 +854,7 @@ elif value: value, = decargs([value]) else: - value = u'' + value = '' parser.values.format = value if target: @@ -889,14 +881,14 @@ By default this affects both items and albums. If add_album_option() is used then the target will be autodetected. - Sets the format property to u'$path' on the options extracted from the + Sets the format property to '$path' on the options extracted from the CLI. """ path = optparse.Option(*flags, nargs=0, action='callback', callback=self._set_format, - callback_kwargs={'fmt': u'$path', + callback_kwargs={'fmt': '$path', 'store_true': True}, - help=u'print paths for matched items or albums') + help='print paths for matched items or albums') self.add_option(path) def add_format_option(self, flags=('-f', '--format'), target=None): @@ -916,7 +908,7 @@ """ kwargs = {} if target: - if isinstance(target, six.string_types): + if isinstance(target, str): target = {'item': library.Item, 'album': library.Album}[target] kwargs['target'] = target @@ -924,7 +916,7 @@ opt = optparse.Option(*flags, action='callback', callback=self._set_format, callback_kwargs=kwargs, - help=u'print with custom format') + help='print with custom format') self.add_option(opt) def add_all_common_options(self): @@ -943,10 +935,11 @@ # There you will also find a better description of the code and a more # succinct example program. -class Subcommand(object): +class Subcommand: """A subcommand of a root command-line application that may be invoked by a SubcommandOptionParser. """ + def __init__(self, name, parser=None, help='', aliases=(), hide=False): """Creates a new subcommand. name is the primary way to invoke the subcommand; aliases are alternate names. parser is an @@ -974,7 +967,7 @@ @root_parser.setter def root_parser(self, root_parser): self._root_parser = root_parser - self.parser.prog = '{0} {1}'.format( + self.parser.prog = '{} {}'.format( as_string(root_parser.get_prog_name()), self.name) @@ -990,13 +983,13 @@ """ # A more helpful default usage. if 'usage' not in kwargs: - kwargs['usage'] = u""" + kwargs['usage'] = """ %prog COMMAND [ARGS...] %prog help COMMAND""" kwargs['add_help_option'] = False # Super constructor. - super(SubcommandsOptionParser, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # Our root parser needs to stop on the first unrecognized argument. self.disable_interspersed_args() @@ -1013,7 +1006,7 @@ # Add the list of subcommands to the help message. def format_help(self, formatter=None): # Get the original help message, to which we will append. - out = super(SubcommandsOptionParser, self).format_help(formatter) + out = super().format_help(formatter) if formatter is None: formatter = self.formatter @@ -1099,7 +1092,7 @@ cmdname = args.pop(0) subcommand = self._subcommand_for_name(cmdname) if not subcommand: - raise UserError(u"unknown command '{0}'".format(cmdname)) + raise UserError(f"unknown command '{cmdname}'") suboptions, subargs = subcommand.parse_args(args) return subcommand, suboptions, subargs @@ -1115,7 +1108,7 @@ """ paths = config['pluginpath'].as_str_seq(split=False) paths = [util.normpath(p) for p in paths] - log.debug(u'plugin paths: {0}', util.displayable_path(paths)) + log.debug('plugin paths: {0}', util.displayable_path(paths)) # On Python 3, the search paths need to be unicode. paths = [util.py3_path(p) for p in paths] @@ -1136,7 +1129,6 @@ plugin_list = config['plugins'].as_str_seq() plugins.load_plugins(plugin_list) - plugins.send("pluginload") return plugins @@ -1152,16 +1144,6 @@ plugins = _load_plugins(options, config) - # Get the default subcommands. - from beets.ui.commands import default_commands - - subcommands = list(default_commands) - subcommands.extend(plugins.commands()) - - if lib is None: - lib = _open_library(config) - plugins.send("library_opened", lib=lib) - # Add types and queries defined by plugins. plugin_types_album = plugins.types(library.Album) library.Album._types.update(plugin_types_album) @@ -1173,6 +1155,18 @@ library.Item._queries.update(plugins.named_queries(library.Item)) library.Album._queries.update(plugins.named_queries(library.Album)) + plugins.send("pluginload") + + # Get the default subcommands. + from beets.ui.commands import default_commands + + subcommands = list(default_commands) + subcommands.extend(plugins.commands()) + + if lib is None: + lib = _open_library(config) + plugins.send("library_opened", lib=lib) + return subcommands, plugins, lib @@ -1197,18 +1191,18 @@ log.set_global_level(logging.INFO) if overlay_path: - log.debug(u'overlaying configuration: {0}', + log.debug('overlaying configuration: {0}', util.displayable_path(overlay_path)) config_path = config.user_config_path() if os.path.isfile(config_path): - log.debug(u'user configuration: {0}', + log.debug('user configuration: {0}', util.displayable_path(config_path)) else: - log.debug(u'no user configuration found at {0}', + log.debug('no user configuration found at {0}', util.displayable_path(config_path)) - log.debug(u'data directory: {0}', + log.debug('data directory: {0}', util.displayable_path(config.config_dir())) return config @@ -1226,13 +1220,13 @@ ) lib.get_item(0) # Test database connection. except (sqlite3.OperationalError, sqlite3.DatabaseError) as db_error: - log.debug(u'{}', traceback.format_exc()) - raise UserError(u"database file {0} cannot not be opened: {1}".format( + log.debug('{}', traceback.format_exc()) + raise UserError("database file {} cannot not be opened: {}".format( util.displayable_path(dbpath), db_error )) - log.debug(u'library database: {0}\n' - u'library directory: {1}', + log.debug('library database: {0}\n' + 'library directory: {1}', util.displayable_path(lib.path), util.displayable_path(lib.directory)) return lib @@ -1246,17 +1240,17 @@ parser.add_format_option(flags=('--format-item',), target=library.Item) parser.add_format_option(flags=('--format-album',), target=library.Album) parser.add_option('-l', '--library', dest='library', - help=u'library database file to use') + help='library database file to use') parser.add_option('-d', '--directory', dest='directory', - help=u"destination music directory") + help="destination music directory") parser.add_option('-v', '--verbose', dest='verbose', action='count', - help=u'log more details (use twice for even more)') + help='log more details (use twice for even more)') parser.add_option('-c', '--config', dest='config', - help=u'path to configuration file') + help='path to configuration file') parser.add_option('-p', '--plugins', dest='plugins', - help=u'a comma-separated list of plugins to load') + help='a comma-separated list of plugins to load') parser.add_option('-h', '--help', dest='help', action='store_true', - help=u'show this help message and exit') + help='show this help message and exit') parser.add_option('--version', dest='version', action='store_true', help=optparse.SUPPRESS_HELP) @@ -1291,7 +1285,7 @@ _raw_main(args) except UserError as exc: message = exc.args[0] if exc.args else None - log.error(u'error: {0}', message) + log.error('error: {0}', message) sys.exit(1) except util.HumanReadableException as exc: exc.log(log) @@ -1303,12 +1297,12 @@ log.error('{}', exc) sys.exit(1) except confuse.ConfigError as exc: - log.error(u'configuration error: {0}', exc) + log.error('configuration error: {0}', exc) sys.exit(1) except db_query.InvalidQueryError as exc: - log.error(u'invalid query: {0}', exc) + log.error('invalid query: {0}', exc) sys.exit(1) - except IOError as exc: + except OSError as exc: if exc.errno == errno.EPIPE: # "Broken pipe". End silently. sys.stderr.close() @@ -1316,11 +1310,11 @@ raise except KeyboardInterrupt: # Silently ignore ^C except in verbose mode. - log.debug(u'{}', traceback.format_exc()) + log.debug('{}', traceback.format_exc()) except db.DBAccessError as exc: log.error( - u'database access error: {0}\n' - u'the library file might have a permissions problem', + 'database access error: {0}\n' + 'the library file might have a permissions problem', exc ) sys.exit(1) diff -Nru beets-1.5.0/beets/util/artresizer.py beets-1.6.0/beets/util/artresizer.py --- beets-1.5.0/beets/util/artresizer.py 2021-03-28 18:23:15.000000000 +0000 +++ beets-1.6.0/beets/util/artresizer.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte # @@ -16,26 +15,22 @@ """Abstraction layer to resize images using PIL, ImageMagick, or a public resizing proxy if neither is available. """ -from __future__ import division, absolute_import, print_function import subprocess import os +import os.path import re from tempfile import NamedTemporaryFile -from six.moves.urllib.parse import urlencode +from urllib.parse import urlencode from beets import logging from beets import util -import six # Resizing methods PIL = 1 IMAGEMAGICK = 2 WEBPROXY = 3 -if util.SNI_SUPPORTED: - PROXY_URL = 'https://images.weserv.nl/' -else: - PROXY_URL = 'http://images.weserv.nl/' +PROXY_URL = 'https://images.weserv.nl/' log = logging.getLogger('beets') @@ -52,7 +47,7 @@ if quality > 0: params['q'] = quality - return '{0}?{1}'.format(PROXY_URL, urlencode(params)) + return '{}?{}'.format(PROXY_URL, urlencode(params)) def temp_file_for(path): @@ -71,7 +66,7 @@ path_out = path_out or temp_file_for(path_in) from PIL import Image - log.debug(u'artresizer: PIL resizing {0} to {1}', + log.debug('artresizer: PIL resizing {0} to {1}', util.displayable_path(path_in), util.displayable_path(path_out)) try: @@ -83,7 +78,10 @@ # Use PIL's default quality. quality = -1 - im.save(util.py3_path(path_out), quality=quality) + # progressive=False only affects JPEGs and is the default, + # but we include it here for explicitness. + im.save(util.py3_path(path_out), quality=quality, progressive=False) + if max_filesize > 0: # If maximum filesize is set, we attempt to lower the quality of # jpeg conversion by a proportional amount, up to 3 attempts @@ -95,7 +93,7 @@ for i in range(5): # 5 attempts is an abitrary choice filesize = os.stat(util.syspath(path_out)).st_size - log.debug(u"PIL Pass {0} : Output size: {1}B", i, filesize) + log.debug("PIL Pass {0} : Output size: {1}B", i, filesize) if filesize <= max_filesize: return path_out # The relationship between filesize & quality will be @@ -105,17 +103,16 @@ if lower_qual < 10: lower_qual = 10 # Use optimize flag to improve filesize decrease - im.save( - util.py3_path(path_out), quality=lower_qual, optimize=True - ) - log.warning(u"PIL Failed to resize file to below {0}B", + im.save(util.py3_path(path_out), quality=lower_qual, + optimize=True, progressive=False) + log.warning("PIL Failed to resize file to below {0}B", max_filesize) return path_out else: return path_out - except IOError: - log.error(u"PIL cannot create thumbnail for '{0}'", + except OSError: + log.error("PIL cannot create thumbnail for '{0}'", util.displayable_path(path_in)) return path_in @@ -127,31 +124,34 @@ the output path of resized image. """ path_out = path_out or temp_file_for(path_in) - log.debug(u'artresizer: ImageMagick resizing {0} to {1}', + log.debug('artresizer: ImageMagick resizing {0} to {1}', util.displayable_path(path_in), util.displayable_path(path_out)) # "-resize WIDTHx>" shrinks images with the width larger # than the given width while maintaining the aspect ratio # with regards to the height. + # ImageMagick already seems to default to no interlace, but we include it + # here for the sake of explicitness. cmd = ArtResizer.shared.im_convert_cmd + [ util.syspath(path_in, prefix=False), - '-resize', '{0}x>'.format(maxwidth), + '-resize', f'{maxwidth}x>', + '-interlace', 'none', ] if quality > 0: - cmd += ['-quality', '{0}'.format(quality)] + cmd += ['-quality', f'{quality}'] # "-define jpeg:extent=SIZEb" sets the target filesize for imagemagick to # SIZE in bytes. if max_filesize > 0: - cmd += ['-define', 'jpeg:extent={0}b'.format(max_filesize)] + cmd += ['-define', f'jpeg:extent={max_filesize}b'] cmd.append(util.syspath(path_out, prefix=False)) try: util.command_output(cmd) except subprocess.CalledProcessError: - log.warning(u'artresizer: IM convert failed for {0}', + log.warning('artresizer: IM convert failed for {0}', util.displayable_path(path_in)) return path_in @@ -170,8 +170,8 @@ try: im = Image.open(util.syspath(path_in)) return im.size - except IOError as exc: - log.error(u"PIL could not read file {}: {}", + except OSError as exc: + log.error("PIL could not read file {}: {}", util.displayable_path(path_in), exc) @@ -182,17 +182,17 @@ try: out = util.command_output(cmd).stdout except subprocess.CalledProcessError as exc: - log.warning(u'ImageMagick size query failed') + log.warning('ImageMagick size query failed') log.debug( - u'`convert` exited with (status {}) when ' - u'getting size with command {}:\n{}', + '`convert` exited with (status {}) when ' + 'getting size with command {}:\n{}', exc.returncode, cmd, exc.output.strip() ) return try: return tuple(map(int, out.split(b' '))) except IndexError: - log.warning(u'Could not understand IM output: {0!r}', out) + log.warning('Could not understand IM output: {0!r}', out) BACKEND_GET_SIZE = { @@ -201,6 +201,106 @@ } +def pil_deinterlace(path_in, path_out=None): + path_out = path_out or temp_file_for(path_in) + from PIL import Image + + try: + im = Image.open(util.syspath(path_in)) + im.save(util.py3_path(path_out), progressive=False) + return path_out + except IOError: + return path_in + + +def im_deinterlace(path_in, path_out=None): + path_out = path_out or temp_file_for(path_in) + + cmd = ArtResizer.shared.im_convert_cmd + [ + util.syspath(path_in, prefix=False), + '-interlace', 'none', + util.syspath(path_out, prefix=False), + ] + + try: + util.command_output(cmd) + return path_out + except subprocess.CalledProcessError: + return path_in + + +DEINTERLACE_FUNCS = { + PIL: pil_deinterlace, + IMAGEMAGICK: im_deinterlace, +} + + +def im_get_format(filepath): + cmd = ArtResizer.shared.im_identify_cmd + [ + '-format', '%[magick]', + util.syspath(filepath) + ] + + try: + return util.command_output(cmd).stdout + except subprocess.CalledProcessError: + return None + + +def pil_get_format(filepath): + from PIL import Image, UnidentifiedImageError + + try: + with Image.open(util.syspath(filepath)) as im: + return im.format + except (ValueError, TypeError, UnidentifiedImageError, FileNotFoundError): + log.exception("failed to detect image format for {}", filepath) + return None + + +BACKEND_GET_FORMAT = { + PIL: pil_get_format, + IMAGEMAGICK: im_get_format, +} + + +def im_convert_format(source, target, deinterlaced): + cmd = ArtResizer.shared.im_convert_cmd + [ + util.syspath(source), + *(["-interlace", "none"] if deinterlaced else []), + util.syspath(target), + ] + + try: + subprocess.check_call( + cmd, + stderr=subprocess.DEVNULL, + stdout=subprocess.DEVNULL + ) + return target + except subprocess.CalledProcessError: + return source + + +def pil_convert_format(source, target, deinterlaced): + from PIL import Image, UnidentifiedImageError + + try: + with Image.open(util.syspath(source)) as im: + im.save(util.py3_path(target), progressive=not deinterlaced) + return target + except (ValueError, TypeError, UnidentifiedImageError, FileNotFoundError, + OSError): + log.exception("failed to convert image {} -> {}", source, target) + return source + + +BACKEND_CONVERT_IMAGE_FORMAT = { + PIL: pil_convert_format, + IMAGEMAGICK: im_convert_format, +} + + class Shareable(type): """A pseudo-singleton metaclass that allows both shared and non-shared instances. The ``MyClass.shared`` property holds a @@ -209,7 +309,7 @@ """ def __init__(cls, name, bases, dict): - super(Shareable, cls).__init__(name, bases, dict) + super().__init__(name, bases, dict) cls._instance = None @property @@ -219,7 +319,7 @@ return cls._instance -class ArtResizer(six.with_metaclass(Shareable, object)): +class ArtResizer(metaclass=Shareable): """A singleton class that performs image resizes. """ @@ -227,7 +327,7 @@ """Create a resizer object with an inferred method. """ self.method = self._check_method() - log.debug(u"artresizer: method is {0}", self.method) + log.debug("artresizer: method is {0}", self.method) self.can_compare = self._can_compare() # Use ImageMagick's magick binary when it's available. If it's @@ -257,6 +357,13 @@ else: return path_in + def deinterlace(self, path_in, path_out=None): + if self.local: + func = DEINTERLACE_FUNCS[self.method[0]] + return func(path_in, path_out) + else: + return path_in + def proxy_url(self, maxwidth, url, quality=0): """Modifies an image URL according the method, returning a new URL. For WEBPROXY, a URL on the proxy server is returned. @@ -278,12 +385,50 @@ """Return the size of an image file as an int couple (width, height) in pixels. - Only available locally + Only available locally. """ if self.local: func = BACKEND_GET_SIZE[self.method[0]] return func(path_in) + def get_format(self, path_in): + """Returns the format of the image as a string. + + Only available locally. + """ + if self.local: + func = BACKEND_GET_FORMAT[self.method[0]] + return func(path_in) + + def reformat(self, path_in, new_format, deinterlaced=True): + """Converts image to desired format, updating its extension, but + keeping the same filename. + + Only available locally. + """ + if not self.local: + return path_in + + new_format = new_format.lower() + # A nonexhaustive map of image "types" to extensions overrides + new_format = { + 'jpeg': 'jpg', + }.get(new_format, new_format) + + fname, ext = os.path.splitext(path_in) + path_new = fname + b'.' + new_format.encode('utf8') + func = BACKEND_CONVERT_IMAGE_FORMAT[self.method[0]] + + # allows the exception to propagate, while still making sure a changed + # file path was removed + result_path = path_in + try: + result_path = func(path_in, path_new, deinterlaced) + finally: + if result_path != path_in: + os.unlink(path_in) + return result_path + def _can_compare(self): """A boolean indicating whether image comparison is available""" @@ -323,7 +468,7 @@ try: out = util.command_output(cmd).stdout except (subprocess.CalledProcessError, OSError) as exc: - log.debug(u'ImageMagick version check failed: {}', exc) + log.debug('ImageMagick version check failed: {}', exc) else: if b'imagemagick' in out.lower(): pattern = br".+ (\d+)\.(\d+)\.(\d+).*" @@ -341,7 +486,7 @@ """Get the PIL/Pillow version, or None if it is unavailable. """ try: - __import__('PIL', fromlist=[str('Image')]) + __import__('PIL', fromlist=['Image']) return (0,) except ImportError: return None diff -Nru beets-1.5.0/beets/util/bluelet.py beets-1.6.0/beets/util/bluelet.py --- beets-1.5.0/beets/util/bluelet.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/util/bluelet.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Extremely simple pure-Python implementation of coroutine-style asynchronous socket I/O. Inspired by, but inferior to, Eventlet. Bluelet can also be thought of as a less-terrible replacement for @@ -7,9 +5,7 @@ Bluelet: easy concurrency without all the messy parallelism. """ -from __future__ import division, absolute_import, print_function -import six import socket import select import sys @@ -22,7 +18,7 @@ # Basic events used for thread scheduling. -class Event(object): +class Event: """Just a base class identifying Bluelet events. An event is an object yielded from a Bluelet thread coroutine to suspend operation and communicate with the scheduler. @@ -201,7 +197,7 @@ self.exc_info = exc_info def reraise(self): - six.reraise(self.exc_info[0], self.exc_info[1], self.exc_info[2]) + raise self.exc_info[1].with_traceback(self.exc_info[2]) SUSPENDED = Event() # Special sentinel placeholder for suspended threads. @@ -336,12 +332,12 @@ break # Wait and fire. - event2coro = dict((v, k) for k, v in threads.items()) + event2coro = {v: k for k, v in threads.items()} for event in _event_select(threads.values()): # Run the IO operation, but catch socket errors. try: value = event.fire() - except socket.error as exc: + except OSError as exc: if isinstance(exc.args, tuple) and \ exc.args[0] == errno.EPIPE: # Broken pipe. Remote host disconnected. @@ -390,7 +386,7 @@ pass -class Listener(object): +class Listener: """A socket wrapper object for listening sockets. """ def __init__(self, host, port): @@ -420,7 +416,7 @@ self.sock.close() -class Connection(object): +class Connection: """A socket wrapper object for connected sockets. """ def __init__(self, sock, addr): @@ -545,7 +541,7 @@ and child coroutines run concurrently. """ if not isinstance(coro, types.GeneratorType): - raise ValueError(u'%s is not a coroutine' % coro) + raise ValueError('%s is not a coroutine' % coro) return SpawnEvent(coro) @@ -555,7 +551,7 @@ returns a value using end(), then this event returns that value. """ if not isinstance(coro, types.GeneratorType): - raise ValueError(u'%s is not a coroutine' % coro) + raise ValueError('%s is not a coroutine' % coro) return DelegationEvent(coro) diff -Nru beets-1.5.0/beets/util/confit.py beets-1.6.0/beets/util/confit.py --- beets-1.5.0/beets/util/confit.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/util/confit.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016-2019, Adrian Sampson. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import confuse diff -Nru beets-1.5.0/beets/util/enumeration.py beets-1.6.0/beets/util/enumeration.py --- beets-1.5.0/beets/util/enumeration.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/util/enumeration.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function from enum import Enum diff -Nru beets-1.5.0/beets/util/functemplate.py beets-1.6.0/beets/util/functemplate.py --- beets-1.5.0/beets/util/functemplate.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beets/util/functemplate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -27,31 +26,30 @@ engine like Jinja2 or Mustache. """ -from __future__ import division, absolute_import, print_function import re import ast import dis import types import sys -import six import functools -SYMBOL_DELIM = u'$' -FUNC_DELIM = u'%' -GROUP_OPEN = u'{' -GROUP_CLOSE = u'}' -ARG_SEP = u',' -ESCAPE_CHAR = u'$' +SYMBOL_DELIM = '$' +FUNC_DELIM = '%' +GROUP_OPEN = '{' +GROUP_CLOSE = '}' +ARG_SEP = ',' +ESCAPE_CHAR = '$' VARIABLE_PREFIX = '__var_' FUNCTION_PREFIX = '__func_' -class Environment(object): +class Environment: """Contains the values and functions to be substituted into a template. """ + def __init__(self, values, functions): self.values = values self.functions = functions @@ -73,26 +71,7 @@ """An int, float, long, bool, string, or None literal with the given value. """ - if sys.version_info[:2] < (3, 4): - if val is None: - return ast.Name('None', ast.Load()) - elif isinstance(val, six.integer_types): - return ast.Num(val) - elif isinstance(val, bool): - return ast.Name(bytes(val), ast.Load()) - elif isinstance(val, six.string_types): - return ast.Str(val) - raise TypeError(u'no literal for {0}'.format(type(val))) - elif sys.version_info[:2] < (3, 6): - if val in [None, True, False]: - return ast.NameConstant(val) - elif isinstance(val, six.integer_types): - return ast.Num(val) - elif isinstance(val, six.string_types): - return ast.Str(val) - raise TypeError(u'no literal for {0}'.format(type(val))) - else: - return ast.Constant(val) + return ast.Constant(val) def ex_varassign(name, expr): @@ -109,7 +88,7 @@ function may be an expression or the name of a function. Each argument may be an expression or a value to be used as a literal. """ - if isinstance(func, six.string_types): + if isinstance(func, str): func = ex_rvalue(func) args = list(args) @@ -117,10 +96,7 @@ if not isinstance(args[i], ast.expr): args[i] = ex_literal(args[i]) - if sys.version_info[:2] < (3, 5): - return ast.Call(func, args, [], None, None) - else: - return ast.Call(func, args, []) + return ast.Call(func, args, []) def compile_func(arg_names, statements, name='_the_func', debug=False): @@ -128,24 +104,15 @@ the resulting Python function. If `debug`, then print out the bytecode of the compiled function. """ - if six.PY2: - name = name.encode('utf-8') - args = ast.arguments( - args=[ast.Name(n, ast.Param()) for n in arg_names], - vararg=None, - kwarg=None, - defaults=[ex_literal(None) for _ in arg_names], - ) - else: - args_fields = { - 'args': [ast.arg(arg=n, annotation=None) for n in arg_names], - 'kwonlyargs': [], - 'kw_defaults': [], - 'defaults': [ex_literal(None) for _ in arg_names], - } - if 'posonlyargs' in ast.arguments._fields: # Added in Python 3.8. - args_fields['posonlyargs'] = [] - args = ast.arguments(**args_fields) + args_fields = { + 'args': [ast.arg(arg=n, annotation=None) for n in arg_names], + 'kwonlyargs': [], + 'kw_defaults': [], + 'defaults': [ex_literal(None) for _ in arg_names], + } + if 'posonlyargs' in ast.arguments._fields: # Added in Python 3.8. + args_fields['posonlyargs'] = [] + args = ast.arguments(**args_fields) func_def = ast.FunctionDef( name=name, @@ -179,14 +146,15 @@ # AST nodes for the template language. -class Symbol(object): +class Symbol: """A variable-substitution symbol in a template.""" + def __init__(self, ident, original): self.ident = ident self.original = original def __repr__(self): - return u'Symbol(%s)' % repr(self.ident) + return 'Symbol(%s)' % repr(self.ident) def evaluate(self, env): """Evaluate the symbol in the environment, returning a Unicode @@ -201,24 +169,22 @@ def translate(self): """Compile the variable lookup.""" - if six.PY2: - ident = self.ident.encode('utf-8') - else: - ident = self.ident + ident = self.ident expr = ex_rvalue(VARIABLE_PREFIX + ident) - return [expr], set([ident]), set() + return [expr], {ident}, set() -class Call(object): +class Call: """A function call in a template.""" + def __init__(self, ident, args, original): self.ident = ident self.args = args self.original = original def __repr__(self): - return u'Call(%s, %s, %s)' % (repr(self.ident), repr(self.args), - repr(self.original)) + return 'Call({}, {}, {})'.format(repr(self.ident), repr(self.args), + repr(self.original)) def evaluate(self, env): """Evaluate the function call in the environment, returning a @@ -231,19 +197,15 @@ except Exception as exc: # Function raised exception! Maybe inlining the name of # the exception will help debug. - return u'<%s>' % six.text_type(exc) - return six.text_type(out) + return '<%s>' % str(exc) + return str(out) else: return self.original def translate(self): """Compile the function call.""" varnames = set() - if six.PY2: - ident = self.ident.encode('utf-8') - else: - ident = self.ident - funcnames = set([ident]) + funcnames = {self.ident} arg_exprs = [] for arg in self.args: @@ -254,32 +216,33 @@ # Create a subexpression that joins the result components of # the arguments. arg_exprs.append(ex_call( - ast.Attribute(ex_literal(u''), 'join', ast.Load()), + ast.Attribute(ex_literal(''), 'join', ast.Load()), [ex_call( 'map', [ - ex_rvalue(six.text_type.__name__), + ex_rvalue(str.__name__), ast.List(subexprs, ast.Load()), ] )], )) subexpr_call = ex_call( - FUNCTION_PREFIX + ident, + FUNCTION_PREFIX + self.ident, arg_exprs ) return [subexpr_call], varnames, funcnames -class Expression(object): +class Expression: """Top-level template construct: contains a list of text blobs, Symbols, and Calls. """ + def __init__(self, parts): self.parts = parts def __repr__(self): - return u'Expression(%s)' % (repr(self.parts)) + return 'Expression(%s)' % (repr(self.parts)) def evaluate(self, env): """Evaluate the entire expression in the environment, returning @@ -287,11 +250,11 @@ """ out = [] for part in self.parts: - if isinstance(part, six.string_types): + if isinstance(part, str): out.append(part) else: out.append(part.evaluate(env)) - return u''.join(map(six.text_type, out)) + return ''.join(map(str, out)) def translate(self): """Compile the expression to a list of Python AST expressions, a @@ -301,7 +264,7 @@ varnames = set() funcnames = set() for part in self.parts: - if isinstance(part, six.string_types): + if isinstance(part, str): expressions.append(ex_literal(part)) else: e, v, f = part.translate() @@ -317,7 +280,7 @@ pass -class Parser(object): +class Parser: """Parses a template expression string. Instantiate the class with the template source and call ``parse_expression``. The ``pos`` field will indicate the character after the expression finished and @@ -330,6 +293,7 @@ replaced with a real, accepted parsing technique (PEG, parser generator, etc.). """ + def __init__(self, string, in_argument=False): """ Create a new parser. :param in_arguments: boolean that indicates the parser is to be @@ -345,7 +309,7 @@ special_chars = (SYMBOL_DELIM, FUNC_DELIM, GROUP_OPEN, GROUP_CLOSE, ESCAPE_CHAR) special_char_re = re.compile(r'[%s]|\Z' % - u''.join(re.escape(c) for c in special_chars)) + ''.join(re.escape(c) for c in special_chars)) escapable_chars = (SYMBOL_DELIM, FUNC_DELIM, GROUP_CLOSE, ARG_SEP) terminator_chars = (GROUP_CLOSE,) @@ -362,7 +326,7 @@ if self.in_argument: extra_special_chars = (ARG_SEP,) special_char_re = re.compile( - r'[%s]|\Z' % u''.join( + r'[%s]|\Z' % ''.join( re.escape(c) for c in self.special_chars + extra_special_chars ) @@ -406,7 +370,7 @@ # Shift all characters collected so far into a single string. if text_parts: - self.parts.append(u''.join(text_parts)) + self.parts.append(''.join(text_parts)) text_parts = [] if char == SYMBOL_DELIM: @@ -428,7 +392,7 @@ # If any parsed characters remain, shift them into a string. if text_parts: - self.parts.append(u''.join(text_parts)) + self.parts.append(''.join(text_parts)) def parse_symbol(self): """Parse a variable reference (like ``$foo`` or ``${foo}``) @@ -583,9 +547,10 @@ # External interface. -class Template(object): +class Template: """A string template, including text, Symbols, and Calls. """ + def __init__(self, template): self.expr = _parse(template) self.original = template @@ -634,7 +599,7 @@ for funcname in funcnames: args[FUNCTION_PREFIX + funcname] = functions[funcname] parts = func(**args) - return u''.join(parts) + return ''.join(parts) return wrapper_func @@ -643,9 +608,9 @@ if __name__ == '__main__': import timeit - _tmpl = Template(u'foo $bar %baz{foozle $bar barzle} $bar') + _tmpl = Template('foo $bar %baz{foozle $bar barzle} $bar') _vars = {'bar': 'qux'} - _funcs = {'baz': six.text_type.upper} + _funcs = {'baz': str.upper} interp_time = timeit.timeit('_tmpl.interpret(_vars, _funcs)', 'from __main__ import _tmpl, _vars, _funcs', number=10000) @@ -654,4 +619,4 @@ 'from __main__ import _tmpl, _vars, _funcs', number=10000) print(comp_time) - print(u'Speedup:', interp_time / comp_time) + print('Speedup:', interp_time / comp_time) diff -Nru beets-1.5.0/beets/util/hidden.py beets-1.6.0/beets/util/hidden.py --- beets-1.5.0/beets/util/hidden.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/util/hidden.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,7 +13,6 @@ # included in all copies or substantial portions of the Software. """Simple library to work out if a file is hidden on different platforms.""" -from __future__ import division, absolute_import, print_function import os import stat diff -Nru beets-1.5.0/beets/util/__init__.py beets-1.6.0/beets/util/__init__.py --- beets-1.5.0/beets/util/__init__.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beets/util/__init__.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,12 +14,12 @@ """Miscellaneous utility functions.""" -from __future__ import division, absolute_import, print_function import os import sys import errno import locale import re +import tempfile import shutil import fnmatch import functools @@ -31,14 +30,12 @@ import platform import shlex from beets.util import hidden -import six from unidecode import unidecode from enum import Enum MAX_FILENAME_LENGTH = 200 -WINDOWS_MAGIC_PREFIX = u'\\\\?\\' -SNI_SUPPORTED = sys.version_info >= (2, 7, 9) +WINDOWS_MAGIC_PREFIX = '\\\\?\\' class HumanReadableException(Exception): @@ -60,27 +57,27 @@ self.reason = reason self.verb = verb self.tb = tb - super(HumanReadableException, self).__init__(self.get_message()) + super().__init__(self.get_message()) def _gerund(self): """Generate a (likely) gerund form of the English verb. """ - if u' ' in self.verb: + if ' ' in self.verb: return self.verb - gerund = self.verb[:-1] if self.verb.endswith(u'e') else self.verb - gerund += u'ing' + gerund = self.verb[:-1] if self.verb.endswith('e') else self.verb + gerund += 'ing' return gerund def _reasonstr(self): """Get the reason as a string.""" - if isinstance(self.reason, six.text_type): + if isinstance(self.reason, str): return self.reason elif isinstance(self.reason, bytes): return self.reason.decode('utf-8', 'ignore') elif hasattr(self.reason, 'strerror'): # i.e., EnvironmentError return self.reason.strerror else: - return u'"{0}"'.format(six.text_type(self.reason)) + return '"{}"'.format(str(self.reason)) def get_message(self): """Create the human-readable description of the error, sans @@ -94,7 +91,7 @@ """ if self.tb: logger.debug(self.tb) - logger.error(u'{0}: {1}', self.error_kind, self.args[0]) + logger.error('{0}: {1}', self.error_kind, self.args[0]) class FilesystemError(HumanReadableException): @@ -102,29 +99,30 @@ via a function in this module. The `paths` field is a sequence of pathnames involved in the operation. """ + def __init__(self, reason, verb, paths, tb=None): self.paths = paths - super(FilesystemError, self).__init__(reason, verb, tb) + super().__init__(reason, verb, tb) def get_message(self): # Use a nicer English phrasing for some specific verbs. if self.verb in ('move', 'copy', 'rename'): - clause = u'while {0} {1} to {2}'.format( + clause = 'while {} {} to {}'.format( self._gerund(), displayable_path(self.paths[0]), displayable_path(self.paths[1]) ) elif self.verb in ('delete', 'write', 'create', 'read'): - clause = u'while {0} {1}'.format( + clause = 'while {} {}'.format( self._gerund(), displayable_path(self.paths[0]) ) else: - clause = u'during {0} of paths {1}'.format( - self.verb, u', '.join(displayable_path(p) for p in self.paths) + clause = 'during {} of paths {}'.format( + self.verb, ', '.join(displayable_path(p) for p in self.paths) ) - return u'{0} {1}'.format(self._reasonstr(), clause) + return f'{self._reasonstr()} {clause}' class MoveOperation(Enum): @@ -186,7 +184,7 @@ contents = os.listdir(syspath(path)) except OSError as exc: if logger: - logger.warning(u'could not list directory {0}: {1}'.format( + logger.warning('could not list directory {}: {}'.format( displayable_path(path), exc.strerror )) return @@ -200,7 +198,7 @@ for pat in ignore: if fnmatch.fnmatch(base, pat): if logger: - logger.debug(u'ignoring {0} due to ignore rule {1}'.format( + logger.debug('ignoring {} due to ignore rule {}'.format( base, pat )) skip = True @@ -225,8 +223,7 @@ for base in dirs: cur = os.path.join(path, base) # yield from sorted_walk(...) - for res in sorted_walk(cur, ignore, ignore_hidden, logger): - yield res + yield from sorted_walk(cur, ignore, ignore_hidden, logger) def path_as_posix(path): @@ -244,7 +241,7 @@ if not os.path.isdir(syspath(ancestor)): try: os.mkdir(syspath(ancestor)) - except (OSError, IOError) as exc: + except OSError as exc: raise FilesystemError(exc, 'create', (ancestor,), traceback.format_exc()) @@ -382,18 +379,18 @@ PATH_SEP = bytestring_path(os.sep) -def displayable_path(path, separator=u'; '): +def displayable_path(path, separator='; '): """Attempts to decode a bytestring path to a unicode object for the purpose of displaying it to the user. If the `path` argument is a list or a tuple, the elements are joined with `separator`. """ if isinstance(path, (list, tuple)): return separator.join(displayable_path(p) for p in path) - elif isinstance(path, six.text_type): + elif isinstance(path, str): return path elif not isinstance(path, bytes): # A non-string object: just get its unicode representation. - return six.text_type(path) + return str(path) try: return path.decode(_fsencoding(), 'ignore') @@ -412,7 +409,7 @@ if os.path.__name__ != 'ntpath': return path - if not isinstance(path, six.text_type): + if not isinstance(path, str): # Beets currently represents Windows paths internally with UTF-8 # arbitrarily. But earlier versions used MBCS because it is # reported as the FS encoding by Windows. Try both. @@ -427,9 +424,9 @@ # Add the magic prefix if it isn't already there. # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx if prefix and not path.startswith(WINDOWS_MAGIC_PREFIX): - if path.startswith(u'\\\\'): + if path.startswith('\\\\'): # UNC path. Final path should look like \\?\UNC\... - path = u'UNC' + path[1:] + path = 'UNC' + path[1:] path = WINDOWS_MAGIC_PREFIX + path return path @@ -451,7 +448,7 @@ return try: os.remove(path) - except (OSError, IOError) as exc: + except OSError as exc: raise FilesystemError(exc, 'delete', (path,), traceback.format_exc()) @@ -466,10 +463,10 @@ path = syspath(path) dest = syspath(dest) if not replace and os.path.exists(dest): - raise FilesystemError(u'file exists', 'copy', (path, dest)) + raise FilesystemError('file exists', 'copy', (path, dest)) try: shutil.copyfile(path, dest) - except (OSError, IOError) as exc: + except OSError as exc: raise FilesystemError(exc, 'copy', (path, dest), traceback.format_exc()) @@ -482,24 +479,37 @@ instead, in which case metadata will *not* be preserved. Paths are translated to system paths. """ + if os.path.isdir(path): + raise FilesystemError(u'source is directory', 'move', (path, dest)) + if os.path.isdir(dest): + raise FilesystemError(u'destination is directory', 'move', + (path, dest)) if samefile(path, dest): return path = syspath(path) dest = syspath(dest) if os.path.exists(dest) and not replace: - raise FilesystemError(u'file exists', 'rename', (path, dest)) + raise FilesystemError('file exists', 'rename', (path, dest)) # First, try renaming the file. try: - os.rename(path, dest) + os.replace(path, dest) except OSError: - # Otherwise, copy and delete the original. + tmp = tempfile.mktemp(suffix='.beets', + prefix=py3_path(b'.' + os.path.basename(dest)), + dir=py3_path(os.path.dirname(dest))) + tmp = syspath(tmp) try: - shutil.copyfile(path, dest) + shutil.copyfile(path, tmp) + os.replace(tmp, dest) + tmp = None os.remove(path) - except (OSError, IOError) as exc: + except OSError as exc: raise FilesystemError(exc, 'move', (path, dest), traceback.format_exc()) + finally: + if tmp is not None: + os.remove(tmp) def link(path, dest, replace=False): @@ -511,18 +521,18 @@ return if os.path.exists(syspath(dest)) and not replace: - raise FilesystemError(u'file exists', 'rename', (path, dest)) + raise FilesystemError('file exists', 'rename', (path, dest)) try: os.symlink(syspath(path), syspath(dest)) except NotImplementedError: # raised on python >= 3.2 and Windows versions before Vista - raise FilesystemError(u'OS does not support symbolic links.' + raise FilesystemError('OS does not support symbolic links.' 'link', (path, dest), traceback.format_exc()) except OSError as exc: # TODO: Windows version checks can be removed for python 3 if hasattr('sys', 'getwindowsversion'): if sys.getwindowsversion()[0] < 6: # is before Vista - exc = u'OS does not support symbolic links.' + exc = 'OS does not support symbolic links.' raise FilesystemError(exc, 'link', (path, dest), traceback.format_exc()) @@ -536,15 +546,15 @@ return if os.path.exists(syspath(dest)) and not replace: - raise FilesystemError(u'file exists', 'rename', (path, dest)) + raise FilesystemError('file exists', 'rename', (path, dest)) try: os.link(syspath(path), syspath(dest)) except NotImplementedError: - raise FilesystemError(u'OS does not support hard links.' + raise FilesystemError('OS does not support hard links.' 'link', (path, dest), traceback.format_exc()) except OSError as exc: if exc.errno == errno.EXDEV: - raise FilesystemError(u'Cannot hard link across devices.' + raise FilesystemError('Cannot hard link across devices.' 'link', (path, dest), traceback.format_exc()) else: raise FilesystemError(exc, 'link', (path, dest), @@ -568,7 +578,7 @@ return if os.path.exists(syspath(dest)) and not replace: - raise FilesystemError(u'file exists', 'rename', (path, dest)) + raise FilesystemError('file exists', 'rename', (path, dest)) try: pyreflink.reflink(path, dest) @@ -576,7 +586,7 @@ if fallback: copy(path, dest, replace) else: - raise FilesystemError(u'OS/filesystem does not support reflinks.', + raise FilesystemError('OS/filesystem does not support reflinks.', 'link', (path, dest), traceback.format_exc()) @@ -597,22 +607,23 @@ num = 0 while True: num += 1 - suffix = u'.{}'.format(num).encode() + ext + suffix = f'.{num}'.encode() + ext new_path = base + suffix if not os.path.exists(new_path): return new_path + # Note: The Windows "reserved characters" are, of course, allowed on # Unix. They are forbidden here because they cause problems on Samba # shares, which are sufficiently common as to cause frequent problems. # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx CHAR_REPLACE = [ - (re.compile(r'[\\/]'), u'_'), # / and \ -- forbidden everywhere. - (re.compile(r'^\.'), u'_'), # Leading dot (hidden files on Unix). - (re.compile(r'[\x00-\x1f]'), u''), # Control characters. - (re.compile(r'[<>:"\?\*\|]'), u'_'), # Windows "reserved characters". - (re.compile(r'\.$'), u'_'), # Trailing dots. - (re.compile(r'\s+$'), u''), # Trailing whitespace. + (re.compile(r'[\\/]'), '_'), # / and \ -- forbidden everywhere. + (re.compile(r'^\.'), '_'), # Leading dot (hidden files on Unix). + (re.compile(r'[\x00-\x1f]'), ''), # Control characters. + (re.compile(r'[<>:"\?\*\|]'), '_'), # Windows "reserved characters". + (re.compile(r'\.$'), '_'), # Trailing dots. + (re.compile(r'\s+$'), ''), # Trailing whitespace. ] @@ -736,36 +747,29 @@ it is. So this function helps us "smuggle" the true bytes data through APIs that took Python 3's Unicode mandate too seriously. """ - if isinstance(path, six.text_type): + if isinstance(path, str): return path assert isinstance(path, bytes) - if six.PY2: - return path return os.fsdecode(path) def str2bool(value): """Returns a boolean reflecting a human-entered string.""" - return value.lower() in (u'yes', u'1', u'true', u't', u'y') + return value.lower() in ('yes', '1', 'true', 't', 'y') def as_string(value): """Convert a value to a Unicode object for matching with a query. None becomes the empty string. Bytestrings are silently decoded. """ - if six.PY2: - buffer_types = buffer, memoryview # noqa: F821 - else: - buffer_types = memoryview - if value is None: - return u'' - elif isinstance(value, buffer_types): + return '' + elif isinstance(value, memoryview): return bytes(value).decode('utf-8', 'ignore') elif isinstance(value, bytes): return value.decode('utf-8', 'ignore') else: - return six.text_type(value) + return str(value) def text_string(value, encoding='utf-8'): @@ -788,7 +792,7 @@ """ c = Counter(objs) if not c: - raise ValueError(u'sequence must be non-empty') + raise ValueError('sequence must be non-empty') return c.most_common(1)[0] @@ -809,7 +813,7 @@ '/usr/sbin/sysctl', '-n', 'hw.ncpu', - ]).stdout) + ]).stdout) except (ValueError, OSError, subprocess.CalledProcessError): num = 0 else: @@ -829,12 +833,8 @@ assert isinstance(args, list) def convert(arg): - if six.PY2: - if isinstance(arg, six.text_type): - arg = arg.encode(arg_encoding()) - else: - if isinstance(arg, bytes): - arg = arg.decode(arg_encoding(), 'surrogateescape') + if isinstance(arg, bytes): + arg = arg.decode(arg_encoding(), 'surrogateescape') return arg return [convert(a) for a in args] @@ -931,25 +931,6 @@ return open_anything() -def shlex_split(s): - """Split a Unicode or bytes string according to shell lexing rules. - - Raise `ValueError` if the string is not a well-formed shell string. - This is a workaround for a bug in some versions of Python. - """ - if not six.PY2 or isinstance(s, bytes): # Shlex works fine. - return shlex.split(s) - - elif isinstance(s, six.text_type): - # Work around a Python bug. - # http://bugs.python.org/issue6988 - bs = s.encode('utf-8') - return [c.decode('utf-8') for c in shlex.split(bs)] - - else: - raise TypeError(u'shlex_split called with non-string') - - def interactive_open(targets, command): """Open the files in `targets` by `exec`ing a new `command`, given as a Unicode string. (The new program takes over, and Python @@ -961,7 +942,7 @@ # Split the command string into its arguments. try: - args = shlex_split(command) + args = shlex.split(command) except ValueError: # Malformed shell tokens. args = [command] @@ -976,7 +957,7 @@ """Use Windows' `GetLongPathNameW` via ctypes to get the canonical, long path given a short filename. """ - if not isinstance(short_path, six.text_type): + if not isinstance(short_path, str): short_path = short_path.decode(_fsencoding()) import ctypes @@ -1037,7 +1018,7 @@ """ match = re.match(r'^(\d+):([0-5]\d)$', string) if not match: - raise ValueError(u'String not in M:SS format') + raise ValueError('String not in M:SS format') minutes, seconds = map(int, match.groups()) return float(minutes * 60 + seconds) @@ -1075,16 +1056,10 @@ The parallelism uses threads (not processes), so this is only useful for IO-bound `transform`s. """ - if sys.version_info[0] < 3: - # multiprocessing.pool.ThreadPool does not seem to work on - # Python 2. We could consider switching to futures instead. - for item in items: - transform(item) - else: - pool = ThreadPool() - pool.map(transform, items) - pool.close() - pool.join() + pool = ThreadPool() + pool.map(transform, items) + pool.close() + pool.join() def lazy_property(func): @@ -1120,13 +1095,9 @@ *reversed* to recover the same bytes before invoking the OS. On Windows, we want to preserve the Unicode filename "as is." """ - if six.PY2: - # On Python 2, substitute the bytestring directly into the template. - return path + # On Python 3, the template is a Unicode string, which only supports + # substitution of Unicode variables. + if platform.system() == 'Windows': + return path.decode(_fsencoding()) else: - # On Python 3, the template is a Unicode string, which only supports - # substitution of Unicode variables. - if platform.system() == 'Windows': - return path.decode(_fsencoding()) - else: - return path.decode(arg_encoding(), 'surrogateescape') + return path.decode(arg_encoding(), 'surrogateescape') diff -Nru beets-1.5.0/beets/util/pipeline.py beets-1.6.0/beets/util/pipeline.py --- beets-1.5.0/beets/util/pipeline.py 2021-03-06 21:56:33.000000000 +0000 +++ beets-1.6.0/beets/util/pipeline.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -32,12 +31,10 @@ in place of any single coroutine. """ -from __future__ import division, absolute_import, print_function -from six.moves import queue +import queue from threading import Thread, Lock import sys -import six BUBBLE = '__PIPELINE_BUBBLE__' POISON = '__PIPELINE_POISON__' @@ -91,6 +88,7 @@ still feeding into it. The queue is poisoned when all threads are finished with the queue. """ + def __init__(self, maxsize=0): queue.Queue.__init__(self, maxsize) self.nthreads = 0 @@ -135,10 +133,11 @@ _invalidate_queue(self, POISON, False) -class MultiMessage(object): +class MultiMessage: """A message yielded by a pipeline stage encapsulating multiple values to be sent to the next stage. """ + def __init__(self, messages): self.messages = messages @@ -210,8 +209,9 @@ class PipelineThread(Thread): """Abstract base class for pipeline-stage threads.""" + def __init__(self, all_threads): - super(PipelineThread, self).__init__() + super().__init__() self.abort_lock = Lock() self.abort_flag = False self.all_threads = all_threads @@ -241,8 +241,9 @@ """The thread running the first stage in a parallel pipeline setup. The coroutine should just be a generator. """ + def __init__(self, coro, out_queue, all_threads): - super(FirstPipelineThread, self).__init__(all_threads) + super().__init__(all_threads) self.coro = coro self.out_queue = out_queue self.out_queue.acquire() @@ -279,8 +280,9 @@ """A thread running any stage in the pipeline except the first or last. """ + def __init__(self, coro, in_queue, out_queue, all_threads): - super(MiddlePipelineThread, self).__init__(all_threads) + super().__init__(all_threads) self.coro = coro self.in_queue = in_queue self.out_queue = out_queue @@ -327,8 +329,9 @@ """A thread running the last stage in a pipeline. The coroutine should yield nothing. """ + def __init__(self, coro, in_queue, all_threads): - super(LastPipelineThread, self).__init__(all_threads) + super().__init__(all_threads) self.coro = coro self.in_queue = in_queue @@ -359,17 +362,18 @@ return -class Pipeline(object): +class Pipeline: """Represents a staged pattern of work. Each stage in the pipeline is a coroutine that receives messages from the previous stage and yields messages to be sent to the next stage. """ + def __init__(self, stages): """Makes a new pipeline from a list of coroutines. There must be at least two stages. """ if len(stages) < 2: - raise ValueError(u'pipeline must have at least two stages') + raise ValueError('pipeline must have at least two stages') self.stages = [] for stage in stages: if isinstance(stage, (list, tuple)): @@ -439,7 +443,7 @@ exc_info = thread.exc_info if exc_info: # Make the exception appear as it was raised originally. - six.reraise(exc_info[0], exc_info[1], exc_info[2]) + raise exc_info[1].with_traceback(exc_info[2]) def pull(self): """Yield elements from the end of the pipeline. Runs the stages @@ -466,6 +470,7 @@ for msg in msgs: yield msg + # Smoke test. if __name__ == '__main__': import time @@ -474,14 +479,14 @@ # in parallel. def produce(): for i in range(5): - print(u'generating %i' % i) + print('generating %i' % i) time.sleep(1) yield i def work(): num = yield while True: - print(u'processing %i' % num) + print('processing %i' % num) time.sleep(2) num = yield num * 2 @@ -489,7 +494,7 @@ while True: num = yield time.sleep(1) - print(u'received %i' % num) + print('received %i' % num) ts_start = time.time() Pipeline([produce(), work(), consume()]).run_sequential() @@ -498,22 +503,22 @@ ts_par = time.time() Pipeline([produce(), (work(), work()), consume()]).run_parallel() ts_end = time.time() - print(u'Sequential time:', ts_seq - ts_start) - print(u'Parallel time:', ts_par - ts_seq) - print(u'Multiply-parallel time:', ts_end - ts_par) + print('Sequential time:', ts_seq - ts_start) + print('Parallel time:', ts_par - ts_seq) + print('Multiply-parallel time:', ts_end - ts_par) print() # Test a pipeline that raises an exception. def exc_produce(): for i in range(10): - print(u'generating %i' % i) + print('generating %i' % i) time.sleep(1) yield i def exc_work(): num = yield while True: - print(u'processing %i' % num) + print('processing %i' % num) time.sleep(3) if num == 3: raise Exception() @@ -522,6 +527,6 @@ def exc_consume(): while True: num = yield - print(u'received %i' % num) + print('received %i' % num) Pipeline([exc_produce(), exc_work(), exc_consume()]).run_parallel(1) diff -Nru beets-1.5.0/beets/vfs.py beets-1.6.0/beets/vfs.py --- beets-1.5.0/beets/vfs.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beets/vfs.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """A simple utility for constructing filesystem-like trees from beets libraries. """ -from __future__ import division, absolute_import, print_function from collections import namedtuple from beets import util diff -Nru beets-1.5.0/beets.egg-info/PKG-INFO beets-1.6.0/beets.egg-info/PKG-INFO --- beets-1.5.0/beets.egg-info/PKG-INFO 2021-08-19 19:56:50.000000000 +0000 +++ beets-1.6.0/beets.egg-info/PKG-INFO 2021-11-27 16:37:58.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: beets -Version: 1.5.0 +Version: 1.6.0 Summary: music tagger and library organizer Home-page: https://beets.io/ Author: Adrian Sampson @@ -13,10 +13,7 @@ Classifier: Environment :: Console Classifier: Environment :: Web Environment Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 @@ -29,7 +26,6 @@ Provides-Extra: embedart Provides-Extra: embyupdate Provides-Extra: chroma -Provides-Extra: gmusic Provides-Extra: discogs Provides-Extra: beatport Provides-Extra: kodiupdate diff -Nru beets-1.5.0/beets.egg-info/requires.txt beets-1.6.0/beets.egg-info/requires.txt --- beets-1.5.0/beets.egg-info/requires.txt 2021-08-19 19:56:50.000000000 +0000 +++ beets-1.6.0/beets.egg-info/requires.txt 2021-11-27 16:37:58.000000000 +0000 @@ -1,4 +1,3 @@ -six>=1.9 unidecode musicbrainzngs>=0.4 pyyaml @@ -20,7 +19,7 @@ pyacoustid [discogs] -python3-discogs-client +python3-discogs-client>=2.3.10 [embedart] Pillow @@ -32,11 +31,9 @@ requests Pillow -[gmusic] -gmusicapi - [import] rarfile +py7zr [kodiupdate] requests @@ -49,9 +46,7 @@ [lint] flake8 -flake8-coding flake8-docstrings -flake8-future-import pep8-naming [lyrics] diff -Nru beets-1.5.0/beets.egg-info/SOURCES.txt beets-1.6.0/beets.egg-info/SOURCES.txt --- beets-1.5.0/beets.egg-info/SOURCES.txt 2021-08-19 19:56:50.000000000 +0000 +++ beets-1.6.0/beets.egg-info/SOURCES.txt 2021-11-27 16:37:58.000000000 +0000 @@ -45,6 +45,7 @@ beetsplug/__init__.py beetsplug/absubmit.py beetsplug/acousticbrainz.py +beetsplug/albumtypes.py beetsplug/aura.py beetsplug/badfiles.py beetsplug/bareasc.py @@ -139,6 +140,7 @@ docs/guides/tagger.rst docs/plugins/absubmit.rst docs/plugins/acousticbrainz.rst +docs/plugins/albumtypes.rst docs/plugins/aura.rst docs/plugins/badfiles.rst docs/plugins/bareasc.rst @@ -222,6 +224,7 @@ test/helper.py test/lyrics_download_samples.py test/test_acousticbrainz.py +test/test_albumtypes.py test/test_art.py test/test_art_resize.py test/test_autotag.py @@ -371,5 +374,7 @@ test/rsrc/lyrics/examplecom/beetssong.txt test/rsrc/lyrics/geniuscom/Wutangclancreamlyrics.txt test/rsrc/lyrics/geniuscom/sample.txt +test/rsrc/spotify/album_info.json test/rsrc/spotify/missing_request.json +test/rsrc/spotify/track_info.json test/rsrc/spotify/track_request.json \ No newline at end of file diff -Nru beets-1.5.0/beetsplug/absubmit.py beets-1.6.0/beetsplug/absubmit.py --- beets-1.5.0/beetsplug/absubmit.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/absubmit.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Pieter Mulder. # @@ -16,7 +15,6 @@ """Calculate acoustic information and submit to AcousticBrainz. """ -from __future__ import division, absolute_import, print_function import errno import hashlib @@ -49,17 +47,17 @@ return util.command_output(args).stdout except subprocess.CalledProcessError as e: raise ABSubmitError( - u'{0} exited with status {1}'.format(args[0], e.returncode) + '{} exited with status {}'.format(args[0], e.returncode) ) class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): def __init__(self): - super(AcousticBrainzSubmitPlugin, self).__init__() + super().__init__() self.config.add({ - 'extractor': u'', + 'extractor': '', 'force': False, 'pretend': False }) @@ -70,7 +68,7 @@ # Expicit path to extractor if not os.path.isfile(self.extractor): raise ui.UserError( - u'Extractor command does not exist: {0}.'. + 'Extractor command does not exist: {0}.'. format(self.extractor) ) else: @@ -80,8 +78,8 @@ call([self.extractor]) except OSError: raise ui.UserError( - u'No extractor command found: please install the extractor' - u' binary from https://acousticbrainz.org/download' + 'No extractor command found: please install the extractor' + ' binary from https://acousticbrainz.org/download' ) except ABSubmitError: # Extractor found, will exit with an error if not called with @@ -103,17 +101,17 @@ def commands(self): cmd = ui.Subcommand( 'absubmit', - help=u'calculate and submit AcousticBrainz analysis' + help='calculate and submit AcousticBrainz analysis' ) cmd.parser.add_option( - u'-f', u'--force', dest='force_refetch', + '-f', '--force', dest='force_refetch', action='store_true', default=False, - help=u're-download data when already present' + help='re-download data when already present' ) cmd.parser.add_option( - u'-p', u'--pretend', dest='pretend_fetch', + '-p', '--pretend', dest='pretend_fetch', action='store_true', default=False, - help=u'pretend to perform action, but show \ + help='pretend to perform action, but show \ only files which would be processed' ) cmd.func = self.command @@ -140,12 +138,12 @@ # If file has no MBID, skip it. if not mbid: - self._log.info(u'Not analysing {}, missing ' - u'musicbrainz track id.', item) + self._log.info('Not analysing {}, missing ' + 'musicbrainz track id.', item) return None if self.opts.pretend_fetch or self.config['pretend']: - self._log.info(u'pretend action - extract item: {}', item) + self._log.info('pretend action - extract item: {}', item) return None # Temporary file to save extractor output to, extractor only works @@ -160,11 +158,11 @@ call([self.extractor, util.syspath(item.path), filename]) except ABSubmitError as e: self._log.warning( - u'Failed to analyse {item} for AcousticBrainz: {error}', + 'Failed to analyse {item} for AcousticBrainz: {error}', item=item, error=e ) return None - with open(filename, 'r') as tmp_file: + with open(filename) as tmp_file: analysis = json.load(tmp_file) # Add the hash to the output. analysis['metadata']['version']['essentia_build_sha'] = \ @@ -188,11 +186,11 @@ try: message = response.json()['message'] except (ValueError, KeyError) as e: - message = u'unable to get error message: {}'.format(e) + message = f'unable to get error message: {e}' self._log.error( - u'Failed to submit AcousticBrainz analysis of {item}: ' - u'{message}).', item=item, message=message + 'Failed to submit AcousticBrainz analysis of {item}: ' + '{message}).', item=item, message=message ) else: - self._log.debug(u'Successfully submitted AcousticBrainz analysis ' - u'for {}.', item) + self._log.debug('Successfully submitted AcousticBrainz analysis ' + 'for {}.', item) diff -Nru beets-1.5.0/beetsplug/acousticbrainz.py beets-1.6.0/beetsplug/acousticbrainz.py --- beets-1.5.0/beetsplug/acousticbrainz.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/acousticbrainz.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2015-2016, Ohm Patel. # @@ -15,7 +14,6 @@ """Fetch various AcousticBrainz metadata using MBID. """ -from __future__ import division, absolute_import, print_function from collections import defaultdict @@ -138,7 +136,7 @@ } def __init__(self): - super(AcousticPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, @@ -152,11 +150,11 @@ def commands(self): cmd = ui.Subcommand('acousticbrainz', - help=u"fetch metadata from AcousticBrainz") + help="fetch metadata from AcousticBrainz") cmd.parser.add_option( - u'-f', u'--force', dest='force_refetch', + '-f', '--force', dest='force_refetch', action='store_true', default=False, - help=u're-download data when already present' + help='re-download data when already present' ) def func(lib, opts, args): @@ -175,22 +173,22 @@ def _get_data(self, mbid): data = {} for url in _generate_urls(mbid): - self._log.debug(u'fetching URL: {}', url) + self._log.debug('fetching URL: {}', url) try: res = requests.get(url) except requests.RequestException as exc: - self._log.info(u'request error: {}', exc) + self._log.info('request error: {}', exc) return {} if res.status_code == 404: - self._log.info(u'recording ID {} not found', mbid) + self._log.info('recording ID {} not found', mbid) return {} try: data.update(res.json()) except ValueError: - self._log.debug(u'Invalid Response: {}', res.text) + self._log.debug('Invalid Response: {}', res.text) return {} return data @@ -205,28 +203,28 @@ # representative field name to check for previously fetched # data. if not force: - mood_str = item.get('mood_acoustic', u'') + mood_str = item.get('mood_acoustic', '') if mood_str: - self._log.info(u'data already present for: {}', item) + self._log.info('data already present for: {}', item) continue # We can only fetch data for tracks with MBIDs. if not item.mb_trackid: continue - self._log.info(u'getting data for: {}', item) + self._log.info('getting data for: {}', item) data = self._get_data(item.mb_trackid) if data: for attr, val in self._map_data_to_scheme(data, ABSCHEME): if not tags or attr in tags: - self._log.debug(u'attribute {} of {} set to {}', + self._log.debug('attribute {} of {} set to {}', attr, item, val) setattr(item, attr, val) else: - self._log.debug(u'skipping attribute {} of {}' - u' (value {}) due to config', + self._log.debug('skipping attribute {} of {}' + ' (value {}) due to config', attr, item, val) @@ -288,10 +286,9 @@ # The recursive traversal. composites = defaultdict(list) - for attr, val in self._data_to_scheme_child(data, - scheme, - composites): - yield attr, val + yield from self._data_to_scheme_child(data, + scheme, + composites) # When composites has been populated, yield the composite attributes # by joining their parts. @@ -311,10 +308,9 @@ for k, v in subscheme.items(): if k in subdata: if type(v) == dict: - for attr, val in self._data_to_scheme_child(subdata[k], - v, - composites): - yield attr, val + yield from self._data_to_scheme_child(subdata[k], + v, + composites) elif type(v) == tuple: composite_attribute, part_number = v attribute_parts = composites[composite_attribute] @@ -325,10 +321,10 @@ else: yield v, subdata[k] else: - self._log.warning(u'Acousticbrainz did not provide info' - u'about {}', k) - self._log.debug(u'Data {} could not be mapped to scheme {} ' - u'because key {} was not found', subdata, v, k) + self._log.warning('Acousticbrainz did not provide info' + 'about {}', k) + self._log.debug('Data {} could not be mapped to scheme {} ' + 'because key {} was not found', subdata, v, k) def _generate_urls(mbid): diff -Nru beets-1.5.0/beetsplug/albumtypes.py beets-1.6.0/beetsplug/albumtypes.py --- beets-1.5.0/beetsplug/albumtypes.py 1970-01-01 00:00:00.000000000 +0000 +++ beets-1.6.0/beetsplug/albumtypes.py 2021-09-28 19:51:02.000000000 +0000 @@ -0,0 +1,65 @@ +# This file is part of beets. +# Copyright 2021, Edgars Supe. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Adds an album template field for formatted album types.""" + + +from beets.autotag.mb import VARIOUS_ARTISTS_ID +from beets.library import Album +from beets.plugins import BeetsPlugin + + +class AlbumTypesPlugin(BeetsPlugin): + """Adds an album template field for formatted album types.""" + + def __init__(self): + """Init AlbumTypesPlugin.""" + super().__init__() + self.album_template_fields['atypes'] = self._atypes + self.config.add({ + 'types': [ + ('ep', 'EP'), + ('single', 'Single'), + ('soundtrack', 'OST'), + ('live', 'Live'), + ('compilation', 'Anthology'), + ('remix', 'Remix') + ], + 'ignore_va': ['compilation'], + 'bracket': '[]' + }) + + def _atypes(self, item: Album): + """Returns a formatted string based on album's types.""" + types = self.config['types'].as_pairs() + ignore_va = self.config['ignore_va'].as_str_seq() + bracket = self.config['bracket'].as_str() + + # Assign a left and right bracket or leave blank if argument is empty. + if len(bracket) == 2: + bracket_l = bracket[0] + bracket_r = bracket[1] + else: + bracket_l = '' + bracket_r = '' + + res = '' + albumtypes = item.albumtypes.split('; ') + is_va = item.mb_albumartistid == VARIOUS_ARTISTS_ID + for type in types: + if type[0] in albumtypes and type[1]: + if not is_va or (type[0] not in ignore_va and is_va): + res += f'{bracket_l}{type[1]}{bracket_r}' + + return res diff -Nru beets-1.5.0/beetsplug/aura.py beets-1.6.0/beetsplug/aura.py --- beets-1.5.0/beetsplug/aura.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/aura.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # This file is part of beets. # Copyright 2020, Callum Brown. # @@ -16,10 +14,10 @@ """An AURA server using Flask.""" -from __future__ import division, absolute_import, print_function from mimetypes import guess_type import re +import os.path from os.path import isfile, getsize from beets.plugins import BeetsPlugin @@ -215,7 +213,7 @@ else: # Increment page token by 1 next_url = request.url.replace( - "page={}".format(page), "page={}".format(page + 1) + f"page={page}", "page={}".format(page + 1) ) # Get only the items in the page range data = [self.resource_object(collection[i]) for i in range(start, end)] @@ -265,7 +263,7 @@ image_id = identifier["id"] included.append(ImageDocument.resource_object(image_id)) else: - raise ValueError("Invalid resource type: {}".format(res_type)) + raise ValueError(f"Invalid resource type: {res_type}") return included def all_resources(self): @@ -462,7 +460,7 @@ if album.artpath: path = py3_path(album.artpath) filename = path.split("/")[-1] - image_id = "album-{}-{}".format(album.id, filename) + image_id = f"album-{album.id}-{filename}" relationships["images"] = { "data": [{"type": "image", "id": image_id}] } @@ -598,6 +596,24 @@ return self.single_resource_document(artist_resource) +def safe_filename(fn): + """Check whether a string is a simple (non-path) filename. + + For example, `foo.txt` is safe because it is a "plain" filename. But + `foo/bar.txt` and `../foo.txt` and `.` are all non-safe because they + can traverse to other directories other than the current one. + """ + # Rule out any directories. + if os.path.basename(fn) != fn: + return False + + # In single names, rule out Unix directory traversal names. + if fn in ('.', '..'): + return False + + return True + + class ImageDocument(AURADocument): """Class for building documents for /images/(id) endpoints.""" @@ -619,6 +635,8 @@ parent_type = id_split[0] parent_id = id_split[1] img_filename = "-".join(id_split[2:]) + if not safe_filename(img_filename): + return None # Get the path to the directory parent's images are in if parent_type == "album": @@ -634,7 +652,7 @@ # Images for other resource types are not supported return None - img_path = dir_path + "/" + img_filename + img_path = os.path.join(dir_path, img_filename) # Check the image actually exists if isfile(img_path): return img_path @@ -886,7 +904,7 @@ """An application factory for use by a WSGI server.""" config["aura"].add( { - "host": u"127.0.0.1", + "host": "127.0.0.1", "port": 8337, "cors": [], "cors_supports_credentials": False, @@ -932,7 +950,7 @@ def __init__(self): """Add configuration options for the AURA plugin.""" - super(AURAPlugin, self).__init__() + super().__init__() def commands(self): """Add subcommand used to run the AURA server.""" @@ -954,13 +972,13 @@ threaded=True, ) - run_aura_cmd = Subcommand("aura", help=u"run an AURA server") + run_aura_cmd = Subcommand("aura", help="run an AURA server") run_aura_cmd.parser.add_option( - u"-d", - u"--debug", + "-d", + "--debug", action="store_true", default=False, - help=u"use Flask debug mode", + help="use Flask debug mode", ) run_aura_cmd.func = run_aura return [run_aura_cmd] diff -Nru beets-1.5.0/beetsplug/badfiles.py beets-1.6.0/beetsplug/badfiles.py --- beets-1.5.0/beetsplug/badfiles.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beetsplug/badfiles.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, François-Xavier Thomas. # @@ -16,7 +15,6 @@ """Use command-line tools to check for audio file corruption. """ -from __future__ import division, absolute_import, print_function from subprocess import check_output, CalledProcessError, list2cmdline, STDOUT @@ -24,7 +22,6 @@ import os import errno import sys -import six import confuse from beets.plugins import BeetsPlugin from beets.ui import Subcommand @@ -52,7 +49,7 @@ class BadFiles(BeetsPlugin): def __init__(self): - super(BadFiles, self).__init__() + super().__init__() self.verbose = False self.register_listener('import_task_start', @@ -61,7 +58,7 @@ self.on_import_task_before_choice) def run_command(self, cmd): - self._log.debug(u"running command: {}", + self._log.debug("running command: {}", displayable_path(list2cmdline(cmd))) try: output = check_output(cmd, stderr=STDOUT) @@ -110,52 +107,52 @@ # First, check whether the path exists. If not, the user # should probably run `beet update` to cleanup your library. dpath = displayable_path(item.path) - self._log.debug(u"checking path: {}", dpath) + self._log.debug("checking path: {}", dpath) if not os.path.exists(item.path): - ui.print_(u"{}: file does not exist".format( + ui.print_("{}: file does not exist".format( ui.colorize('text_error', dpath))) # Run the checker against the file if one is found ext = os.path.splitext(item.path)[1][1:].decode('utf8', 'ignore') checker = self.get_checker(ext) if not checker: - self._log.error(u"no checker specified in the config for {}", + self._log.error("no checker specified in the config for {}", ext) return [] path = item.path - if not isinstance(path, six.text_type): + if not isinstance(path, str): path = item.path.decode(sys.getfilesystemencoding()) try: status, errors, output = checker(path) except CheckerCommandException as e: if e.errno == errno.ENOENT: self._log.error( - u"command not found: {} when validating file: {}", + "command not found: {} when validating file: {}", e.checker, e.path ) else: - self._log.error(u"error invoking {}: {}", e.checker, e.msg) + self._log.error("error invoking {}: {}", e.checker, e.msg) return [] error_lines = [] if status > 0: error_lines.append( - u"{}: checker exited with status {}" + "{}: checker exited with status {}" .format(ui.colorize('text_error', dpath), status)) for line in output: - error_lines.append(u" {}".format(line)) + error_lines.append(f" {line}") elif errors > 0: error_lines.append( - u"{}: checker found {} errors or warnings" + "{}: checker found {} errors or warnings" .format(ui.colorize('text_warning', dpath), errors)) for line in output: - error_lines.append(u" {}".format(line)) + error_lines.append(f" {line}") elif self.verbose: error_lines.append( - u"{}: ok".format(ui.colorize('text_success', dpath))) + "{}: ok".format(ui.colorize('text_success', dpath))) return error_lines @@ -193,7 +190,7 @@ elif sel == 'b': raise importer.ImportAbort() else: - raise Exception('Unexpected selection: {}'.format(sel)) + raise Exception(f'Unexpected selection: {sel}') def command(self, lib, opts, args): # Get items from arguments @@ -208,11 +205,11 @@ def commands(self): bad_command = Subcommand('bad', - help=u'check for corrupt or missing files') + help='check for corrupt or missing files') bad_command.parser.add_option( - u'-v', u'--verbose', + '-v', '--verbose', action='store_true', default=False, dest='verbose', - help=u'view results for both the bad and uncorrupted files' + help='view results for both the bad and uncorrupted files' ) bad_command.func = self.command return [bad_command] diff -Nru beets-1.5.0/beetsplug/bareasc.py beets-1.6.0/beetsplug/bareasc.py --- beets-1.5.0/beetsplug/bareasc.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/bareasc.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Philippe Mongeau. # Copyright 2021, Graham R. Cobb. @@ -19,14 +18,12 @@ """Provides a bare-ASCII matching query.""" -from __future__ import division, absolute_import, print_function from beets import ui from beets.ui import print_, decargs from beets.plugins import BeetsPlugin from beets.dbcore.query import StringFieldQuery from unidecode import unidecode -import six class BareascQuery(StringFieldQuery): @@ -50,7 +47,7 @@ """Plugin to provide bare-ASCII option for beets matching.""" def __init__(self): """Default prefix for selecting bare-ASCII matching is #.""" - super(BareascPlugin, self).__init__() + super().__init__() self.config.add({ 'prefix': '#', }) @@ -64,8 +61,8 @@ """Add bareasc command as unidecode version of 'list'.""" cmd = ui.Subcommand('bareasc', help='unidecode version of beet list command') - cmd.parser.usage += u"\n" \ - u'Example: %prog -f \'$album: $title\' artist:beatles' + cmd.parser.usage += "\n" \ + 'Example: %prog -f \'$album: $title\' artist:beatles' cmd.parser.add_all_common_options() cmd.func = self.unidecode_list return [cmd] @@ -77,9 +74,9 @@ # Copied from commands.py - list_items if album: for album in lib.albums(query): - bare = unidecode(six.ensure_text(str(album))) - print_(six.ensure_text(bare)) + bare = unidecode(str(album)) + print_(bare) else: for item in lib.items(query): - bare = unidecode(six.ensure_text(str(item))) - print_(six.ensure_text(bare)) + bare = unidecode(str(item)) + print_(bare) diff -Nru beets-1.5.0/beetsplug/beatport.py beets-1.6.0/beetsplug/beatport.py --- beets-1.5.0/beetsplug/beatport.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beetsplug/beatport.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,11 +14,9 @@ """Adds Beatport release and track search support to the autotagger """ -from __future__ import division, absolute_import, print_function import json import re -import six from datetime import datetime, timedelta from requests_oauthlib import OAuth1Session @@ -34,29 +31,29 @@ AUTH_ERRORS = (TokenRequestDenied, TokenMissing, VerifierMissing) -USER_AGENT = u'beets/{0} +https://beets.io/'.format(beets.__version__) +USER_AGENT = f'beets/{beets.__version__} +https://beets.io/' class BeatportAPIError(Exception): pass -class BeatportObject(object): +class BeatportObject: def __init__(self, data): self.beatport_id = data['id'] - self.name = six.text_type(data['name']) + self.name = str(data['name']) if 'releaseDate' in data: self.release_date = datetime.strptime(data['releaseDate'], '%Y-%m-%d') if 'artists' in data: - self.artists = [(x['id'], six.text_type(x['name'])) + self.artists = [(x['id'], str(x['name'])) for x in data['artists']] if 'genres' in data: - self.genres = [six.text_type(x['name']) + self.genres = [str(x['name']) for x in data['genres']] -class BeatportClient(object): +class BeatportClient: _api_base = 'https://oauth-api.beatport.com' def __init__(self, c_key, c_secret, auth_key=None, auth_secret=None): @@ -131,7 +128,7 @@ """ response = self._get('catalog/3/search', query=query, perPage=5, - facets=['fieldType:{0}'.format(release_type)]) + facets=[f'fieldType:{release_type}']) for item in response: if release_type == 'release': if details: @@ -201,21 +198,20 @@ return response.json()['results'] -@six.python_2_unicode_compatible class BeatportRelease(BeatportObject): def __str__(self): if len(self.artists) < 4: artist_str = ", ".join(x[1] for x in self.artists) else: artist_str = "Various Artists" - return u"".format( + return "".format( artist_str, self.name, self.catalog_number, ) def __repr__(self): - return six.text_type(self).encode('utf-8') + return str(self).encode('utf-8') def __init__(self, data): BeatportObject.__init__(self, data) @@ -226,27 +222,26 @@ if 'category' in data: self.category = data['category'] if 'slug' in data: - self.url = "https://beatport.com/release/{0}/{1}".format( + self.url = "https://beatport.com/release/{}/{}".format( data['slug'], data['id']) self.genre = data.get('genre') -@six.python_2_unicode_compatible class BeatportTrack(BeatportObject): def __str__(self): artist_str = ", ".join(x[1] for x in self.artists) - return (u"" + return ("" .format(artist_str, self.name, self.mix_name)) def __repr__(self): - return six.text_type(self).encode('utf-8') + return str(self).encode('utf-8') def __init__(self, data): BeatportObject.__init__(self, data) if 'title' in data: - self.title = six.text_type(data['title']) + self.title = str(data['title']) if 'mixName' in data: - self.mix_name = six.text_type(data['mixName']) + self.mix_name = str(data['mixName']) self.length = timedelta(milliseconds=data.get('lengthMs', 0) or 0) if not self.length: try: @@ -255,26 +250,26 @@ except ValueError: pass if 'slug' in data: - self.url = "https://beatport.com/track/{0}/{1}" \ + self.url = "https://beatport.com/track/{}/{}" \ .format(data['slug'], data['id']) self.track_number = data.get('trackNumber') self.bpm = data.get('bpm') - self.initial_key = six.text_type( + self.initial_key = str( (data.get('key') or {}).get('shortName') ) # Use 'subgenre' and if not present, 'genre' as a fallback. if data.get('subGenres'): - self.genre = six.text_type(data['subGenres'][0].get('name')) + self.genre = str(data['subGenres'][0].get('name')) elif data.get('genres'): - self.genre = six.text_type(data['genres'][0].get('name')) + self.genre = str(data['genres'][0].get('name')) class BeatportPlugin(BeetsPlugin): data_source = 'Beatport' def __init__(self): - super(BeatportPlugin, self).__init__() + super().__init__() self.config.add({ 'apikey': '57713c3906af6f5def151b33601389176b37b429', 'apisecret': 'b3fe08c93c80aefd749fe871a16cd2bb32e2b954', @@ -294,7 +289,7 @@ try: with open(self._tokenfile()) as f: tokendata = json.load(f) - except IOError: + except OSError: # No token yet. Generate one. token, secret = self.authenticate(c_key, c_secret) else: @@ -309,22 +304,22 @@ try: url = auth_client.get_authorize_url() except AUTH_ERRORS as e: - self._log.debug(u'authentication error: {0}', e) - raise beets.ui.UserError(u'communication with Beatport failed') + self._log.debug('authentication error: {0}', e) + raise beets.ui.UserError('communication with Beatport failed') - beets.ui.print_(u"To authenticate with Beatport, visit:") + beets.ui.print_("To authenticate with Beatport, visit:") beets.ui.print_(url) # Ask for the verifier data and validate it. - data = beets.ui.input_(u"Enter the string displayed in your browser:") + data = beets.ui.input_("Enter the string displayed in your browser:") try: token, secret = auth_client.get_access_token(data) except AUTH_ERRORS as e: - self._log.debug(u'authentication error: {0}', e) - raise beets.ui.UserError(u'Beatport token request failed') + self._log.debug('authentication error: {0}', e) + raise beets.ui.UserError('Beatport token request failed') # Save the token for later use. - self._log.debug(u'Beatport token {0}, secret {1}', token, secret) + self._log.debug('Beatport token {0}, secret {1}', token, secret) with open(self._tokenfile(), 'w') as f: json.dump({'token': token, 'secret': secret}, f) @@ -362,32 +357,32 @@ if va_likely: query = release else: - query = '%s %s' % (artist, release) + query = f'{artist} {release}' try: return self._get_releases(query) except BeatportAPIError as e: - self._log.debug(u'API Error: {0} (query: {1})', e, query) + self._log.debug('API Error: {0} (query: {1})', e, query) return [] def item_candidates(self, item, artist, title): """Returns a list of TrackInfo objects for beatport search results matching title and artist. """ - query = '%s %s' % (artist, title) + query = f'{artist} {title}' try: return self._get_tracks(query) except BeatportAPIError as e: - self._log.debug(u'API Error: {0} (query: {1})', e, query) + self._log.debug('API Error: {0} (query: {1})', e, query) return [] def album_for_id(self, release_id): """Fetches a release by its Beatport ID and returns an AlbumInfo object or None if the query is not a valid ID or release is not found. """ - self._log.debug(u'Searching for release {0}', release_id) + self._log.debug('Searching for release {0}', release_id) match = re.search(r'(^|beatport\.com/release/.+/)(\d+)$', release_id) if not match: - self._log.debug(u'Not a valid Beatport release ID.') + self._log.debug('Not a valid Beatport release ID.') return None release = self.client.get_release(match.group(2)) if release: @@ -398,10 +393,10 @@ """Fetches a track by its Beatport ID and returns a TrackInfo object or None if the track is not a valid Beatport ID or track is not found. """ - self._log.debug(u'Searching for track {0}', track_id) + self._log.debug('Searching for track {0}', track_id) match = re.search(r'(^|beatport\.com/track/.+/)(\d+)$', track_id) if not match: - self._log.debug(u'Not a valid Beatport track ID.') + self._log.debug('Not a valid Beatport track ID.') return None bp_track = self.client.get_track(match.group(2)) if bp_track is not None: @@ -429,7 +424,7 @@ va = len(release.artists) > 3 artist, artist_id = self._get_artist(release.artists) if va: - artist = u"Various Artists" + artist = "Various Artists" tracks = [self._get_track_info(x) for x in release.tracks] return AlbumInfo(album=release.name, album_id=release.beatport_id, @@ -439,7 +434,7 @@ month=release.release_date.month, day=release.release_date.day, label=release.label_name, - catalognum=release.catalog_number, media=u'Digital', + catalognum=release.catalog_number, media='Digital', data_source=self.data_source, data_url=release.url, genre=release.genre) @@ -447,8 +442,8 @@ """Returns a TrackInfo object for a Beatport Track object. """ title = track.name - if track.mix_name != u"Original Mix": - title += u" ({0})".format(track.mix_name) + if track.mix_name != "Original Mix": + title += f" ({track.mix_name})" artist, artist_id = self._get_artist(track.artists) length = track.length.total_seconds() return TrackInfo(title=title, track_id=track.beatport_id, diff -Nru beets-1.5.0/beetsplug/bench.py beets-1.6.0/beetsplug/bench.py --- beets-1.5.0/beetsplug/bench.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/bench.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Some simple performance benchmarks for beets. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import ui diff -Nru beets-1.5.0/beetsplug/bpd/gstplayer.py beets-1.6.0/beetsplug/bpd/gstplayer.py --- beets-1.5.0/beetsplug/bpd/gstplayer.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/bpd/gstplayer.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -17,15 +16,13 @@ music player. """ -from __future__ import division, absolute_import, print_function -import six import sys import time -from six.moves import _thread +import _thread import os import copy -from six.moves import urllib +import urllib from beets import ui import gi @@ -40,7 +37,7 @@ pass -class GstPlayer(object): +class GstPlayer: """A music player abstracting GStreamer's Playbin element. Create a player object, then call run() to start a thread with a @@ -110,7 +107,7 @@ # error self.player.set_state(Gst.State.NULL) err, debug = message.parse_error() - print(u"Error: {0}".format(err)) + print(f"Error: {err}") self.playing = False def _set_volume(self, volume): @@ -130,7 +127,7 @@ path. """ self.player.set_state(Gst.State.NULL) - if isinstance(path, six.text_type): + if isinstance(path, str): path = path.encode('utf-8') uri = 'file://' + urllib.parse.quote(path) self.player.set_property("uri", uri) diff -Nru beets-1.5.0/beetsplug/bpd/__init__.py beets-1.6.0/beetsplug/bpd/__init__.py --- beets-1.5.0/beetsplug/bpd/__init__.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/bpd/__init__.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -18,7 +17,6 @@ use of the wide range of MPD clients. """ -from __future__ import division, absolute_import, print_function import re import sys @@ -38,20 +36,19 @@ from beets.library import Item from beets import dbcore from mediafile import MediaFile -import six PROTOCOL_VERSION = '0.16.0' BUFSIZE = 1024 -HELLO = u'OK MPD %s' % PROTOCOL_VERSION -CLIST_BEGIN = u'command_list_begin' -CLIST_VERBOSE_BEGIN = u'command_list_ok_begin' -CLIST_END = u'command_list_end' -RESP_OK = u'OK' -RESP_CLIST_VERBOSE = u'list_OK' -RESP_ERR = u'ACK' +HELLO = 'OK MPD %s' % PROTOCOL_VERSION +CLIST_BEGIN = 'command_list_begin' +CLIST_VERBOSE_BEGIN = 'command_list_ok_begin' +CLIST_END = 'command_list_end' +RESP_OK = 'OK' +RESP_CLIST_VERBOSE = 'list_OK' +RESP_ERR = 'ACK' -NEWLINE = u"\n" +NEWLINE = "\n" ERROR_NOT_LIST = 1 ERROR_ARG = 2 @@ -71,15 +68,15 @@ SAFE_COMMANDS = ( # Commands that are available when unauthenticated. - u'close', u'commands', u'notcommands', u'password', u'ping', + 'close', 'commands', 'notcommands', 'password', 'ping', ) # List of subsystems/events used by the `idle` command. SUBSYSTEMS = [ - u'update', u'player', u'mixer', u'options', u'playlist', u'database', + 'update', 'player', 'mixer', 'options', 'playlist', 'database', # Related to unsupported commands: - u'stored_playlist', u'output', u'subscription', u'sticker', u'message', - u'partition', + 'stored_playlist', 'output', 'subscription', 'sticker', 'message', + 'partition', ] ITEM_KEYS_WRITABLE = set(MediaFile.fields()).intersection(Item._fields.keys()) @@ -102,7 +99,7 @@ self.cmd_name = cmd_name self.index = index - template = Template(u'$resp [$code@$index] {$cmd_name} $message') + template = Template('$resp [$code@$index] {$cmd_name} $message') def response(self): """Returns a string to be used as the response code for the @@ -131,9 +128,9 @@ pass return NewBPDError -ArgumentTypeError = make_bpd_error(ERROR_ARG, u'invalid type for argument') -ArgumentIndexError = make_bpd_error(ERROR_ARG, u'argument out of range') -ArgumentNotFoundError = make_bpd_error(ERROR_NO_EXIST, u'argument not found') +ArgumentTypeError = make_bpd_error(ERROR_ARG, 'invalid type for argument') +ArgumentIndexError = make_bpd_error(ERROR_ARG, 'argument out of range') +ArgumentNotFoundError = make_bpd_error(ERROR_NO_EXIST, 'argument not found') def cast_arg(t, val): @@ -163,14 +160,14 @@ and should be notified when a relevant event happens. """ def __init__(self, subsystems): - super(BPDIdle, self).__init__() + super().__init__() self.subsystems = set(subsystems) # Generic server infrastructure, implementing the basic protocol. -class BaseServer(object): +class BaseServer: """A MPD-compatible music player server. The functions with the `cmd_` prefix are invoked in response to @@ -258,7 +255,7 @@ if not self.ctrl_sock: self.ctrl_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.ctrl_sock.connect((self.ctrl_host, self.ctrl_port)) - self.ctrl_sock.sendall((message + u'\n').encode('utf-8')) + self.ctrl_sock.sendall((message + '\n').encode('utf-8')) def _send_event(self, event): """Notify subscribed connections of an event.""" @@ -330,7 +327,7 @@ for system in subsystems: if system not in SUBSYSTEMS: raise BPDError(ERROR_ARG, - u'Unrecognised idle event: {}'.format(system)) + f'Unrecognised idle event: {system}') raise BPDIdle(subsystems) # put the connection into idle mode def cmd_kill(self, conn): @@ -347,20 +344,20 @@ conn.authenticated = True else: conn.authenticated = False - raise BPDError(ERROR_PASSWORD, u'incorrect password') + raise BPDError(ERROR_PASSWORD, 'incorrect password') def cmd_commands(self, conn): """Lists the commands available to the user.""" if self.password and not conn.authenticated: # Not authenticated. Show limited list of commands. for cmd in SAFE_COMMANDS: - yield u'command: ' + cmd + yield 'command: ' + cmd else: # Authenticated. Show all commands. for func in dir(self): if func.startswith('cmd_'): - yield u'command: ' + func[4:] + yield 'command: ' + func[4:] def cmd_notcommands(self, conn): """Lists all unavailable commands.""" @@ -370,7 +367,7 @@ if func.startswith('cmd_'): cmd = func[4:] if cmd not in SAFE_COMMANDS: - yield u'command: ' + cmd + yield 'command: ' + cmd else: # Authenticated. No commands are unavailable. @@ -384,43 +381,43 @@ playlist, playlistlength, and xfade. """ yield ( - u'repeat: ' + six.text_type(int(self.repeat)), - u'random: ' + six.text_type(int(self.random)), - u'consume: ' + six.text_type(int(self.consume)), - u'single: ' + six.text_type(int(self.single)), - u'playlist: ' + six.text_type(self.playlist_version), - u'playlistlength: ' + six.text_type(len(self.playlist)), - u'mixrampdb: ' + six.text_type(self.mixrampdb), + 'repeat: ' + str(int(self.repeat)), + 'random: ' + str(int(self.random)), + 'consume: ' + str(int(self.consume)), + 'single: ' + str(int(self.single)), + 'playlist: ' + str(self.playlist_version), + 'playlistlength: ' + str(len(self.playlist)), + 'mixrampdb: ' + str(self.mixrampdb), ) if self.volume > 0: - yield u'volume: ' + six.text_type(self.volume) + yield 'volume: ' + str(self.volume) if not math.isnan(self.mixrampdelay): - yield u'mixrampdelay: ' + six.text_type(self.mixrampdelay) + yield 'mixrampdelay: ' + str(self.mixrampdelay) if self.crossfade > 0: - yield u'xfade: ' + six.text_type(self.crossfade) + yield 'xfade: ' + str(self.crossfade) if self.current_index == -1: - state = u'stop' + state = 'stop' elif self.paused: - state = u'pause' + state = 'pause' else: - state = u'play' - yield u'state: ' + state + state = 'play' + yield 'state: ' + state if self.current_index != -1: # i.e., paused or playing current_id = self._item_id(self.playlist[self.current_index]) - yield u'song: ' + six.text_type(self.current_index) - yield u'songid: ' + six.text_type(current_id) + yield 'song: ' + str(self.current_index) + yield 'songid: ' + str(current_id) if len(self.playlist) > self.current_index + 1: # If there's a next song, report its index too. next_id = self._item_id(self.playlist[self.current_index + 1]) - yield u'nextsong: ' + six.text_type(self.current_index + 1) - yield u'nextsongid: ' + six.text_type(next_id) + yield 'nextsong: ' + str(self.current_index + 1) + yield 'nextsongid: ' + str(next_id) if self.error: - yield u'error: ' + self.error + yield 'error: ' + self.error def cmd_clearerror(self, conn): """Removes the persistent error state of the server. This @@ -454,7 +451,7 @@ """Set the player's volume level (0-100).""" vol = cast_arg(int, vol) if vol < VOLUME_MIN or vol > VOLUME_MAX: - raise BPDError(ERROR_ARG, u'volume out of range') + raise BPDError(ERROR_ARG, 'volume out of range') self.volume = vol self._send_event('mixer') @@ -467,8 +464,8 @@ """Set the number of seconds of crossfading.""" crossfade = cast_arg(int, crossfade) if crossfade < 0: - raise BPDError(ERROR_ARG, u'crossfade time must be nonnegative') - self._log.warning(u'crossfade is not implemented in bpd') + raise BPDError(ERROR_ARG, 'crossfade time must be nonnegative') + self._log.warning('crossfade is not implemented in bpd') self.crossfade = crossfade self._send_event('options') @@ -476,7 +473,7 @@ """Set the mixramp normalised max volume in dB.""" db = cast_arg(float, db) if db > 0: - raise BPDError(ERROR_ARG, u'mixrampdb time must be negative') + raise BPDError(ERROR_ARG, 'mixrampdb time must be negative') self._log.warning('mixramp is not implemented in bpd') self.mixrampdb = db self._send_event('options') @@ -485,7 +482,7 @@ """Set the mixramp delay in seconds.""" delay = cast_arg(float, delay) if delay < 0: - raise BPDError(ERROR_ARG, u'mixrampdelay time must be nonnegative') + raise BPDError(ERROR_ARG, 'mixrampdelay time must be nonnegative') self._log.warning('mixramp is not implemented in bpd') self.mixrampdelay = delay self._send_event('options') @@ -493,14 +490,14 @@ def cmd_replay_gain_mode(self, conn, mode): """Set the replay gain mode.""" if mode not in ['off', 'track', 'album', 'auto']: - raise BPDError(ERROR_ARG, u'Unrecognised replay gain mode') + raise BPDError(ERROR_ARG, 'Unrecognised replay gain mode') self._log.warning('replay gain is not implemented in bpd') self.replay_gain_mode = mode self._send_event('options') def cmd_replay_gain_status(self, conn): """Get the replaygain mode.""" - yield u'replay_gain_mode: ' + six.text_type(self.replay_gain_mode) + yield 'replay_gain_mode: ' + str(self.replay_gain_mode) def cmd_clear(self, conn): """Clear the playlist.""" @@ -621,8 +618,8 @@ Also a dummy implementation. """ for idx, track in enumerate(self.playlist): - yield u'cpos: ' + six.text_type(idx) - yield u'Id: ' + six.text_type(track.id) + yield 'cpos: ' + str(idx) + yield 'Id: ' + str(track.id) def cmd_currentsong(self, conn): """Sends information about the currently-playing song. @@ -731,7 +728,7 @@ 'a' + 2 -class Connection(object): +class Connection: """A connection between a client and the server. """ def __init__(self, server, sock): @@ -739,12 +736,12 @@ """ self.server = server self.sock = sock - self.address = u'{}:{}'.format(*sock.sock.getpeername()) + self.address = '{}:{}'.format(*sock.sock.getpeername()) def debug(self, message, kind=' '): """Log a debug message about this connection. """ - self.server._log.debug(u'{}[{}]: {}', kind, self.address, message) + self.server._log.debug('{}[{}]: {}', kind, self.address, message) def run(self): pass @@ -755,12 +752,12 @@ added after every string. Returns a Bluelet event that sends the data. """ - if isinstance(lines, six.string_types): + if isinstance(lines, str): lines = [lines] out = NEWLINE.join(lines) + NEWLINE for l in out.split(NEWLINE)[:-1]: self.debug(l, kind='>') - if isinstance(out, six.text_type): + if isinstance(out, str): out = out.encode('utf-8') return self.sock.sendall(out) @@ -779,7 +776,7 @@ def __init__(self, server, sock): """Create a new connection for the accepted socket `client`. """ - super(MPDConnection, self).__init__(server, sock) + super().__init__(server, sock) self.authenticated = False self.notifications = set() self.idle_subscriptions = set() @@ -813,7 +810,7 @@ pending = self.notifications.intersection(self.idle_subscriptions) try: for event in pending: - yield self.send(u'changed: {}'.format(event)) + yield self.send(f'changed: {event}') if pending or force_close_idle: self.idle_subscriptions = set() self.notifications = self.notifications.difference(pending) @@ -837,7 +834,7 @@ break line = line.strip() if not line: - err = BPDError(ERROR_UNKNOWN, u'No command given') + err = BPDError(ERROR_UNKNOWN, 'No command given') yield self.send(err.response()) self.disconnect() # Client sent a blank line. break @@ -847,15 +844,15 @@ if self.idle_subscriptions: # The connection is in idle mode. - if line == u'noidle': + if line == 'noidle': yield bluelet.call(self.send_notifications(True)) else: err = BPDError(ERROR_UNKNOWN, - u'Got command while idle: {}'.format(line)) + f'Got command while idle: {line}') yield self.send(err.response()) break continue - if line == u'noidle': + if line == 'noidle': # When not in idle, this command sends no response. continue @@ -894,10 +891,10 @@ def __init__(self, server, sock): """Create a new connection for the accepted socket `client`. """ - super(ControlConnection, self).__init__(server, sock) + super().__init__(server, sock) def debug(self, message, kind=' '): - self.server._log.debug(u'CTRL {}[{}]: {}', kind, self.address, message) + self.server._log.debug('CTRL {}[{}]: {}', kind, self.address, message) def run(self): """Listen for control commands and delegate to `ctrl_*` methods. @@ -943,10 +940,10 @@ c.address = newlabel break else: - yield self.send(u'ERROR: no such client: {}'.format(oldlabel)) + yield self.send(f'ERROR: no such client: {oldlabel}') -class Command(object): +class Command: """A command issued by the client for processing by the server. """ @@ -966,7 +963,7 @@ if match[0]: # Quoted argument. arg = match[0] - arg = arg.replace(u'\\"', u'"').replace(u'\\\\', u'\\') + arg = arg.replace('\\"', '"').replace('\\\\', '\\') else: # Unquoted argument. arg = match[1] @@ -981,15 +978,10 @@ # Attempt to get correct command function. func_name = prefix + self.name if not hasattr(target, func_name): - raise AttributeError(u'unknown command "{}"'.format(self.name)) + raise AttributeError(f'unknown command "{self.name}"') func = getattr(target, func_name) - if six.PY2: - # caution: the fields of the namedtuple are slightly different - # between the results of getargspec and getfullargspec. - argspec = inspect.getargspec(func) - else: - argspec = inspect.getfullargspec(func) + argspec = inspect.getfullargspec(func) # Check that `func` is able to handle the number of arguments sent # by the client (so we can raise ERROR_ARG instead of ERROR_SYSTEM). @@ -1002,7 +994,7 @@ wrong_num = (len(self.args) > max_args) or (len(self.args) < min_args) # If the command accepts a variable number of arguments skip the check. if wrong_num and not argspec.varargs: - raise TypeError(u'wrong number of arguments for "{}"' + raise TypeError('wrong number of arguments for "{}"' .format(self.name), self.name) return func @@ -1023,7 +1015,7 @@ if conn.server.password and \ not conn.authenticated and \ self.name not in SAFE_COMMANDS: - raise BPDError(ERROR_PERMISSION, u'insufficient privileges') + raise BPDError(ERROR_PERMISSION, 'insufficient privileges') try: args = [conn] + self.args @@ -1049,7 +1041,7 @@ except Exception: # An "unintentional" error. Hide it from the client. conn.server._log.error('{}', traceback.format_exc()) - raise BPDError(ERROR_SYSTEM, u'server error', self.name) + raise BPDError(ERROR_SYSTEM, 'server error', self.name) class CommandList(list): @@ -1101,46 +1093,46 @@ raise NoGstreamerError() else: raise - log.info(u'Starting server...') - super(Server, self).__init__(host, port, password, ctrl_port, log) + log.info('Starting server...') + super().__init__(host, port, password, ctrl_port, log) self.lib = library self.player = gstplayer.GstPlayer(self.play_finished) self.cmd_update(None) - log.info(u'Server ready and listening on {}:{}'.format( + log.info('Server ready and listening on {}:{}'.format( host, port)) - log.debug(u'Listening for control signals on {}:{}'.format( + log.debug('Listening for control signals on {}:{}'.format( host, ctrl_port)) def run(self): self.player.run() - super(Server, self).run() + super().run() def play_finished(self): """A callback invoked every time our player finishes a track. """ self.cmd_next(None) - self._ctrl_send(u'play_finished') + self._ctrl_send('play_finished') # Metadata helper functions. def _item_info(self, item): info_lines = [ - u'file: ' + item.destination(fragment=True), - u'Time: ' + six.text_type(int(item.length)), - u'duration: ' + u'{:.3f}'.format(item.length), - u'Id: ' + six.text_type(item.id), + 'file: ' + item.destination(fragment=True), + 'Time: ' + str(int(item.length)), + 'duration: ' + f'{item.length:.3f}', + 'Id: ' + str(item.id), ] try: pos = self._id_to_index(item.id) - info_lines.append(u'Pos: ' + six.text_type(pos)) + info_lines.append('Pos: ' + str(pos)) except ArgumentNotFoundError: # Don't include position if not in playlist. pass for tagtype, field in self.tagtype_map.items(): - info_lines.append(u'{}: {}'.format( - tagtype, six.text_type(getattr(item, field)))) + info_lines.append('{}: {}'.format( + tagtype, str(getattr(item, field)))) return info_lines @@ -1154,7 +1146,7 @@ except ValueError: if accept_single_number: return [cast_arg(int, items)] - raise BPDError(ERROR_ARG, u'bad range syntax') + raise BPDError(ERROR_ARG, 'bad range syntax') start = cast_arg(int, start) stop = cast_arg(int, stop) return range(start, stop) @@ -1164,14 +1156,14 @@ # Database updating. - def cmd_update(self, conn, path=u'/'): + def cmd_update(self, conn, path='/'): """Updates the catalog to reflect the current database state. """ # Path is ignored. Also, the real MPD does this asynchronously; # this is done inline. - self._log.debug(u'Building directory tree...') + self._log.debug('Building directory tree...') self.tree = vfs.libtree(self.lib) - self._log.debug(u'Finished building directory tree.') + self._log.debug('Finished building directory tree.') self.updated_time = time.time() self._send_event('update') self._send_event('database') @@ -1182,7 +1174,7 @@ """Returns a VFS node or an item ID located at the path given. If the path does not exist, raises a """ - components = path.split(u'/') + components = path.split('/') node = self.tree for component in components: @@ -1204,25 +1196,25 @@ def _path_join(self, p1, p2): """Smashes together two BPD paths.""" - out = p1 + u'/' + p2 - return out.replace(u'//', u'/').replace(u'//', u'/') + out = p1 + '/' + p2 + return out.replace('//', '/').replace('//', '/') - def cmd_lsinfo(self, conn, path=u"/"): + def cmd_lsinfo(self, conn, path="/"): """Sends info on all the items in the path.""" node = self._resolve_path(path) if isinstance(node, int): # Trying to list a track. - raise BPDError(ERROR_ARG, u'this is not a directory') + raise BPDError(ERROR_ARG, 'this is not a directory') else: for name, itemid in iter(sorted(node.files.items())): item = self.lib.get_item(itemid) yield self._item_info(item) for name, _ in iter(sorted(node.dirs.items())): dirpath = self._path_join(path, name) - if dirpath.startswith(u"/"): + if dirpath.startswith("/"): # Strip leading slash (libmpc rejects this). dirpath = dirpath[1:] - yield u'directory: %s' % dirpath + yield 'directory: %s' % dirpath def _listall(self, basepath, node, info=False): """Helper function for recursive listing. If info, show @@ -1234,25 +1226,23 @@ item = self.lib.get_item(node) yield self._item_info(item) else: - yield u'file: ' + basepath + yield 'file: ' + basepath else: # List a directory. Recurse into both directories and files. for name, itemid in sorted(node.files.items()): newpath = self._path_join(basepath, name) # "yield from" - for v in self._listall(newpath, itemid, info): - yield v + yield from self._listall(newpath, itemid, info) for name, subdir in sorted(node.dirs.items()): newpath = self._path_join(basepath, name) - yield u'directory: ' + newpath - for v in self._listall(newpath, subdir, info): - yield v + yield 'directory: ' + newpath + yield from self._listall(newpath, subdir, info) - def cmd_listall(self, conn, path=u"/"): + def cmd_listall(self, conn, path="/"): """Send the paths all items in the directory, recursively.""" return self._listall(path, self._resolve_path(path), False) - def cmd_listallinfo(self, conn, path=u"/"): + def cmd_listallinfo(self, conn, path="/"): """Send info on all the items in the directory, recursively.""" return self._listall(path, self._resolve_path(path), True) @@ -1269,11 +1259,9 @@ # Recurse into a directory. for name, itemid in sorted(node.files.items()): # "yield from" - for v in self._all_items(itemid): - yield v + yield from self._all_items(itemid) for name, subdir in sorted(node.dirs.items()): - for v in self._all_items(subdir): - yield v + yield from self._all_items(subdir) def _add(self, path, send_id=False): """Adds a track or directory to the playlist, specified by the @@ -1282,7 +1270,7 @@ for item in self._all_items(self._resolve_path(path)): self.playlist.append(item) if send_id: - yield u'Id: ' + six.text_type(item.id) + yield 'Id: ' + str(item.id) self.playlist_version += 1 self._send_event('playlist') @@ -1299,28 +1287,27 @@ # Server info. def cmd_status(self, conn): - for line in super(Server, self).cmd_status(conn): - yield line + yield from super().cmd_status(conn) if self.current_index > -1: item = self.playlist[self.current_index] yield ( - u'bitrate: ' + six.text_type(item.bitrate / 1000), - u'audio: {}:{}:{}'.format( - six.text_type(item.samplerate), - six.text_type(item.bitdepth), - six.text_type(item.channels), + 'bitrate: ' + str(item.bitrate / 1000), + 'audio: {}:{}:{}'.format( + str(item.samplerate), + str(item.bitdepth), + str(item.channels), ), ) (pos, total) = self.player.time() yield ( - u'time: {}:{}'.format( - six.text_type(int(pos)), - six.text_type(int(total)), + 'time: {}:{}'.format( + str(int(pos)), + str(int(total)), ), - u'elapsed: ' + u'{:.3f}'.format(pos), - u'duration: ' + u'{:.3f}'.format(total), + 'elapsed: ' + f'{pos:.3f}', + 'duration: ' + f'{total:.3f}', ) # Also missing 'updating_db'. @@ -1336,47 +1323,47 @@ artists, albums, songs, totaltime = tx.query(statement)[0] yield ( - u'artists: ' + six.text_type(artists), - u'albums: ' + six.text_type(albums), - u'songs: ' + six.text_type(songs), - u'uptime: ' + six.text_type(int(time.time() - self.startup_time)), - u'playtime: ' + u'0', # Missing. - u'db_playtime: ' + six.text_type(int(totaltime)), - u'db_update: ' + six.text_type(int(self.updated_time)), + 'artists: ' + str(artists), + 'albums: ' + str(albums), + 'songs: ' + str(songs), + 'uptime: ' + str(int(time.time() - self.startup_time)), + 'playtime: ' + '0', # Missing. + 'db_playtime: ' + str(int(totaltime)), + 'db_update: ' + str(int(self.updated_time)), ) def cmd_decoders(self, conn): """Send list of supported decoders and formats.""" decoders = self.player.get_decoders() for name, (mimes, exts) in decoders.items(): - yield u'plugin: {}'.format(name) + yield f'plugin: {name}' for ext in exts: - yield u'suffix: {}'.format(ext) + yield f'suffix: {ext}' for mime in mimes: - yield u'mime_type: {}'.format(mime) + yield f'mime_type: {mime}' # Searching. tagtype_map = { - u'Artist': u'artist', - u'ArtistSort': u'artist_sort', - u'Album': u'album', - u'Title': u'title', - u'Track': u'track', - u'AlbumArtist': u'albumartist', - u'AlbumArtistSort': u'albumartist_sort', - u'Label': u'label', - u'Genre': u'genre', - u'Date': u'year', - u'OriginalDate': u'original_year', - u'Composer': u'composer', - u'Disc': u'disc', - u'Comment': u'comments', - u'MUSICBRAINZ_TRACKID': u'mb_trackid', - u'MUSICBRAINZ_ALBUMID': u'mb_albumid', - u'MUSICBRAINZ_ARTISTID': u'mb_artistid', - u'MUSICBRAINZ_ALBUMARTISTID': u'mb_albumartistid', - u'MUSICBRAINZ_RELEASETRACKID': u'mb_releasetrackid', + 'Artist': 'artist', + 'ArtistSort': 'artist_sort', + 'Album': 'album', + 'Title': 'title', + 'Track': 'track', + 'AlbumArtist': 'albumartist', + 'AlbumArtistSort': 'albumartist_sort', + 'Label': 'label', + 'Genre': 'genre', + 'Date': 'year', + 'OriginalDate': 'original_year', + 'Composer': 'composer', + 'Disc': 'disc', + 'Comment': 'comments', + 'MUSICBRAINZ_TRACKID': 'mb_trackid', + 'MUSICBRAINZ_ALBUMID': 'mb_albumid', + 'MUSICBRAINZ_ARTISTID': 'mb_artistid', + 'MUSICBRAINZ_ALBUMARTISTID': 'mb_albumartistid', + 'MUSICBRAINZ_RELEASETRACKID': 'mb_releasetrackid', } def cmd_tagtypes(self, conn): @@ -1384,7 +1371,7 @@ searching. """ for tag in self.tagtype_map: - yield u'tagtype: ' + tag + yield 'tagtype: ' + tag def _tagtype_lookup(self, tag): """Uses `tagtype_map` to look up the beets column name for an @@ -1396,7 +1383,7 @@ # Match case-insensitively. if test_tag.lower() == tag.lower(): return test_tag, key - raise BPDError(ERROR_UNKNOWN, u'no such tagtype') + raise BPDError(ERROR_UNKNOWN, 'no such tagtype') def _metadata_query(self, query_type, any_query_type, kv): """Helper function returns a query object that will find items @@ -1409,13 +1396,13 @@ # Iterate pairwise over the arguments. it = iter(kv) for tag, value in zip(it, it): - if tag.lower() == u'any': + if tag.lower() == 'any': if any_query_type: queries.append(any_query_type(value, ITEM_KEYS_WRITABLE, query_type)) else: - raise BPDError(ERROR_UNKNOWN, u'no such tagtype') + raise BPDError(ERROR_UNKNOWN, 'no such tagtype') else: _, key = self._tagtype_lookup(tag) queries.append(query_type(key, value)) @@ -1452,9 +1439,9 @@ # rely on this behaviour (e.g. MPDroid, M.A.L.P.). kv = ('Artist', kv[0]) else: - raise BPDError(ERROR_ARG, u'should be "Album" for 3 arguments') + raise BPDError(ERROR_ARG, 'should be "Album" for 3 arguments') elif len(kv) % 2 != 0: - raise BPDError(ERROR_ARG, u'Incorrect number of filter arguments') + raise BPDError(ERROR_ARG, 'Incorrect number of filter arguments') query = self._metadata_query(dbcore.query.MatchQuery, None, kv) clause, subvals = query.clause() @@ -1469,7 +1456,7 @@ if not row[0]: # Skip any empty values of the field. continue - yield show_tag_canon + u': ' + six.text_type(row[0]) + yield show_tag_canon + ': ' + str(row[0]) def cmd_count(self, conn, tag, value): """Returns the number and total time of songs matching the @@ -1481,44 +1468,44 @@ for item in self.lib.items(dbcore.query.MatchQuery(key, value)): songs += 1 playtime += item.length - yield u'songs: ' + six.text_type(songs) - yield u'playtime: ' + six.text_type(int(playtime)) + yield 'songs: ' + str(songs) + yield 'playtime: ' + str(int(playtime)) # Persistent playlist manipulation. In MPD this is an optional feature so # these dummy implementations match MPD's behaviour with the feature off. def cmd_listplaylist(self, conn, playlist): - raise BPDError(ERROR_NO_EXIST, u'No such playlist') + raise BPDError(ERROR_NO_EXIST, 'No such playlist') def cmd_listplaylistinfo(self, conn, playlist): - raise BPDError(ERROR_NO_EXIST, u'No such playlist') + raise BPDError(ERROR_NO_EXIST, 'No such playlist') def cmd_listplaylists(self, conn): - raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled') + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') def cmd_load(self, conn, playlist): - raise BPDError(ERROR_NO_EXIST, u'Stored playlists are disabled') + raise BPDError(ERROR_NO_EXIST, 'Stored playlists are disabled') def cmd_playlistadd(self, conn, playlist, uri): - raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled') + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') def cmd_playlistclear(self, conn, playlist): - raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled') + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') def cmd_playlistdelete(self, conn, playlist, index): - raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled') + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') def cmd_playlistmove(self, conn, playlist, from_index, to_index): - raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled') + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') def cmd_rename(self, conn, playlist, new_name): - raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled') + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') def cmd_rm(self, conn, playlist): - raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled') + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') def cmd_save(self, conn, playlist): - raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled') + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') # "Outputs." Just a dummy implementation because we don't control # any outputs. @@ -1526,9 +1513,9 @@ def cmd_outputs(self, conn): """List the available outputs.""" yield ( - u'outputid: 0', - u'outputname: gstreamer', - u'outputenabled: 1', + 'outputid: 0', + 'outputname: gstreamer', + 'outputenabled: 1', ) def cmd_enableoutput(self, conn, output_id): @@ -1539,7 +1526,7 @@ def cmd_disableoutput(self, conn, output_id): output_id = cast_arg(int, output_id) if output_id == 0: - raise BPDError(ERROR_ARG, u'cannot disable this output') + raise BPDError(ERROR_ARG, 'cannot disable this output') else: raise ArgumentIndexError() @@ -1550,7 +1537,7 @@ def cmd_play(self, conn, index=-1): new_index = index != -1 and index != self.current_index was_paused = self.paused - super(Server, self).cmd_play(conn, index) + super().cmd_play(conn, index) if self.current_index > -1: # Not stopped. if was_paused and not new_index: @@ -1560,28 +1547,28 @@ self.player.play_file(self.playlist[self.current_index].path) def cmd_pause(self, conn, state=None): - super(Server, self).cmd_pause(conn, state) + super().cmd_pause(conn, state) if self.paused: self.player.pause() elif self.player.playing: self.player.play() def cmd_stop(self, conn): - super(Server, self).cmd_stop(conn) + super().cmd_stop(conn) self.player.stop() def cmd_seek(self, conn, index, pos): """Seeks to the specified position in the specified song.""" index = cast_arg(int, index) pos = cast_arg(float, pos) - super(Server, self).cmd_seek(conn, index, pos) + super().cmd_seek(conn, index, pos) self.player.seek(pos) # Volume control. def cmd_setvol(self, conn, vol): vol = cast_arg(int, vol) - super(Server, self).cmd_setvol(conn, vol) + super().cmd_setvol(conn, vol) self.player.volume = float(vol) / 100 @@ -1592,12 +1579,12 @@ server. """ def __init__(self): - super(BPDPlugin, self).__init__() + super().__init__() self.config.add({ - 'host': u'', + 'host': '', 'port': 6600, 'control_port': 6601, - 'password': u'', + 'password': '', 'volume': VOLUME_MAX, }) self.config['password'].redact = True @@ -1609,13 +1596,13 @@ server.cmd_setvol(None, volume) server.run() except NoGstreamerError: - self._log.error(u'Gstreamer Python bindings not found.') - self._log.error(u'Install "gstreamer1.0" and "python-gi"' - u'or similar package to use BPD.') + self._log.error('Gstreamer Python bindings not found.') + self._log.error('Install "gstreamer1.0" and "python-gi"' + 'or similar package to use BPD.') def commands(self): cmd = beets.ui.Subcommand( - 'bpd', help=u'run an MPD-compatible music player server' + 'bpd', help='run an MPD-compatible music player server' ) def func(lib, opts, args): @@ -1627,7 +1614,7 @@ else: ctrl_port = self.config['control_port'].get(int) if args: - raise beets.ui.UserError(u'too many arguments') + raise beets.ui.UserError('too many arguments') password = self.config['password'].as_str() volume = self.config['volume'].get(int) self.start_bpd(lib, host, int(port), password, volume, diff -Nru beets-1.5.0/beetsplug/bpm.py beets-1.6.0/beetsplug/bpm.py --- beets-1.5.0/beetsplug/bpm.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/bpm.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, aroquen # @@ -15,10 +14,8 @@ """Determine BPM by pressing a key to the rhythm.""" -from __future__ import division, absolute_import, print_function import time -from six.moves import input from beets import ui from beets.plugins import BeetsPlugin @@ -51,16 +48,16 @@ class BPMPlugin(BeetsPlugin): def __init__(self): - super(BPMPlugin, self).__init__() + super().__init__() self.config.add({ - u'max_strokes': 3, - u'overwrite': True, + 'max_strokes': 3, + 'overwrite': True, }) def commands(self): cmd = ui.Subcommand('bpm', - help=u'determine bpm of a song by pressing ' - u'a key to the rhythm') + help='determine bpm of a song by pressing ' + 'a key to the rhythm') cmd.func = self.command return [cmd] @@ -72,19 +69,19 @@ def get_bpm(self, items, write=False): overwrite = self.config['overwrite'].get(bool) if len(items) > 1: - raise ValueError(u'Can only get bpm of one song at time') + raise ValueError('Can only get bpm of one song at time') item = items[0] if item['bpm']: - self._log.info(u'Found bpm {0}', item['bpm']) + self._log.info('Found bpm {0}', item['bpm']) if not overwrite: return - self._log.info(u'Press Enter {0} times to the rhythm or Ctrl-D ' - u'to exit', self.config['max_strokes'].get(int)) + self._log.info('Press Enter {0} times to the rhythm or Ctrl-D ' + 'to exit', self.config['max_strokes'].get(int)) new_bpm = bpm(self.config['max_strokes'].get(int)) item['bpm'] = int(new_bpm) if write: item.try_write() item.store() - self._log.info(u'Added new bpm {0}', item['bpm']) + self._log.info('Added new bpm {0}', item['bpm']) diff -Nru beets-1.5.0/beetsplug/bpsync.py beets-1.6.0/beetsplug/bpsync.py --- beets-1.5.0/beetsplug/bpsync.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/bpsync.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2019, Rahul Ahuja. # @@ -15,7 +14,6 @@ """Update library's tags using Beatport. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin, apply_item_changes from beets import autotag, library, ui, util @@ -25,39 +23,39 @@ class BPSyncPlugin(BeetsPlugin): def __init__(self): - super(BPSyncPlugin, self).__init__() + super().__init__() self.beatport_plugin = BeatportPlugin() self.beatport_plugin.setup() def commands(self): - cmd = ui.Subcommand('bpsync', help=u'update metadata from Beatport') + cmd = ui.Subcommand('bpsync', help='update metadata from Beatport') cmd.parser.add_option( - u'-p', - u'--pretend', + '-p', + '--pretend', action='store_true', - help=u'show all changes but do nothing', + help='show all changes but do nothing', ) cmd.parser.add_option( - u'-m', - u'--move', + '-m', + '--move', action='store_true', dest='move', - help=u"move files in the library directory", + help="move files in the library directory", ) cmd.parser.add_option( - u'-M', - u'--nomove', + '-M', + '--nomove', action='store_false', dest='move', - help=u"don't move files in library", + help="don't move files in library", ) cmd.parser.add_option( - u'-W', - u'--nowrite', + '-W', + '--nowrite', action='store_false', default=None, dest='write', - help=u"don't write updated metadata to files", + help="don't write updated metadata to files", ) cmd.parser.add_format_option() cmd.func = self.func @@ -78,16 +76,16 @@ """Retrieve and apply info from the autotagger for items matched by query. """ - for item in lib.items(query + [u'singleton:true']): + for item in lib.items(query + ['singleton:true']): if not item.mb_trackid: self._log.info( - u'Skipping singleton with no mb_trackid: {}', item + 'Skipping singleton with no mb_trackid: {}', item ) continue if not self.is_beatport_track(item): self._log.info( - u'Skipping non-{} singleton: {}', + 'Skipping non-{} singleton: {}', self.beatport_plugin.data_source, item, ) @@ -108,11 +106,11 @@ def get_album_tracks(self, album): if not album.mb_albumid: - self._log.info(u'Skipping album with no mb_albumid: {}', album) + self._log.info('Skipping album with no mb_albumid: {}', album) return False if not album.mb_albumid.isnumeric(): self._log.info( - u'Skipping album with invalid {} ID: {}', + 'Skipping album with invalid {} ID: {}', self.beatport_plugin.data_source, album, ) @@ -122,7 +120,7 @@ return items if not all(self.is_beatport_track(item) for item in items): self._log.info( - u'Skipping non-{} release: {}', + 'Skipping non-{} release: {}', self.beatport_plugin.data_source, album, ) @@ -144,7 +142,7 @@ albuminfo = self.beatport_plugin.album_for_id(album.mb_albumid) if not albuminfo: self._log.info( - u'Release ID {} not found for album {}', + 'Release ID {} not found for album {}', album.mb_albumid, album, ) @@ -161,7 +159,7 @@ for track_id, item in library_trackid_to_item.items() } - self._log.info(u'applying changes to {}', album) + self._log.info('applying changes to {}', album) with lib.transaction(): autotag.apply_metadata(albuminfo, item_to_trackinfo) changed = False @@ -184,5 +182,5 @@ # Move album art (and any inconsistent items). if move and lib.directory in util.ancestry(items[0].path): - self._log.debug(u'moving album {}', album) + self._log.debug('moving album {}', album) album.move() diff -Nru beets-1.5.0/beetsplug/bucket.py beets-1.6.0/beetsplug/bucket.py --- beets-1.5.0/beetsplug/bucket.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/bucket.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte. # @@ -16,12 +15,10 @@ """Provides the %bucket{} function for path formatting. """ -from __future__ import division, absolute_import, print_function from datetime import datetime import re import string -from six.moves import zip from itertools import tee from beets import plugins, ui @@ -49,7 +46,7 @@ """Convert string to a 4 digits year """ if yearfrom < 100: - raise BucketError(u"%d must be expressed on 4 digits" % yearfrom) + raise BucketError("%d must be expressed on 4 digits" % yearfrom) # if two digits only, pick closest year that ends by these two # digits starting from yearfrom @@ -62,12 +59,12 @@ years = [int(x) for x in re.findall(r'\d+', span_str)] if not years: - raise ui.UserError(u"invalid range defined for year bucket '%s': no " - u"year found" % span_str) + raise ui.UserError("invalid range defined for year bucket '%s': no " + "year found" % span_str) try: years = [normalize_year(x, years[0]) for x in years] except BucketError as exc: - raise ui.UserError(u"invalid range defined for year bucket '%s': %s" % + raise ui.UserError("invalid range defined for year bucket '%s': %s" % (span_str, exc)) res = {'from': years[0], 'str': span_str} @@ -128,10 +125,10 @@ res = {'fromnchars': len(m.group('fromyear')), 'tonchars': len(m.group('toyear'))} - res['fmt'] = "%s%%s%s%s%s" % (m.group('bef'), - m.group('sep'), - '%s' if res['tonchars'] else '', - m.group('after')) + res['fmt'] = "{}%s{}{}{}".format(m.group('bef'), + m.group('sep'), + '%s' if res['tonchars'] else '', + m.group('after')) return res @@ -170,8 +167,8 @@ begin_index = ASCII_DIGITS.index(bucket[0]) end_index = ASCII_DIGITS.index(bucket[-1]) else: - raise ui.UserError(u"invalid range defined for alpha bucket " - u"'%s': no alphanumeric character found" % + raise ui.UserError("invalid range defined for alpha bucket " + "'%s': no alphanumeric character found" % elem) spans.append( re.compile( @@ -184,7 +181,7 @@ class BucketPlugin(plugins.BeetsPlugin): def __init__(self): - super(BucketPlugin, self).__init__() + super().__init__() self.template_funcs['bucket'] = self._tmpl_bucket self.config.add({ diff -Nru beets-1.5.0/beetsplug/chroma.py beets-1.6.0/beetsplug/chroma.py --- beets-1.5.0/beetsplug/chroma.py 2021-03-06 21:56:33.000000000 +0000 +++ beets-1.6.0/beetsplug/chroma.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Adds Chromaprint/Acoustid acoustic fingerprinting support to the autotagger. Requires the pyacoustid library. """ -from __future__ import division, absolute_import, print_function from beets import plugins from beets import ui @@ -90,7 +88,7 @@ try: duration, fp = acoustid.fingerprint_file(util.syspath(path)) except acoustid.FingerprintGenerationError as exc: - log.error(u'fingerprinting of {0} failed: {1}', + log.error('fingerprinting of {0} failed: {1}', util.displayable_path(repr(path)), exc) return None fp = fp.decode() @@ -99,25 +97,25 @@ res = acoustid.lookup(API_KEY, fp, duration, meta='recordings releases') except acoustid.AcoustidError as exc: - log.debug(u'fingerprint matching {0} failed: {1}', + log.debug('fingerprint matching {0} failed: {1}', util.displayable_path(repr(path)), exc) return None - log.debug(u'chroma: fingerprinted {0}', + log.debug('chroma: fingerprinted {0}', util.displayable_path(repr(path))) # Ensure the response is usable and parse it. if res['status'] != 'ok' or not res.get('results'): - log.debug(u'no match found') + log.debug('no match found') return None result = res['results'][0] # Best match. if result['score'] < SCORE_THRESH: - log.debug(u'no results above threshold') + log.debug('no results above threshold') return None _acoustids[path] = result['id'] # Get recording and releases from the result if not result.get('recordings'): - log.debug(u'no recordings found') + log.debug('no recordings found') return None recording_ids = [] releases = [] @@ -138,7 +136,7 @@ original_year=original_year)) release_ids = [rel['id'] for rel in releases] - log.debug(u'matched recordings {0} on releases {1}', + log.debug('matched recordings {0} on releases {1}', recording_ids, release_ids) _matches[path] = recording_ids, release_ids @@ -167,7 +165,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): def __init__(self): - super(AcoustidPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, @@ -198,7 +196,7 @@ if album: albums.append(album) - self._log.debug(u'acoustid album candidates: {0}', len(albums)) + self._log.debug('acoustid album candidates: {0}', len(albums)) return albums def item_candidates(self, item, artist, title): @@ -211,24 +209,24 @@ track = hooks.track_for_mbid(recording_id) if track: tracks.append(track) - self._log.debug(u'acoustid item candidates: {0}', len(tracks)) + self._log.debug('acoustid item candidates: {0}', len(tracks)) return tracks def commands(self): submit_cmd = ui.Subcommand('submit', - help=u'submit Acoustid fingerprints') + help='submit Acoustid fingerprints') def submit_cmd_func(lib, opts, args): try: apikey = config['acoustid']['apikey'].as_str() except confuse.NotFoundError: - raise ui.UserError(u'no Acoustid user API key provided') + raise ui.UserError('no Acoustid user API key provided') submit_items(self._log, apikey, lib.items(ui.decargs(args))) submit_cmd.func = submit_cmd_func fingerprint_cmd = ui.Subcommand( 'fingerprint', - help=u'generate fingerprints for items without them' + help='generate fingerprints for items without them' ) def fingerprint_cmd_func(lib, opts, args): @@ -271,11 +269,11 @@ def submit_chunk(): """Submit the current accumulated fingerprint data.""" - log.info(u'submitting {0} fingerprints', len(data)) + log.info('submitting {0} fingerprints', len(data)) try: acoustid.submit(API_KEY, userkey, data) except acoustid.AcoustidError as exc: - log.warning(u'acoustid submission error: {0}', exc) + log.warning('acoustid submission error: {0}', exc) del data[:] for item in items: @@ -288,7 +286,7 @@ } if item.mb_trackid: item_data['mbid'] = item.mb_trackid - log.debug(u'submitting MBID') + log.debug('submitting MBID') else: item_data.update({ 'track': item.title, @@ -299,7 +297,7 @@ 'trackno': item.track, 'discno': item.disc, }) - log.debug(u'submitting textual metadata') + log.debug('submitting textual metadata') data.append(item_data) # If we have enough data, submit a chunk. @@ -320,28 +318,28 @@ """ # Get a fingerprint and length for this track. if not item.length: - log.info(u'{0}: no duration available', + log.info('{0}: no duration available', util.displayable_path(item.path)) elif item.acoustid_fingerprint: if write: - log.info(u'{0}: fingerprint exists, skipping', + log.info('{0}: fingerprint exists, skipping', util.displayable_path(item.path)) else: - log.info(u'{0}: using existing fingerprint', + log.info('{0}: using existing fingerprint', util.displayable_path(item.path)) return item.acoustid_fingerprint else: - log.info(u'{0}: fingerprinting', + log.info('{0}: fingerprinting', util.displayable_path(item.path)) try: _, fp = acoustid.fingerprint_file(util.syspath(item.path)) item.acoustid_fingerprint = fp.decode() if write: - log.info(u'{0}: writing fingerprint', + log.info('{0}: writing fingerprint', util.displayable_path(item.path)) item.try_write() if item._db: item.store() return item.acoustid_fingerprint except acoustid.FingerprintGenerationError as exc: - log.info(u'fingerprint generation failed: {0}', exc) + log.info('fingerprint generation failed: {0}', exc) diff -Nru beets-1.5.0/beetsplug/convert.py beets-1.6.0/beetsplug/convert.py --- beets-1.5.0/beetsplug/convert.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beetsplug/convert.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Jakob Schnitzer. # @@ -15,7 +14,6 @@ """Converts tracks or albums to external directory """ -from __future__ import division, absolute_import, print_function from beets.util import par_map, decode_commandline_path, arg_encoding import os @@ -23,7 +21,6 @@ import subprocess import tempfile import shlex -import six from string import Template from beets import ui, util, plugins, config @@ -39,8 +36,8 @@ # Some convenient alternate names for formats. ALIASES = { - u'wma': u'windows media', - u'vorbis': u'ogg', + 'wma': 'windows media', + 'vorbis': 'ogg', } LOSSLESS_FORMATS = ['ape', 'flac', 'alac', 'wav', 'aiff'] @@ -68,7 +65,7 @@ extension = format_info.get('extension', fmt) except KeyError: raise ui.UserError( - u'convert: format {0} needs the "command" field' + 'convert: format {} needs the "command" field' .format(fmt) ) except ConfigTypeError: @@ -81,7 +78,7 @@ command = config['convert']['command'].as_str() elif 'opts' in keys: # Undocumented option for backwards compatibility with < 1.3.1. - command = u'ffmpeg -i $source -y {0} $dest'.format( + command = 'ffmpeg -i $source -y {} $dest'.format( config['convert']['opts'].as_str() ) if 'extension' in keys: @@ -110,72 +107,72 @@ class ConvertPlugin(BeetsPlugin): def __init__(self): - super(ConvertPlugin, self).__init__() + super().__init__() self.config.add({ - u'dest': None, - u'pretend': False, - u'link': False, - u'hardlink': False, - u'threads': util.cpu_count(), - u'format': u'mp3', - u'id3v23': u'inherit', - u'formats': { - u'aac': { - u'command': u'ffmpeg -i $source -y -vn -acodec aac ' - u'-aq 1 $dest', - u'extension': u'm4a', + 'dest': None, + 'pretend': False, + 'link': False, + 'hardlink': False, + 'threads': util.cpu_count(), + 'format': 'mp3', + 'id3v23': 'inherit', + 'formats': { + 'aac': { + 'command': 'ffmpeg -i $source -y -vn -acodec aac ' + '-aq 1 $dest', + 'extension': 'm4a', }, - u'alac': { - u'command': u'ffmpeg -i $source -y -vn -acodec alac $dest', - u'extension': u'm4a', + 'alac': { + 'command': 'ffmpeg -i $source -y -vn -acodec alac $dest', + 'extension': 'm4a', }, - u'flac': u'ffmpeg -i $source -y -vn -acodec flac $dest', - u'mp3': u'ffmpeg -i $source -y -vn -aq 2 $dest', - u'opus': - u'ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest', - u'ogg': - u'ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest', - u'wma': - u'ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest', + 'flac': 'ffmpeg -i $source -y -vn -acodec flac $dest', + 'mp3': 'ffmpeg -i $source -y -vn -aq 2 $dest', + 'opus': + 'ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest', + 'ogg': + 'ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest', + 'wma': + 'ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest', }, - u'max_bitrate': 500, - u'auto': False, - u'tmpdir': None, - u'quiet': False, - u'embed': True, - u'paths': {}, - u'no_convert': u'', - u'never_convert_lossy_files': False, - u'copy_album_art': False, - u'album_art_maxwidth': 0, - u'delete_originals': False, + 'max_bitrate': 500, + 'auto': False, + 'tmpdir': None, + 'quiet': False, + 'embed': True, + 'paths': {}, + 'no_convert': '', + 'never_convert_lossy_files': False, + 'copy_album_art': False, + 'album_art_maxwidth': 0, + 'delete_originals': False, }) self.early_import_stages = [self.auto_convert] self.register_listener('import_task_files', self._cleanup) def commands(self): - cmd = ui.Subcommand('convert', help=u'convert to external location') + cmd = ui.Subcommand('convert', help='convert to external location') cmd.parser.add_option('-p', '--pretend', action='store_true', - help=u'show actions but do nothing') + help='show actions but do nothing') cmd.parser.add_option('-t', '--threads', action='store', type='int', - help=u'change the number of threads, \ + help='change the number of threads, \ defaults to maximum available processors') cmd.parser.add_option('-k', '--keep-new', action='store_true', - dest='keep_new', help=u'keep only the converted \ + dest='keep_new', help='keep only the converted \ and move the old files') cmd.parser.add_option('-d', '--dest', action='store', - help=u'set the destination directory') + help='set the destination directory') cmd.parser.add_option('-f', '--format', action='store', dest='format', - help=u'set the target format of the tracks') + help='set the target format of the tracks') cmd.parser.add_option('-y', '--yes', action='store_true', dest='yes', - help=u'do not ask for confirmation') + help='do not ask for confirmation') cmd.parser.add_option('-l', '--link', action='store_true', dest='link', - help=u'symlink files that do not \ + help='symlink files that do not \ need transcoding.') cmd.parser.add_option('-H', '--hardlink', action='store_true', dest='hardlink', - help=u'hardlink files that do not \ + help='hardlink files that do not \ need transcoding. Overrides --link.') cmd.parser.add_album_option() cmd.func = self.convert_func @@ -202,11 +199,9 @@ quiet = self.config['quiet'].get(bool) if not quiet and not pretend: - self._log.info(u'Encoding {0}', util.displayable_path(source)) - - if not six.PY2: - command = command.decode(arg_encoding(), 'surrogateescape') + self._log.info('Encoding {0}', util.displayable_path(source)) + command = command.decode(arg_encoding(), 'surrogateescape') source = decode_commandline_path(source) dest = decode_commandline_path(dest) @@ -218,22 +213,19 @@ 'source': source, 'dest': dest, }) - if six.PY2: - encode_cmd.append(args[i]) - else: - encode_cmd.append(args[i].encode(util.arg_encoding())) + encode_cmd.append(args[i].encode(util.arg_encoding())) if pretend: - self._log.info(u'{0}', u' '.join(ui.decargs(args))) + self._log.info('{0}', ' '.join(ui.decargs(args))) return try: util.command_output(encode_cmd) except subprocess.CalledProcessError as exc: # Something went wrong (probably Ctrl+C), remove temporary files - self._log.info(u'Encoding {0} failed. Cleaning up...', + self._log.info('Encoding {0} failed. Cleaning up...', util.displayable_path(source)) - self._log.debug(u'Command {0} exited with status {1}: {2}', + self._log.debug('Command {0} exited with status {1}: {2}', args, exc.returncode, exc.output) @@ -242,13 +234,13 @@ raise except OSError as exc: raise ui.UserError( - u"convert: couldn't invoke '{0}': {1}".format( - u' '.join(ui.decargs(args)), exc + "convert: couldn't invoke '{}': {}".format( + ' '.join(ui.decargs(args)), exc ) ) if not quiet and not pretend: - self._log.info(u'Finished encoding {0}', + self._log.info('Finished encoding {0}', util.displayable_path(source)) def convert_item(self, dest_dir, keep_new, path_formats, fmt, @@ -285,17 +277,17 @@ util.mkdirall(dest) if os.path.exists(util.syspath(dest)): - self._log.info(u'Skipping {0} (target file exists)', + self._log.info('Skipping {0} (target file exists)', util.displayable_path(item.path)) continue if keep_new: if pretend: - self._log.info(u'mv {0} {1}', + self._log.info('mv {0} {1}', util.displayable_path(item.path), util.displayable_path(original)) else: - self._log.info(u'Moving to {0}', + self._log.info('Moving to {0}', util.displayable_path(original)) util.move(item.path, original) @@ -310,7 +302,7 @@ if pretend: msg = 'ln' if hardlink else ('ln -s' if link else 'cp') - self._log.info(u'{2} {0} {1}', + self._log.info('{2} {0} {1}', util.displayable_path(original), util.displayable_path(converted), msg) @@ -319,7 +311,7 @@ msg = 'Hardlinking' if hardlink \ else ('Linking' if link else 'Copying') - self._log.info(u'{1} {0}', + self._log.info('{1} {0}', util.displayable_path(item.path), msg) @@ -350,7 +342,7 @@ if self.config['embed'] and not linked: album = item._cached_album if album and album.artpath: - self._log.debug(u'embedding album art from {}', + self._log.debug('embedding album art from {}', util.displayable_path(album.artpath)) art.embed_item(self._log, item, album.artpath, itempath=converted, id3v23=id3v23) @@ -391,7 +383,7 @@ util.mkdirall(dest) if os.path.exists(util.syspath(dest)): - self._log.info(u'Skipping {0} (target file exists)', + self._log.info('Skipping {0} (target file exists)', util.displayable_path(album.artpath)) return @@ -405,12 +397,12 @@ if size: resize = size[0] > maxwidth else: - self._log.warning(u'Could not get size of image (please see ' - u'documentation for dependencies).') + self._log.warning('Could not get size of image (please see ' + 'documentation for dependencies).') # Either copy or resize (while copying) the image. if resize: - self._log.info(u'Resizing cover art from {0} to {1}', + self._log.info('Resizing cover art from {0} to {1}', util.displayable_path(album.artpath), util.displayable_path(dest)) if not pretend: @@ -419,7 +411,7 @@ if pretend: msg = 'ln' if hardlink else ('ln -s' if link else 'cp') - self._log.info(u'{2} {0} {1}', + self._log.info('{2} {0} {1}', util.displayable_path(album.artpath), util.displayable_path(dest), msg) @@ -427,7 +419,7 @@ msg = 'Hardlinking' if hardlink \ else ('Linking' if link else 'Copying') - self._log.info(u'{2} cover art from {0} to {1}', + self._log.info('{2} cover art from {0} to {1}', util.displayable_path(album.artpath), util.displayable_path(dest), msg) @@ -441,7 +433,7 @@ def convert_func(self, lib, opts, args): dest = opts.dest or self.config['dest'].get() if not dest: - raise ui.UserError(u'no convert destination set') + raise ui.UserError('no convert destination set') dest = util.bytestring_path(dest) threads = opts.threads or self.config['threads'].get(int) @@ -470,17 +462,17 @@ items = [i for a in albums for i in a.items()] if not pretend: for a in albums: - ui.print_(format(a, u'')) + ui.print_(format(a, '')) else: items = list(lib.items(ui.decargs(args))) if not pretend: for i in items: - ui.print_(format(i, u'')) + ui.print_(format(i, '')) if not items: - self._log.error(u'Empty query result.') + self._log.error('Empty query result.') return - if not (pretend or opts.yes or ui.input_yn(u"Convert? (Y/n)")): + if not (pretend or opts.yes or ui.input_yn("Convert? (Y/n)")): return if opts.album and self.config['copy_album_art']: @@ -531,7 +523,7 @@ item.store() if self.config['delete_originals']: - self._log.info(u'Removing original file {0}', source_path) + self._log.info('Removing original file {0}', source_path) util.remove(source_path, False) def _cleanup(self, task, session): diff -Nru beets-1.5.0/beetsplug/deezer.py beets-1.6.0/beetsplug/deezer.py --- beets-1.5.0/beetsplug/deezer.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/deezer.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2019, Rahul Ahuja. # @@ -15,11 +14,9 @@ """Adds Deezer release and track search support to the autotagger """ -from __future__ import absolute_import, print_function, division import collections -import six import unidecode import requests @@ -43,7 +40,7 @@ } def __init__(self): - super(DeezerPlugin, self).__init__() + super().__init__() def album_for_id(self, album_id): """Fetch an album by its Deezer ID or URL and return an @@ -76,8 +73,8 @@ day = None else: raise ui.UserError( - u"Invalid `release_date` returned " - u"by {} API: '{}'".format(self.data_source, release_date) + "Invalid `release_date` returned " + "by {} API: '{}'".format(self.data_source, release_date) ) tracks_data = requests.get( @@ -188,10 +185,10 @@ """ query_components = [ keywords, - ' '.join('{}:"{}"'.format(k, v) for k, v in filters.items()), + ' '.join(f'{k}:"{v}"' for k, v in filters.items()), ] query = ' '.join([q for q in query_components if q]) - if not isinstance(query, six.text_type): + if not isinstance(query, str): query = query.decode('utf8') return unidecode.unidecode(query) @@ -217,7 +214,7 @@ if not query: return None self._log.debug( - u"Searching {} for '{}'".format(self.data_source, query) + f"Searching {self.data_source} for '{query}'" ) response = requests.get( self.search_url + query_type, params={'q': query} @@ -225,7 +222,7 @@ response.raise_for_status() response_data = response.json().get('data', []) self._log.debug( - u"Found {} result(s) from {} for '{}'", + "Found {} result(s) from {} for '{}'", len(response_data), self.data_source, query, diff -Nru beets-1.5.0/beetsplug/discogs.py beets-1.6.0/beetsplug/discogs.py --- beets-1.5.0/beetsplug/discogs.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/discogs.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Adds Discogs album search support to the autotagger. Requires the python3-discogs-client library. """ -from __future__ import division, absolute_import, print_function import beets.ui from beets import config @@ -26,7 +24,7 @@ from discogs_client import Release, Master, Client from discogs_client.exceptions import DiscogsAPIError from requests.exceptions import ConnectionError -from six.moves import http_client +import http.client import beets import re import time @@ -37,12 +35,12 @@ from string import ascii_lowercase -USER_AGENT = u'beets/{0} +https://beets.io/'.format(beets.__version__) +USER_AGENT = f'beets/{beets.__version__} +https://beets.io/' API_KEY = 'rAzVUQYRaoFjeBjyWuWZ' API_SECRET = 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy' # Exceptions that discogs_client should really handle but does not. -CONNECTION_ERRORS = (ConnectionError, socket.error, http_client.HTTPException, +CONNECTION_ERRORS = (ConnectionError, socket.error, http.client.HTTPException, ValueError, # JSON decoding raises a ValueError. DiscogsAPIError) @@ -50,14 +48,14 @@ class DiscogsPlugin(BeetsPlugin): def __init__(self): - super(DiscogsPlugin, self).__init__() + super().__init__() self.config.add({ 'apikey': API_KEY, 'apisecret': API_SECRET, 'tokenfile': 'discogs_token.json', 'source_weight': 0.5, 'user_token': '', - 'separator': u', ', + 'separator': ', ', 'index_tracks': False, }) self.config['apikey'].redact = True @@ -65,8 +63,6 @@ self.config['user_token'].redact = True self.discogs_client = None self.register_listener('import_begin', self.setup) - self.rate_limit_per_minute = 25 - self.last_request_timestamp = 0 def setup(self, session=None): """Create the `discogs_client` field. Authenticate if necessary. @@ -79,7 +75,6 @@ if user_token: # The rate limit for authenticated users goes up to 60 # requests per minute. - self.rate_limit_per_minute = 60 self.discogs_client = Client(USER_AGENT, user_token=user_token) return @@ -87,7 +82,7 @@ try: with open(self._tokenfile()) as f: tokendata = json.load(f) - except IOError: + except OSError: # No token yet. Generate one. token, secret = self.authenticate(c_key, c_secret) else: @@ -97,26 +92,6 @@ self.discogs_client = Client(USER_AGENT, c_key, c_secret, token, secret) - def _time_to_next_request(self): - seconds_between_requests = 60 / self.rate_limit_per_minute - seconds_since_last_request = time.time() - self.last_request_timestamp - seconds_to_wait = seconds_between_requests - seconds_since_last_request - return seconds_to_wait - - def request_start(self): - """wait for rate limit if needed - """ - time_to_next_request = self._time_to_next_request() - if time_to_next_request > 0: - self._log.debug('hit rate limit, waiting for {0} seconds', - time_to_next_request) - time.sleep(time_to_next_request) - - def request_finished(self): - """update timestamp for rate limiting - """ - self.last_request_timestamp = time.time() - def reset_auth(self): """Delete token file & redo the auth steps. """ @@ -134,24 +109,24 @@ try: _, _, url = auth_client.get_authorize_url() except CONNECTION_ERRORS as e: - self._log.debug(u'connection error: {0}', e) - raise beets.ui.UserError(u'communication with Discogs failed') + self._log.debug('connection error: {0}', e) + raise beets.ui.UserError('communication with Discogs failed') - beets.ui.print_(u"To authenticate with Discogs, visit:") + beets.ui.print_("To authenticate with Discogs, visit:") beets.ui.print_(url) # Ask for the code and validate it. - code = beets.ui.input_(u"Enter the code:") + code = beets.ui.input_("Enter the code:") try: token, secret = auth_client.get_access_token(code) except DiscogsAPIError: - raise beets.ui.UserError(u'Discogs authorization failed') + raise beets.ui.UserError('Discogs authorization failed') except CONNECTION_ERRORS as e: - self._log.debug(u'connection error: {0}', e) - raise beets.ui.UserError(u'Discogs token request failed') + self._log.debug('connection error: {0}', e) + raise beets.ui.UserError('Discogs token request failed') # Save the token for later use. - self._log.debug(u'Discogs token {0}, secret {1}', token, secret) + self._log.debug('Discogs token {0}, secret {1}', token, secret) with open(self._tokenfile(), 'w') as f: json.dump({'token': token, 'secret': secret}, f) @@ -185,20 +160,45 @@ if va_likely: query = album else: - query = '%s %s' % (artist, album) + query = f'{artist} {album}' try: return self.get_albums(query) except DiscogsAPIError as e: - self._log.debug(u'API Error: {0} (query: {1})', e, query) + self._log.debug('API Error: {0} (query: {1})', e, query) if e.status_code == 401: self.reset_auth() return self.candidates(items, artist, album, va_likely) else: return [] except CONNECTION_ERRORS: - self._log.debug(u'Connection error in album search', exc_info=True) + self._log.debug('Connection error in album search', exc_info=True) return [] + @staticmethod + def extract_release_id_regex(album_id): + """Returns the Discogs_id or None.""" + # Discogs-IDs are simple integers. In order to avoid confusion with + # other metadata plugins, we only look for very specific formats of the + # input string: + # - plain integer, optionally wrapped in brackets and prefixed by an + # 'r', as this is how discogs displays the release ID on its webpage. + # - legacy url format: discogs.com//release/ + # - current url format: discogs.com/release/- + # See #291, #4080 and #4085 for the discussions leading up to these + # patterns. + # Regex has been tested here https://regex101.com/r/wyLdB4/2 + + for pattern in [ + r'^\[?r?(?P\d+)\]?$', + r'discogs\.com/release/(?P\d+)-', + r'discogs\.com/[^/]+/release/(?P\d+)', + ]: + match = re.search(pattern, album_id) + if match: + return int(match.group('id')) + + return None + def album_for_id(self, album_id): """Fetches an album by its Discogs ID and returns an AlbumInfo object or None if the album is not found. @@ -206,29 +206,28 @@ if not self.discogs_client: return - self._log.debug(u'Searching for release {0}', album_id) - # Discogs-IDs are simple integers. We only look for those at the end - # of an input string as to avoid confusion with other metadata plugins. - # An optional bracket can follow the integer, as this is how discogs - # displays the release ID on its webpage. - match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])', - album_id) - if not match: + self._log.debug('Searching for release {0}', album_id) + + discogs_id = self.extract_release_id_regex(album_id) + + if not discogs_id: return None - result = Release(self.discogs_client, {'id': int(match.group(2))}) + + result = Release(self.discogs_client, {'id': discogs_id}) # Try to obtain title to verify that we indeed have a valid Release try: getattr(result, 'title') except DiscogsAPIError as e: if e.status_code != 404: - self._log.debug(u'API Error: {0} (query: {1})', e, + self._log.debug('API Error: {0} (query: {1})', e, result.data['resource_url']) if e.status_code == 401: self.reset_auth() return self.album_for_id(album_id) return None except CONNECTION_ERRORS: - self._log.debug(u'Connection error in album lookup', exc_info=True) + self._log.debug('Connection error in album lookup', + exc_info=True) return None return self.get_album_info(result) @@ -244,14 +243,12 @@ # can also negate an otherwise positive result. query = re.sub(r'(?i)\b(CD|disc)\s*\d+', '', query) - self.request_start() try: releases = self.discogs_client.search(query, type='release').page(1) - self.request_finished() except CONNECTION_ERRORS: - self._log.debug(u"Communication error while searching for {0!r}", + self._log.debug("Communication error while searching for {0!r}", query, exc_info=True) return [] return [album for album in map(self.get_album_info, releases[:5]) @@ -261,24 +258,22 @@ """Fetches a master release given its Discogs ID and returns its year or None if the master release is not found. """ - self._log.debug(u'Searching for master release {0}', master_id) + self._log.debug('Searching for master release {0}', master_id) result = Master(self.discogs_client, {'id': master_id}) - self.request_start() try: year = result.fetch('year') - self.request_finished() return year except DiscogsAPIError as e: if e.status_code != 404: - self._log.debug(u'API Error: {0} (query: {1})', e, + self._log.debug('API Error: {0} (query: {1})', e, result.data['resource_url']) if e.status_code == 401: self.reset_auth() return self.get_master_year(master_id) return None except CONNECTION_ERRORS: - self._log.debug(u'Connection error in master release lookup', + self._log.debug('Connection error in master release lookup', exc_info=True) return None @@ -297,7 +292,7 @@ # https://www.discogs.com/help/doc/submission-guidelines-general-rules if not all([result.data.get(k) for k in ['artists', 'title', 'id', 'tracklist']]): - self._log.warning(u"Release does not contain the required fields") + self._log.warning("Release does not contain the required fields") return None artist, artist_id = MetadataSourcePlugin.get_artist( @@ -386,8 +381,8 @@ # FIXME: this is an extra precaution for making sure there are no # side effects after #2222. It should be removed after further # testing. - self._log.debug(u'{}', traceback.format_exc()) - self._log.error(u'uncaught exception in coalesce_tracks: {}', exc) + self._log.debug('{}', traceback.format_exc()) + self._log.error('uncaught exception in coalesce_tracks: {}', exc) clean_tracklist = tracklist tracks = [] index_tracks = {} @@ -425,7 +420,7 @@ # If a medium has two sides (ie. vinyl or cassette), each pair of # consecutive sides should belong to the same medium. if all([track.medium is not None for track in tracks]): - m = sorted(set([track.medium.lower() for track in tracks])) + m = sorted({track.medium.lower() for track in tracks}) # If all track.medium are single consecutive letters, assume it is # a 2-sided medium. if ''.join(m) in ascii_lowercase: @@ -484,7 +479,7 @@ # Calculate position based on first subtrack, without subindex. idx, medium_idx, sub_idx = \ self.get_track_index(subtracks[0]['position']) - position = '%s%s' % (idx or '', medium_idx or '') + position = '{}{}'.format(idx or '', medium_idx or '') if tracklist and not tracklist[-1]['position']: # Assume the previous index track contains the track title. @@ -507,7 +502,7 @@ if self.config['index_tracks']: for subtrack in subtracks: subtrack['title'] = '{}: {}'.format( - index_track['title'], subtrack['title']) + index_track['title'], subtrack['title']) tracklist.extend(subtracks) else: # Merge the subtracks, pick a title, and append the new track. @@ -561,7 +556,7 @@ if self.config['index_tracks']: prefix = ', '.join(divisions) if prefix: - title = '{}: {}'.format(prefix, title) + title = f'{prefix}: {title}' track_id = None medium, medium_index, _ = self.get_track_index(track['position']) artist, artist_id = MetadataSourcePlugin.get_artist( @@ -597,7 +592,7 @@ if subindex and subindex.startswith('.'): subindex = subindex[1:] else: - self._log.debug(u'Invalid position: {0}', position) + self._log.debug('Invalid position: {0}', position) medium = index = subindex = None return medium or None, index or None, subindex or None diff -Nru beets-1.5.0/beetsplug/duplicates.py beets-1.6.0/beetsplug/duplicates.py --- beets-1.5.0/beetsplug/duplicates.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beetsplug/duplicates.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Pedro Silva. # @@ -15,10 +14,8 @@ """List duplicate tracks or albums. """ -from __future__ import division, absolute_import, print_function import shlex -import six from beets.plugins import BeetsPlugin from beets.ui import decargs, print_, Subcommand, UserError @@ -34,7 +31,7 @@ """List duplicate tracks or albums """ def __init__(self): - super(DuplicatesPlugin, self).__init__() + super().__init__() self.config.add({ 'album': False, @@ -57,54 +54,54 @@ help=__doc__, aliases=['dup']) self._command.parser.add_option( - u'-c', u'--count', dest='count', + '-c', '--count', dest='count', action='store_true', - help=u'show duplicate counts', + help='show duplicate counts', ) self._command.parser.add_option( - u'-C', u'--checksum', dest='checksum', + '-C', '--checksum', dest='checksum', action='store', metavar='PROG', - help=u'report duplicates based on arbitrary command', + help='report duplicates based on arbitrary command', ) self._command.parser.add_option( - u'-d', u'--delete', dest='delete', + '-d', '--delete', dest='delete', action='store_true', - help=u'delete items from library and disk', + help='delete items from library and disk', ) self._command.parser.add_option( - u'-F', u'--full', dest='full', + '-F', '--full', dest='full', action='store_true', - help=u'show all versions of duplicate tracks or albums', + help='show all versions of duplicate tracks or albums', ) self._command.parser.add_option( - u'-s', u'--strict', dest='strict', + '-s', '--strict', dest='strict', action='store_true', - help=u'report duplicates only if all attributes are set', + help='report duplicates only if all attributes are set', ) self._command.parser.add_option( - u'-k', u'--key', dest='keys', + '-k', '--key', dest='keys', action='append', metavar='KEY', - help=u'report duplicates based on keys (use multiple times)', + help='report duplicates based on keys (use multiple times)', ) self._command.parser.add_option( - u'-M', u'--merge', dest='merge', + '-M', '--merge', dest='merge', action='store_true', - help=u'merge duplicate items', + help='merge duplicate items', ) self._command.parser.add_option( - u'-m', u'--move', dest='move', + '-m', '--move', dest='move', action='store', metavar='DEST', - help=u'move items to dest', + help='move items to dest', ) self._command.parser.add_option( - u'-o', u'--copy', dest='copy', + '-o', '--copy', dest='copy', action='store', metavar='DEST', - help=u'copy items to dest', + help='copy items to dest', ) self._command.parser.add_option( - u'-t', u'--tag', dest='tag', + '-t', '--tag', dest='tag', action='store', - help=u'tag matched items with \'k=v\' attribute', + help='tag matched items with \'k=v\' attribute', ) self._command.parser.add_all_common_options() @@ -142,15 +139,15 @@ return if path: - fmt = u'$path' + fmt = '$path' # Default format string for count mode. if count and not fmt: if album: - fmt = u'$albumartist - $album' + fmt = '$albumartist - $album' else: - fmt = u'$albumartist - $album - $title' - fmt += u': {0}' + fmt = '$albumartist - $album - $title' + fmt += ': {0}' if checksum: for i in items: @@ -176,7 +173,7 @@ return [self._command] def _process_item(self, item, copy=False, move=False, delete=False, - tag=False, fmt=u''): + tag=False, fmt=''): """Process Item `item`. """ print_(format(item, fmt)) @@ -193,7 +190,7 @@ k, v = tag.split('=') except Exception: raise UserError( - u"{}: can't parse k=v tag: {}".format(PLUGIN, tag) + f"{PLUGIN}: can't parse k=v tag: {tag}" ) setattr(item, k, v) item.store() @@ -208,21 +205,21 @@ key = args[0] checksum = getattr(item, key, False) if not checksum: - self._log.debug(u'key {0} on item {1} not cached:' - u'computing checksum', + self._log.debug('key {0} on item {1} not cached:' + 'computing checksum', key, displayable_path(item.path)) try: checksum = command_output(args).stdout setattr(item, key, checksum) item.store() - self._log.debug(u'computed checksum for {0} using {1}', + self._log.debug('computed checksum for {0} using {1}', item.title, key) except subprocess.CalledProcessError as e: - self._log.debug(u'failed to checksum {0}: {1}', + self._log.debug('failed to checksum {0}: {1}', displayable_path(item.path), e) else: - self._log.debug(u'key {0} on item {1} cached:' - u'not computing checksum', + self._log.debug('key {0} on item {1} cached:' + 'not computing checksum', key, displayable_path(item.path)) return key, checksum @@ -238,12 +235,12 @@ values = [getattr(obj, k, None) for k in keys] values = [v for v in values if v not in (None, '')] if strict and len(values) < len(keys): - self._log.debug(u'some keys {0} on item {1} are null or empty:' - u' skipping', + self._log.debug('some keys {0} on item {1} are null or empty:' + ' skipping', keys, displayable_path(obj.path)) elif (not strict and not len(values)): - self._log.debug(u'all keys {0} on item {1} are null or empty:' - u' skipping', + self._log.debug('all keys {0} on item {1} are null or empty:' + ' skipping', keys, displayable_path(obj.path)) else: key = tuple(values) @@ -271,7 +268,7 @@ # between a bytes object and the empty Unicode # string ''. return v is not None and \ - (v != '' if isinstance(v, six.text_type) else True) + (v != '' if isinstance(v, str) else True) fields = Item.all_keys() key = lambda x: sum(1 for f in fields if truthy(getattr(x, f))) else: @@ -291,8 +288,8 @@ if getattr(objs[0], f, None) in (None, ''): value = getattr(o, f, None) if value: - self._log.debug(u'key {0} on item {1} is null ' - u'or empty: setting from item {2}', + self._log.debug('key {0} on item {1} is null ' + 'or empty: setting from item {2}', f, displayable_path(objs[0].path), displayable_path(o.path)) setattr(objs[0], f, value) @@ -312,8 +309,8 @@ missing = Item.from_path(i.path) missing.album_id = objs[0].id missing.add(i._db) - self._log.debug(u'item {0} missing from album {1}:' - u' merging from {2} into {3}', + self._log.debug('item {0} missing from album {1}:' + ' merging from {2} into {3}', missing, objs[0], displayable_path(o.path), diff -Nru beets-1.5.0/beetsplug/edit.py beets-1.6.0/beetsplug/edit.py --- beets-1.5.0/beetsplug/edit.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/edit.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016 # @@ -15,7 +14,6 @@ """Open metadata information in a text editor to let the user edit it. """ -from __future__ import division, absolute_import, print_function from beets import plugins from beets import util @@ -28,7 +26,7 @@ import yaml from tempfile import NamedTemporaryFile import os -import six +import shlex # These "safe" types can avoid the format/parse cycle that most fields go @@ -45,13 +43,13 @@ def edit(filename, log): """Open `filename` in a text editor. """ - cmd = util.shlex_split(util.editor_command()) + cmd = shlex.split(util.editor_command()) cmd.append(filename) - log.debug(u'invoking editor command: {!r}', cmd) + log.debug('invoking editor command: {!r}', cmd) try: subprocess.call(cmd) except OSError as exc: - raise ui.UserError(u'could not run editor command {!r}: {}'.format( + raise ui.UserError('could not run editor command {!r}: {}'.format( cmd[0], exc )) @@ -77,17 +75,17 @@ for d in yaml.safe_load_all(s): if not isinstance(d, dict): raise ParseError( - u'each entry must be a dictionary; found {}'.format( + 'each entry must be a dictionary; found {}'.format( type(d).__name__ ) ) # Convert all keys to strings. They started out as strings, # but the user may have inadvertently messed this up. - out.append({six.text_type(k): v for k, v in d.items()}) + out.append({str(k): v for k, v in d.items()}) except yaml.YAMLError as e: - raise ParseError(u'invalid YAML: {}'.format(e)) + raise ParseError(f'invalid YAML: {e}') return out @@ -143,13 +141,13 @@ else: # Either the field was stringified originally or the user changed # it from a safe type to an unsafe one. Parse it as a string. - obj.set_parse(key, six.text_type(value)) + obj.set_parse(key, str(value)) class EditPlugin(plugins.BeetsPlugin): def __init__(self): - super(EditPlugin, self).__init__() + super().__init__() self.config.add({ # The default fields to edit. @@ -166,18 +164,18 @@ def commands(self): edit_command = ui.Subcommand( 'edit', - help=u'interactively edit metadata' + help='interactively edit metadata' ) edit_command.parser.add_option( - u'-f', u'--field', + '-f', '--field', metavar='FIELD', action='append', - help=u'edit this field also', + help='edit this field also', ) edit_command.parser.add_option( - u'--all', + '--all', action='store_true', dest='all', - help=u'edit all fields', + help='edit all fields', ) edit_command.parser.add_album_option() edit_command.func = self._edit_command @@ -191,7 +189,7 @@ items, albums = _do_query(lib, query, opts.album, False) objs = albums if opts.album else items if not objs: - ui.print_(u'Nothing to edit.') + ui.print_('Nothing to edit.') return # Get the fields to edit. @@ -244,15 +242,10 @@ old_data = [flatten(o, fields) for o in objs] # Set up a temporary file with the initial data for editing. - if six.PY2: - new = NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) - else: - new = NamedTemporaryFile(mode='w', suffix='.yaml', delete=False, - encoding='utf-8') + new = NamedTemporaryFile(mode='w', suffix='.yaml', delete=False, + encoding='utf-8') old_str = dump(old_data) new.write(old_str) - if six.PY2: - old_str = old_str.decode('utf-8') new.close() # Loop until we have parseable data and the user confirms. @@ -266,15 +259,15 @@ with codecs.open(new.name, encoding='utf-8') as f: new_str = f.read() if new_str == old_str: - ui.print_(u"No changes; aborting.") + ui.print_("No changes; aborting.") return False # Parse the updated data. try: new_data = load(new_str) except ParseError as e: - ui.print_(u"Could not read data: {}".format(e)) - if ui.input_yn(u"Edit again to fix? (Y/n)", True): + ui.print_(f"Could not read data: {e}") + if ui.input_yn("Edit again to fix? (Y/n)", True): continue else: return False @@ -289,18 +282,18 @@ for obj, obj_old in zip(objs, objs_old): changed |= ui.show_model_changes(obj, obj_old) if not changed: - ui.print_(u'No changes to apply.') + ui.print_('No changes to apply.') return False # Confirm the changes. choice = ui.input_options( - (u'continue Editing', u'apply', u'cancel') + ('continue Editing', 'apply', 'cancel') ) - if choice == u'a': # Apply. + if choice == 'a': # Apply. return True - elif choice == u'c': # Cancel. + elif choice == 'c': # Cancel. return False - elif choice == u'e': # Keep editing. + elif choice == 'e': # Keep editing. # Reset the temporary changes to the objects. I we have a # copy from above, use that, else reload from the database. objs = [(old_obj or obj) @@ -322,7 +315,7 @@ are temporary. """ if len(old_data) != len(new_data): - self._log.warning(u'number of objects changed from {} to {}', + self._log.warning('number of objects changed from {} to {}', len(old_data), len(new_data)) obj_by_id = {o.id: o for o in objs} @@ -333,7 +326,7 @@ forbidden = False for key in ignore_fields: if old_dict.get(key) != new_dict.get(key): - self._log.warning(u'ignoring object whose {} changed', key) + self._log.warning('ignoring object whose {} changed', key) forbidden = True break if forbidden: @@ -348,7 +341,7 @@ # Save to the database and possibly write tags. for ob in objs: if ob._dirty: - self._log.debug(u'saving changes to {}', ob) + self._log.debug('saving changes to {}', ob) ob.try_sync(ui.should_write(), ui.should_move()) # Methods for interactive importer execution. diff -Nru beets-1.5.0/beetsplug/embedart.py beets-1.6.0/beetsplug/embedart.py --- beets-1.5.0/beetsplug/embedart.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beetsplug/embedart.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,7 +13,6 @@ # included in all copies or substantial portions of the Software. """Allows beets to embed album art into file metadata.""" -from __future__ import division, absolute_import, print_function import os.path @@ -34,11 +32,11 @@ `album` is a Boolean indicating whether these are albums (as opposed to items). """ - noun = u'album' if album else u'file' - prompt = u'Modify artwork for {} {}{} (Y/n)?'.format( + noun = 'album' if album else 'file' + prompt = 'Modify artwork for {} {}{} (Y/n)?'.format( len(objs), noun, - u's' if len(objs) > 1 else u'' + 's' if len(objs) > 1 else '' ) # Show all the items or albums. @@ -53,7 +51,7 @@ """Allows albumart to be embedded into the actual files. """ def __init__(self): - super(EmbedCoverArtPlugin, self).__init__() + super().__init__() self.config.add({ 'maxwidth': 0, 'auto': True, @@ -65,26 +63,26 @@ if self.config['maxwidth'].get(int) and not ArtResizer.shared.local: self.config['maxwidth'] = 0 - self._log.warning(u"ImageMagick or PIL not found; " - u"'maxwidth' option ignored") + self._log.warning("ImageMagick or PIL not found; " + "'maxwidth' option ignored") if self.config['compare_threshold'].get(int) and not \ ArtResizer.shared.can_compare: self.config['compare_threshold'] = 0 - self._log.warning(u"ImageMagick 6.8.7 or higher not installed; " - u"'compare_threshold' option ignored") + self._log.warning("ImageMagick 6.8.7 or higher not installed; " + "'compare_threshold' option ignored") self.register_listener('art_set', self.process_album) def commands(self): # Embed command. embed_cmd = ui.Subcommand( - 'embedart', help=u'embed image files into file metadata' + 'embedart', help='embed image files into file metadata' ) embed_cmd.parser.add_option( - u'-f', u'--file', metavar='PATH', help=u'the image file to embed' + '-f', '--file', metavar='PATH', help='the image file to embed' ) embed_cmd.parser.add_option( - u"-y", u"--yes", action="store_true", help=u"skip confirmation" + "-y", "--yes", action="store_true", help="skip confirmation" ) maxwidth = self.config['maxwidth'].get(int) quality = self.config['quality'].get(int) @@ -95,7 +93,7 @@ if opts.file: imagepath = normpath(opts.file) if not os.path.isfile(syspath(imagepath)): - raise ui.UserError(u'image file {0} not found'.format( + raise ui.UserError('image file {} not found'.format( displayable_path(imagepath) )) @@ -127,15 +125,15 @@ # Extract command. extract_cmd = ui.Subcommand( 'extractart', - help=u'extract an image from file metadata', + help='extract an image from file metadata', ) extract_cmd.parser.add_option( - u'-o', dest='outpath', - help=u'image output file', + '-o', dest='outpath', + help='image output file', ) extract_cmd.parser.add_option( - u'-n', dest='filename', - help=u'image filename to create for all matched albums', + '-n', dest='filename', + help='image filename to create for all matched albums', ) extract_cmd.parser.add_option( '-a', dest='associate', action='store_true', @@ -151,7 +149,7 @@ config['art_filename'].get()) if os.path.dirname(filename) != b'': self._log.error( - u"Only specify a name rather than a path for -n") + "Only specify a name rather than a path for -n") return for album in lib.albums(decargs(args)): artpath = normpath(os.path.join(album.path, filename)) @@ -165,10 +163,10 @@ # Clear command. clear_cmd = ui.Subcommand( 'clearart', - help=u'remove images from file metadata', + help='remove images from file metadata', ) clear_cmd.parser.add_option( - u"-y", u"--yes", action="store_true", help=u"skip confirmation" + "-y", "--yes", action="store_true", help="skip confirmation" ) def clear_func(lib, opts, args): @@ -197,7 +195,7 @@ """ if self.config['remove_art_file'] and album.artpath: if os.path.isfile(album.artpath): - self._log.debug(u'Removing album art file for {0}', album) + self._log.debug('Removing album art file for {0}', album) os.remove(album.artpath) album.artpath = None album.store() diff -Nru beets-1.5.0/beetsplug/embyupdate.py beets-1.6.0/beetsplug/embyupdate.py --- beets-1.5.0/beetsplug/embyupdate.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/embyupdate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Updates the Emby Library whenever the beets library is changed. emby: @@ -9,14 +7,11 @@ apikey: apikey password: password """ -from __future__ import division, absolute_import, print_function import hashlib import requests -from six.moves.urllib.parse import urlencode -from six.moves.urllib.parse import urljoin, parse_qs, urlsplit, urlunsplit - +from urllib.parse import urlencode, urljoin, parse_qs, urlsplit, urlunsplit from beets import config from beets.plugins import BeetsPlugin @@ -146,14 +141,14 @@ class EmbyUpdate(BeetsPlugin): def __init__(self): - super(EmbyUpdate, self).__init__() + super().__init__() # Adding defaults. config['emby'].add({ - u'host': u'http://localhost', - u'port': 8096, - u'apikey': None, - u'password': None, + 'host': 'http://localhost', + 'port': 8096, + 'apikey': None, + 'password': None, }) self.register_listener('database_change', self.listen_for_db_change) @@ -166,7 +161,7 @@ def update(self, lib): """When the client exists try to send refresh request to Emby. """ - self._log.info(u'Updating Emby library...') + self._log.info('Updating Emby library...') host = config['emby']['host'].get() port = config['emby']['port'].get() @@ -176,13 +171,13 @@ # Check if at least a apikey or password is given. if not any([password, token]): - self._log.warning(u'Provide at least Emby password or apikey.') + self._log.warning('Provide at least Emby password or apikey.') return # Get user information from the Emby API. user = get_user(host, port, username) if not user: - self._log.warning(u'User {0} could not be found.'.format(username)) + self._log.warning(f'User {username} could not be found.') return if not token: @@ -194,7 +189,7 @@ token = get_token(host, port, headers, auth_data) if not token: self._log.warning( - u'Could not get token for user {0}', username + 'Could not get token for user {0}', username ) return @@ -205,6 +200,6 @@ url = api_url(host, port, '/Library/Refresh') r = requests.post(url, headers=headers) if r.status_code != 204: - self._log.warning(u'Update could not be triggered') + self._log.warning('Update could not be triggered') else: - self._log.info(u'Update triggered.') + self._log.info('Update triggered.') diff -Nru beets-1.5.0/beetsplug/export.py beets-1.6.0/beetsplug/export.py --- beets-1.5.0/beetsplug/export.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beetsplug/export.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # # Permission is hereby granted, free of charge, to any person obtaining @@ -15,7 +14,6 @@ """Exports data from beets """ -from __future__ import division, absolute_import, print_function import sys import codecs @@ -42,7 +40,7 @@ class ExportPlugin(BeetsPlugin): def __init__(self): - super(ExportPlugin, self).__init__() + super().__init__() self.config.add({ 'default_format': 'json', @@ -81,30 +79,32 @@ }) def commands(self): - # TODO: Add option to use albums - - cmd = ui.Subcommand('export', help=u'export data from beets') + cmd = ui.Subcommand('export', help='export data from beets') cmd.func = self.run cmd.parser.add_option( - u'-l', u'--library', action='store_true', - help=u'show library fields instead of tags', + '-l', '--library', action='store_true', + help='show library fields instead of tags', + ) + cmd.parser.add_option( + '-a', '--album', action='store_true', + help='show album fields instead of tracks (implies "--library")', ) cmd.parser.add_option( - u'--append', action='store_true', default=False, - help=u'if should append data to the file', + '--append', action='store_true', default=False, + help='if should append data to the file', ) cmd.parser.add_option( - u'-i', u'--include-keys', default=[], + '-i', '--include-keys', default=[], action='append', dest='included_keys', - help=u'comma separated list of keys to show', + help='comma separated list of keys to show', ) cmd.parser.add_option( - u'-o', u'--output', - help=u'path for the output file. If not given, will print the data' + '-o', '--output', + help='path for the output file. If not given, will print the data' ) cmd.parser.add_option( - u'-f', u'--format', default='json', - help=u"the output format: json (default), jsonlines, csv, or xml" + '-f', '--format', default='json', + help="the output format: json (default), jsonlines, csv, or xml" ) return [cmd] @@ -123,26 +123,30 @@ } ) - items = [] - data_collector = library_data if opts.library else tag_data + if opts.library or opts.album: + data_collector = library_data + else: + data_collector = tag_data included_keys = [] for keys in opts.included_keys: included_keys.extend(keys.split(',')) - for data_emitter in data_collector(lib, ui.decargs(args)): + items = [] + for data_emitter in data_collector( + lib, ui.decargs(args), + album=opts.album, + ): try: data, item = data_emitter(included_keys or '*') - except (mediafile.UnreadableFileError, IOError) as ex: - self._log.error(u'cannot read file: {0}', ex) + except (mediafile.UnreadableFileError, OSError) as ex: + self._log.error('cannot read file: {0}', ex) continue for key, value in data.items(): if isinstance(value, bytes): data[key] = util.displayable_path(value) - items += [data] - if file_format_is_line_based: export_format.export(data, **format_options) else: @@ -152,9 +156,9 @@ export_format.export(items, **format_options) -class ExportFormat(object): +class ExportFormat: """The output format type""" - def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): + def __init__(self, file_path, file_mode='w', encoding='utf-8'): self.path = file_path self.mode = file_mode self.encoding = encoding @@ -179,8 +183,8 @@ class JsonFormat(ExportFormat): """Saves in a json file""" - def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): - super(JsonFormat, self).__init__(file_path, file_mode, encoding) + def __init__(self, file_path, file_mode='w', encoding='utf-8'): + super().__init__(file_path, file_mode, encoding) def export(self, data, **kwargs): json.dump(data, self.out_stream, cls=ExportEncoder, **kwargs) @@ -189,8 +193,8 @@ class CSVFormat(ExportFormat): """Saves in a csv file""" - def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): - super(CSVFormat, self).__init__(file_path, file_mode, encoding) + def __init__(self, file_path, file_mode='w', encoding='utf-8'): + super().__init__(file_path, file_mode, encoding) def export(self, data, **kwargs): header = list(data[0].keys()) if data else [] @@ -201,16 +205,16 @@ class XMLFormat(ExportFormat): """Saves in a xml file""" - def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): - super(XMLFormat, self).__init__(file_path, file_mode, encoding) + def __init__(self, file_path, file_mode='w', encoding='utf-8'): + super().__init__(file_path, file_mode, encoding) def export(self, data, **kwargs): # Creates the XML file structure. - library = ElementTree.Element(u'library') - tracks = ElementTree.SubElement(library, u'tracks') + library = ElementTree.Element('library') + tracks = ElementTree.SubElement(library, 'tracks') if data and isinstance(data[0], dict): for index, item in enumerate(data): - track = ElementTree.SubElement(tracks, u'track') + track = ElementTree.SubElement(tracks, 'track') for key, value in item.items(): track_details = ElementTree.SubElement(track, key) track_details.text = value diff -Nru beets-1.5.0/beetsplug/fetchart.py beets-1.6.0/beetsplug/fetchart.py --- beets-1.5.0/beetsplug/fetchart.py 2021-03-28 18:23:15.000000000 +0000 +++ beets-1.6.0/beetsplug/fetchart.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """Fetches album art. """ -from __future__ import division, absolute_import, print_function from contextlib import closing import os @@ -35,7 +33,6 @@ from beets.util import sorted_walk from beets.util import syspath, bytestring_path, py3_path import confuse -import six CONTENT_TYPES = { 'image/jpeg': [b'jpg', b'jpeg'], @@ -44,7 +41,7 @@ IMAGE_EXTENSIONS = [ext for exts in CONTENT_TYPES.values() for ext in exts] -class Candidate(object): +class Candidate: """Holds information about a matching artwork, deals with validation of dimension restrictions and resizing. """ @@ -52,11 +49,13 @@ CANDIDATE_EXACT = 1 CANDIDATE_DOWNSCALE = 2 CANDIDATE_DOWNSIZE = 3 + CANDIDATE_DEINTERLACE = 4 + CANDIDATE_REFORMAT = 5 MATCH_EXACT = 0 MATCH_FALLBACK = 1 - def __init__(self, log, path=None, url=None, source=u'', + def __init__(self, log, path=None, url=None, source='', match=None, size=None): self._log = log self.path = path @@ -75,25 +74,28 @@ Return `CANDIDATE_DOWNSCALE` if the file must be rescaled. Return `CANDIDATE_DOWNSIZE` if the file must be resized, and possibly also rescaled. + Return `CANDIDATE_DEINTERLACE` if the file must be deinterlaced. + Return `CANDIDATE_REFORMAT` if the file has to be converted. """ if not self.path: return self.CANDIDATE_BAD if (not (plugin.enforce_ratio or plugin.minwidth or plugin.maxwidth - or plugin.max_filesize)): + or plugin.max_filesize or plugin.deinterlace + or plugin.cover_format)): return self.CANDIDATE_EXACT # get_size returns None if no local imaging backend is available if not self.size: self.size = ArtResizer.shared.get_size(self.path) - self._log.debug(u'image size: {}', self.size) + self._log.debug('image size: {}', self.size) if not self.size: - self._log.warning(u'Could not get size of image (please see ' - u'documentation for dependencies). ' - u'The configuration options `minwidth`, ' - u'`enforce_ratio` and `max_filesize` ' - u'may be violated.') + self._log.warning('Could not get size of image (please see ' + 'documentation for dependencies). ' + 'The configuration options `minwidth`, ' + '`enforce_ratio` and `max_filesize` ' + 'may be violated.') return self.CANDIDATE_EXACT short_edge = min(self.size) @@ -101,7 +103,7 @@ # Check minimum dimension. if plugin.minwidth and self.size[0] < plugin.minwidth: - self._log.debug(u'image too small ({} < {})', + self._log.debug('image too small ({} < {})', self.size[0], plugin.minwidth) return self.CANDIDATE_BAD @@ -110,27 +112,27 @@ if plugin.enforce_ratio: if plugin.margin_px: if edge_diff > plugin.margin_px: - self._log.debug(u'image is not close enough to being ' - u'square, ({} - {} > {})', + self._log.debug('image is not close enough to being ' + 'square, ({} - {} > {})', long_edge, short_edge, plugin.margin_px) return self.CANDIDATE_BAD elif plugin.margin_percent: margin_px = plugin.margin_percent * long_edge if edge_diff > margin_px: - self._log.debug(u'image is not close enough to being ' - u'square, ({} - {} > {})', + self._log.debug('image is not close enough to being ' + 'square, ({} - {} > {})', long_edge, short_edge, margin_px) return self.CANDIDATE_BAD elif edge_diff: # also reached for margin_px == 0 and margin_percent == 0.0 - self._log.debug(u'image is not square ({} != {})', + self._log.debug('image is not square ({} != {})', self.size[0], self.size[1]) return self.CANDIDATE_BAD # Check maximum dimension. downscale = False if plugin.maxwidth and self.size[0] > plugin.maxwidth: - self._log.debug(u'image needs rescaling ({} > {})', + self._log.debug('image needs rescaling ({} > {})', self.size[0], plugin.maxwidth) downscale = True @@ -139,14 +141,27 @@ if plugin.max_filesize: filesize = os.stat(syspath(self.path)).st_size if filesize > plugin.max_filesize: - self._log.debug(u'image needs resizing ({}B > {}B)', + self._log.debug('image needs resizing ({}B > {}B)', filesize, plugin.max_filesize) downsize = True + # Check image format + reformat = False + if plugin.cover_format: + fmt = ArtResizer.shared.get_format(self.path) + reformat = fmt != plugin.cover_format + if reformat: + self._log.debug('image needs reformatting: {} -> {}', + fmt, plugin.cover_format) + if downscale: return self.CANDIDATE_DOWNSCALE elif downsize: return self.CANDIDATE_DOWNSIZE + elif plugin.deinterlace: + return self.CANDIDATE_DEINTERLACE + elif reformat: + return self.CANDIDATE_REFORMAT else: return self.CANDIDATE_EXACT @@ -166,6 +181,14 @@ ArtResizer.shared.resize(max(self.size), self.path, quality=plugin.quality, max_filesize=plugin.max_filesize) + elif self.check == self.CANDIDATE_DEINTERLACE: + self.path = ArtResizer.shared.deinterlace(self.path) + elif self.check == self.CANDIDATE_REFORMAT: + self.path = ArtResizer.shared.reformat( + self.path, + plugin.cover_format, + deinterlaced=plugin.deinterlace, + ) def _logged_get(log, *args, **kwargs): @@ -206,7 +229,7 @@ return s.send(prepped, **send_kwargs) -class RequestMixin(object): +class RequestMixin: """Adds a Requests wrapper to the class that uses the logger, which must be named `self._log`. """ @@ -244,7 +267,7 @@ class LocalArtSource(ArtSource): IS_LOCAL = True - LOC_STR = u'local' + LOC_STR = 'local' def fetch_image(self, candidate, plugin): pass @@ -252,7 +275,7 @@ class RemoteArtSource(ArtSource): IS_LOCAL = False - LOC_STR = u'remote' + LOC_STR = 'remote' def fetch_image(self, candidate, plugin): """Downloads an image from a URL and checks whether it seems to @@ -264,7 +287,7 @@ candidate.url) try: with closing(self.request(candidate.url, stream=True, - message=u'downloading image')) as resp: + message='downloading image')) as resp: ct = resp.headers.get('Content-Type', None) # Download the image to a temporary file. As some servers @@ -292,16 +315,16 @@ real_ct = ct if real_ct not in CONTENT_TYPES: - self._log.debug(u'not a supported image: {}', - real_ct or u'unknown content type') + self._log.debug('not a supported image: {}', + real_ct or 'unknown content type') return ext = b'.' + CONTENT_TYPES[real_ct][0] if real_ct != ct: - self._log.warning(u'Server specified {}, but returned a ' - u'{} image. Correcting the extension ' - u'to {}', + self._log.warning('Server specified {}, but returned a ' + '{} image. Correcting the extension ' + 'to {}', ct, real_ct, ext) suffix = py3_path(ext) @@ -311,15 +334,15 @@ # download the remaining part of the image for chunk in data: fh.write(chunk) - self._log.debug(u'downloaded art to: {0}', + self._log.debug('downloaded art to: {0}', util.displayable_path(fh.name)) candidate.path = util.bytestring_path(fh.name) return - except (IOError, requests.RequestException, TypeError) as exc: + except (OSError, requests.RequestException, TypeError) as exc: # Handling TypeError works around a urllib3 bug: # https://github.com/shazow/urllib3/issues/556 - self._log.debug(u'error fetching art: {}', exc) + self._log.debug('error fetching art: {}', exc) return def cleanup(self, candidate): @@ -327,20 +350,16 @@ try: util.remove(path=candidate.path) except util.FilesystemError as exc: - self._log.debug(u'error cleaning up tmp art: {}', exc) + self._log.debug('error cleaning up tmp art: {}', exc) class CoverArtArchive(RemoteArtSource): - NAME = u"Cover Art Archive" + NAME = "Cover Art Archive" VALID_MATCHING_CRITERIA = ['release', 'releasegroup'] VALID_THUMBNAIL_SIZES = [250, 500, 1200] - if util.SNI_SUPPORTED: - URL = 'https://coverartarchive.org/release/{mbid}' - GROUP_URL = 'https://coverartarchive.org/release-group/{mbid}' - else: - URL = 'http://coverartarchive.org/release/{mbid}' - GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}' + URL = 'https://coverartarchive.org/release/{mbid}' + GROUP_URL = 'https://coverartarchive.org/release-group/{mbid}' def get(self, album, plugin, paths): """Return the Cover Art Archive and Cover Art Archive release group URLs @@ -351,14 +370,14 @@ try: response = self.request(url) except requests.RequestException: - self._log.debug(u'{0}: error receiving response' + self._log.debug('{}: error receiving response' .format(self.NAME)) return try: data = response.json() except ValueError: - self._log.debug(u'{0}: error loading response: {1}' + self._log.debug('{}: error loading response: {}' .format(self.NAME, response.text)) return @@ -395,11 +414,8 @@ class Amazon(RemoteArtSource): - NAME = u"Amazon" - if util.SNI_SUPPORTED: - URL = 'https://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' - else: - URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' + NAME = "Amazon" + URL = 'https://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' INDICES = (1, 2) def get(self, album, plugin, paths): @@ -412,11 +428,8 @@ class AlbumArtOrg(RemoteArtSource): - NAME = u"AlbumArt.org scraper" - if util.SNI_SUPPORTED: - URL = 'https://www.albumart.org/index_detail.php' - else: - URL = 'http://www.albumart.org/index_detail.php' + NAME = "AlbumArt.org scraper" + URL = 'https://www.albumart.org/index_detail.php' PAT = r'href\s*=\s*"([^>"]*)"[^>]*title\s*=\s*"View larger image"' def get(self, album, plugin, paths): @@ -427,9 +440,9 @@ # Get the page from albumart.org. try: resp = self.request(self.URL, params={'asin': album.asin}) - self._log.debug(u'scraped art URL: {0}', resp.url) + self._log.debug('scraped art URL: {0}', resp.url) except requests.RequestException: - self._log.debug(u'error scraping art page') + self._log.debug('error scraping art page') return # Search the page for the image URL. @@ -438,15 +451,15 @@ image_url = m.group(1) yield self._candidate(url=image_url, match=Candidate.MATCH_EXACT) else: - self._log.debug(u'no image found on page') + self._log.debug('no image found on page') class GoogleImages(RemoteArtSource): - NAME = u"Google Images" - URL = u'https://www.googleapis.com/customsearch/v1' + NAME = "Google Images" + URL = 'https://www.googleapis.com/customsearch/v1' def __init__(self, *args, **kwargs): - super(GoogleImages, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.key = self._config['google_key'].get(), self.cx = self._config['google_engine'].get(), @@ -466,20 +479,20 @@ 'searchType': 'image' }) except requests.RequestException: - self._log.debug(u'google: error receiving response') + self._log.debug('google: error receiving response') return # Get results using JSON. try: data = response.json() except ValueError: - self._log.debug(u'google: error loading response: {}' + self._log.debug('google: error loading response: {}' .format(response.text)) return if 'error' in data: reason = data['error']['errors'][0]['reason'] - self._log.debug(u'google fetchart error: {0}', reason) + self._log.debug('google fetchart error: {0}', reason) return if 'items' in data.keys(): @@ -490,13 +503,13 @@ class FanartTV(RemoteArtSource): """Art from fanart.tv requested using their API""" - NAME = u"fanart.tv" + NAME = "fanart.tv" API_URL = 'https://webservice.fanart.tv/v3/' API_ALBUMS = API_URL + 'music/albums/' PROJECT_KEY = '61a7d0ab4e67162b7a0c7c35915cd48e' def __init__(self, *args, **kwargs): - super(FanartTV, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.client_key = self._config['fanarttv_key'].get() def get(self, album, plugin, paths): @@ -509,51 +522,51 @@ headers={'api-key': self.PROJECT_KEY, 'client-key': self.client_key}) except requests.RequestException: - self._log.debug(u'fanart.tv: error receiving response') + self._log.debug('fanart.tv: error receiving response') return try: data = response.json() except ValueError: - self._log.debug(u'fanart.tv: error loading response: {}', + self._log.debug('fanart.tv: error loading response: {}', response.text) return - if u'status' in data and data[u'status'] == u'error': - if u'not found' in data[u'error message'].lower(): - self._log.debug(u'fanart.tv: no image found') - elif u'api key' in data[u'error message'].lower(): - self._log.warning(u'fanart.tv: Invalid API key given, please ' - u'enter a valid one in your config file.') + if 'status' in data and data['status'] == 'error': + if 'not found' in data['error message'].lower(): + self._log.debug('fanart.tv: no image found') + elif 'api key' in data['error message'].lower(): + self._log.warning('fanart.tv: Invalid API key given, please ' + 'enter a valid one in your config file.') else: - self._log.debug(u'fanart.tv: error on request: {}', - data[u'error message']) + self._log.debug('fanart.tv: error on request: {}', + data['error message']) return matches = [] # can there be more than one releasegroupid per response? - for mbid, art in data.get(u'albums', {}).items(): + for mbid, art in data.get('albums', {}).items(): # there might be more art referenced, e.g. cdart, and an albumcover # might not be present, even if the request was successful - if album.mb_releasegroupid == mbid and u'albumcover' in art: - matches.extend(art[u'albumcover']) + if album.mb_releasegroupid == mbid and 'albumcover' in art: + matches.extend(art['albumcover']) # can this actually occur? else: - self._log.debug(u'fanart.tv: unexpected mb_releasegroupid in ' - u'response!') + self._log.debug('fanart.tv: unexpected mb_releasegroupid in ' + 'response!') - matches.sort(key=lambda x: x[u'likes'], reverse=True) + matches.sort(key=lambda x: x['likes'], reverse=True) for item in matches: # fanart.tv has a strict size requirement for album art to be # uploaded - yield self._candidate(url=item[u'url'], + yield self._candidate(url=item['url'], match=Candidate.MATCH_EXACT, size=(1000, 1000)) class ITunesStore(RemoteArtSource): - NAME = u"iTunes Store" - API_URL = u'https://itunes.apple.com/search' + NAME = "iTunes Store" + API_URL = 'https://itunes.apple.com/search' def get(self, album, plugin, paths): """Return art URL from iTunes Store given an album title. @@ -562,31 +575,31 @@ return payload = { - 'term': album.albumartist + u' ' + album.album, - 'entity': u'album', - 'media': u'music', + 'term': album.albumartist + ' ' + album.album, + 'entity': 'album', + 'media': 'music', 'limit': 200 } try: r = self.request(self.API_URL, params=payload) r.raise_for_status() except requests.RequestException as e: - self._log.debug(u'iTunes search failed: {0}', e) + self._log.debug('iTunes search failed: {0}', e) return try: candidates = r.json()['results'] except ValueError as e: - self._log.debug(u'Could not decode json response: {0}', e) + self._log.debug('Could not decode json response: {0}', e) return except KeyError as e: - self._log.debug(u'{} not found in json. Fields are {} ', + self._log.debug('{} not found in json. Fields are {} ', e, list(r.json().keys())) return if not candidates: - self._log.debug(u'iTunes search for {!r} got no results', + self._log.debug('iTunes search for {!r} got no results', payload['term']) return @@ -605,7 +618,7 @@ yield self._candidate(url=art_url, match=Candidate.MATCH_EXACT) except KeyError as e: - self._log.debug(u'Malformed itunes candidate: {} not found in {}', # NOQA E501 + self._log.debug('Malformed itunes candidate: {} not found in {}', # NOQA E501 e, list(c.keys())) @@ -616,16 +629,16 @@ yield self._candidate(url=fallback_art_url, match=Candidate.MATCH_FALLBACK) except KeyError as e: - self._log.debug(u'Malformed itunes candidate: {} not found in {}', + self._log.debug('Malformed itunes candidate: {} not found in {}', e, list(c.keys())) class Wikipedia(RemoteArtSource): - NAME = u"Wikipedia (queried through DBpedia)" + NAME = "Wikipedia (queried through DBpedia)" DBPEDIA_URL = 'https://dbpedia.org/sparql' WIKIPEDIA_URL = 'https://en.wikipedia.org/w/api.php' - SPARQL_QUERY = u'''PREFIX rdf: + SPARQL_QUERY = '''PREFIX rdf: PREFIX dbpprop: PREFIX owl: PREFIX rdfs: @@ -666,7 +679,7 @@ headers={'content-type': 'application/json'}, ) except requests.RequestException: - self._log.debug(u'dbpedia: error receiving response') + self._log.debug('dbpedia: error receiving response') return try: @@ -676,9 +689,9 @@ cover_filename = 'File:' + results[0]['coverFilename']['value'] page_id = results[0]['pageId']['value'] else: - self._log.debug(u'wikipedia: album not found on dbpedia') + self._log.debug('wikipedia: album not found on dbpedia') except (ValueError, KeyError, IndexError): - self._log.debug(u'wikipedia: error scraping dbpedia response: {}', + self._log.debug('wikipedia: error scraping dbpedia response: {}', dbpedia_response.text) # Ensure we have a filename before attempting to query wikipedia @@ -693,7 +706,7 @@ if ' .' in cover_filename and \ '.' not in cover_filename.split(' .')[-1]: self._log.debug( - u'wikipedia: dbpedia provided incomplete cover_filename' + 'wikipedia: dbpedia provided incomplete cover_filename' ) lpart, rpart = cover_filename.rsplit(' .', 1) @@ -711,7 +724,7 @@ headers={'content-type': 'application/json'}, ) except requests.RequestException: - self._log.debug(u'wikipedia: error receiving response') + self._log.debug('wikipedia: error receiving response') return # Try to see if one of the images on the pages matches our @@ -726,7 +739,7 @@ break except (ValueError, KeyError): self._log.debug( - u'wikipedia: failed to retrieve a cover_filename' + 'wikipedia: failed to retrieve a cover_filename' ) return @@ -745,7 +758,7 @@ headers={'content-type': 'application/json'}, ) except requests.RequestException: - self._log.debug(u'wikipedia: error receiving response') + self._log.debug('wikipedia: error receiving response') return try: @@ -756,12 +769,12 @@ yield self._candidate(url=image_url, match=Candidate.MATCH_EXACT) except (ValueError, KeyError, IndexError): - self._log.debug(u'wikipedia: error scraping imageinfo') + self._log.debug('wikipedia: error scraping imageinfo') return class FileSystem(LocalArtSource): - NAME = u"Filesystem" + NAME = "Filesystem" @staticmethod def filename_priority(filename, cover_names): @@ -806,7 +819,7 @@ remaining = [] for fn in images: if re.search(cover_pat, os.path.splitext(fn)[0], re.I): - self._log.debug(u'using well-named art file {0}', + self._log.debug('using well-named art file {0}', util.displayable_path(fn)) yield self._candidate(path=os.path.join(path, fn), match=Candidate.MATCH_EXACT) @@ -815,14 +828,14 @@ # Fall back to any image in the folder. if remaining and not plugin.cautious: - self._log.debug(u'using fallback art file {0}', + self._log.debug('using fallback art file {0}', util.displayable_path(remaining[0])) yield self._candidate(path=os.path.join(path, remaining[0]), match=Candidate.MATCH_FALLBACK) class LastFM(RemoteArtSource): - NAME = u"Last.fm" + NAME = "Last.fm" # Sizes in priority order. SIZES = OrderedDict([ @@ -833,13 +846,10 @@ ('small', (34, 34)), ]) - if util.SNI_SUPPORTED: - API_URL = 'https://ws.audioscrobbler.com/2.0' - else: - API_URL = 'http://ws.audioscrobbler.com/2.0' + API_URL = 'https://ws.audioscrobbler.com/2.0' def __init__(self, *args, **kwargs): - super(LastFM, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.key = self._config['lastfm_key'].get(), def get(self, album, plugin, paths): @@ -854,7 +864,7 @@ 'format': 'json', }) except requests.RequestException: - self._log.debug(u'lastfm: error receiving response') + self._log.debug('lastfm: error receiving response') return try: @@ -878,26 +888,26 @@ yield self._candidate(url=images[size], size=self.SIZES[size]) except ValueError: - self._log.debug(u'lastfm: error loading response: {}' + self._log.debug('lastfm: error loading response: {}' .format(response.text)) return # Try each source in turn. -SOURCES_ALL = [u'filesystem', - u'coverart', u'itunes', u'amazon', u'albumart', - u'wikipedia', u'google', u'fanarttv', u'lastfm'] +SOURCES_ALL = ['filesystem', + 'coverart', 'itunes', 'amazon', 'albumart', + 'wikipedia', 'google', 'fanarttv', 'lastfm'] ART_SOURCES = { - u'filesystem': FileSystem, - u'coverart': CoverArtArchive, - u'itunes': ITunesStore, - u'albumart': AlbumArtOrg, - u'amazon': Amazon, - u'wikipedia': Wikipedia, - u'google': GoogleImages, - u'fanarttv': FanartTV, - u'lastfm': LastFM, + 'filesystem': FileSystem, + 'coverart': CoverArtArchive, + 'itunes': ITunesStore, + 'albumart': AlbumArtOrg, + 'amazon': Amazon, + 'wikipedia': Wikipedia, + 'google': GoogleImages, + 'fanarttv': FanartTV, + 'lastfm': LastFM, } SOURCE_NAMES = {v: k for k, v in ART_SOURCES.items()} @@ -909,7 +919,7 @@ PAT_PERCENT = r"(100(\.00?)?|[1-9]?[0-9](\.[0-9]{1,2})?)%" def __init__(self): - super(FetchArtPlugin, self).__init__() + super().__init__() # Holds candidates corresponding to downloaded images between # fetching them and placing them in the filesystem. @@ -927,11 +937,13 @@ 'sources': ['filesystem', 'coverart', 'itunes', 'amazon', 'albumart'], 'google_key': None, - 'google_engine': u'001442825323518660753:hrh5ch1gjzm', + 'google_engine': '001442825323518660753:hrh5ch1gjzm', 'fanarttv_key': None, 'lastfm_key': None, 'store_source': False, 'high_resolution': False, + 'deinterlace': False, + 'cover_format': None, }) self.config['google_key'].redact = True self.config['fanarttv_key'].redact = True @@ -949,10 +961,11 @@ confuse.String(pattern=self.PAT_PERCENT)])) self.margin_px = None self.margin_percent = None - if type(self.enforce_ratio) is six.text_type: - if self.enforce_ratio[-1] == u'%': + self.deinterlace = self.config['deinterlace'].get(bool) + if type(self.enforce_ratio) is str: + if self.enforce_ratio[-1] == '%': self.margin_percent = float(self.enforce_ratio[:-1]) / 100 - elif self.enforce_ratio[-2:] == u'px': + elif self.enforce_ratio[-2:] == 'px': self.margin_px = int(self.enforce_ratio[:-2]) else: # shouldn't happen @@ -967,6 +980,10 @@ self.src_removed = (config['import']['delete'].get(bool) or config['import']['move'].get(bool)) + self.cover_format = self.config['cover_format'].get( + confuse.Optional(str) + ) + if self.config['auto']: # Enable two import hooks when fetching is enabled. self.import_stages = [self.fetch_art] @@ -974,11 +991,11 @@ available_sources = list(SOURCES_ALL) if not self.config['google_key'].get() and \ - u'google' in available_sources: - available_sources.remove(u'google') + 'google' in available_sources: + available_sources.remove('google') if not self.config['lastfm_key'].get() and \ - u'lastfm' in available_sources: - available_sources.remove(u'lastfm') + 'lastfm' in available_sources: + available_sources.remove('lastfm') available_sources = [(s, c) for s in available_sources for c in ART_SOURCES[s].VALID_MATCHING_CRITERIA] @@ -988,9 +1005,9 @@ if 'remote_priority' in self.config: self._log.warning( - u'The `fetch_art.remote_priority` configuration option has ' - u'been deprecated. Instead, place `filesystem` at the end of ' - u'your `sources` list.') + 'The `fetch_art.remote_priority` configuration option has ' + 'been deprecated. Instead, place `filesystem` at the end of ' + 'your `sources` list.') if self.config['remote_priority'].get(bool): fs = [] others = [] @@ -1032,7 +1049,7 @@ if self.store_source: # store the source of the chosen artwork in a flexible field self._log.debug( - u"Storing art_source for {0.albumartist} - {0.album}", + "Storing art_source for {0.albumartist} - {0.album}", album) album.art_source = SOURCE_NAMES[type(candidate.source)] album.store() @@ -1052,14 +1069,14 @@ def commands(self): cmd = ui.Subcommand('fetchart', help='download album art') cmd.parser.add_option( - u'-f', u'--force', dest='force', + '-f', '--force', dest='force', action='store_true', default=False, - help=u're-download art when already present' + help='re-download art when already present' ) cmd.parser.add_option( - u'-q', u'--quiet', dest='quiet', + '-q', '--quiet', dest='quiet', action='store_true', default=False, - help=u'quiet mode: do not output albums that already have artwork' + help='quiet mode: do not output albums that already have artwork' ) def func(lib, opts, args): @@ -1083,7 +1100,7 @@ for source in self.sources: if source.IS_LOCAL or not local_only: self._log.debug( - u'trying source {0} for album {1.albumartist} - {1.album}', + 'trying source {0} for album {1.albumartist} - {1.album}', SOURCE_NAMES[type(source)], album, ) @@ -1094,7 +1111,7 @@ if candidate.validate(self): out = candidate self._log.debug( - u'using {0.LOC_STR} image {1}'.format( + 'using {0.LOC_STR} image {1}'.format( source, util.displayable_path(out.path))) break # Remove temporary files for invalid candidates. @@ -1115,8 +1132,8 @@ if album.artpath and not force and os.path.isfile(album.artpath): if not quiet: message = ui.colorize('text_highlight_minor', - u'has album art') - self._log.info(u'{0}: {1}', album, message) + 'has album art') + self._log.info('{0}: {1}', album, message) else: # In ordinary invocations, look for images on the # filesystem. When forcing, however, always go to the Web @@ -1126,7 +1143,7 @@ candidate = self.art_for_album(album, local_paths) if candidate: self._set_art(album, candidate) - message = ui.colorize('text_success', u'found album art') + message = ui.colorize('text_success', 'found album art') else: - message = ui.colorize('text_error', u'no art found') - self._log.info(u'{0}: {1}', album, message) + message = ui.colorize('text_error', 'no art found') + self._log.info('{0}: {1}', album, message) diff -Nru beets-1.5.0/beetsplug/filefilter.py beets-1.6.0/beetsplug/filefilter.py --- beets-1.5.0/beetsplug/filefilter.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/filefilter.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Malte Ried. # @@ -16,7 +15,6 @@ """Filter imported files using a regular expression. """ -from __future__ import division, absolute_import, print_function import re from beets import config @@ -27,7 +25,7 @@ class FileFilterPlugin(BeetsPlugin): def __init__(self): - super(FileFilterPlugin, self).__init__() + super().__init__() self.register_listener('import_task_created', self.import_task_created_event) self.config.add({ diff -Nru beets-1.5.0/beetsplug/fish.py beets-1.6.0/beetsplug/fish.py --- beets-1.5.0/beetsplug/fish.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/fish.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2015, winters jean-marie. # Copyright 2020, Justin Mayer @@ -23,7 +22,6 @@ `beet fish -e genre -e albumartist` """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import library, ui diff -Nru beets-1.5.0/beetsplug/freedesktop.py beets-1.6.0/beetsplug/freedesktop.py --- beets-1.5.0/beetsplug/freedesktop.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/freedesktop.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Matt Lichtenberg. # @@ -16,7 +15,6 @@ """Creates freedesktop.org-compliant .directory files on an album level. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import ui @@ -26,12 +24,12 @@ def commands(self): deprecated = ui.Subcommand( "freedesktop", - help=u"Print a message to redirect to thumbnails --dolphin") + help="Print a message to redirect to thumbnails --dolphin") deprecated.func = self.deprecation_message return [deprecated] def deprecation_message(self, lib, opts, args): - ui.print_(u"This plugin is deprecated. Its functionality is " - u"superseded by the 'thumbnails' plugin") - ui.print_(u"'thumbnails --dolphin' replaces freedesktop. See doc & " - u"changelog for more information") + ui.print_("This plugin is deprecated. Its functionality is " + "superseded by the 'thumbnails' plugin") + ui.print_("'thumbnails --dolphin' replaces freedesktop. See doc & " + "changelog for more information") diff -Nru beets-1.5.0/beetsplug/fromfilename.py beets-1.6.0/beetsplug/fromfilename.py --- beets-1.5.0/beetsplug/fromfilename.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/fromfilename.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Jan-Erik Dahlin # @@ -16,13 +15,11 @@ """If the title is empty, try to extract track and title from the filename. """ -from __future__ import division, absolute_import, print_function from beets import plugins from beets.util import displayable_path import os import re -import six # Filename field extraction patterns. @@ -124,7 +121,7 @@ # Apply the title and track. for item in d: if bad_title(item.title): - item.title = six.text_type(d[item][title_field]) + item.title = str(d[item][title_field]) if 'track' in d[item] and item.track == 0: item.track = int(d[item]['track']) @@ -133,7 +130,7 @@ class FromFilenamePlugin(plugins.BeetsPlugin): def __init__(self): - super(FromFilenamePlugin, self).__init__() + super().__init__() self.register_listener('import_task_start', filename_task) diff -Nru beets-1.5.0/beetsplug/ftintitle.py beets-1.6.0/beetsplug/ftintitle.py --- beets-1.5.0/beetsplug/ftintitle.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/ftintitle.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Verrus, # @@ -15,7 +14,6 @@ """Moves "featured" artists to the title from the artist field. """ -from __future__ import division, absolute_import, print_function import re @@ -75,22 +73,22 @@ class FtInTitlePlugin(plugins.BeetsPlugin): def __init__(self): - super(FtInTitlePlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, 'drop': False, - 'format': u'feat. {0}', + 'format': 'feat. {0}', }) self._command = ui.Subcommand( 'ftintitle', - help=u'move featured artists to the title field') + help='move featured artists to the title field') self._command.parser.add_option( - u'-d', u'--drop', dest='drop', + '-d', '--drop', dest='drop', action='store_true', default=None, - help=u'drop featuring from artists and ignore title update') + help='drop featuring from artists and ignore title update') if self.config['auto']: self.import_stages = [self.imported] @@ -127,7 +125,7 @@ remove it from the artist field. """ # In all cases, update the artist fields. - self._log.info(u'artist: {0} -> {1}', item.artist, item.albumartist) + self._log.info('artist: {0} -> {1}', item.artist, item.albumartist) item.artist = item.albumartist if item.artist_sort: # Just strip the featured artist from the sort name. @@ -138,8 +136,8 @@ if not drop_feat and not contains_feat(item.title): feat_format = self.config['format'].as_str() new_format = feat_format.format(feat_part) - new_title = u"{0} {1}".format(item.title, new_format) - self._log.info(u'title: {0} -> {1}', item.title, new_title) + new_title = f"{item.title} {new_format}" + self._log.info('title: {0} -> {1}', item.title, new_title) item.title = new_title def ft_in_title(self, item, drop_feat): @@ -165,4 +163,4 @@ if feat_part: self.update_metadata(item, feat_part, drop_feat) else: - self._log.info(u'no featuring artists found') + self._log.info('no featuring artists found') diff -Nru beets-1.5.0/beetsplug/fuzzy.py beets-1.6.0/beetsplug/fuzzy.py --- beets-1.5.0/beetsplug/fuzzy.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/fuzzy.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Philippe Mongeau. # @@ -16,7 +15,6 @@ """Provides a fuzzy matching query. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.dbcore.query import StringFieldQuery @@ -37,7 +35,7 @@ class FuzzyPlugin(BeetsPlugin): def __init__(self): - super(FuzzyPlugin, self).__init__() + super().__init__() self.config.add({ 'prefix': '~', 'threshold': 0.7, diff -Nru beets-1.5.0/beetsplug/gmusic.py beets-1.6.0/beetsplug/gmusic.py --- beets-1.5.0/beetsplug/gmusic.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/gmusic.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # This file is part of beets. -# Copyright 2017, Tigran Kostandyan. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -13,125 +11,15 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Upload files to Google Play Music and list songs in its library.""" - -from __future__ import absolute_import, division, print_function -import os.path +"""Deprecation warning for the removed gmusic plugin.""" from beets.plugins import BeetsPlugin -from beets import ui -from beets import config -from beets.ui import Subcommand -from gmusicapi import Musicmanager, Mobileclient -from gmusicapi.exceptions import NotLoggedIn -import gmusicapi.clients class Gmusic(BeetsPlugin): def __init__(self): - super(Gmusic, self).__init__() - self.m = Musicmanager() - - # OAUTH_FILEPATH was moved in gmusicapi 12.0.0. - if hasattr(Musicmanager, 'OAUTH_FILEPATH'): - oauth_file = Musicmanager.OAUTH_FILEPATH - else: - oauth_file = gmusicapi.clients.OAUTH_FILEPATH - - self.config.add({ - u'auto': False, - u'uploader_id': '', - u'uploader_name': '', - u'device_id': '', - u'oauth_file': oauth_file, - }) - if self.config['auto']: - self.import_stages = [self.autoupload] - - def commands(self): - gupload = Subcommand('gmusic-upload', - help=u'upload your tracks to Google Play Music') - gupload.func = self.upload - - search = Subcommand('gmusic-songs', - help=u'list of songs in Google Play Music library') - search.parser.add_option('-t', '--track', dest='track', - action='store_true', - help='Search by track name') - search.parser.add_option('-a', '--artist', dest='artist', - action='store_true', - help='Search by artist') - search.func = self.search - return [gupload, search] - - def authenticate(self): - if self.m.is_authenticated(): - return - # Checks for OAuth2 credentials, - # if they don't exist - performs authorization - oauth_file = self.config['oauth_file'].as_filename() - if os.path.isfile(oauth_file): - uploader_id = self.config['uploader_id'] - uploader_name = self.config['uploader_name'] - self.m.login(oauth_credentials=oauth_file, - uploader_id=uploader_id.as_str().upper() or None, - uploader_name=uploader_name.as_str() or None) - else: - self.m.perform_oauth(oauth_file) - - def upload(self, lib, opts, args): - items = lib.items(ui.decargs(args)) - files = self.getpaths(items) - self.authenticate() - ui.print_(u'Uploading your files...') - self.m.upload(filepaths=files) - ui.print_(u'Your files were successfully added to library') - - def autoupload(self, session, task): - items = task.imported_items() - files = self.getpaths(items) - self.authenticate() - self._log.info(u'Uploading files to Google Play Music...', files) - self.m.upload(filepaths=files) - self._log.info(u'Your files were successfully added to your ' - + 'Google Play Music library') - - def getpaths(self, items): - return [x.path for x in items] - - def search(self, lib, opts, args): - password = config['gmusic']['password'] - email = config['gmusic']['email'] - uploader_id = config['gmusic']['uploader_id'] - device_id = config['gmusic']['device_id'] - password.redact = True - email.redact = True - # Since Musicmanager doesn't support library management - # we need to use mobileclient interface - mobile = Mobileclient() - try: - new_device_id = (device_id.as_str() - or uploader_id.as_str().replace(':', '') - or Mobileclient.FROM_MAC_ADDRESS).upper() - mobile.login(email.as_str(), password.as_str(), new_device_id) - files = mobile.get_all_songs() - except NotLoggedIn: - ui.print_( - u'Authentication error. Please check your email and password.' - ) - return - if not args: - for i, file in enumerate(files, start=1): - print(i, ui.colorize('blue', file['artist']), - file['title'], ui.colorize('red', file['album'])) - else: - if opts.track: - self.match(files, args, 'title') - else: - self.match(files, args, 'artist') + super().__init__() - @staticmethod - def match(files, args, search_by): - for file in files: - if ' '.join(ui.decargs(args)) in file[search_by]: - print(file['artist'], file['title'], file['album']) + self._log.warning("The 'gmusic' plugin has been removed following the" + " shutdown of Google Play Music. Remove the plugin" + " from your configuration to silence this warning.") diff -Nru beets-1.5.0/beetsplug/hook.py beets-1.6.0/beetsplug/hook.py --- beets-1.5.0/beetsplug/hook.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/hook.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2015, Adrian Sampson. # @@ -14,13 +13,13 @@ # included in all copies or substantial portions of the Software. """Allows custom commands to be run when an event is emitted by beets""" -from __future__ import division, absolute_import, print_function import string import subprocess +import shlex from beets.plugins import BeetsPlugin -from beets.util import shlex_split, arg_encoding +from beets.util import arg_encoding class CodingFormatter(string.Formatter): @@ -48,8 +47,8 @@ if isinstance(format_string, bytes): format_string = format_string.decode(self._coding) - return super(CodingFormatter, self).format(format_string, *args, - **kwargs) + return super().format(format_string, *args, + **kwargs) def convert_field(self, value, conversion): """Converts the provided value given a conversion type. @@ -58,8 +57,8 @@ See string.Formatter.convert_field. """ - converted = super(CodingFormatter, self).convert_field(value, - conversion) + converted = super().convert_field(value, + conversion) if isinstance(converted, bytes): return converted.decode(self._coding) @@ -69,8 +68,9 @@ class HookPlugin(BeetsPlugin): """Allows custom commands to be run when an event is emitted by beets""" + def __init__(self): - super(HookPlugin, self).__init__() + super().__init__() self.config.add({ 'hooks': [] @@ -95,21 +95,21 @@ # Use a string formatter that works on Unicode strings. formatter = CodingFormatter(arg_encoding()) - command_pieces = shlex_split(command) + command_pieces = shlex.split(command) for i, piece in enumerate(command_pieces): command_pieces[i] = formatter.format(piece, event=event, **kwargs) - self._log.debug(u'running command "{0}" for event {1}', - u' '.join(command_pieces), event) + self._log.debug('running command "{0}" for event {1}', + ' '.join(command_pieces), event) try: subprocess.check_call(command_pieces) except subprocess.CalledProcessError as exc: - self._log.error(u'hook for {0} exited with status {1}', + self._log.error('hook for {0} exited with status {1}', event, exc.returncode) except OSError as exc: - self._log.error(u'hook for {0} failed: {1}', event, exc) + self._log.error('hook for {0} failed: {1}', event, exc) self.register_listener(event, hook_function) diff -Nru beets-1.5.0/beetsplug/ihate.py beets-1.6.0/beetsplug/ihate.py --- beets-1.5.0/beetsplug/ihate.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/ihate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Blemjhoo Tezoulbr . # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function """Warns you about things you hate (or even blocks import).""" @@ -33,14 +31,14 @@ object. """ if task.is_album: - return u'{0} - {1}'.format(task.cur_artist, task.cur_album) + return f'{task.cur_artist} - {task.cur_album}' else: - return u'{0} - {1}'.format(task.item.artist, task.item.title) + return f'{task.item.artist} - {task.item.title}' class IHatePlugin(BeetsPlugin): def __init__(self): - super(IHatePlugin, self).__init__() + super().__init__() self.register_listener('import_task_choice', self.import_task_choice_event) self.config.add({ @@ -69,14 +67,14 @@ if task.choice_flag == action.APPLY: if skip_queries or warn_queries: - self._log.debug(u'processing your hate') + self._log.debug('processing your hate') if self.do_i_hate_this(task, skip_queries): task.choice_flag = action.SKIP - self._log.info(u'skipped: {0}', summary(task)) + self._log.info('skipped: {0}', summary(task)) return if self.do_i_hate_this(task, warn_queries): - self._log.info(u'you may hate this: {0}', summary(task)) + self._log.info('you may hate this: {0}', summary(task)) else: - self._log.debug(u'nothing to do') + self._log.debug('nothing to do') else: - self._log.debug(u'user made a decision, nothing to do') + self._log.debug('user made a decision, nothing to do') diff -Nru beets-1.5.0/beetsplug/importadded.py beets-1.6.0/beetsplug/importadded.py --- beets-1.5.0/beetsplug/importadded.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/importadded.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,11 +1,8 @@ -# -*- coding: utf-8 -*- - """Populate an item's `added` and `mtime` fields by using the file modification time (mtime) of the item's source file before import. Reimported albums and items are skipped. """ -from __future__ import division, absolute_import, print_function import os @@ -16,7 +13,7 @@ class ImportAddedPlugin(BeetsPlugin): def __init__(self): - super(ImportAddedPlugin, self).__init__() + super().__init__() self.config.add({ 'preserve_mtimes': False, 'preserve_write_mtimes': False, @@ -53,8 +50,8 @@ def record_if_inplace(self, task, session): if not (session.config['copy'] or session.config['move'] or session.config['link'] or session.config['hardlink']): - self._log.debug(u"In place import detected, recording mtimes from " - u"source paths") + self._log.debug("In place import detected, recording mtimes from " + "source paths") items = [task.item] \ if isinstance(task, importer.SingletonImportTask) \ else task.items @@ -62,9 +59,9 @@ self.record_import_mtime(item, item.path, item.path) def record_reimported(self, task, session): - self.reimported_item_ids = set(item.id for item, replaced_items - in task.replaced_items.items() - if replaced_items) + self.reimported_item_ids = {item.id for item, replaced_items + in task.replaced_items.items() + if replaced_items} self.replaced_album_paths = set(task.replaced_albums.keys()) def write_file_mtime(self, path, mtime): @@ -86,14 +83,14 @@ """ mtime = os.stat(util.syspath(source)).st_mtime self.item_mtime[destination] = mtime - self._log.debug(u"Recorded mtime {0} for item '{1}' imported from " - u"'{2}'", mtime, util.displayable_path(destination), + self._log.debug("Recorded mtime {0} for item '{1}' imported from " + "'{2}'", mtime, util.displayable_path(destination), util.displayable_path(source)) def update_album_times(self, lib, album): if self.reimported_album(album): - self._log.debug(u"Album '{0}' is reimported, skipping import of " - u"added dates for the album and its items.", + self._log.debug("Album '{0}' is reimported, skipping import of " + "added dates for the album and its items.", util.displayable_path(album.path)) return @@ -106,21 +103,21 @@ self.write_item_mtime(item, mtime) item.store() album.added = min(album_mtimes) - self._log.debug(u"Import of album '{0}', selected album.added={1} " - u"from item file mtimes.", album.album, album.added) + self._log.debug("Import of album '{0}', selected album.added={1} " + "from item file mtimes.", album.album, album.added) album.store() def update_item_times(self, lib, item): if self.reimported_item(item): - self._log.debug(u"Item '{0}' is reimported, skipping import of " - u"added date.", util.displayable_path(item.path)) + self._log.debug("Item '{0}' is reimported, skipping import of " + "added date.", util.displayable_path(item.path)) return mtime = self.item_mtime.pop(item.path, None) if mtime: item.added = mtime if self.config['preserve_mtimes'].get(bool): self.write_item_mtime(item, mtime) - self._log.debug(u"Import of item '{0}', selected item.added={1}", + self._log.debug("Import of item '{0}', selected item.added={1}", util.displayable_path(item.path), item.added) item.store() @@ -131,5 +128,5 @@ if item.added: if self.config['preserve_write_mtimes'].get(bool): self.write_item_mtime(item, item.added) - self._log.debug(u"Write of item '{0}', selected item.added={1}", + self._log.debug("Write of item '{0}', selected item.added={1}", util.displayable_path(item.path), item.added) diff -Nru beets-1.5.0/beetsplug/importfeeds.py beets-1.6.0/beetsplug/importfeeds.py --- beets-1.5.0/beetsplug/importfeeds.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/importfeeds.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function """Write paths of imported files in various formats to ease later import in a music player. Also allow printing the new file locations to stdout in case @@ -54,11 +52,11 @@ class ImportFeedsPlugin(BeetsPlugin): def __init__(self): - super(ImportFeedsPlugin, self).__init__() + super().__init__() self.config.add({ 'formats': [], - 'm3u_name': u'imported.m3u', + 'm3u_name': 'imported.m3u', 'dir': None, 'relative_to': None, 'absolute_path': False, @@ -118,9 +116,9 @@ link(path, dest) if 'echo' in formats: - self._log.info(u"Location of imported music:") + self._log.info("Location of imported music:") for path in paths: - self._log.info(u" {0}", path) + self._log.info(" {0}", path) def album_imported(self, lib, album): self._record_items(lib, album.album, album.items()) diff -Nru beets-1.5.0/beetsplug/info.py beets-1.6.0/beetsplug/info.py --- beets-1.5.0/beetsplug/info.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beetsplug/info.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Shows file metadata. """ -from __future__ import division, absolute_import, print_function import os @@ -27,7 +25,7 @@ from beets.util import displayable_path, normpath, syspath -def tag_data(lib, args): +def tag_data(lib, args, album=False): query = [] for arg in args: path = normpath(arg) @@ -71,8 +69,8 @@ return emitter -def library_data(lib, args): - for item in lib.items(args): +def library_data(lib, args, album=False): + for item in lib.albums(args) if album else lib.items(args): yield library_data_emitter(item) @@ -110,7 +108,7 @@ formatted = {} for key, value in data.items(): if isinstance(value, list): - formatted[key] = u'; '.join(value) + formatted[key] = '; '.join(value) if value is not None: formatted[key] = value @@ -118,7 +116,7 @@ return maxwidth = max(len(key) for key in formatted) - lineformat = u'{{0:>{0}}}: {{1}}'.format(maxwidth) + lineformat = f'{{0:>{maxwidth}}}: {{1}}' if path: ui.print_(displayable_path(path)) @@ -126,7 +124,7 @@ for field in sorted(formatted): value = formatted[field] if isinstance(value, list): - value = u'; '.join(value) + value = '; '.join(value) ui.print_(lineformat.format(field, value)) @@ -141,7 +139,7 @@ if len(formatted) == 0: return - line_format = u'{0}{{0}}'.format(u' ' * 4) + line_format = '{0}{{0}}'.format(' ' * 4) if path: ui.print_(displayable_path(path)) @@ -152,24 +150,28 @@ class InfoPlugin(BeetsPlugin): def commands(self): - cmd = ui.Subcommand('info', help=u'show file metadata') + cmd = ui.Subcommand('info', help='show file metadata') cmd.func = self.run cmd.parser.add_option( - u'-l', u'--library', action='store_true', - help=u'show library fields instead of tags', + '-l', '--library', action='store_true', + help='show library fields instead of tags', ) cmd.parser.add_option( - u'-s', u'--summarize', action='store_true', - help=u'summarize the tags of all files', + '-a', '--album', action='store_true', + help='show album fields instead of tracks (implies "--library")', ) cmd.parser.add_option( - u'-i', u'--include-keys', default=[], + '-s', '--summarize', action='store_true', + help='summarize the tags of all files', + ) + cmd.parser.add_option( + '-i', '--include-keys', default=[], action='append', dest='included_keys', - help=u'comma separated list of keys to show', + help='comma separated list of keys to show', ) cmd.parser.add_option( - u'-k', u'--keys-only', action='store_true', - help=u'show only the keys', + '-k', '--keys-only', action='store_true', + help='show only the keys', ) cmd.parser.add_format_option(target='item') return [cmd] @@ -188,7 +190,7 @@ dictionary and only prints that. If two files have different values for the same tag, the value is set to '[various]' """ - if opts.library: + if opts.library or opts.album: data_collector = library_data else: data_collector = tag_data @@ -201,11 +203,14 @@ first = True summary = {} - for data_emitter in data_collector(lib, ui.decargs(args)): + for data_emitter in data_collector( + lib, ui.decargs(args), + album=opts.album, + ): try: data, item = data_emitter(included_keys or '*') - except (mediafile.UnreadableFileError, IOError) as ex: - self._log.error(u'cannot read file: {0}', ex) + except (mediafile.UnreadableFileError, OSError) as ex: + self._log.error('cannot read file: {0}', ex) continue if opts.summarize: diff -Nru beets-1.5.0/beetsplug/__init__.py beets-1.6.0/beetsplug/__init__.py --- beets-1.5.0/beetsplug/__init__.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/__init__.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """A namespace package for beets plugins.""" -from __future__ import division, absolute_import, print_function # Make this a namespace package. from pkgutil import extend_path diff -Nru beets-1.5.0/beetsplug/inline.py beets-1.6.0/beetsplug/inline.py --- beets-1.5.0/beetsplug/inline.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/inline.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,25 +14,23 @@ """Allows inline path template customization code in the config file. """ -from __future__ import division, absolute_import, print_function import traceback import itertools from beets.plugins import BeetsPlugin from beets import config -import six -FUNC_NAME = u'__INLINE_FUNC__' +FUNC_NAME = '__INLINE_FUNC__' class InlineError(Exception): """Raised when a runtime error occurs in an inline expression. """ def __init__(self, code, exc): - super(InlineError, self).__init__( - (u"error in inline path field code:\n" - u"%s\n%s: %s") % (code, type(exc).__name__, six.text_type(exc)) + super().__init__( + ("error in inline path field code:\n" + "%s\n%s: %s") % (code, type(exc).__name__, str(exc)) ) @@ -41,7 +38,7 @@ """Given Python code for a function body, return a compiled callable that invokes that code. """ - body = u'def {0}():\n {1}'.format( + body = 'def {}():\n {}'.format( FUNC_NAME, body.replace('\n', '\n ') ) @@ -53,7 +50,7 @@ class InlinePlugin(BeetsPlugin): def __init__(self): - super(InlinePlugin, self).__init__() + super().__init__() config.add({ 'pathfields': {}, # Legacy name. @@ -64,14 +61,14 @@ # Item fields. for key, view in itertools.chain(config['item_fields'].items(), config['pathfields'].items()): - self._log.debug(u'adding item field {0}', key) + self._log.debug('adding item field {0}', key) func = self.compile_inline(view.as_str(), False) if func is not None: self.template_fields[key] = func # Album fields. for key, view in config['album_fields'].items(): - self._log.debug(u'adding album field {0}', key) + self._log.debug('adding album field {0}', key) func = self.compile_inline(view.as_str(), True) if func is not None: self.album_template_fields[key] = func @@ -84,14 +81,14 @@ """ # First, try compiling as a single function. try: - code = compile(u'({0})'.format(python_code), 'inline', 'eval') + code = compile(f'({python_code})', 'inline', 'eval') except SyntaxError: # Fall back to a function body. try: func = _compile_func(python_code) except SyntaxError: - self._log.error(u'syntax error in inline field definition:\n' - u'{0}', traceback.format_exc()) + self._log.error('syntax error in inline field definition:\n' + '{0}', traceback.format_exc()) return else: is_expr = False diff -Nru beets-1.5.0/beetsplug/ipfs.py beets-1.6.0/beetsplug/ipfs.py --- beets-1.5.0/beetsplug/ipfs.py 2020-08-10 22:29:51.000000000 +0000 +++ beets-1.6.0/beetsplug/ipfs.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # # Permission is hereby granted, free of charge, to any person obtaining @@ -15,7 +14,6 @@ """Adds support for ipfs. Requires go-ipfs and a running ipfs daemon """ -from __future__ import division, absolute_import, print_function from beets import ui, util, library, config from beets.plugins import BeetsPlugin @@ -29,7 +27,7 @@ class IPFSPlugin(BeetsPlugin): def __init__(self): - super(IPFSPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, 'nocopy': False, @@ -125,7 +123,7 @@ try: output = util.command_output(cmd).stdout.split() except (OSError, subprocess.CalledProcessError) as exc: - self._log.error(u'Failed to add {0}, error: {1}', album_dir, exc) + self._log.error('Failed to add {0}, error: {1}', album_dir, exc) return False length = len(output) @@ -187,7 +185,7 @@ cmd.append(tmp.name) output = util.command_output(cmd).stdout except (OSError, subprocess.CalledProcessError) as err: - msg = "Failed to publish library. Error: {0}".format(err) + msg = f"Failed to publish library. Error: {err}" self._log.error(msg) return False self._log.info("hash of library: {0}", output) @@ -204,17 +202,17 @@ try: os.makedirs(remote_libs) except OSError as e: - msg = "Could not create {0}. Error: {1}".format(remote_libs, e) + msg = f"Could not create {remote_libs}. Error: {e}" self._log.error(msg) return False path = os.path.join(remote_libs, lib_name.encode() + b".db") if not os.path.exists(path): - cmd = "ipfs get {0} -o".format(_hash).split() + cmd = f"ipfs get {_hash} -o".split() cmd.append(path) try: util.command_output(cmd) except (OSError, subprocess.CalledProcessError): - self._log.error("Could not import {0}".format(_hash)) + self._log.error(f"Could not import {_hash}") return False # add all albums from remotes into a combined library @@ -241,7 +239,7 @@ fmt = config['format_album'].get() try: albums = self.query(lib, args) - except IOError: + except OSError: ui.print_("No imported libraries yet.") return @@ -258,7 +256,7 @@ remote_libs = os.path.join(lib_root, b"remotes") path = os.path.join(remote_libs, b"joined.db") if not os.path.isfile(path): - raise IOError + raise OSError return library.Library(path) def ipfs_added_albums(self, rlib, tmpname): @@ -285,7 +283,7 @@ util._fsencoding(), 'ignore' ) # Clear current path from item - item.path = '/ipfs/{0}/{1}'.format(album.ipfs, item_path) + item.path = f'/ipfs/{album.ipfs}/{item_path}' item.id = None items.append(item) diff -Nru beets-1.5.0/beetsplug/keyfinder.py beets-1.6.0/beetsplug/keyfinder.py --- beets-1.5.0/beetsplug/keyfinder.py 2020-12-15 12:48:01.000000000 +0000 +++ beets-1.6.0/beetsplug/keyfinder.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -16,7 +15,6 @@ """Uses the `KeyFinder` program to add the `initial_key` field. """ -from __future__ import division, absolute_import, print_function import os.path import subprocess @@ -29,11 +27,11 @@ class KeyFinderPlugin(BeetsPlugin): def __init__(self): - super(KeyFinderPlugin, self).__init__() + super().__init__() self.config.add({ - u'bin': u'KeyFinder', - u'auto': True, - u'overwrite': False, + 'bin': 'KeyFinder', + 'auto': True, + 'overwrite': False, }) if self.config['auto'].get(bool): @@ -41,7 +39,7 @@ def commands(self): cmd = ui.Subcommand('keyfinder', - help=u'detect and add initial key from audio') + help='detect and add initial key from audio') cmd.func = self.command return [cmd] @@ -67,12 +65,12 @@ output = util.command_output(command + [util.syspath( item.path)]).stdout except (subprocess.CalledProcessError, OSError) as exc: - self._log.error(u'execution failed: {0}', exc) + self._log.error('execution failed: {0}', exc) continue except UnicodeEncodeError: # Workaround for Python 2 Windows bug. # https://bugs.python.org/issue1759845 - self._log.error(u'execution failed for Unicode path: {0!r}', + self._log.error('execution failed for Unicode path: {0!r}', item.path) continue @@ -81,17 +79,17 @@ except IndexError: # Sometimes keyfinder-cli returns 0 but with no key, usually # when the file is silent or corrupt, so we log and skip. - self._log.error(u'no key returned for path: {0}', item.path) + self._log.error('no key returned for path: {0}', item.path) continue try: key = util.text_string(key_raw) except UnicodeDecodeError: - self._log.error(u'output is invalid UTF-8') + self._log.error('output is invalid UTF-8') continue item['initial_key'] = key - self._log.info(u'added computed initial key {0} for {1}', + self._log.info('added computed initial key {0} for {1}', key, util.displayable_path(item.path)) if write: diff -Nru beets-1.5.0/beetsplug/kodiupdate.py beets-1.6.0/beetsplug/kodiupdate.py --- beets-1.5.0/beetsplug/kodiupdate.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/kodiupdate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2017, Pauli Kettunen. # @@ -23,18 +22,16 @@ user: user pwd: secret """ -from __future__ import division, absolute_import, print_function import requests from beets import config from beets.plugins import BeetsPlugin -import six def update_kodi(host, port, user, password): """Sends request to the Kodi api to start a library refresh. """ - url = "http://{0}:{1}/jsonrpc".format(host, port) + url = f"http://{host}:{port}/jsonrpc" """Content-Type: application/json is mandatory according to the kodi jsonrpc documentation""" @@ -54,14 +51,14 @@ class KodiUpdate(BeetsPlugin): def __init__(self): - super(KodiUpdate, self).__init__() + super().__init__() # Adding defaults. config['kodi'].add({ - u'host': u'localhost', - u'port': 8080, - u'user': u'kodi', - u'pwd': u'kodi'}) + 'host': 'localhost', + 'port': 8080, + 'user': 'kodi', + 'pwd': 'kodi'}) config['kodi']['pwd'].redact = True self.register_listener('database_change', self.listen_for_db_change) @@ -73,7 +70,7 @@ def update(self, lib): """When the client exists try to send refresh request to Kodi server. """ - self._log.info(u'Requesting a Kodi library update...') + self._log.info('Requesting a Kodi library update...') # Try to send update request. try: @@ -85,14 +82,14 @@ r.raise_for_status() except requests.exceptions.RequestException as e: - self._log.warning(u'Kodi update failed: {0}', - six.text_type(e)) + self._log.warning('Kodi update failed: {0}', + str(e)) return json = r.json() if json.get('result') != 'OK': - self._log.warning(u'Kodi update failed: JSON response was {0!r}', + self._log.warning('Kodi update failed: JSON response was {0!r}', json) return - self._log.info(u'Kodi update triggered') + self._log.info('Kodi update triggered') diff -Nru beets-1.5.0/beetsplug/lastgenre/__init__.py beets-1.6.0/beetsplug/lastgenre/__init__.py --- beets-1.5.0/beetsplug/lastgenre/__init__.py 2020-12-21 13:49:02.000000000 +0000 +++ beets-1.6.0/beetsplug/lastgenre/__init__.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,9 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function - -import six """Gets genres for imported music based on Last.fm tags. @@ -47,7 +43,7 @@ ) REPLACE = { - u'\u2010': '-', + '\u2010': '-', } @@ -74,7 +70,7 @@ for sub in elem: flatten_tree(sub, path, branches) else: - branches.append(path + [six.text_type(elem)]) + branches.append(path + [str(elem)]) def find_parents(candidate, branches): @@ -98,7 +94,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): def __init__(self): - super(LastGenrePlugin, self).__init__() + super().__init__() self.config.add({ 'whitelist': True, @@ -109,7 +105,7 @@ 'source': 'album', 'force': True, 'auto': True, - 'separator': u', ', + 'separator': ', ', 'prefer_specific': False, 'title_case': True, }) @@ -134,7 +130,7 @@ with open(wl_filename, 'rb') as f: for line in f: line = line.decode('utf-8').strip().lower() - if line and not line.startswith(u'#'): + if line and not line.startswith('#'): self.whitelist.add(line) # Read the genres tree for canonicalization if enabled. @@ -267,8 +263,8 @@ if any(not s for s in args): return None - key = u'{0}.{1}'.format(entity, - u'-'.join(six.text_type(a) for a in args)) + key = '{}.{}'.format(entity, + '-'.join(str(a) for a in args)) if key in self._genre_cache: return self._genre_cache[key] else: @@ -286,28 +282,28 @@ """Return the album genre for this Item or Album. """ return self._last_lookup( - u'album', LASTFM.get_album, obj.albumartist, obj.album + 'album', LASTFM.get_album, obj.albumartist, obj.album ) def fetch_album_artist_genre(self, obj): """Return the album artist genre for this Item or Album. """ return self._last_lookup( - u'artist', LASTFM.get_artist, obj.albumartist + 'artist', LASTFM.get_artist, obj.albumartist ) def fetch_artist_genre(self, item): """Returns the track artist genre for this Item. """ return self._last_lookup( - u'artist', LASTFM.get_artist, item.artist + 'artist', LASTFM.get_artist, item.artist ) def fetch_track_genre(self, obj): """Returns the track genre for this Item. """ return self._last_lookup( - u'track', LASTFM.get_track, obj.artist, obj.title + 'track', LASTFM.get_track, obj.artist, obj.title ) def _get_genre(self, obj): @@ -377,22 +373,22 @@ return None, None def commands(self): - lastgenre_cmd = ui.Subcommand('lastgenre', help=u'fetch genres') + lastgenre_cmd = ui.Subcommand('lastgenre', help='fetch genres') lastgenre_cmd.parser.add_option( - u'-f', u'--force', dest='force', + '-f', '--force', dest='force', action='store_true', - help=u're-download genre when already present' + help='re-download genre when already present' ) lastgenre_cmd.parser.add_option( - u'-s', u'--source', dest='source', type='string', - help=u'genre source: artist, album, or track' + '-s', '--source', dest='source', type='string', + help='genre source: artist, album, or track' ) lastgenre_cmd.parser.add_option( - u'-A', u'--items', action='store_false', dest='album', - help=u'match items instead of albums') + '-A', '--items', action='store_false', dest='album', + help='match items instead of albums') lastgenre_cmd.parser.add_option( - u'-a', u'--albums', action='store_true', dest='album', - help=u'match albums instead of items') + '-a', '--albums', action='store_true', dest='album', + help='match albums instead of items') lastgenre_cmd.parser.set_defaults(album=True) def lastgenre_func(lib, opts, args): @@ -403,7 +399,7 @@ # Fetch genres for whole albums for album in lib.albums(ui.decargs(args)): album.genre, src = self._get_genre(album) - self._log.info(u'genre for album {0} ({1}): {0.genre}', + self._log.info('genre for album {0} ({1}): {0.genre}', album, src) album.store() @@ -414,7 +410,7 @@ item.genre, src = self._get_genre(item) item.store() self._log.info( - u'genre for track {0} ({1}): {0.genre}', + 'genre for track {0} ({1}): {0.genre}', item, src) if write: @@ -424,7 +420,7 @@ # an album for item in lib.items(ui.decargs(args)): item.genre, src = self._get_genre(item) - self._log.debug(u'added last.fm item genre ({0}): {1}', + self._log.debug('added last.fm item genre ({0}): {1}', src, item.genre) item.store() @@ -436,21 +432,21 @@ if task.is_album: album = task.album album.genre, src = self._get_genre(album) - self._log.debug(u'added last.fm album genre ({0}): {1}', + self._log.debug('added last.fm album genre ({0}): {1}', src, album.genre) album.store() if 'track' in self.sources: for item in album.items(): item.genre, src = self._get_genre(item) - self._log.debug(u'added last.fm item genre ({0}): {1}', + self._log.debug('added last.fm item genre ({0}): {1}', src, item.genre) item.store() else: item = task.item item.genre, src = self._get_genre(item) - self._log.debug(u'added last.fm item genre ({0}): {1}', + self._log.debug('added last.fm item genre ({0}): {1}', src, item.genre) item.store() @@ -472,12 +468,12 @@ try: res = obj.get_top_tags() except PYLAST_EXCEPTIONS as exc: - self._log.debug(u'last.fm error: {0}', exc) + self._log.debug('last.fm error: {0}', exc) return [] except Exception as exc: # Isolate bugs in pylast. - self._log.debug(u'{}', traceback.format_exc()) - self._log.error(u'error in pylast library: {0}', exc) + self._log.debug('{}', traceback.format_exc()) + self._log.error('error in pylast library: {0}', exc) return [] # Filter by weight (optionally). diff -Nru beets-1.5.0/beetsplug/lastimport.py beets-1.6.0/beetsplug/lastimport.py --- beets-1.5.0/beetsplug/lastimport.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/lastimport.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Rafael Bodill https://github.com/rafi # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import pylast from pylast import TopItem, _extract, _number @@ -28,7 +26,7 @@ class LastImportPlugin(plugins.BeetsPlugin): def __init__(self): - super(LastImportPlugin, self).__init__() + super().__init__() config['lastfm'].add({ 'user': '', 'api_key': plugins.LASTFM_KEY, @@ -43,7 +41,7 @@ } def commands(self): - cmd = ui.Subcommand('lastimport', help=u'import last.fm play-count') + cmd = ui.Subcommand('lastimport', help='import last.fm play-count') def func(lib, opts, args): import_lastfm(lib, self._log) @@ -59,7 +57,7 @@ tracks. """ def __init__(self, *args, **kwargs): - super(CustomUser, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _get_things(self, method, thing, thing_type, params=None, cacheable=True): @@ -114,9 +112,9 @@ per_page = config['lastimport']['per_page'].get(int) if not user: - raise ui.UserError(u'You must specify a user name for lastimport') + raise ui.UserError('You must specify a user name for lastimport') - log.info(u'Fetching last.fm library for @{0}', user) + log.info('Fetching last.fm library for @{0}', user) page_total = 1 page_current = 0 @@ -125,15 +123,15 @@ retry_limit = config['lastimport']['retry_limit'].get(int) # Iterate through a yet to be known page total count while page_current < page_total: - log.info(u'Querying page #{0}{1}...', + log.info('Querying page #{0}{1}...', page_current + 1, - '/{}'.format(page_total) if page_total > 1 else '') + f'/{page_total}' if page_total > 1 else '') for retry in range(0, retry_limit): tracks, page_total = fetch_tracks(user, page_current + 1, per_page) if page_total < 1: # It means nothing to us! - raise ui.UserError(u'Last.fm reported no data.') + raise ui.UserError('Last.fm reported no data.') if tracks: found, unknown = process_tracks(lib, tracks, log) @@ -141,22 +139,22 @@ unknown_total += unknown break else: - log.error(u'ERROR: unable to read page #{0}', + log.error('ERROR: unable to read page #{0}', page_current + 1) if retry < retry_limit: log.info( - u'Retrying page #{0}... ({1}/{2} retry)', + 'Retrying page #{0}... ({1}/{2} retry)', page_current + 1, retry + 1, retry_limit ) else: - log.error(u'FAIL: unable to fetch page #{0}, ', - u'tried {1} times', page_current, retry + 1) + log.error('FAIL: unable to fetch page #{0}, ', + 'tried {1} times', page_current, retry + 1) page_current += 1 - log.info(u'... done!') - log.info(u'finished processing {0} song pages', page_total) - log.info(u'{0} unknown play-counts', unknown_total) - log.info(u'{0} play-counts imported', found_total) + log.info('... done!') + log.info('finished processing {0} song pages', page_total) + log.info('{0} unknown play-counts', unknown_total) + log.info('{0} play-counts imported', found_total) def fetch_tracks(user, page, limit): @@ -190,7 +188,7 @@ total = len(tracks) total_found = 0 total_fails = 0 - log.info(u'Received {0} tracks in this page, processing...', total) + log.info('Received {0} tracks in this page, processing...', total) for num in range(0, total): song = None @@ -201,7 +199,7 @@ if 'album' in tracks[num]: album = tracks[num]['album'].get('name', '').strip() - log.debug(u'query: {0} - {1} ({2})', artist, title, album) + log.debug('query: {0} - {1} ({2})', artist, title, album) # First try to query by musicbrainz's trackid if trackid: @@ -211,7 +209,7 @@ # If not, try just artist/title if song is None: - log.debug(u'no album match, trying by artist/title') + log.debug('no album match, trying by artist/title') query = dbcore.AndQuery([ dbcore.query.SubstringQuery('artist', artist), dbcore.query.SubstringQuery('title', title) @@ -220,8 +218,8 @@ # Last resort, try just replacing to utf-8 quote if song is None: - title = title.replace("'", u'\u2019') - log.debug(u'no title match, trying utf-8 single quote') + title = title.replace("'", '\u2019') + log.debug('no title match, trying utf-8 single quote') query = dbcore.AndQuery([ dbcore.query.SubstringQuery('artist', artist), dbcore.query.SubstringQuery('title', title) @@ -231,19 +229,19 @@ if song is not None: count = int(song.get('play_count', 0)) new_count = int(tracks[num]['playcount']) - log.debug(u'match: {0} - {1} ({2}) ' - u'updating: play_count {3} => {4}', + log.debug('match: {0} - {1} ({2}) ' + 'updating: play_count {3} => {4}', song.artist, song.title, song.album, count, new_count) song['play_count'] = new_count song.store() total_found += 1 else: total_fails += 1 - log.info(u' - No match: {0} - {1} ({2})', + log.info(' - No match: {0} - {1} ({2})', artist, title, album) if total_fails > 0: - log.info(u'Acquired {0}/{1} play-counts ({2} unknown)', + log.info('Acquired {0}/{1} play-counts ({2} unknown)', total_found, total, total_fails) return total_found, total_fails diff -Nru beets-1.5.0/beetsplug/loadext.py beets-1.6.0/beetsplug/loadext.py --- beets-1.5.0/beetsplug/loadext.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/loadext.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2019, Jack Wilsdon # @@ -16,7 +15,6 @@ """Load SQLite extensions. """ -from __future__ import division, absolute_import, print_function from beets.dbcore import Database from beets.plugins import BeetsPlugin @@ -25,7 +23,7 @@ class LoadExtPlugin(BeetsPlugin): def __init__(self): - super(LoadExtPlugin, self).__init__() + super().__init__() if not Database.supports_extensions: self._log.warn('loadext is enabled but the current SQLite ' @@ -38,9 +36,9 @@ for v in self.config: ext = v.as_filename() - self._log.debug(u'loading extension {}', ext) + self._log.debug('loading extension {}', ext) try: lib.load_extension(ext) except sqlite3.OperationalError as e: - self._log.error(u'failed to load extension {}: {}', ext, e) + self._log.error('failed to load extension {}: {}', ext, e) diff -Nru beets-1.5.0/beetsplug/lyrics.py beets-1.6.0/beetsplug/lyrics.py --- beets-1.5.0/beetsplug/lyrics.py 2021-07-15 13:51:38.000000000 +0000 +++ beets-1.6.0/beetsplug/lyrics.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Fetches, embeds, and displays lyrics. """ -from __future__ import absolute_import, division, print_function import difflib import errno @@ -29,8 +27,7 @@ import unicodedata from unidecode import unidecode import warnings -import six -from six.moves import urllib +import urllib try: import bs4 @@ -49,7 +46,7 @@ # PY3: HTMLParseError was removed in 3.5 as strict mode # was deprecated in 3.3. # https://docs.python.org/3.3/library/html.parser.html - from six.moves.html_parser import HTMLParseError + from html.parser import HTMLParseError except ImportError: class HTMLParseError(Exception): pass @@ -63,23 +60,23 @@ TAG_RE = re.compile(r'<[^>]*>') BREAK_RE = re.compile(r'\n?\s*]*)*>\s*\n?', re.I) URL_CHARACTERS = { - u'\u2018': u"'", - u'\u2019': u"'", - u'\u201c': u'"', - u'\u201d': u'"', - u'\u2010': u'-', - u'\u2011': u'-', - u'\u2012': u'-', - u'\u2013': u'-', - u'\u2014': u'-', - u'\u2015': u'-', - u'\u2016': u'-', - u'\u2026': u'...', + '\u2018': "'", + '\u2019': "'", + '\u201c': '"', + '\u201d': '"', + '\u2010': '-', + '\u2011': '-', + '\u2012': '-', + '\u2013': '-', + '\u2014': '-', + '\u2015': '-', + '\u2016': '-', + '\u2026': '...', } -USER_AGENT = 'beets/{}'.format(beets.__version__) +USER_AGENT = f'beets/{beets.__version__}' # The content for the base index.rst generated in ReST mode. -REST_INDEX_TEMPLATE = u'''Lyrics +REST_INDEX_TEMPLATE = '''Lyrics ====== * :ref:`Song index ` @@ -95,11 +92,11 @@ ''' # The content for the base conf.py generated. -REST_CONF_TEMPLATE = u'''# -*- coding: utf-8 -*- +REST_CONF_TEMPLATE = '''# -*- coding: utf-8 -*- master_doc = 'index' -project = u'Lyrics' -copyright = u'none' -author = u'Various Authors' +project = 'Lyrics' +copyright = 'none' +author = 'Various Authors' latex_documents = [ (master_doc, 'Lyrics.tex', project, author, 'manual'), @@ -118,7 +115,7 @@ def unichar(i): try: - return six.unichr(i) + return chr(i) except ValueError: return struct.pack('i', i).decode('utf-32') @@ -127,12 +124,12 @@ """Resolve &#xxx; HTML entities (and some others).""" if isinstance(text, bytes): text = text.decode('utf-8', 'ignore') - out = text.replace(u' ', u' ') + out = text.replace(' ', ' ') def replchar(m): num = m.group(1) return unichar(int(num)) - out = re.sub(u"&#(\\d+);", replchar, out) + out = re.sub("&#(\\d+);", replchar, out) return out @@ -141,7 +138,7 @@ _, html = html.split(start_marker, 1) html, _ = html.split(end_marker, 1) except ValueError: - return u'' + return '' return html @@ -174,7 +171,7 @@ patterns = [ # Remove any featuring artists from the artists name - r"(.*?) {0}".format(plugins.feat_tokens())] + fr"(.*?) {plugins.feat_tokens()}"] artists = generate_alternatives(artist, patterns) # Use the artist_sort as fallback only if it differs from artist to avoid # repeated remote requests with the same search terms @@ -186,7 +183,7 @@ # examples include (live), (remix), and (acoustic). r"(.+?)\s+[(].*[)]$", # Remove any featuring artists from the title - r"(.*?) {0}".format(plugins.feat_tokens(for_artist=False)), + r"(.*?) {}".format(plugins.feat_tokens(for_artist=False)), # Remove part of title after colon ':' for songs with subtitles r"(.+?)\s*:.*"] titles = generate_alternatives(title, patterns) @@ -231,7 +228,7 @@ return None -class Backend(object): +class Backend: REQUIRES_BS = False def __init__(self, config, log): @@ -240,7 +237,7 @@ @staticmethod def _encode(s): """Encode the string for inclusion in a URL""" - if isinstance(s, six.text_type): + if isinstance(s, str): for char, repl in URL_CHARACTERS.items(): s = s.replace(char, repl) s = s.encode('utf-8', 'ignore') @@ -265,12 +262,12 @@ 'User-Agent': USER_AGENT, }) except requests.RequestException as exc: - self._log.debug(u'lyrics request failed: {0}', exc) + self._log.debug('lyrics request failed: {0}', exc) return if r.status_code == requests.codes.ok: return r.text else: - self._log.debug(u'failed to fetch: {0} ({1})', url, r.status_code) + self._log.debug('failed to fetch: {0} ({1})', url, r.status_code) return None def fetch(self, artist, title): @@ -294,7 +291,7 @@ for old, new in cls.REPLACEMENTS.items(): s = re.sub(old, new, s) - return super(MusiXmatch, cls)._encode(s) + return super()._encode(s) def fetch(self, artist, title): url = self.build_url(artist, title) @@ -303,7 +300,7 @@ if not html: return None if "We detected that your IP is blocked" in html: - self._log.warning(u'we are blocked at MusixMatch: url %s failed' + self._log.warning('we are blocked at MusixMatch: url %s failed' % url) return None html_parts = html.split('

eats up surrounding '\n'. html = re.sub(r'(?s)<(script).*?', '', html) # Strip script tags. - html = re.sub(u'\u2005', " ", html) # replace unicode with regular space + html = re.sub('\u2005', " ", html) # replace unicode with regular space if plain_text_out: # Strip remaining HTML tags html = COMMENT_RE.sub('', html) @@ -568,7 +565,7 @@ REQUIRES_BS = True def __init__(self, config, log): - super(Google, self).__init__(config, log) + super().__init__(config, log) self.api_key = config['google_API_key'].as_str() self.engine_id = config['google_engine_ID'].as_str() @@ -580,7 +577,7 @@ bad_triggers_occ = [] nb_lines = text.count('\n') if nb_lines <= 1: - self._log.debug(u"Ignoring too short lyrics '{0}'", text) + self._log.debug("Ignoring too short lyrics '{0}'", text) return False elif nb_lines < 5: bad_triggers_occ.append('too_short') @@ -598,7 +595,7 @@ text, re.I)) if bad_triggers_occ: - self._log.debug(u'Bad triggers detected: {0}', bad_triggers_occ) + self._log.debug('Bad triggers detected: {0}', bad_triggers_occ) return len(bad_triggers_occ) < 2 def slugify(self, text): @@ -611,9 +608,9 @@ try: text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore') - text = six.text_type(re.sub(r'[-\s]+', ' ', text.decode('utf-8'))) + text = str(re.sub(r'[-\s]+', ' ', text.decode('utf-8'))) except UnicodeDecodeError: - self._log.exception(u"Failing to normalize '{0}'", text) + self._log.exception("Failing to normalize '{0}'", text) return text BY_TRANS = ['by', 'par', 'de', 'von'] @@ -625,7 +622,7 @@ """ title = self.slugify(title.lower()) artist = self.slugify(artist.lower()) - sitename = re.search(u"//([^/]+)/.*", + sitename = re.search("//([^/]+)/.*", self.slugify(url_link.lower())).group(1) url_title = self.slugify(url_title.lower()) @@ -639,7 +636,7 @@ [artist, sitename, sitename.replace('www.', '')] + \ self.LYRICS_TRANS tokens = [re.escape(t) for t in tokens] - song_title = re.sub(u'(%s)' % u'|'.join(tokens), u'', url_title) + song_title = re.sub('(%s)' % '|'.join(tokens), '', url_title) song_title = song_title.strip('_|') typo_ratio = .9 @@ -647,28 +644,28 @@ return ratio >= typo_ratio def fetch(self, artist, title): - query = u"%s %s" % (artist, title) - url = u'https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s' \ + query = f"{artist} {title}" + url = 'https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s' \ % (self.api_key, self.engine_id, urllib.parse.quote(query.encode('utf-8'))) data = self.fetch_url(url) if not data: - self._log.debug(u'google backend returned no data') + self._log.debug('google backend returned no data') return None try: data = json.loads(data) except ValueError as exc: - self._log.debug(u'google backend returned malformed JSON: {}', exc) + self._log.debug('google backend returned malformed JSON: {}', exc) if 'error' in data: reason = data['error']['errors'][0]['reason'] - self._log.debug(u'google backend error: {0}', reason) + self._log.debug('google backend error: {0}', reason) return None if 'items' in data.keys(): for item in data['items']: url_link = item['link'] - url_title = item.get('title', u'') + url_title = item.get('title', '') if not self.is_page_candidate(url_link, url_title, title, artist): continue @@ -680,7 +677,7 @@ continue if self.is_lyrics(lyrics, artist): - self._log.debug(u'got lyrics from {0}', + self._log.debug('got lyrics from {0}', item['displayLink']) return lyrics @@ -697,7 +694,7 @@ } def __init__(self): - super(LyricsPlugin, self).__init__() + super().__init__() self.import_stages = [self.imported] self.config.add({ 'auto': True, @@ -705,7 +702,7 @@ 'bing_lang_from': [], 'bing_lang_to': None, 'google_API_key': None, - 'google_engine_ID': u'009217259823014548361:lndtuqkycfu', + 'google_engine_ID': '009217259823014548361:lndtuqkycfu', 'genius_api_key': "Ryq93pUGm8bM6eUWwD_M3NOFFDAtp2yEE7W" "76V-uFL5jks5dNvcGCdarqFjDhP9c", @@ -721,7 +718,7 @@ # State information for the ReST writer. # First, the current artist we're writing. - self.artist = u'Unknown artist' + self.artist = 'Unknown artist' # The current album: False means no album yet. self.album = False # The current rest file content. None means the file is not @@ -741,8 +738,8 @@ # configuration includes `google`. This way, the source # is silent by default but can be enabled just by # setting an API key. - self._log.debug(u'Disabling google source: ' - u'no API key configured.') + self._log.debug('Disabling google source: ' + 'no API key configured.') sources.remove('google') self.config['bing_lang_from'] = [ @@ -750,9 +747,9 @@ self.bing_auth_token = None if not HAS_LANGDETECT and self.config['bing_client_secret'].get(): - self._log.warning(u'To use bing translations, you need to ' - u'install the langdetect module. See the ' - u'documentation for further details.') + self._log.warning('To use bing translations, you need to ' + 'install the langdetect module. See the ' + 'documentation for further details.') self.backends = [self.SOURCE_BACKENDS[source](self.config, self._log) for source in sources] @@ -760,10 +757,10 @@ def sanitize_bs_sources(self, sources): enabled_sources = [] for source in sources: - if source.REQUIRES_BS: - self._log.debug(u'To use the %s lyrics source, you must ' - u'install the beautifulsoup4 module. See ' - u'the documentation for further details.' + if self.SOURCE_BACKENDS[source].REQUIRES_BS: + self._log.debug('To use the %s lyrics source, you must ' + 'install the beautifulsoup4 module. See ' + 'the documentation for further details.' % source) else: enabled_sources.append(source) @@ -785,30 +782,30 @@ if 'access_token' in oauth_token: return "Bearer " + oauth_token['access_token'] else: - self._log.warning(u'Could not get Bing Translate API access token.' - u' Check your "bing_client_secret" password') + self._log.warning('Could not get Bing Translate API access token.' + ' Check your "bing_client_secret" password') def commands(self): cmd = ui.Subcommand('lyrics', help='fetch song lyrics') cmd.parser.add_option( - u'-p', u'--print', dest='printlyr', + '-p', '--print', dest='printlyr', action='store_true', default=False, - help=u'print lyrics to console', + help='print lyrics to console', ) cmd.parser.add_option( - u'-r', u'--write-rest', dest='writerest', + '-r', '--write-rest', dest='writerest', action='store', default=None, metavar='dir', - help=u'write lyrics to given directory as ReST files', + help='write lyrics to given directory as ReST files', ) cmd.parser.add_option( - u'-f', u'--force', dest='force_refetch', + '-f', '--force', dest='force_refetch', action='store_true', default=False, - help=u'always re-download lyrics', + help='always re-download lyrics', ) cmd.parser.add_option( - u'-l', u'--local', dest='local_only', + '-l', '--local', dest='local_only', action='store_true', default=False, - help=u'do not fetch missing lyrics', + help='do not fetch missing lyrics', ) def func(lib, opts, args): @@ -832,13 +829,13 @@ if opts.writerest and items: # flush last artist & write to ReST self.writerest(opts.writerest) - ui.print_(u'ReST files generated. to build, use one of:') - ui.print_(u' sphinx-build -b html %s _build/html' + ui.print_('ReST files generated. to build, use one of:') + ui.print_(' sphinx-build -b html %s _build/html' % opts.writerest) - ui.print_(u' sphinx-build -b epub %s _build/epub' + ui.print_(' sphinx-build -b epub %s _build/epub' % opts.writerest) - ui.print_((u' sphinx-build -b latex %s _build/latex ' - u'&& make -C _build/latex all-pdf') + ui.print_((' sphinx-build -b latex %s _build/latex ' + '&& make -C _build/latex all-pdf') % opts.writerest) cmd.func = func return [cmd] @@ -854,27 +851,27 @@ # Write current file and start a new one ~ item.albumartist self.writerest(directory) self.artist = item.albumartist.strip() - self.rest = u"%s\n%s\n\n.. contents::\n :local:\n\n" \ + self.rest = "%s\n%s\n\n.. contents::\n :local:\n\n" \ % (self.artist, - u'=' * len(self.artist)) + '=' * len(self.artist)) if self.album != item.album: tmpalbum = self.album = item.album.strip() if self.album == '': - tmpalbum = u'Unknown album' - self.rest += u"%s\n%s\n\n" % (tmpalbum, u'-' * len(tmpalbum)) - title_str = u":index:`%s`" % item.title.strip() - block = u'| ' + item.lyrics.replace(u'\n', u'\n| ') - self.rest += u"%s\n%s\n\n%s\n\n" % (title_str, - u'~' * len(title_str), - block) + tmpalbum = 'Unknown album' + self.rest += "{}\n{}\n\n".format(tmpalbum, '-' * len(tmpalbum)) + title_str = ":index:`%s`" % item.title.strip() + block = '| ' + item.lyrics.replace('\n', '\n| ') + self.rest += "{}\n{}\n\n{}\n\n".format(title_str, + '~' * len(title_str), + block) def writerest(self, directory): """Write self.rest to a ReST file """ if self.rest is not None and self.artist is not None: path = os.path.join(directory, 'artists', - slug(self.artist) + u'.rst') + slug(self.artist) + '.rst') with open(path, 'wb') as output: output.write(self.rest.encode('utf-8')) @@ -914,7 +911,7 @@ """ # Skip if the item already has lyrics. if not force and item.lyrics: - self._log.info(u'lyrics already present: {0}', item) + self._log.info('lyrics already present: {0}', item) return lyrics = None @@ -923,10 +920,10 @@ if any(lyrics): break - lyrics = u"\n\n---\n\n".join([l for l in lyrics if l]) + lyrics = "\n\n---\n\n".join([l for l in lyrics if l]) if lyrics: - self._log.info(u'fetched lyrics: {0}', item) + self._log.info('fetched lyrics: {0}', item) if HAS_LANGDETECT and self.config['bing_client_secret'].get(): lang_from = langdetect.detect(lyrics) if self.config['bing_lang_to'].get() != lang_from and ( @@ -936,7 +933,7 @@ lyrics = self.append_translation( lyrics, self.config['bing_lang_to']) else: - self._log.info(u'lyrics not found: {0}', item) + self._log.info('lyrics not found: {0}', item) fallback = self.config['fallback'].get() if fallback: lyrics = fallback @@ -954,7 +951,7 @@ for backend in self.backends: lyrics = backend.fetch(artist, title) if lyrics: - self._log.debug(u'got lyrics from backend: {0}', + self._log.debug('got lyrics from backend: {0}', backend.__class__.__name__) return _scrape_strip_cruft(lyrics, True) @@ -983,5 +980,5 @@ translations = dict(zip(text_lines, lines_translated.split('|'))) result = '' for line in text.split('\n'): - result += '%s / %s\n' % (line, translations[line]) + result += '{} / {}\n'.format(line, translations[line]) return result diff -Nru beets-1.5.0/beetsplug/mbcollection.py beets-1.6.0/beetsplug/mbcollection.py --- beets-1.5.0/beetsplug/mbcollection.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/mbcollection.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright (c) 2011, Jeffrey Aylesworth # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.ui import Subcommand @@ -34,11 +32,11 @@ try: return func(*args, **kwargs) except musicbrainzngs.AuthenticationError: - raise ui.UserError(u'authentication with MusicBrainz failed') + raise ui.UserError('authentication with MusicBrainz failed') except (musicbrainzngs.ResponseError, musicbrainzngs.NetworkError) as exc: - raise ui.UserError(u'MusicBrainz API error: {0}'.format(exc)) + raise ui.UserError(f'MusicBrainz API error: {exc}') except musicbrainzngs.UsageError: - raise ui.UserError(u'MusicBrainz credentials missing') + raise ui.UserError('MusicBrainz credentials missing') def submit_albums(collection_id, release_ids): @@ -55,7 +53,7 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): def __init__(self): - super(MusicBrainzCollectionPlugin, self).__init__() + super().__init__() config['musicbrainz']['pass'].redact = True musicbrainzngs.auth( config['musicbrainz']['user'].as_str(), @@ -63,7 +61,7 @@ ) self.config.add({ 'auto': False, - 'collection': u'', + 'collection': '', 'remove': False, }) if self.config['auto']: @@ -72,18 +70,18 @@ def _get_collection(self): collections = mb_call(musicbrainzngs.get_collections) if not collections['collection-list']: - raise ui.UserError(u'no collections exist for user') + raise ui.UserError('no collections exist for user') # Get all collection IDs, avoiding event collections collection_ids = [x['id'] for x in collections['collection-list']] if not collection_ids: - raise ui.UserError(u'No collection found.') + raise ui.UserError('No collection found.') # Check that the collection exists so we can present a nice error collection = self.config['collection'].as_str() if collection: if collection not in collection_ids: - raise ui.UserError(u'invalid collection ID: {}' + raise ui.UserError('invalid collection ID: {}' .format(collection)) return collection @@ -110,7 +108,7 @@ def commands(self): mbupdate = Subcommand('mbupdate', - help=u'Update MusicBrainz collection') + help='Update MusicBrainz collection') mbupdate.parser.add_option('-r', '--remove', action='store_true', default=None, @@ -120,7 +118,7 @@ return [mbupdate] def remove_missing(self, collection_id, lib_albums): - lib_ids = set([x.mb_albumid for x in lib_albums]) + lib_ids = {x.mb_albumid for x in lib_albums} albums_in_collection = self._get_albums_in_collection(collection_id) remove_me = list(set(albums_in_collection) - lib_ids) for i in range(0, len(remove_me), FETCH_CHUNK_SIZE): @@ -154,13 +152,13 @@ if re.match(UUID_REGEX, aid): album_ids.append(aid) else: - self._log.info(u'skipping invalid MBID: {0}', aid) + self._log.info('skipping invalid MBID: {0}', aid) # Submit to MusicBrainz. self._log.info( - u'Updating MusicBrainz collection {0}...', collection_id + 'Updating MusicBrainz collection {0}...', collection_id ) submit_albums(collection_id, album_ids) if remove_missing: self.remove_missing(collection_id, lib.albums()) - self._log.info(u'...MusicBrainz collection updated.') + self._log.info('...MusicBrainz collection updated.') diff -Nru beets-1.5.0/beetsplug/mbsubmit.py beets-1.6.0/beetsplug/mbsubmit.py --- beets-1.5.0/beetsplug/mbsubmit.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/mbsubmit.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson and Diego Moreda. # @@ -22,8 +21,6 @@ [1] https://wiki.musicbrainz.org/History:How_To_Parse_Track_Listings """ -from __future__ import division, absolute_import, print_function - from beets.autotag import Recommendation from beets.plugins import BeetsPlugin @@ -33,10 +30,10 @@ class MBSubmitPlugin(BeetsPlugin): def __init__(self): - super(MBSubmitPlugin, self).__init__() + super().__init__() self.config.add({ - 'format': u'$track. $title - $artist ($length)', + 'format': '$track. $title - $artist ($length)', 'threshold': 'medium', }) @@ -53,7 +50,7 @@ def before_choose_candidate_event(self, session, task): if task.rec <= self.threshold: - return [PromptChoice(u'p', u'Print tracks', self.print_tracks)] + return [PromptChoice('p', 'Print tracks', self.print_tracks)] def print_tracks(self, session, task): for i in sorted(task.items, key=lambda i: i.track): diff -Nru beets-1.5.0/beetsplug/mbsync.py beets-1.6.0/beetsplug/mbsync.py --- beets-1.5.0/beetsplug/mbsync.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/mbsync.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Jakob Schnitzer. # @@ -15,7 +14,6 @@ """Update library's tags using MusicBrainz. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin, apply_item_changes from beets import autotag, library, ui, util @@ -29,24 +27,24 @@ class MBSyncPlugin(BeetsPlugin): def __init__(self): - super(MBSyncPlugin, self).__init__() + super().__init__() def commands(self): cmd = ui.Subcommand('mbsync', - help=u'update metadata from musicbrainz') + help='update metadata from musicbrainz') cmd.parser.add_option( - u'-p', u'--pretend', action='store_true', - help=u'show all changes but do nothing') + '-p', '--pretend', action='store_true', + help='show all changes but do nothing') cmd.parser.add_option( - u'-m', u'--move', action='store_true', dest='move', - help=u"move files in the library directory") + '-m', '--move', action='store_true', dest='move', + help="move files in the library directory") cmd.parser.add_option( - u'-M', u'--nomove', action='store_false', dest='move', - help=u"don't move files in library") + '-M', '--nomove', action='store_false', dest='move', + help="don't move files in library") cmd.parser.add_option( - u'-W', u'--nowrite', action='store_false', + '-W', '--nowrite', action='store_false', default=None, dest='write', - help=u"don't write updated metadata to files") + help="don't write updated metadata to files") cmd.parser.add_format_option() cmd.func = self.func return [cmd] @@ -66,23 +64,23 @@ """Retrieve and apply info from the autotagger for items matched by query. """ - for item in lib.items(query + [u'singleton:true']): + for item in lib.items(query + ['singleton:true']): item_formatted = format(item) if not item.mb_trackid: - self._log.info(u'Skipping singleton with no mb_trackid: {0}', + self._log.info('Skipping singleton with no mb_trackid: {0}', item_formatted) continue # Do we have a valid MusicBrainz track ID? if not re.match(MBID_REGEX, item.mb_trackid): - self._log.info(u'Skipping singleton with invalid mb_trackid:' + + self._log.info('Skipping singleton with invalid mb_trackid:' + ' {0}', item_formatted) continue # Get the MusicBrainz recording info. track_info = hooks.track_for_mbid(item.mb_trackid) if not track_info: - self._log.info(u'Recording ID not found: {0} for track {0}', + self._log.info('Recording ID not found: {0} for track {0}', item.mb_trackid, item_formatted) continue @@ -100,7 +98,7 @@ for a in lib.albums(query): album_formatted = format(a) if not a.mb_albumid: - self._log.info(u'Skipping album with no mb_albumid: {0}', + self._log.info('Skipping album with no mb_albumid: {0}', album_formatted) continue @@ -108,14 +106,14 @@ # Do we have a valid MusicBrainz album ID? if not re.match(MBID_REGEX, a.mb_albumid): - self._log.info(u'Skipping album with invalid mb_albumid: {0}', + self._log.info('Skipping album with invalid mb_albumid: {0}', album_formatted) continue # Get the MusicBrainz album information. album_info = hooks.album_for_mbid(a.mb_albumid) if not album_info: - self._log.info(u'Release ID {0} not found for album {1}', + self._log.info('Release ID {0} not found for album {1}', a.mb_albumid, album_formatted) continue @@ -151,7 +149,7 @@ break # Apply. - self._log.debug(u'applying changes to {}', album_formatted) + self._log.debug('applying changes to {}', album_formatted) with lib.transaction(): autotag.apply_metadata(album_info, mapping) changed = False @@ -176,5 +174,5 @@ # Move album art (and any inconsistent items). if move and lib.directory in util.ancestry(items[0].path): - self._log.debug(u'moving album {0}', album_formatted) + self._log.debug('moving album {0}', album_formatted) a.move() diff -Nru beets-1.5.0/beetsplug/metasync/amarok.py beets-1.6.0/beetsplug/metasync/amarok.py --- beets-1.5.0/beetsplug/metasync/amarok.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beetsplug/metasync/amarok.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Heinz Wiesinger. # @@ -16,7 +15,6 @@ """Synchronize information from amarok's library via dbus """ -from __future__ import division, absolute_import, print_function from os.path import basename from datetime import datetime @@ -49,14 +47,14 @@ 'amarok_lastplayed': DateType(), } - query_xml = u' \ + query_xml = ' \ \ \ \ ' def __init__(self, config, log): - super(Amarok, self).__init__(config, log) + super().__init__(config, log) if not dbus: raise ImportError('failed to import dbus') diff -Nru beets-1.5.0/beetsplug/metasync/__init__.py beets-1.6.0/beetsplug/metasync/__init__.py --- beets-1.5.0/beetsplug/metasync/__init__.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/metasync/__init__.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Heinz Wiesinger. # @@ -16,7 +15,6 @@ """Synchronize information from music player libraries """ -from __future__ import division, absolute_import, print_function from abc import abstractmethod, ABCMeta from importlib import import_module @@ -24,7 +22,6 @@ from confuse import ConfigValueError from beets import ui from beets.plugins import BeetsPlugin -import six METASYNC_MODULE = 'beetsplug.metasync' @@ -36,7 +33,7 @@ } -class MetaSource(six.with_metaclass(ABCMeta, object)): +class MetaSource(metaclass=ABCMeta): def __init__(self, config, log): self.item_types = {} self.config = config @@ -77,7 +74,7 @@ item_types = load_item_types() def __init__(self): - super(MetaSyncPlugin, self).__init__() + super().__init__() def commands(self): cmd = ui.Subcommand('metasync', @@ -108,7 +105,7 @@ # Avoid needlessly instantiating meta sources (can be expensive) if not items: - self._log.info(u'No items found matching query') + self._log.info('No items found matching query') return # Instantiate the meta sources @@ -116,18 +113,18 @@ try: cls = META_SOURCES[player] except KeyError: - self._log.error(u'Unknown metadata source \'{0}\''.format( + self._log.error('Unknown metadata source \'{}\''.format( player)) try: meta_source_instances[player] = cls(self.config, self._log) except (ImportError, ConfigValueError) as e: - self._log.error(u'Failed to instantiate metadata source ' - u'\'{0}\': {1}'.format(player, e)) + self._log.error('Failed to instantiate metadata source ' + '\'{}\': {}'.format(player, e)) # Avoid needlessly iterating over items if not meta_source_instances: - self._log.error(u'No valid metadata sources found') + self._log.error('No valid metadata sources found') return # Sync the items with all of the meta sources diff -Nru beets-1.5.0/beetsplug/metasync/itunes.py beets-1.6.0/beetsplug/metasync/itunes.py --- beets-1.5.0/beetsplug/metasync/itunes.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/beetsplug/metasync/itunes.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Tom Jaspers. # @@ -16,7 +15,6 @@ """Synchronize information from iTunes's library """ -from __future__ import division, absolute_import, print_function from contextlib import contextmanager import os @@ -24,8 +22,7 @@ import tempfile import plistlib -import six -from six.moves.urllib.parse import urlparse, unquote +from urllib.parse import urlparse, unquote from time import mktime from beets import util @@ -64,16 +61,16 @@ class Itunes(MetaSource): item_types = { - 'itunes_rating': types.INTEGER, # 0..100 scale - 'itunes_playcount': types.INTEGER, - 'itunes_skipcount': types.INTEGER, - 'itunes_lastplayed': DateType(), + 'itunes_rating': types.INTEGER, # 0..100 scale + 'itunes_playcount': types.INTEGER, + 'itunes_skipcount': types.INTEGER, + 'itunes_lastplayed': DateType(), 'itunes_lastskipped': DateType(), - 'itunes_dateadded': DateType(), + 'itunes_dateadded': DateType(), } def __init__(self, config, log): - super(Itunes, self).__init__(config, log) + super().__init__(config, log) config.add({'itunes': { 'library': '~/Music/iTunes/iTunes Library.xml' @@ -84,23 +81,20 @@ try: self._log.debug( - u'loading iTunes library from {0}'.format(library_path)) + f'loading iTunes library from {library_path}') with create_temporary_copy(library_path) as library_copy: - if six.PY2: - raw_library = plistlib.readPlist(library_copy) - else: - with open(library_copy, 'rb') as library_copy_f: - raw_library = plistlib.load(library_copy_f) - except IOError as e: - raise ConfigValueError(u'invalid iTunes library: ' + e.strerror) + with open(library_copy, 'rb') as library_copy_f: + raw_library = plistlib.load(library_copy_f) + except OSError as e: + raise ConfigValueError('invalid iTunes library: ' + e.strerror) except Exception: # It's likely the user configured their '.itl' library (<> xml) if os.path.splitext(library_path)[1].lower() != '.xml': - hint = u': please ensure that the configured path' \ - u' points to the .XML library' + hint = ': please ensure that the configured path' \ + ' points to the .XML library' else: hint = '' - raise ConfigValueError(u'invalid iTunes library' + hint) + raise ConfigValueError('invalid iTunes library' + hint) # Make the iTunes library queryable using the path self.collection = {_norm_itunes_path(track['Location']): track @@ -111,7 +105,7 @@ result = self.collection.get(util.bytestring_path(item.path).lower()) if not result: - self._log.warning(u'no iTunes match found for {0}'.format(item)) + self._log.warning(f'no iTunes match found for {item}') return item.itunes_rating = result.get('Rating') diff -Nru beets-1.5.0/beetsplug/missing.py beets-1.6.0/beetsplug/missing.py --- beets-1.5.0/beetsplug/missing.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/missing.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Pedro Silva. # Copyright 2017, Quentin Young. @@ -16,7 +15,6 @@ """List missing tracks. """ -from __future__ import division, absolute_import, print_function import musicbrainzngs @@ -93,7 +91,7 @@ } def __init__(self): - super(MissingPlugin, self).__init__() + super().__init__() self.config.add({ 'count': False, @@ -107,14 +105,14 @@ help=__doc__, aliases=['miss']) self._command.parser.add_option( - u'-c', u'--count', dest='count', action='store_true', - help=u'count missing tracks per album') + '-c', '--count', dest='count', action='store_true', + help='count missing tracks per album') self._command.parser.add_option( - u'-t', u'--total', dest='total', action='store_true', - help=u'count total of missing tracks') + '-t', '--total', dest='total', action='store_true', + help='count total of missing tracks') self._command.parser.add_option( - u'-a', u'--album', dest='album', action='store_true', - help=u'show missing albums for artist instead of tracks') + '-a', '--album', dest='album', action='store_true', + help='show missing albums for artist instead of tracks') self._command.parser.add_format_option() def commands(self): @@ -173,10 +171,10 @@ # build dict mapping artist to list of all albums for artist, albums in albums_by_artist.items(): if artist[1] is None or artist[1] == "": - albs_no_mbid = [u"'" + a['album'] + u"'" for a in albums] + albs_no_mbid = ["'" + a['album'] + "'" for a in albums] self._log.info( - u"No musicbrainz ID for artist '{}' found in album(s) {}; " - "skipping", artist[0], u", ".join(albs_no_mbid) + "No musicbrainz ID for artist '{}' found in album(s) {}; " + "skipping", artist[0], ", ".join(albs_no_mbid) ) continue @@ -185,7 +183,7 @@ release_groups = resp['release-group-list'] except MusicBrainzError as err: self._log.info( - u"Couldn't fetch info for artist '{}' ({}) - '{}'", + "Couldn't fetch info for artist '{}' ({}) - '{}'", artist[0], artist[1], err ) continue @@ -207,7 +205,7 @@ missing_titles = {rg['title'] for rg in missing} for release_title in missing_titles: - print_(u"{} - {}".format(artist[0], release_title)) + print_("{} - {}".format(artist[0], release_title)) if total: print(total_missing) @@ -223,6 +221,6 @@ for track_info in getattr(album_info, 'tracks', []): if track_info.track_id not in item_mbids: item = _item(track_info, album_info, album.id) - self._log.debug(u'track {0} in album {1}', + self._log.debug('track {0} in album {1}', track_info.track_id, album_info.album_id) yield item diff -Nru beets-1.5.0/beetsplug/mpdstats.py beets-1.6.0/beetsplug/mpdstats.py --- beets-1.5.0/beetsplug/mpdstats.py 2021-03-06 21:56:33.000000000 +0000 +++ beets-1.6.0/beetsplug/mpdstats.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Peter Schnebel and Johann Klähn. # @@ -13,12 +12,8 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import mpd -import socket -import select -import sys import time import os @@ -46,7 +41,7 @@ return path.split('://', 1)[0] in ['http', 'https'] -class MPDClientWrapper(object): +class MPDClientWrapper: def __init__(self, log): self._log = log @@ -60,13 +55,7 @@ self._log.debug('music_directory: {0}', self.music_directory) self._log.debug('strip_path: {0}', self.strip_path) - if sys.version_info < (3, 0): - # On Python 2, use_unicode will enable the utf-8 mode for - # python-mpd2 - self.client = mpd.MPDClient(use_unicode=True) - else: - # On Python 3, python-mpd2 always uses Unicode - self.client = mpd.MPDClient() + self.client = mpd.MPDClient() def connect(self): """Connect to the MPD. @@ -77,11 +66,11 @@ if host[0] in ['/', '~']: host = os.path.expanduser(host) - self._log.info(u'connecting to {0}:{1}', host, port) + self._log.info('connecting to {0}:{1}', host, port) try: self.client.connect(host, port) - except socket.error as e: - raise ui.UserError(u'could not connect to MPD: {0}'.format(e)) + except OSError as e: + raise ui.UserError(f'could not connect to MPD: {e}') password = mpd_config['password'].as_str() if password: @@ -89,7 +78,7 @@ self.client.password(password) except mpd.CommandError as e: raise ui.UserError( - u'could not authenticate to MPD: {0}'.format(e) + f'could not authenticate to MPD: {e}' ) def disconnect(self): @@ -104,12 +93,12 @@ """ try: return getattr(self.client, command)() - except (select.error, mpd.ConnectionError) as err: - self._log.error(u'{0}', err) + except (OSError, mpd.ConnectionError) as err: + self._log.error('{0}', err) if retries <= 0: # if we exited without breaking, we couldn't reconnect in time :( - raise ui.UserError(u'communication with MPD server failed') + raise ui.UserError('communication with MPD server failed') time.sleep(RETRY_INTERVAL) @@ -154,7 +143,7 @@ return self.get('idle') -class MPDStats(object): +class MPDStats: def __init__(self, lib, log): self.lib = lib self._log = log @@ -186,7 +175,7 @@ if item: return item else: - self._log.info(u'item not found: {0}', displayable_path(path)) + self._log.info('item not found: {0}', displayable_path(path)) def update_item(self, item, attribute, value=None, increment=None): """Update the beets item. Set attribute to value or increment the value @@ -204,7 +193,7 @@ item[attribute] = value item.store() - self._log.debug(u'updated: {0} = {1} [{2}]', + self._log.debug('updated: {0} = {1} [{2}]', attribute, item[attribute], displayable_path(item.path)) @@ -251,16 +240,16 @@ """Updates the play count of a song. """ self.update_item(song['beets_item'], 'play_count', increment=1) - self._log.info(u'played {0}', displayable_path(song['path'])) + self._log.info('played {0}', displayable_path(song['path'])) def handle_skipped(self, song): """Updates the skip count of a song. """ self.update_item(song['beets_item'], 'skip_count', increment=1) - self._log.info(u'skipped {0}', displayable_path(song['path'])) + self._log.info('skipped {0}', displayable_path(song['path'])) def on_stop(self, status): - self._log.info(u'stop') + self._log.info('stop') # if the current song stays the same it means that we stopped on the # current track and should not record a skip. @@ -270,7 +259,7 @@ self.now_playing = None def on_pause(self, status): - self._log.info(u'pause') + self._log.info('pause') self.now_playing = None def on_play(self, status): @@ -300,17 +289,17 @@ self.handle_song_change(self.now_playing) if is_url(path): - self._log.info(u'playing stream {0}', displayable_path(path)) + self._log.info('playing stream {0}', displayable_path(path)) self.now_playing = None return - self._log.info(u'playing {0}', displayable_path(path)) + self._log.info('playing {0}', displayable_path(path)) self.now_playing = { - 'started': time.time(), - 'remaining': remaining, - 'path': path, - 'id': songid, + 'started': time.time(), + 'remaining': remaining, + 'path': path, + 'id': songid, 'beets_item': self.get_item(path), } @@ -330,7 +319,7 @@ if handler: handler(status) else: - self._log.debug(u'unhandled status "{0}"', status) + self._log.debug('unhandled status "{0}"', status) events = self.mpd.events() @@ -338,38 +327,38 @@ class MPDStatsPlugin(plugins.BeetsPlugin): item_types = { - 'play_count': types.INTEGER, - 'skip_count': types.INTEGER, + 'play_count': types.INTEGER, + 'skip_count': types.INTEGER, 'last_played': library.DateType(), - 'rating': types.FLOAT, + 'rating': types.FLOAT, } def __init__(self): - super(MPDStatsPlugin, self).__init__() + super().__init__() mpd_config.add({ 'music_directory': config['directory'].as_filename(), - 'strip_path': u'', - 'rating': True, - 'rating_mix': 0.75, - 'host': os.environ.get('MPD_HOST', u'localhost'), - 'port': int(os.environ.get('MPD_PORT', 6600)), - 'password': u'', + 'strip_path': '', + 'rating': True, + 'rating_mix': 0.75, + 'host': os.environ.get('MPD_HOST', 'localhost'), + 'port': int(os.environ.get('MPD_PORT', 6600)), + 'password': '', }) mpd_config['password'].redact = True def commands(self): cmd = ui.Subcommand( 'mpdstats', - help=u'run a MPD client to gather play statistics') + help='run a MPD client to gather play statistics') cmd.parser.add_option( - u'--host', dest='host', type='string', - help=u'set the hostname of the server to connect to') + '--host', dest='host', type='string', + help='set the hostname of the server to connect to') cmd.parser.add_option( - u'--port', dest='port', type='int', - help=u'set the port of the MPD server to connect to') + '--port', dest='port', type='int', + help='set the port of the MPD server to connect to') cmd.parser.add_option( - u'--password', dest='password', type='string', - help=u'set the password of the MPD server to connect to') + '--password', dest='password', type='string', + help='set the password of the MPD server to connect to') def func(lib, opts, args): mpd_config.set_args(opts) diff -Nru beets-1.5.0/beetsplug/mpdupdate.py beets-1.6.0/beetsplug/mpdupdate.py --- beets-1.5.0/beetsplug/mpdupdate.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/mpdupdate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -21,19 +20,17 @@ port: 6600 password: seekrit """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin import os import socket from beets import config -import six # No need to introduce a dependency on an MPD library for such a # simple use case. Here's a simple socket abstraction to make things # easier. -class BufferedSocket(object): +class BufferedSocket: """Socket abstraction that allows reading by line.""" def __init__(self, host, port, sep=b'\n'): if host[0] in ['/', '~']: @@ -66,11 +63,11 @@ class MPDUpdatePlugin(BeetsPlugin): def __init__(self): - super(MPDUpdatePlugin, self).__init__() + super().__init__() config['mpd'].add({ - 'host': os.environ.get('MPD_HOST', u'localhost'), + 'host': os.environ.get('MPD_HOST', 'localhost'), 'port': int(os.environ.get('MPD_PORT', 6600)), - 'password': u'', + 'password': '', }) config['mpd']['password'].redact = True @@ -100,21 +97,21 @@ try: s = BufferedSocket(host, port) - except socket.error as e: - self._log.warning(u'MPD connection failed: {0}', - six.text_type(e.strerror)) + except OSError as e: + self._log.warning('MPD connection failed: {0}', + str(e.strerror)) return resp = s.readline() if b'OK MPD' not in resp: - self._log.warning(u'MPD connection failed: {0!r}', resp) + self._log.warning('MPD connection failed: {0!r}', resp) return if password: s.send(b'password "%s"\n' % password.encode('utf8')) resp = s.readline() if b'OK' not in resp: - self._log.warning(u'Authentication failed: {0!r}', resp) + self._log.warning('Authentication failed: {0!r}', resp) s.send(b'close\n') s.close() return @@ -122,8 +119,8 @@ s.send(b'update\n') resp = s.readline() if b'updating_db' not in resp: - self._log.warning(u'Update failed: {0!r}', resp) + self._log.warning('Update failed: {0!r}', resp) s.send(b'close\n') s.close() - self._log.info(u'Database updated.') + self._log.info('Database updated.') diff -Nru beets-1.5.0/beetsplug/parentwork.py beets-1.6.0/beetsplug/parentwork.py --- beets-1.5.0/beetsplug/parentwork.py 2021-01-08 18:07:39.000000000 +0000 +++ beets-1.6.0/beetsplug/parentwork.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2017, Dorian Soergel. # @@ -17,7 +16,6 @@ and work composition date """ -from __future__ import division, absolute_import, print_function from beets import ui from beets.plugins import BeetsPlugin @@ -71,7 +69,7 @@ class ParentWorkPlugin(BeetsPlugin): def __init__(self): - super(ParentWorkPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': False, @@ -96,12 +94,12 @@ item.try_write() command = ui.Subcommand( 'parentwork', - help=u'fetch parent works, composers and dates') + help='fetch parent works, composers and dates') command.parser.add_option( - u'-f', u'--force', dest='force', + '-f', '--force', dest='force', action='store_true', default=None, - help=u're-fetch when parent work is already present') + help='re-fetch when parent work is already present') command.func = func return [command] @@ -135,8 +133,8 @@ if 'end' in artist.keys(): parentwork_info["parentwork_date"] = artist['end'] - parentwork_info['parent_composer'] = u', '.join(parent_composer) - parentwork_info['parent_composer_sort'] = u', '.join( + parentwork_info['parent_composer'] = ', '.join(parent_composer) + parentwork_info['parent_composer_sort'] = ', '.join( parent_composer_sort) if not composer_exists: diff -Nru beets-1.5.0/beetsplug/permissions.py beets-1.6.0/beetsplug/permissions.py --- beets-1.5.0/beetsplug/permissions.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/permissions.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- - -from __future__ import division, absolute_import, print_function - """Fixes file permissions after the file gets written on import. Put something like the following in your config.yaml to configure: @@ -13,7 +9,6 @@ from beets import config, util from beets.plugins import BeetsPlugin from beets.util import ancestry -import six def convert_perm(perm): @@ -21,8 +16,8 @@ Or, if `perm` is an integer, reinterpret it as an octal number that has been "misinterpreted" as decimal. """ - if isinstance(perm, six.integer_types): - perm = six.text_type(perm) + if isinstance(perm, int): + perm = str(perm) return int(perm, 8) @@ -40,11 +35,11 @@ """ if not check_permissions(util.syspath(path), permission): log.warning( - u'could not set permissions on {}', + 'could not set permissions on {}', util.displayable_path(path), ) log.debug( - u'set permissions to {}, but permissions are now {}', + 'set permissions to {}, but permissions are now {}', permission, os.stat(util.syspath(path)).st_mode & 0o777, ) @@ -60,20 +55,39 @@ class Permissions(BeetsPlugin): def __init__(self): - super(Permissions, self).__init__() + super().__init__() # Adding defaults. self.config.add({ - u'file': '644', - u'dir': '755', + 'file': '644', + 'dir': '755', }) self.register_listener('item_imported', self.fix) self.register_listener('album_imported', self.fix) + self.register_listener('art_set', self.fix_art) def fix(self, lib, item=None, album=None): """Fix the permissions for an imported Item or Album. """ + files = [] + dirs = set() + if item: + files.append(item.path) + dirs.update(dirs_in_library(lib.directory, item.path)) + elif album: + for album_item in album.items(): + files.append(album_item.path) + dirs.update(dirs_in_library(lib.directory, album_item.path)) + self.set_permissions(files=files, dirs=dirs) + + def fix_art(self, album): + """Fix the permission for Album art file. + """ + if album.artpath: + self.set_permissions(files=[album.artpath]) + + def set_permissions(self, files=[], dirs=[]): # Get the configured permissions. The user can specify this either a # string (in YAML quotes) or, for convenience, as an integer so the # quotes can be omitted. In the latter case, we need to reinterpret the @@ -83,21 +97,10 @@ file_perm = convert_perm(file_perm) dir_perm = convert_perm(dir_perm) - # Create chmod_queue. - file_chmod_queue = [] - if item: - file_chmod_queue.append(item.path) - elif album: - for album_item in album.items(): - file_chmod_queue.append(album_item.path) - - # A set of directories to change permissions for. - dir_chmod_queue = set() - - for path in file_chmod_queue: + for path in files: # Changing permissions on the destination file. self._log.debug( - u'setting file permissions on {}', + 'setting file permissions on {}', util.displayable_path(path), ) os.chmod(util.syspath(path), file_perm) @@ -105,16 +108,11 @@ # Checks if the destination path has the permissions configured. assert_permissions(path, file_perm, self._log) - # Adding directories to the directory chmod queue. - dir_chmod_queue.update( - dirs_in_library(lib.directory, - path)) - # Change permissions for the directories. - for path in dir_chmod_queue: - # Chaning permissions on the destination directory. + for path in dirs: + # Changing permissions on the destination directory. self._log.debug( - u'setting directory permissions on {}', + 'setting directory permissions on {}', util.displayable_path(path), ) os.chmod(util.syspath(path), dir_perm) diff -Nru beets-1.5.0/beetsplug/playlist.py beets-1.6.0/beetsplug/playlist.py --- beets-1.5.0/beetsplug/playlist.py 2020-08-10 22:29:53.000000000 +0000 +++ beets-1.6.0/beetsplug/playlist.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # # Permission is hereby granted, free of charge, to any person obtaining @@ -12,7 +11,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import os import fnmatch @@ -33,7 +31,7 @@ pattern, os.path.abspath(os.path.join( config['playlist_dir'].as_filename(), - '{0}.m3u'.format(pattern), + f'{pattern}.m3u', )), ) @@ -45,7 +43,7 @@ try: f = open(beets.util.syspath(playlist_path), mode='rb') - except (OSError, IOError): + except OSError: continue if config['relative_to'].get() == 'library': @@ -71,7 +69,7 @@ if not self.paths: # Playlist is empty return '0', () - clause = 'path IN ({0})'.format(', '.join('?' for path in self.paths)) + clause = 'path IN ({})'.format(', '.join('?' for path in self.paths)) return clause, (beets.library.BLOB_TYPE(p) for p in self.paths) def match(self, item): @@ -82,7 +80,7 @@ item_queries = {'playlist': PlaylistQuery} def __init__(self): - super(PlaylistPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': False, 'playlist_dir': '.', @@ -116,7 +114,7 @@ def cli_exit(self, lib): for playlist in self.find_playlists(): - self._log.info('Updating playlist: {0}'.format(playlist)) + self._log.info(f'Updating playlist: {playlist}') base_dir = beets.util.bytestring_path( self.relative_to if self.relative_to else os.path.dirname(playlist) @@ -125,7 +123,7 @@ try: self.update_playlist(playlist, base_dir) except beets.util.FilesystemError: - self._log.error('Failed to update playlist: {0}'.format( + self._log.error('Failed to update playlist: {}'.format( beets.util.displayable_path(playlist))) def find_playlists(self): @@ -133,7 +131,7 @@ try: dir_contents = os.listdir(beets.util.syspath(self.playlist_dir)) except OSError: - self._log.warning('Unable to open playlist directory {0}'.format( + self._log.warning('Unable to open playlist directory {}'.format( beets.util.displayable_path(self.playlist_dir))) return @@ -181,7 +179,7 @@ if changes or deletions: self._log.info( - 'Updated playlist {0} ({1} changes, {2} deletions)'.format( + 'Updated playlist {} ({} changes, {} deletions)'.format( filename, changes, deletions)) beets.util.copy(new_playlist, filename, replace=True) beets.util.remove(new_playlist) diff -Nru beets-1.5.0/beetsplug/play.py beets-1.6.0/beetsplug/play.py --- beets-1.5.0/beetsplug/play.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/play.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, David Hamp-Gonsalves # @@ -15,7 +14,6 @@ """Send the results of a query to the configured music player as a playlist. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.ui import Subcommand @@ -26,6 +24,7 @@ from os.path import relpath from tempfile import NamedTemporaryFile import subprocess +import shlex # Indicate where arguments should be inserted into the command string. # If this is missing, they're placed at the end. @@ -39,25 +38,25 @@ """ # Print number of tracks or albums to be played, log command to be run. item_type += 's' if len(selection) > 1 else '' - ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) - log.debug(u'executing command: {} {!r}', command_str, open_args) + ui.print_('Playing {} {}.'.format(len(selection), item_type)) + log.debug('executing command: {} {!r}', command_str, open_args) try: if keep_open: - command = util.shlex_split(command_str) + command = shlex.split(command_str) command = command + open_args subprocess.call(command) else: util.interactive_open(open_args, command_str) except OSError as exc: raise ui.UserError( - "Could not play the query: {0}".format(exc)) + f"Could not play the query: {exc}") class PlayPlugin(BeetsPlugin): def __init__(self): - super(PlayPlugin, self).__init__() + super().__init__() config['play'].add({ 'command': None, @@ -74,18 +73,18 @@ def commands(self): play_command = Subcommand( 'play', - help=u'send music to a player as a playlist' + help='send music to a player as a playlist' ) play_command.parser.add_album_option() play_command.parser.add_option( - u'-A', u'--args', + '-A', '--args', action='store', - help=u'add additional arguments to the command', + help='add additional arguments to the command', ) play_command.parser.add_option( - u'-y', u'--yes', + '-y', '--yes', action="store_true", - help=u'skip the warning threshold', + help='skip the warning threshold', ) play_command.func = self._play_command return [play_command] @@ -124,7 +123,7 @@ if not selection: ui.print_(ui.colorize('text_warning', - u'No {0} to play.'.format(item_type))) + f'No {item_type} to play.')) return open_args = self._playlist_or_paths(paths) @@ -148,7 +147,7 @@ if ARGS_MARKER in command_str: return command_str.replace(ARGS_MARKER, args) else: - return u"{} {}".format(command_str, args) + return f"{command_str} {args}" else: # Don't include the marker in the command. return command_str.replace(" " + ARGS_MARKER, "") @@ -175,10 +174,10 @@ ui.print_(ui.colorize( 'text_warning', - u'You are about to queue {0} {1}.'.format( + 'You are about to queue {} {}.'.format( len(selection), item_type))) - if ui.input_options((u'Continue', u'Abort')) == 'a': + if ui.input_options(('Continue', 'Abort')) == 'a': return True return False diff -Nru beets-1.5.0/beetsplug/plexupdate.py beets-1.6.0/beetsplug/plexupdate.py --- beets-1.5.0/beetsplug/plexupdate.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/beetsplug/plexupdate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Updates an Plex library whenever the beets library is changed. Plex Home users enter the Plex Token to enable updating. @@ -9,11 +7,10 @@ port: 32400 token: token """ -from __future__ import division, absolute_import, print_function import requests from xml.etree import ElementTree -from six.moves.urllib.parse import urljoin, urlencode +from urllib.parse import urljoin, urlencode from beets import config from beets.plugins import BeetsPlugin @@ -23,7 +20,7 @@ """Getting the section key for the music library in Plex. """ api_endpoint = append_token('library/sections', token) - url = urljoin('{0}://{1}:{2}'.format(get_protocol(secure), host, + url = urljoin('{}://{}:{}'.format(get_protocol(secure), host, port), api_endpoint) # Sends request. @@ -48,9 +45,9 @@ # Getting section key and build url. section_key = get_music_section(host, port, token, library_name, secure, ignore_cert_errors) - api_endpoint = 'library/sections/{0}/refresh'.format(section_key) + api_endpoint = f'library/sections/{section_key}/refresh' api_endpoint = append_token(api_endpoint, token) - url = urljoin('{0}://{1}:{2}'.format(get_protocol(secure), host, + url = urljoin('{}://{}:{}'.format(get_protocol(secure), host, port), api_endpoint) # Sends request and returns requests object. @@ -75,16 +72,16 @@ class PlexUpdate(BeetsPlugin): def __init__(self): - super(PlexUpdate, self).__init__() + super().__init__() # Adding defaults. config['plex'].add({ - u'host': u'localhost', - u'port': 32400, - u'token': u'', - u'library_name': u'Music', - u'secure': False, - u'ignore_cert_errors': False}) + 'host': 'localhost', + 'port': 32400, + 'token': '', + 'library_name': 'Music', + 'secure': False, + 'ignore_cert_errors': False}) config['plex']['token'].redact = True self.register_listener('database_change', self.listen_for_db_change) @@ -96,7 +93,7 @@ def update(self, lib): """When the client exists try to send refresh request to Plex server. """ - self._log.info(u'Updating Plex library...') + self._log.info('Updating Plex library...') # Try to send update request. try: @@ -107,7 +104,7 @@ config['plex']['library_name'].get(), config['plex']['secure'].get(bool), config['plex']['ignore_cert_errors'].get(bool)) - self._log.info(u'... started.') + self._log.info('... started.') except requests.exceptions.RequestException: - self._log.warning(u'Update failed.') + self._log.warning('Update failed.') diff -Nru beets-1.5.0/beetsplug/random.py beets-1.6.0/beetsplug/random.py --- beets-1.5.0/beetsplug/random.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/random.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Philippe Mongeau. # @@ -15,7 +14,6 @@ """Get a random song or album from the library. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs, print_ @@ -40,16 +38,16 @@ random_cmd = Subcommand('random', - help=u'choose a random track or album') + help='choose a random track or album') random_cmd.parser.add_option( - u'-n', u'--number', action='store', type="int", - help=u'number of objects to choose', default=1) + '-n', '--number', action='store', type="int", + help='number of objects to choose', default=1) random_cmd.parser.add_option( - u'-e', u'--equal-chance', action='store_true', - help=u'each artist has the same chance') + '-e', '--equal-chance', action='store_true', + help='each artist has the same chance') random_cmd.parser.add_option( - u'-t', u'--time', action='store', type="float", - help=u'total length in minutes of objects to choose') + '-t', '--time', action='store', type="float", + help='total length in minutes of objects to choose') random_cmd.parser.add_all_common_options() random_cmd.func = random_func diff -Nru beets-1.5.0/beetsplug/replaygain.py beets-1.6.0/beetsplug/replaygain.py --- beets-1.5.0/beetsplug/replaygain.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/replaygain.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte, Yevgeny Bezman, and Adrian Sampson. # @@ -13,19 +12,17 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import collections import enum import math import os import signal -import six import subprocess import sys import warnings from multiprocessing.pool import ThreadPool, RUN -from six.moves import zip, queue +from six.moves import queue from threading import Thread, Event from beets import ui @@ -60,13 +57,13 @@ return command_output(args, **kwargs) except subprocess.CalledProcessError as e: raise ReplayGainError( - u"{0} exited with status {1}".format(args[0], e.returncode) + "{} exited with status {}".format(args[0], e.returncode) ) except UnicodeEncodeError: # Due to a bug in Python 2's subprocess on Windows, Unicode # filenames can fail to encode on that platform. See: # https://github.com/google-code-export/beets/issues/499 - raise ReplayGainError(u"argument encoding failed") + raise ReplayGainError("argument encoding failed") def after_version(version_a, version_b): @@ -108,7 +105,7 @@ sample = 2 -class Backend(object): +class Backend: """An abstract class representing engine for calculating RG values. """ @@ -141,7 +138,7 @@ do_parallel = True def __init__(self, config, log): - super(FfmpegBackend, self).__init__(config, log) + super().__init__(config, log) self._ffmpeg_path = "ffmpeg" # check that ffmpeg is installed @@ -149,7 +146,7 @@ ffmpeg_version_out = call([self._ffmpeg_path, "-version"]) except OSError: raise FatalReplayGainError( - u"could not find ffmpeg at {0}".format(self._ffmpeg_path) + f"could not find ffmpeg at {self._ffmpeg_path}" ) incompatible_ffmpeg = True for line in ffmpeg_version_out.stdout.splitlines(): @@ -163,9 +160,9 @@ incompatible_ffmpeg = False if incompatible_ffmpeg: raise FatalReplayGainError( - u"Installed FFmpeg version does not support ReplayGain." - u"calculation. Either libavfilter version 6.67.100 or above or" - u"the --enable-libebur128 configuration option is required." + "Installed FFmpeg version does not support ReplayGain." + "calculation. Either libavfilter version 6.67.100 or above or" + "the --enable-libebur128 configuration option is required." ) def compute_track_gain(self, items, target_level, peak): @@ -236,7 +233,7 @@ album_gain = target_level_lufs - album_gain self._log.debug( - u"{0}: gain {1} LU, peak {2}" + "{}: gain {} LU, peak {}" .format(items, album_gain, album_peak) ) @@ -253,7 +250,7 @@ "-map", "a:0", "-filter", - "ebur128=peak={0}".format(peak_method), + f"ebur128=peak={peak_method}", "-f", "null", "-", @@ -270,10 +267,10 @@ peak_method = peak.name # call ffmpeg - self._log.debug(u"analyzing {0}".format(item)) + self._log.debug(f"analyzing {item}") cmd = self._construct_cmd(item, peak_method) self._log.debug( - u'executing {0}', u' '.join(map(displayable_path, cmd)) + 'executing {0}', ' '.join(map(displayable_path, cmd)) ) output = call(cmd).stderr.splitlines() @@ -284,7 +281,7 @@ else: line_peak = self._find_line( output, - " {0} peak:".format(peak_method.capitalize()).encode(), + f" {peak_method.capitalize()} peak:".encode(), start_line=len(output) - 1, step_size=-1, ) peak = self._parse_float( @@ -329,12 +326,12 @@ if self._parse_float(b"M: " + line[1]) >= gating_threshold: n_blocks += 1 self._log.debug( - u"{0}: {1} blocks over {2} LUFS" + "{}: {} blocks over {} LUFS" .format(item, n_blocks, gating_threshold) ) self._log.debug( - u"{0}: gain {1} LU, peak {2}" + "{}: gain {} LU, peak {}" .format(item, gain, peak) ) @@ -350,7 +347,7 @@ if output[i].startswith(search): return i raise ReplayGainError( - u"ffmpeg output: missing {0} after line {1}" + "ffmpeg output: missing {} after line {}" .format(repr(search), start_line) ) @@ -364,7 +361,7 @@ value = line.split(b":", 1) if len(value) < 2: raise ReplayGainError( - u"ffmpeg output: expected key value pair, found {0}" + "ffmpeg output: expected key value pair, found {}" .format(line) ) value = value[1].lstrip() @@ -375,7 +372,7 @@ return float(value) except ValueError: raise ReplayGainError( - u"ffmpeg output: expected float value, found {0}" + "ffmpeg output: expected float value, found {}" .format(value) ) @@ -385,9 +382,9 @@ do_parallel = True def __init__(self, config, log): - super(CommandBackend, self).__init__(config, log) + super().__init__(config, log) config.add({ - 'command': u"", + 'command': "", 'noclip': True, }) @@ -397,7 +394,7 @@ # Explicit executable path. if not os.path.isfile(self.command): raise FatalReplayGainError( - u'replaygain command does not exist: {0}'.format( + 'replaygain command does not exist: {}'.format( self.command) ) else: @@ -410,7 +407,7 @@ pass if not self.command: raise FatalReplayGainError( - u'no replaygain command found: install mp3gain or aacgain' + 'no replaygain command found: install mp3gain or aacgain' ) self.noclip = config['noclip'].get(bool) @@ -432,7 +429,7 @@ supported_items = list(filter(self.format_supported, items)) if len(supported_items) != len(items): - self._log.debug(u'tracks are of unsupported format') + self._log.debug('tracks are of unsupported format') return AlbumGain(None, []) output = self.compute_gain(supported_items, target_level, True) @@ -455,7 +452,7 @@ the album gain """ if len(items) == 0: - self._log.debug(u'no supported tracks to analyze') + self._log.debug('no supported tracks to analyze') return [] """Compute ReplayGain values and return a list of results @@ -477,10 +474,10 @@ cmd = cmd + ['-d', str(int(target_level - 89))] cmd = cmd + [syspath(i.path) for i in items] - self._log.debug(u'analyzing {0} files', len(items)) - self._log.debug(u"executing {0}", " ".join(map(displayable_path, cmd))) + self._log.debug('analyzing {0} files', len(items)) + self._log.debug("executing {0}", " ".join(map(displayable_path, cmd))) output = call(cmd).stdout - self._log.debug(u'analysis finished') + self._log.debug('analysis finished') return self.parse_tool_output(output, len(items) + (1 if is_album else 0)) @@ -493,8 +490,8 @@ for line in text.split(b'\n')[1:num_lines + 1]: parts = line.split(b'\t') if len(parts) != 6 or parts[0] == b'File': - self._log.debug(u'bad tool output: {0}', text) - raise ReplayGainError(u'mp3gain failed') + self._log.debug('bad tool output: {0}', text) + raise ReplayGainError('mp3gain failed') d = { 'file': parts[0], 'mp3gain': int(parts[1]), @@ -512,7 +509,7 @@ class GStreamerBackend(Backend): def __init__(self, config, log): - super(GStreamerBackend, self).__init__(config, log) + super().__init__(config, log) self._import_gst() # Initialized a GStreamer pipeline of the form filesrc -> @@ -529,7 +526,7 @@ if self._src is None or self._decbin is None or self._conv is None \ or self._res is None or self._rg is None: raise FatalGstreamerPluginReplayGainError( - u"Failed to load required GStreamer plugins" + "Failed to load required GStreamer plugins" ) # We check which files need gain ourselves, so all files given @@ -574,14 +571,14 @@ import gi except ImportError: raise FatalReplayGainError( - u"Failed to load GStreamer: python-gi not found" + "Failed to load GStreamer: python-gi not found" ) try: gi.require_version('Gst', '1.0') except ValueError as e: raise FatalReplayGainError( - u"Failed to load GStreamer 1.0: {0}".format(e) + f"Failed to load GStreamer 1.0: {e}" ) from gi.repository import GObject, Gst, GLib @@ -618,7 +615,7 @@ def compute_track_gain(self, items, target_level, peak): self.compute(items, target_level, False) if len(self._file_tags) != len(items): - raise ReplayGainError(u"Some tracks did not receive tags") + raise ReplayGainError("Some tracks did not receive tags") ret = [] for item in items: @@ -631,7 +628,7 @@ items = list(items) self.compute(items, target_level, True) if len(self._file_tags) != len(items): - raise ReplayGainError(u"Some items in album did not receive tags") + raise ReplayGainError("Some items in album did not receive tags") # Collect track gains. track_gains = [] @@ -640,7 +637,7 @@ gain = self._file_tags[item]["TRACK_GAIN"] peak = self._file_tags[item]["TRACK_PEAK"] except KeyError: - raise ReplayGainError(u"results missing for track") + raise ReplayGainError("results missing for track") track_gains.append(Gain(gain, peak)) # Get album gain information from the last track. @@ -649,7 +646,7 @@ gain = last_tags["ALBUM_GAIN"] peak = last_tags["ALBUM_PEAK"] except KeyError: - raise ReplayGainError(u"results missing for album") + raise ReplayGainError("results missing for album") return AlbumGain(Gain(gain, peak), track_gains) @@ -671,7 +668,7 @@ f = self._src.get_property("location") # A GStreamer error, either an unsupported format or a bug. self._error = ReplayGainError( - u"Error {0!r} - {1!r} on file {2!r}".format(err, debug, f) + f"Error {err!r} - {debug!r} on file {f!r}" ) def _on_tag(self, bus, message): @@ -784,7 +781,7 @@ """ def __init__(self, config, log): - super(AudioToolsBackend, self).__init__(config, log) + super().__init__(config, log) self._import_audiotools() def _import_audiotools(self): @@ -798,7 +795,7 @@ import audiotools.replaygain except ImportError: raise FatalReplayGainError( - u"Failed to load audiotools: audiotools not found" + "Failed to load audiotools: audiotools not found" ) self._mod_audiotools = audiotools self._mod_replaygain = audiotools.replaygain @@ -814,13 +811,13 @@ """ try: audiofile = self._mod_audiotools.open(py3_path(syspath(item.path))) - except IOError: + except OSError: raise ReplayGainError( - u"File {} was not found".format(item.path) + f"File {item.path} was not found" ) except self._mod_audiotools.UnsupportedFile: raise ReplayGainError( - u"Unsupported file type {}".format(item.format) + f"Unsupported file type {item.format}" ) return audiofile @@ -839,7 +836,7 @@ rg = self._mod_replaygain.ReplayGain(audiofile.sample_rate()) except ValueError: raise ReplayGainError( - u"Unsupported sample rate {}".format(item.samplerate)) + f"Unsupported sample rate {item.samplerate}") return return rg @@ -871,8 +868,8 @@ except ValueError as exc: # `audiotools.replaygain` can raise a `ValueError` if the sample # rate is incorrect. - self._log.debug(u'error in rg.title_gain() call: {}', exc) - raise ReplayGainError(u'audiotools audio data error') + self._log.debug('error in rg.title_gain() call: {}', exc) + raise ReplayGainError('audiotools audio data error') return self._with_target_level(gain, target_level), peak def _compute_track_gain(self, item, target_level): @@ -889,7 +886,7 @@ rg, audiofile, target_level ) - self._log.debug(u'ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}', + self._log.debug('ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}', item.artist, item.title, rg_track_gain, rg_track_peak) return Gain(gain=rg_track_gain, peak=rg_track_peak) @@ -914,14 +911,14 @@ track_gains.append( Gain(gain=rg_track_gain, peak=rg_track_peak) ) - self._log.debug(u'ReplayGain for track {0}: {1:.2f}, {2:.2f}', + self._log.debug('ReplayGain for track {0}: {1:.2f}, {2:.2f}', item, rg_track_gain, rg_track_peak) # After getting the values for all tracks, it's possible to get the # album values. rg_album_gain, rg_album_peak = rg.album_gain() rg_album_gain = self._with_target_level(rg_album_gain, target_level) - self._log.debug(u'ReplayGain for album {0}: {1:.2f}, {2:.2f}', + self._log.debug('ReplayGain for album {0}: {1:.2f}, {2:.2f}', items[0].album, rg_album_gain, rg_album_peak) return AlbumGain( @@ -946,7 +943,7 @@ try: exc = self._queue.get_nowait() self._callback() - six.reraise(exc[0], exc[1], exc[2]) + raise exc[1].with_traceback(exc[2]) except queue.Empty: # No exceptions yet, loop back to check # whether `_stopevent` is set @@ -976,13 +973,13 @@ } def __init__(self): - super(ReplayGainPlugin, self).__init__() + super().__init__() # default backend is 'command' for backward-compatibility. self.config.add({ 'overwrite': False, 'auto': True, - 'backend': u'command', + 'backend': 'command', 'threads': cpu_count(), 'parallel_on_import': False, 'per_disc': False, @@ -1000,19 +997,19 @@ if self.backend_name not in self.backends: raise ui.UserError( - u"Selected ReplayGain backend {0} is not supported. " - u"Please select one of: {1}".format( + "Selected ReplayGain backend {} is not supported. " + "Please select one of: {}".format( self.backend_name, - u', '.join(self.backends.keys()) + ', '.join(self.backends.keys()) ) ) peak_method = self.config["peak"].as_str() if peak_method not in self.peak_methods: raise ui.UserError( - u"Selected ReplayGain peak method {0} is not supported. " - u"Please select one of: {1}".format( + "Selected ReplayGain peak method {} is not supported. " + "Please select one of: {}".format( peak_method, - u', '.join(self.peak_methods.keys()) + ', '.join(self.peak_methods.keys()) ) ) self._peak_method = self.peak_methods[peak_method] @@ -1032,7 +1029,7 @@ ) except (ReplayGainError, FatalReplayGainError) as e: raise ui.UserError( - u'replaygain initialization failed: {0}'.format(e)) + f'replaygain initialization failed: {e}') def should_use_r128(self, item): """Checks the plugin setting to decide whether the calculation @@ -1063,27 +1060,27 @@ item.rg_track_gain = track_gain.gain item.rg_track_peak = track_gain.peak item.store() - self._log.debug(u'applied track gain {0} LU, peak {1} of FS', + self._log.debug('applied track gain {0} LU, peak {1} of FS', item.rg_track_gain, item.rg_track_peak) def store_album_gain(self, item, album_gain): item.rg_album_gain = album_gain.gain item.rg_album_peak = album_gain.peak item.store() - self._log.debug(u'applied album gain {0} LU, peak {1} of FS', + self._log.debug('applied album gain {0} LU, peak {1} of FS', item.rg_album_gain, item.rg_album_peak) def store_track_r128_gain(self, item, track_gain): item.r128_track_gain = track_gain.gain item.store() - self._log.debug(u'applied r128 track gain {0} LU', + self._log.debug('applied r128 track gain {0} LU', item.r128_track_gain) def store_album_r128_gain(self, item, album_gain): item.r128_album_gain = album_gain.gain item.store() - self._log.debug(u'applied r128 album gain {0} LU', + self._log.debug('applied r128 album gain {0} LU', item.r128_album_gain) def tag_specific_values(self, items): @@ -1114,17 +1111,17 @@ items, nothing is done. """ if not force and not self.album_requires_gain(album): - self._log.info(u'Skipping album {0}', album) + self._log.info('Skipping album {0}', album) return if (any([self.should_use_r128(item) for item in album.items()]) and not - all(([self.should_use_r128(item) for item in album.items()]))): + all([self.should_use_r128(item) for item in album.items()])): self._log.error( - u"Cannot calculate gain for album {0} (incompatible formats)", + "Cannot calculate gain for album {0} (incompatible formats)", album) return - self._log.info(u'analyzing {0}', album) + self._log.info('analyzing {0}', album) tag_vals = self.tag_specific_values(album.items()) store_track_gain, store_album_gain, target_level, peak = tag_vals @@ -1146,8 +1143,8 @@ # `album_gain` without throwing FatalReplayGainError # => raise non-fatal exception & continue raise ReplayGainError( - u"ReplayGain backend `{}` failed " - u"for some tracks in album {}" + "ReplayGain backend `{}` failed " + "for some tracks in album {}" .format(self.backend_name, album) ) for item, track_gain in zip(items, @@ -1156,7 +1153,7 @@ store_album_gain(item, album_gain.album_gain) if write: item.try_write() - self._log.debug(u'done analyzing {0}', item) + self._log.debug('done analyzing {0}', item) try: self._apply( @@ -1169,10 +1166,10 @@ callback=_store_album ) except ReplayGainError as e: - self._log.info(u"ReplayGain error: {0}", e) + self._log.info("ReplayGain error: {0}", e) except FatalReplayGainError as e: raise ui.UserError( - u"Fatal replay gain error: {0}".format(e)) + f"Fatal replay gain error: {e}") def handle_track(self, item, write, force=False): """Compute track replay gain and store it in the item. @@ -1182,7 +1179,7 @@ in the item, nothing is done. """ if not force and not self.track_requires_gain(item): - self._log.info(u'Skipping track {0}', item) + self._log.info('Skipping track {0}', item) return tag_vals = self.tag_specific_values([item]) @@ -1194,14 +1191,14 @@ # `track_gains` without throwing FatalReplayGainError # => raise non-fatal exception & continue raise ReplayGainError( - u"ReplayGain backend `{}` failed for track {}" + "ReplayGain backend `{}` failed for track {}" .format(self.backend_name, item) ) store_track_gain(item, track_gains[0]) if write: item.try_write() - self._log.debug(u'done analyzing {0}', item) + self._log.debug('done analyzing {0}', item) try: self._apply( @@ -1214,9 +1211,9 @@ callback=_store_track ) except ReplayGainError as e: - self._log.info(u"ReplayGain error: {0}", e) + self._log.info("ReplayGain error: {0}", e) except FatalReplayGainError as e: - raise ui.UserError(u"Fatal replay gain error: {0}".format(e)) + raise ui.UserError(f"Fatal replay gain error: {e}") def _has_pool(self): """Check whether a `ThreadPool` is running instance in `self.pool` @@ -1350,22 +1347,22 @@ def commands(self): """Return the "replaygain" ui subcommand. """ - cmd = ui.Subcommand('replaygain', help=u'analyze for ReplayGain') + cmd = ui.Subcommand('replaygain', help='analyze for ReplayGain') cmd.parser.add_album_option() cmd.parser.add_option( "-t", "--threads", dest="threads", type=int, - help=u'change the number of threads, \ + help='change the number of threads, \ defaults to maximum available processors' ) cmd.parser.add_option( "-f", "--force", dest="force", action="store_true", default=False, - help=u"analyze all files, including those that " + help="analyze all files, including those that " "already have ReplayGain metadata") cmd.parser.add_option( "-w", "--write", default=None, action="store_true", - help=u"write new metadata to files' tags") + help="write new metadata to files' tags") cmd.parser.add_option( "-W", "--nowrite", dest="write", action="store_false", - help=u"don't write metadata (opposite of -w)") + help="don't write metadata (opposite of -w)") cmd.func = self.command_func return [cmd] diff -Nru beets-1.5.0/beetsplug/rewrite.py beets-1.6.0/beetsplug/rewrite.py --- beets-1.5.0/beetsplug/rewrite.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/rewrite.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Uses user-specified rewriting rules to canonicalize names for path formats. """ -from __future__ import division, absolute_import, print_function import re from collections import defaultdict @@ -44,7 +42,7 @@ class RewritePlugin(BeetsPlugin): def __init__(self): - super(RewritePlugin, self).__init__() + super().__init__() self.config.add({}) @@ -55,11 +53,11 @@ try: fieldname, pattern = key.split(None, 1) except ValueError: - raise ui.UserError(u"invalid rewrite specification") + raise ui.UserError("invalid rewrite specification") if fieldname not in library.Item._fields: - raise ui.UserError(u"invalid field name (%s) in rewriter" % + raise ui.UserError("invalid field name (%s) in rewriter" % fieldname) - self._log.debug(u'adding template field {0}', key) + self._log.debug('adding template field {0}', key) pattern = re.compile(pattern.lower()) rules[fieldname].append((pattern, value)) if fieldname == 'artist': diff -Nru beets-1.5.0/beetsplug/scrub.py beets-1.6.0/beetsplug/scrub.py --- beets-1.5.0/beetsplug/scrub.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/scrub.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -17,7 +16,6 @@ automatically whenever tags are written. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import ui @@ -48,7 +46,7 @@ class ScrubPlugin(BeetsPlugin): """Removes extraneous metadata from files' tags.""" def __init__(self): - super(ScrubPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, }) @@ -60,15 +58,15 @@ def scrub_func(lib, opts, args): # Walk through matching files and remove tags. for item in lib.items(ui.decargs(args)): - self._log.info(u'scrubbing: {0}', + self._log.info('scrubbing: {0}', util.displayable_path(item.path)) self._scrub_item(item, opts.write) - scrub_cmd = ui.Subcommand('scrub', help=u'clean audio tags') + scrub_cmd = ui.Subcommand('scrub', help='clean audio tags') scrub_cmd.parser.add_option( - u'-W', u'--nowrite', dest='write', + '-W', '--nowrite', dest='write', action='store_false', default=True, - help=u'leave tags empty') + help='leave tags empty') scrub_cmd.func = scrub_func return [scrub_cmd] @@ -79,7 +77,7 @@ """ classes = [] for modname, clsname in _MUTAGEN_FORMATS.items(): - mod = __import__('mutagen.{0}'.format(modname), + mod = __import__(f'mutagen.{modname}', fromlist=[clsname]) classes.append(getattr(mod, clsname)) return classes @@ -107,8 +105,8 @@ for tag in f.keys(): del f[tag] f.save() - except (IOError, mutagen.MutagenError) as exc: - self._log.error(u'could not scrub {0}: {1}', + except (OSError, mutagen.MutagenError) as exc: + self._log.error('could not scrub {0}: {1}', util.displayable_path(path), exc) def _scrub_item(self, item, restore=True): @@ -121,7 +119,7 @@ mf = mediafile.MediaFile(util.syspath(item.path), config['id3v23'].get(bool)) except mediafile.UnreadableFileError as exc: - self._log.error(u'could not open file to scrub: {0}', + self._log.error('could not open file to scrub: {0}', exc) return images = mf.images @@ -131,21 +129,21 @@ # Restore tags, if enabled. if restore: - self._log.debug(u'writing new tags after scrub') + self._log.debug('writing new tags after scrub') item.try_write() if images: - self._log.debug(u'restoring art') + self._log.debug('restoring art') try: mf = mediafile.MediaFile(util.syspath(item.path), config['id3v23'].get(bool)) mf.images = images mf.save() except mediafile.UnreadableFileError as exc: - self._log.error(u'could not write tags: {0}', exc) + self._log.error('could not write tags: {0}', exc) def import_task_files(self, session, task): """Automatically scrub imported files.""" for item in task.imported_items(): - self._log.debug(u'auto-scrubbing {0}', + self._log.debug('auto-scrubbing {0}', util.displayable_path(item.path)) self._scrub_item(item) diff -Nru beets-1.5.0/beetsplug/smartplaylist.py beets-1.6.0/beetsplug/smartplaylist.py --- beets-1.5.0/beetsplug/smartplaylist.py 2021-08-19 18:58:59.000000000 +0000 +++ beets-1.6.0/beetsplug/smartplaylist.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Dang Mai . # @@ -16,7 +15,6 @@ """Generates smart playlists based on beets queries. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import ui @@ -26,7 +24,6 @@ from beets.dbcore import OrQuery from beets.dbcore.query import MultipleSort, ParsingError import os -import six try: from urllib.request import pathname2url @@ -38,14 +35,14 @@ class SmartPlaylistPlugin(BeetsPlugin): def __init__(self): - super(SmartPlaylistPlugin, self).__init__() + super().__init__() self.config.add({ 'relative_to': None, - 'playlist_dir': u'.', + 'playlist_dir': '.', 'auto': True, 'playlists': [], 'forward_slash': False, - 'prefix': u'', + 'prefix': '', 'urlencode': False, }) @@ -59,8 +56,8 @@ def commands(self): spl_update = ui.Subcommand( 'splupdate', - help=u'update the smart playlists. Playlist names may be ' - u'passed as arguments.' + help='update the smart playlists. Playlist names may be ' + 'passed as arguments.' ) spl_update.func = self.update_cmd return [spl_update] @@ -71,14 +68,14 @@ args = set(ui.decargs(args)) for a in list(args): if not a.endswith(".m3u"): - args.add("{0}.m3u".format(a)) + args.add(f"{a}.m3u") - playlists = set((name, q, a_q) - for name, q, a_q in self._unmatched_playlists - if name in args) + playlists = {(name, q, a_q) + for name, q, a_q in self._unmatched_playlists + if name in args} if not playlists: raise ui.UserError( - u'No playlist matching any of {0} found'.format( + 'No playlist matching any of {} found'.format( [name for name, _, _ in self._unmatched_playlists]) ) @@ -109,7 +106,7 @@ for playlist in self.config['playlists'].get(list): if 'name' not in playlist: - self._log.warning(u"playlist configuration is missing name") + self._log.warning("playlist configuration is missing name") continue playlist_data = (playlist['name'],) @@ -119,7 +116,7 @@ qs = playlist.get(key) if qs is None: query_and_sort = None, None - elif isinstance(qs, six.string_types): + elif isinstance(qs, str): query_and_sort = parse_query_string(qs, model_cls) elif len(qs) == 1: query_and_sort = parse_query_string(qs[0], model_cls) @@ -146,7 +143,7 @@ playlist_data += (query_and_sort,) except ParsingError as exc: - self._log.warning(u"invalid query in playlist {}: {}", + self._log.warning("invalid query in playlist {}: {}", playlist['name'], exc) continue @@ -167,14 +164,14 @@ n, (q, _), (a_q, _) = playlist if self.matches(model, q, a_q): self._log.debug( - u"{0} will be updated because of {1}", n, model) + "{0} will be updated because of {1}", n, model) self._matched_playlists.add(playlist) self.register_listener('cli_exit', self.update_playlists) self._unmatched_playlists -= self._matched_playlists def update_playlists(self, lib): - self._log.info(u"Updating {0} smart playlists...", + self._log.info("Updating {0} smart playlists...", len(self._matched_playlists)) playlist_dir = self.config['playlist_dir'].as_filename() @@ -188,7 +185,7 @@ for playlist in self._matched_playlists: name, (query, q_sort), (album_query, a_q_sort) = playlist - self._log.debug(u"Creating playlist {0}", name) + self._log.debug("Creating playlist {0}", name) items = [] if query: @@ -224,4 +221,4 @@ path = bytestring_path(pathname2url(path)) f.write(prefix + path + b'\n') - self._log.info(u"{0} playlists updated", len(self._matched_playlists)) + self._log.info("{0} playlists updated", len(self._matched_playlists)) diff -Nru beets-1.5.0/beetsplug/sonosupdate.py beets-1.6.0/beetsplug/sonosupdate.py --- beets-1.5.0/beetsplug/sonosupdate.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/sonosupdate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2018, Tobias Sauerwein. # @@ -16,7 +15,6 @@ """Updates a Sonos library whenever the beets library is changed. This is based on the Kodi Update plugin. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin import soco @@ -24,7 +22,7 @@ class SonosUpdate(BeetsPlugin): def __init__(self): - super(SonosUpdate, self).__init__() + super().__init__() self.register_listener('database_change', self.listen_for_db_change) def listen_for_db_change(self, lib, model): @@ -35,14 +33,14 @@ """When the client exists try to send refresh request to a Sonos controler. """ - self._log.info(u'Requesting a Sonos library update...') + self._log.info('Requesting a Sonos library update...') device = soco.discovery.any_soco() if device: device.music_library.start_library_update() else: - self._log.warning(u'Could not find a Sonos device.') + self._log.warning('Could not find a Sonos device.') return - self._log.info(u'Sonos update triggered') + self._log.info('Sonos update triggered') diff -Nru beets-1.5.0/beetsplug/spotify.py beets-1.6.0/beetsplug/spotify.py --- beets-1.5.0/beetsplug/spotify.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/spotify.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2019, Rahul Ahuja. # @@ -16,7 +15,6 @@ """Adds Spotify release and track search support to the autotagger, along with Spotify playlist construction. """ -from __future__ import division, absolute_import, print_function import re import json @@ -24,7 +22,6 @@ import webbrowser import collections -import six import unidecode import requests import confuse @@ -53,7 +50,7 @@ } def __init__(self): - super(SpotifyPlugin, self).__init__() + super().__init__() self.config.add( { 'mode': 'list', @@ -81,7 +78,7 @@ try: with open(self.tokenfile) as f: token_data = json.load(f) - except IOError: + except OSError: self._authenticate() else: self.access_token = token_data['access_token'] @@ -109,7 +106,7 @@ response.raise_for_status() except requests.exceptions.HTTPError as e: raise ui.UserError( - u'Spotify authorization failed: {}\n{}'.format( + 'Spotify authorization failed: {}\n{}'.format( e, response.text ) ) @@ -117,7 +114,7 @@ # Save the token for later use. self._log.debug( - u'{} access token: {}', self.data_source, self.access_token + '{} access token: {}', self.data_source, self.access_token ) with open(self.tokenfile, 'w') as f: json.dump({'access_token': self.access_token}, f) @@ -138,11 +135,11 @@ """ response = request_type( url, - headers={'Authorization': 'Bearer {}'.format(self.access_token)}, + headers={'Authorization': f'Bearer {self.access_token}'}, params=params, ) if response.status_code != 200: - if u'token expired' in response.text: + if 'token expired' in response.text: self._log.debug( '{} access token has expired. Reauthenticating.', self.data_source, @@ -151,7 +148,7 @@ return self._handle_response(request_type, url, params=params) else: raise ui.UserError( - u'{} API error:\n{}\nURL:\n{}\nparams:\n{}'.format( + '{} API error:\n{}\nURL:\n{}\nparams:\n{}'.format( self.data_source, response.text, url, params ) ) @@ -191,8 +188,8 @@ day = None else: raise ui.UserError( - u"Invalid `release_date_precision` returned " - u"by {} API: '{}'".format( + "Invalid `release_date_precision` returned " + "by {} API: '{}'".format( self.data_source, release_date_precision ) ) @@ -303,7 +300,7 @@ ' '.join(':'.join((k, v)) for k, v in filters.items()), ] query = ' '.join([q for q in query_components if q]) - if not isinstance(query, six.text_type): + if not isinstance(query, str): query = query.decode('utf8') return unidecode.unidecode(query) @@ -328,7 +325,7 @@ if not query: return None self._log.debug( - u"Searching {} for '{}'".format(self.data_source, query) + f"Searching {self.data_source} for '{query}'" ) response_data = ( self._handle_response( @@ -340,7 +337,7 @@ .get('items', []) ) self._log.debug( - u"Found {} result(s) from {} for '{}'", + "Found {} result(s) from {} for '{}'", len(response_data), self.data_source, query, @@ -355,21 +352,21 @@ self._output_match_results(results) spotify_cmd = ui.Subcommand( - 'spotify', help=u'build a {} playlist'.format(self.data_source) + 'spotify', help=f'build a {self.data_source} playlist' ) spotify_cmd.parser.add_option( - u'-m', - u'--mode', + '-m', + '--mode', action='store', - help=u'"open" to open {} with playlist, ' - u'"list" to print (default)'.format(self.data_source), + help='"open" to open {} with playlist, ' + '"list" to print (default)'.format(self.data_source), ) spotify_cmd.parser.add_option( - u'-f', - u'--show-failures', + '-f', + '--show-failures', action='store_true', dest='show_failures', - help=u'list tracks that did not match a {} ID'.format( + help='list tracks that did not match a {} ID'.format( self.data_source ), ) @@ -385,7 +382,7 @@ if self.config['mode'].get() not in ['list', 'open']: self._log.warning( - u'{0} is not a valid mode', self.config['mode'].get() + '{0} is not a valid mode', self.config['mode'].get() ) return False @@ -411,12 +408,12 @@ if not items: self._log.debug( - u'Your beets query returned no items, skipping {}.', + 'Your beets query returned no items, skipping {}.', self.data_source, ) return - self._log.info(u'Processing {} tracks...', len(items)) + self._log.info('Processing {} tracks...', len(items)) for item in items: # Apply regex transformations if provided @@ -464,7 +461,7 @@ or self.config['tiebreak'].get() == 'first' ): self._log.debug( - u'{} track(s) found, count: {}', + '{} track(s) found, count: {}', self.data_source, len(response_data_tracks), ) @@ -472,7 +469,7 @@ else: # Use the popularity filter self._log.debug( - u'Most popular track chosen, count: {}', + 'Most popular track chosen, count: {}', len(response_data_tracks), ) chosen_result = max( @@ -484,17 +481,17 @@ if failure_count > 0: if self.config['show_failures'].get(): self._log.info( - u'{} track(s) did not match a {} ID:', + '{} track(s) did not match a {} ID:', failure_count, self.data_source, ) for track in failures: - self._log.info(u'track: {}', track) - self._log.info(u'') + self._log.info('track: {}', track) + self._log.info('') else: self._log.warning( - u'{} track(s) did not match a {} ID:\n' - u'use --show-failures to display', + '{} track(s) did not match a {} ID:\n' + 'use --show-failures to display', failure_count, self.data_source, ) @@ -513,7 +510,7 @@ spotify_ids = [track_data['id'] for track_data in results] if self.config['mode'].get() == 'open': self._log.info( - u'Attempting to open {} with playlist'.format( + 'Attempting to open {} with playlist'.format( self.data_source ) ) @@ -526,5 +523,5 @@ print(self.open_track_url + spotify_id) else: self._log.warning( - u'No {} tracks found from beets query'.format(self.data_source) + f'No {self.data_source} tracks found from beets query' ) diff -Nru beets-1.5.0/beetsplug/subsonicplaylist.py beets-1.6.0/beetsplug/subsonicplaylist.py --- beets-1.5.0/beetsplug/subsonicplaylist.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/subsonicplaylist.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2019, Joris Jensen # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import absolute_import, division, print_function import random import string @@ -45,8 +43,8 @@ def to_be_removed(item): for artist, album, title in keys: if artist == item['artist'] and\ - album == item['album'] and\ - title == item['title']: + album == item['album'] and\ + title == item['title']: return False return True @@ -56,7 +54,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def __init__(self): - super(SubsonicPlaylistPlugin, self).__init__() + super().__init__() self.config.add( { 'delete': False, @@ -76,7 +74,7 @@ MatchQuery("title", query[2])]) items = lib.items(query) if not items: - self._log.warn(u"{} | track not found ({})", playlist_tag, + self._log.warn("{} | track not found ({})", playlist_tag, query) continue for item in items: @@ -130,13 +128,13 @@ self.update_tags(playlist_dict, lib) subsonicplaylist_cmds = Subcommand( - 'subsonicplaylist', help=u'import a subsonic playlist' + 'subsonicplaylist', help='import a subsonic playlist' ) subsonicplaylist_cmds.parser.add_option( - u'-d', - u'--delete', + '-d', + '--delete', action='store_true', - help=u'delete tag from items not in any playlist anymore', + help='delete tag from items not in any playlist anymore', ) subsonicplaylist_cmds.func = build_playlist return [subsonicplaylist_cmds] diff -Nru beets-1.5.0/beetsplug/subsonicupdate.py beets-1.6.0/beetsplug/subsonicupdate.py --- beets-1.5.0/beetsplug/subsonicupdate.py 2021-08-19 18:58:59.000000000 +0000 +++ beets-1.6.0/beetsplug/subsonicupdate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -29,7 +28,6 @@ pass: password auth: pass """ -from __future__ import division, absolute_import, print_function import hashlib import random @@ -46,7 +44,7 @@ class SubsonicUpdate(BeetsPlugin): def __init__(self): - super(SubsonicUpdate, self).__init__() + super().__init__() # Set default configuration values config['subsonic'].add({ 'user': 'admin', @@ -94,16 +92,16 @@ context_path = config['subsonic']['contextpath'].as_str() if context_path == '/': context_path = '' - url = "http://{}:{}{}".format(host, port, context_path) + url = f"http://{host}:{port}{context_path}" - return url + '/rest/{}'.format(endpoint) + return url + f'/rest/{endpoint}' def start_scan(self): user = config['subsonic']['user'].as_str() auth = config['subsonic']['auth'].as_str() url = self.__format_url("startScan") - self._log.debug(u'URL is {0}', url) - self._log.debug(u'auth type is {0}', config['subsonic']['auth']) + self._log.debug('URL is {0}', url) + self._log.debug('auth type is {0}', config['subsonic']['auth']) if auth == "token": salt, token = self.__create_token() @@ -120,7 +118,7 @@ encpass = hexlify(password.encode()).decode() payload = { 'u': user, - 'p': 'enc:{}'.format(encpass), + 'p': f'enc:{encpass}', 'v': '1.12.0', 'c': 'beets', 'f': 'json' @@ -135,12 +133,12 @@ json['subsonic-response']['status'] == "ok": count = json['subsonic-response']['scanStatus']['count'] self._log.info( - u'Updating Subsonic; scanning {0} tracks'.format(count)) + f'Updating Subsonic; scanning {count} tracks') elif response.status_code == 200 and \ json['subsonic-response']['status'] == "failed": error_message = json['subsonic-response']['error']['message'] - self._log.error(u'Error: {0}'.format(error_message)) + self._log.error(f'Error: {error_message}') else: - self._log.error(u'Error: {0}', json) + self._log.error('Error: {0}', json) except Exception as error: - self._log.error(u'Error: {0}'.format(error)) + self._log.error(f'Error: {error}') diff -Nru beets-1.5.0/beetsplug/the.py beets-1.6.0/beetsplug/the.py --- beets-1.5.0/beetsplug/the.py 2020-08-10 22:29:53.000000000 +0000 +++ beets-1.6.0/beetsplug/the.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Blemjhoo Tezoulbr . # @@ -15,7 +14,6 @@ """Moves patterns in path formats (suitable for moving articles).""" -from __future__ import division, absolute_import, print_function import re from beets.plugins import BeetsPlugin @@ -23,9 +21,9 @@ __author__ = 'baobab@heresiarch.info' __version__ = '1.1' -PATTERN_THE = u'^the\\s' -PATTERN_A = u'^[a][n]?\\s' -FORMAT = u'{0}, {1}' +PATTERN_THE = '^the\\s' +PATTERN_A = '^[a][n]?\\s' +FORMAT = '{0}, {1}' class ThePlugin(BeetsPlugin): @@ -33,14 +31,14 @@ patterns = [] def __init__(self): - super(ThePlugin, self).__init__() + super().__init__() self.template_funcs['the'] = self.the_template_func self.config.add({ 'the': True, 'a': True, - 'format': u'{0}, {1}', + 'format': '{0}, {1}', 'strip': False, 'patterns': [], }) @@ -51,17 +49,17 @@ try: re.compile(p) except re.error: - self._log.error(u'invalid pattern: {0}', p) + self._log.error('invalid pattern: {0}', p) else: if not (p.startswith('^') or p.endswith('$')): - self._log.warning(u'warning: \"{0}\" will not ' - u'match string start/end', p) + self._log.warning('warning: \"{0}\" will not ' + 'match string start/end', p) if self.config['a']: self.patterns = [PATTERN_A] + self.patterns if self.config['the']: self.patterns = [PATTERN_THE] + self.patterns if not self.patterns: - self._log.warning(u'no patterns defined!') + self._log.warning('no patterns defined!') def unthe(self, text, pattern): """Moves pattern in the path format string or strips it @@ -84,7 +82,7 @@ fmt = self.config['format'].as_str() return fmt.format(r, t.strip()).strip() else: - return u'' + return '' def the_template_func(self, text): if not self.patterns: @@ -93,8 +91,8 @@ for p in self.patterns: r = self.unthe(text, p) if r != text: - self._log.debug(u'\"{0}\" -> \"{1}\"', text, r) + self._log.debug('\"{0}\" -> \"{1}\"', text, r) break return r else: - return u'' + return '' diff -Nru beets-1.5.0/beetsplug/thumbnails.py beets-1.6.0/beetsplug/thumbnails.py --- beets-1.5.0/beetsplug/thumbnails.py 2020-08-10 22:29:51.000000000 +0000 +++ beets-1.6.0/beetsplug/thumbnails.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Bruno Cauet # @@ -19,7 +18,6 @@ Spec: standards.freedesktop.org/thumbnail-spec/latest/index.html """ -from __future__ import division, absolute_import, print_function from hashlib import md5 import os @@ -35,7 +33,6 @@ from beets.ui import Subcommand, decargs from beets import util from beets.util.artresizer import ArtResizer, get_im_version, get_pil_version -import six BASE_DIR = os.path.join(BaseDirectory.xdg_cache_home, "thumbnails") @@ -45,7 +42,7 @@ class ThumbnailsPlugin(BeetsPlugin): def __init__(self): - super(ThumbnailsPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, 'force': False, @@ -58,15 +55,15 @@ def commands(self): thumbnails_command = Subcommand("thumbnails", - help=u"Create album thumbnails") + help="Create album thumbnails") thumbnails_command.parser.add_option( - u'-f', u'--force', + '-f', '--force', dest='force', action='store_true', default=False, - help=u'force regeneration of thumbnails deemed fine (existing & ' - u'recent enough)') + help='force regeneration of thumbnails deemed fine (existing & ' + 'recent enough)') thumbnails_command.parser.add_option( - u'--dolphin', dest='dolphin', action='store_true', default=False, - help=u"create Dolphin-compatible thumbnail information (for KDE)") + '--dolphin', dest='dolphin', action='store_true', default=False, + help="create Dolphin-compatible thumbnail information (for KDE)") thumbnails_command.func = self.process_query return [thumbnails_command] @@ -85,8 +82,8 @@ - detect whether we'll use GIO or Python to get URIs """ if not ArtResizer.shared.local: - self._log.warning(u"No local image resizing capabilities, " - u"cannot generate thumbnails") + self._log.warning("No local image resizing capabilities, " + "cannot generate thumbnails") return False for dir in (NORMAL_DIR, LARGE_DIR): @@ -100,12 +97,12 @@ assert get_pil_version() # since we're local self.write_metadata = write_metadata_pil tool = "PIL" - self._log.debug(u"using {0} to write metadata", tool) + self._log.debug("using {0} to write metadata", tool) uri_getter = GioURI() if not uri_getter.available: uri_getter = PathlibURI() - self._log.debug(u"using {0.name} to compute URIs", uri_getter) + self._log.debug("using {0.name} to compute URIs", uri_getter) self.get_uri = uri_getter.uri return True @@ -113,9 +110,9 @@ def process_album(self, album): """Produce thumbnails for the album folder. """ - self._log.debug(u'generating thumbnail for {0}', album) + self._log.debug('generating thumbnail for {0}', album) if not album.artpath: - self._log.info(u'album {0} has no art', album) + self._log.info('album {0} has no art', album) return if self.config['dolphin']: @@ -123,7 +120,7 @@ size = ArtResizer.shared.get_size(album.artpath) if not size: - self._log.warning(u'problem getting the picture size for {0}', + self._log.warning('problem getting the picture size for {0}', album.artpath) return @@ -133,9 +130,9 @@ wrote &= self.make_cover_thumbnail(album, 128, NORMAL_DIR) if wrote: - self._log.info(u'wrote thumbnail for {0}', album) + self._log.info('wrote thumbnail for {0}', album) else: - self._log.info(u'nothing to do for {0}', album) + self._log.info('nothing to do for {0}', album) def make_cover_thumbnail(self, album, size, target_dir): """Make a thumbnail of given size for `album` and put it in @@ -146,11 +143,11 @@ if os.path.exists(target) and \ os.stat(target).st_mtime > os.stat(album.artpath).st_mtime: if self.config['force']: - self._log.debug(u"found a suitable {1}x{1} thumbnail for {0}, " - u"forcing regeneration", album, size) + self._log.debug("found a suitable {1}x{1} thumbnail for {0}, " + "forcing regeneration", album, size) else: - self._log.debug(u"{1}x{1} thumbnail for {0} exists and is " - u"recent enough", album, size) + self._log.debug("{1}x{1} thumbnail for {0} exists and is " + "recent enough", album, size) return False resized = ArtResizer.shared.resize(size, album.artpath, util.syspath(target)) @@ -164,7 +161,7 @@ """ uri = self.get_uri(path) hash = md5(uri.encode('utf-8')).hexdigest() - return util.bytestring_path("{0}.png".format(hash)) + return util.bytestring_path(f"{hash}.png") def add_tags(self, album, image_path): """Write required metadata to the thumbnail @@ -172,11 +169,11 @@ """ mtime = os.stat(album.artpath).st_mtime metadata = {"Thumb::URI": self.get_uri(album.artpath), - "Thumb::MTime": six.text_type(mtime)} + "Thumb::MTime": str(mtime)} try: self.write_metadata(image_path, metadata) except Exception: - self._log.exception(u"could not write metadata to {0}", + self._log.exception("could not write metadata to {0}", util.displayable_path(image_path)) def make_dolphin_cover_thumbnail(self, album): @@ -186,9 +183,9 @@ artfile = os.path.split(album.artpath)[1] with open(outfilename, 'w') as f: f.write('[Desktop Entry]\n') - f.write('Icon=./{0}'.format(artfile.decode('utf-8'))) + f.write('Icon=./{}'.format(artfile.decode('utf-8'))) f.close() - self._log.debug(u"Wrote file {0}", util.displayable_path(outfilename)) + self._log.debug("Wrote file {0}", util.displayable_path(outfilename)) def write_metadata_im(file, metadata): @@ -211,7 +208,7 @@ return True -class URIGetter(object): +class URIGetter: available = False name = "Abstract base" @@ -269,7 +266,7 @@ def uri(self, path): g_file_ptr = self.libgio.g_file_new_for_path(path) if not g_file_ptr: - raise RuntimeError(u"No gfile pointer received for {0}".format( + raise RuntimeError("No gfile pointer received for {}".format( util.displayable_path(path))) try: @@ -278,8 +275,8 @@ self.libgio.g_object_unref(g_file_ptr) if not uri_ptr: self.libgio.g_free(uri_ptr) - raise RuntimeError(u"No URI received from the gfile pointer for " - u"{0}".format(util.displayable_path(path))) + raise RuntimeError("No URI received from the gfile pointer for " + "{}".format(util.displayable_path(path))) try: uri = copy_c_string(uri_ptr) @@ -290,5 +287,5 @@ return uri.decode(util._fsencoding()) except UnicodeDecodeError: raise RuntimeError( - "Could not decode filename from GIO: {!r}".format(uri) + f"Could not decode filename from GIO: {uri!r}" ) diff -Nru beets-1.5.0/beetsplug/types.py beets-1.6.0/beetsplug/types.py --- beets-1.5.0/beetsplug/types.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/types.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.dbcore import types @@ -47,6 +45,6 @@ mytypes[key] = library.DateType() else: raise ConfigValueError( - u"unknown type '{0}' for the '{1}' field" + "unknown type '{}' for the '{}' field" .format(value, key)) return mytypes diff -Nru beets-1.5.0/beetsplug/unimported.py beets-1.6.0/beetsplug/unimported.py --- beets-1.5.0/beetsplug/unimported.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/unimported.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2019, Joris Jensen # @@ -18,7 +17,6 @@ beets library database, including art files """ -from __future__ import absolute_import, division, print_function import os from beets import util @@ -31,7 +29,7 @@ class Unimported(BeetsPlugin): def __init__(self): - super(Unimported, self).__init__() + super().__init__() self.config.add( { 'ignore_extensions': [] @@ -40,15 +38,25 @@ def commands(self): def print_unimported(lib, opts, args): - ignore_exts = [('.' + x).encode() for x - in self.config['ignore_extensions'].as_str_seq()] - in_folder = set( - (os.path.join(r, file) for r, d, f in os.walk(lib.directory) - for file in f if not any( - [file.endswith(extension) for extension in - ignore_exts]))) - in_library = set(x.path for x in lib.items()) - art_files = set(x.artpath for x in lib.albums()) + ignore_exts = [ + ('.' + x).encode() + for x in self.config["ignore_extensions"].as_str_seq() + ] + ignore_dirs = [ + os.path.join(lib.directory, x.encode()) + for x in self.config["ignore_subdirectories"].as_str_seq() + ] + in_folder = { + os.path.join(r, file) + for r, d, f in os.walk(lib.directory) + for file in f + if not any( + [file.endswith(ext) for ext in ignore_exts] + + [r in ignore_dirs] + ) + } + in_library = {x.path for x in lib.items()} + art_files = {x.artpath for x in lib.albums()} for f in in_folder - in_library - art_files: print_(util.displayable_path(f)) diff -Nru beets-1.5.0/beetsplug/web/__init__.py beets-1.6.0/beetsplug/web/__init__.py --- beets-1.5.0/beetsplug/web/__init__.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/beetsplug/web/__init__.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,7 +13,6 @@ # included in all copies or substantial portions of the Software. """A Web interface to beets.""" -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import ui @@ -155,7 +153,7 @@ else: return flask.abort(405) - responder.__name__ = 'get_{0}'.format(name) + responder.__name__ = f'get_{name}' return responder return make_responder @@ -203,7 +201,7 @@ else: return flask.abort(405) - responder.__name__ = 'query_{0}'.format(name) + responder.__name__ = f'query_{name}' return responder @@ -220,7 +218,7 @@ json_generator(list_all(), root=name, expand=is_expand()), mimetype='application/json' ) - responder.__name__ = 'all_{0}'.format(name) + responder.__name__ = f'all_{name}' return responder return make_responder @@ -230,7 +228,7 @@ if field not in model.all_keys() or sort_field not in model.all_keys(): raise KeyError with g.lib.transaction() as tx: - rows = tx.query('SELECT DISTINCT "{0}" FROM "{1}" ORDER BY "{2}"' + rows = tx.query('SELECT DISTINCT "{}" FROM "{}" ORDER BY "{}"' .format(field, model._table, sort_field)) return [row[0] for row in rows] @@ -434,9 +432,9 @@ class WebPlugin(BeetsPlugin): def __init__(self): - super(WebPlugin, self).__init__() + super().__init__() self.config.add({ - 'host': u'127.0.0.1', + 'host': '127.0.0.1', 'port': 8337, 'cors': '', 'cors_supports_credentials': False, @@ -446,9 +444,9 @@ }) def commands(self): - cmd = ui.Subcommand('web', help=u'start a Web interface') - cmd.parser.add_option(u'-d', u'--debug', action='store_true', - default=False, help=u'debug mode') + cmd = ui.Subcommand('web', help='start a Web interface') + cmd.parser.add_option('-d', '--debug', action='store_true', + default=False, help='debug mode') def func(lib, opts, args): args = ui.decargs(args) @@ -466,7 +464,7 @@ # Enable CORS if required. if self.config['cors']: - self._log.info(u'Enabling CORS with origin: {0}', + self._log.info('Enabling CORS with origin: {0}', self.config['cors']) from flask_cors import CORS app.config['CORS_ALLOW_HEADERS'] = "Content-Type" @@ -492,7 +490,7 @@ return [cmd] -class ReverseProxied(object): +class ReverseProxied: '''Wrap the application in this middleware and configure the front-end server to add these headers, to let you quietly bind this to a URL other than / and to an HTTP scheme that is diff -Nru beets-1.5.0/beetsplug/zero.py beets-1.6.0/beetsplug/zero.py --- beets-1.5.0/beetsplug/zero.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/beetsplug/zero.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Blemjhoo Tezoulbr . # @@ -15,8 +14,6 @@ """ Clears tag fields in media files.""" -from __future__ import division, absolute_import, print_function -import six import re @@ -31,7 +28,7 @@ class ZeroPlugin(BeetsPlugin): def __init__(self): - super(ZeroPlugin, self).__init__() + super().__init__() self.register_listener('write', self.write_event) self.register_listener('import_task_choice', @@ -56,7 +53,7 @@ """ if self.config['fields'] and self.config['keep_fields']: self._log.warning( - u'cannot blacklist and whitelist at the same time' + 'cannot blacklist and whitelist at the same time' ) # Blacklist mode. elif self.config['fields']: @@ -75,7 +72,7 @@ def zero_fields(lib, opts, args): if not decargs(args) and not input_yn( - u"Remove fields for all items? (Y/n)", + "Remove fields for all items? (Y/n)", True): return for item in lib.items(decargs(args)): @@ -89,10 +86,10 @@ Do some sanity checks then compile the regexes. """ if field not in MediaFile.fields(): - self._log.error(u'invalid field: {0}', field) + self._log.error('invalid field: {0}', field) elif field in ('id', 'path', 'album_id'): - self._log.warning(u'field \'{0}\' ignored, zeroing ' - u'it would be dangerous', field) + self._log.warning('field \'{0}\' ignored, zeroing ' + 'it would be dangerous', field) else: try: for pattern in self.config[field].as_str_seq(): @@ -104,7 +101,7 @@ def import_task_choice_event(self, session, task): if task.choice_flag == action.ASIS and not self.warned: - self._log.warning(u'cannot zero in \"as-is\" mode') + self._log.warning('cannot zero in \"as-is\" mode') self.warned = True # TODO request write in as-is mode @@ -122,7 +119,7 @@ fields_set = False if not self.fields_to_progs: - self._log.warning(u'no fields, nothing to do') + self._log.warning('no fields, nothing to do') return False for field, progs in self.fields_to_progs.items(): @@ -135,7 +132,7 @@ if match: fields_set = True - self._log.debug(u'{0}: {1} -> None', field, value) + self._log.debug('{0}: {1} -> None', field, value) tags[field] = None if self.config['update_database']: item[field] = None @@ -158,6 +155,6 @@ if not progs: return True for prog in progs: - if prog.search(six.text_type(value)): + if prog.search(str(value)): return True return False diff -Nru beets-1.5.0/debian/changelog beets-1.6.0/debian/changelog --- beets-1.5.0/debian/changelog 2021-10-11 19:11:35.000000000 +0000 +++ beets-1.6.0/debian/changelog 2021-11-29 18:28:35.000000000 +0000 @@ -1,3 +1,11 @@ +beets (1.6.0-1) unstable; urgency=medium + + * New upstream release. + * Refresh patches. + * Update copyright. + + -- Stefano Rivera Mon, 29 Nov 2021 14:28:35 -0400 + beets (1.5.0-1) unstable; urgency=medium [ Stefano Rivera ] diff -Nru beets-1.5.0/debian/copyright beets-1.6.0/debian/copyright --- beets-1.5.0/debian/copyright 2021-10-11 19:11:35.000000000 +0000 +++ beets-1.6.0/debian/copyright 2021-11-29 18:28:35.000000000 +0000 @@ -23,6 +23,7 @@ 2020, David Swarbrick 2016, Diego Moreda 2017, Dorian Soergel + 2021, Edgars Supe 2012-2016, Fabrice Laporte 2016, François-Xavier Thomas 2021, Graham R. Cobb @@ -45,7 +46,6 @@ 2016, Rafael Bodill 2019, Rahul Ahuja 2014-2016, Thomas Scholtes - 2017, Tigran Kostandyan 2018, Tobias Sauerwein 2016, Tom Jaspers 2013-2016, Verrus diff -Nru beets-1.5.0/debian/patches/skip-broken-test beets-1.6.0/debian/patches/skip-broken-test --- beets-1.5.0/debian/patches/skip-broken-test 2021-10-11 19:11:35.000000000 +0000 +++ beets-1.6.0/debian/patches/skip-broken-test 2021-11-29 18:28:35.000000000 +0000 @@ -8,10 +8,10 @@ 1 file changed, 1 insertion(+) diff --git a/test/test_ui.py b/test/test_ui.py -index 5cfed1f..289788e 100644 +index 9804b0a..877642d 100644 --- a/test/test_ui.py +++ b/test/test_ui.py -@@ -954,6 +954,7 @@ class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions): +@@ -953,6 +953,7 @@ class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions): os.path.join(self.beetsdir, b'state') ) diff -Nru beets-1.5.0/debian/patches/skip-buildd-failures beets-1.6.0/debian/patches/skip-buildd-failures --- beets-1.5.0/debian/patches/skip-buildd-failures 2021-10-11 19:11:35.000000000 +0000 +++ beets-1.6.0/debian/patches/skip-buildd-failures 2021-11-29 18:28:35.000000000 +0000 @@ -9,10 +9,10 @@ 1 file changed, 5 insertions(+) diff --git a/test/test_art.py b/test/test_art.py -index d84ca4a..b795f10 100644 +index 498c4ce..cfd6d69 100644 --- a/test/test_art.py +++ b/test/test_art.py -@@ -396,6 +396,7 @@ class ITunesStoreTest(UseThePlugin): +@@ -390,6 +390,7 @@ class ITunesStoreTest(UseThePlugin): self.assertEqual(candidate.url, 'url_to_the_image') self.assertEqual(candidate.match, fetchart.Candidate.MATCH_EXACT) @@ -20,7 +20,7 @@ def test_itunesstore_no_result(self): json = '{"results": []}' self.mock_response(fetchart.ITunesStore.API_URL, json) -@@ -406,6 +407,7 @@ class ITunesStoreTest(UseThePlugin): +@@ -400,6 +401,7 @@ class ITunesStoreTest(UseThePlugin): next(self.source.get(self.album, self.settings, [])) self.assertIn(expected, logs[1]) @@ -28,7 +28,7 @@ def test_itunesstore_requestexception(self): responses.add(responses.GET, fetchart.ITunesStore.API_URL, json={'error': 'not found'}, status=404) -@@ -431,6 +433,7 @@ class ITunesStoreTest(UseThePlugin): +@@ -425,6 +427,7 @@ class ITunesStoreTest(UseThePlugin): self.assertEqual(candidate.url, 'url_to_the_image') self.assertEqual(candidate.match, fetchart.Candidate.MATCH_FALLBACK) @@ -36,7 +36,7 @@ def test_itunesstore_returns_result_without_artwork(self): json = """{ "results": -@@ -449,6 +452,7 @@ class ITunesStoreTest(UseThePlugin): +@@ -443,6 +446,7 @@ class ITunesStoreTest(UseThePlugin): next(self.source.get(self.album, self.settings, [])) self.assertIn(expected, logs[1]) @@ -44,7 +44,7 @@ def test_itunesstore_returns_no_result_when_error_received(self): json = '{"error": {"errors": [{"reason": "some reason"}]}}' self.mock_response(fetchart.ITunesStore.API_URL, json) -@@ -459,6 +463,7 @@ class ITunesStoreTest(UseThePlugin): +@@ -453,6 +457,7 @@ class ITunesStoreTest(UseThePlugin): next(self.source.get(self.album, self.settings, [])) self.assertIn(expected, logs[1]) diff -Nru beets-1.5.0/debian/patches/skip-unreliable-tests beets-1.6.0/debian/patches/skip-unreliable-tests --- beets-1.5.0/debian/patches/skip-unreliable-tests 2021-10-11 19:11:35.000000000 +0000 +++ beets-1.6.0/debian/patches/skip-unreliable-tests 2021-11-29 18:28:35.000000000 +0000 @@ -9,10 +9,10 @@ 2 files changed, 3 insertions(+) diff --git a/test/test_library.py b/test/test_library.py -index 51171b1..7f1ab2a 100644 +index 6981b87..667d92c 100644 --- a/test/test_library.py +++ b/test/test_library.py -@@ -1151,6 +1151,7 @@ class WriteTest(unittest.TestCase, TestHelper): +@@ -1171,6 +1171,7 @@ class WriteTest(unittest.TestCase, TestHelper): with self.assertRaises(beets.library.ReadError): item.write() @@ -21,10 +21,10 @@ item = self.add_item_fixture() path = syspath(item.path) diff --git a/test/test_plugins.py b/test/test_plugins.py -index 884aa78..5f198c5 100644 +index 2e5b243..30d25eb 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py -@@ -211,6 +211,7 @@ class EventsTest(unittest.TestCase, ImportHelper, TestHelper): +@@ -209,6 +209,7 @@ class EventsTest(unittest.TestCase, ImportHelper, TestHelper): self.__copy_file(dest_path, metadata) self.file_paths.append(dest_path) @@ -32,8 +32,8 @@ def test_import_task_created(self): import_files = [self.import_dir] self._setup_import_session(singletons=False) -@@ -233,6 +234,7 @@ class EventsTest(unittest.TestCase, ImportHelper, TestHelper): - u' {0}'.format(displayable_path(self.file_paths[1])), +@@ -231,6 +232,7 @@ class EventsTest(unittest.TestCase, ImportHelper, TestHelper): + ' {}'.format(displayable_path(self.file_paths[1])), ]) + @unittest.skip('unreliable') diff -Nru beets-1.5.0/docs/changelog.rst beets-1.6.0/docs/changelog.rst --- beets-1.5.0/docs/changelog.rst 2021-08-19 19:56:07.000000000 +0000 +++ beets-1.6.0/docs/changelog.rst 2021-11-27 16:35:40.000000000 +0000 @@ -1,6 +1,97 @@ Changelog ========= +1.6.0 (November 27, 2021) +------------------------- + +This release is our first experiment with time-based releases! We are aiming +to publish a new release of beets every 3 months. We therefore have a healthy +but not dizzyingly long list of new features and fixes. + +With this release, beets now requires Python 3.6 or later (it removes support +for Python 2.7, 3.4, and 3.5). There are also a few other dependency +changes---if you're a maintainer of a beets package for a package manager, +thank you for your ongoing efforts, and please see the list of notes below. + +Major new features: + +* When fetching genres from MusicBrainz, we now include genres from the + release group (in addition to the release). We also prioritize genres based + on the number of votes. + Thanks to :user:`aereaux`. +* Primary and secondary release types from MusicBrainz are now stored in a new + ``albumtypes`` field. + Thanks to :user:`edgars-supe`. + :bug:`2200` +* An accompanying new :doc:`/plugins/albumtypes` includes some options for + formatting this new ``albumtypes`` field. + Thanks to :user:`edgars-supe`. + +Other new things: + +* :doc:`/plugins/permissions`: The plugin now sets cover art permissions to + match the audio file permissions. +* :doc:`/plugins/unimported`: A new configuration option supports excluding + specific subdirectories in library. +* :doc:`/plugins/info`: Add support for an ``--album`` flag. +* :doc:`/plugins/export`: Similarly add support for an ``--album`` flag. +* ``beet move`` now highlights path differences in color (when enabled). +* When moving files and a direct rename of a file is not possible (for + example, when crossing filesystems), beets now copies to a temporary file in + the target folder first and then moves to the destination instead of + directly copying the target path. This gets us closer to always updating + files atomically. + Thanks to :user:`catap`. + :bug:`4060` +* :doc:`/plugins/fetchart`: Add a new option to store cover art as + non-progressive image. This is useful for DAPs that do not support + progressive images. Set ``deinterlace: yes`` in your configuration to enable + this conversion. +* :doc:`/plugins/fetchart`: Add a new option to change the file format of + cover art images. This may also be useful for DAPs that only support some + image formats. +* Support flexible attributes in ``%aunique``. + :bug:`2678` :bug:`3553` +* Make ``%aunique`` faster, especially when using inline fields. + :bug:`4145` + +Bug fixes: + +* :doc:`/plugins/lyrics`: Fix a crash when Beautiful Soup is not installed. + :bug:`4027` +* :doc:`/plugins/discogs`: Support a new Discogs URL format for IDs. + :bug:`4080` +* :doc:`/plugins/discogs`: Remove built-in rate-limiting because the Discogs + Python library we use now has its own rate-limiting. + :bug: `4108` +* :doc:`/plugins/export`: Fix some duplicated output. +* :doc:`/plugins/aura`: Fix a potential security hole when serving image + files. + :bug:`4160` + +For plugin developers: + +* :py:meth:`beets.library.Item.destination` now accepts a `replacements` + argument to be used in favor of the default. +* The `pluginload` event is now sent after plugin types and queries are + available, not before. +* A new plugin event, `album_removed`, is called when an album is removed from + the library (even when its file is not deleted from disk). + +Here are some notes for packagers: + +* As noted above, the minimum Python version is now 3.6. +* We fixed a flaky test, named `test_album_art` in the `test_zero.py` file, + that some distributions had disabled. Disabling this test should no longer + be necessary. + :bug:`4037` :bug:`4038` +* This version of beets no longer depends on the `six`_ library. + :bug:`4030` +* The `gmusic` plugin was removed since Google Play Music has been shut down. + Thus, the optional dependency on `gmusicapi` does not exist anymore. + :bug:`4089` + + 1.5.0 (August 19, 2021) ----------------------- diff -Nru beets-1.5.0/docs/conf.py beets-1.6.0/docs/conf.py --- beets-1.5.0/docs/conf.py 2020-10-27 00:23:46.000000000 +0000 +++ beets-1.6.0/docs/conf.py 2021-11-27 16:16:45.000000000 +0000 @@ -1,8 +1,4 @@ -# -*- coding: utf-8 -*- - -from __future__ import division, absolute_import, print_function - -AUTHOR = u'Adrian Sampson' +AUTHOR = 'Adrian Sampson' # General configuration @@ -12,11 +8,11 @@ source_suffix = '.rst' master_doc = 'index' -project = u'beets' -copyright = u'2016, Adrian Sampson' +project = 'beets' +copyright = '2016, Adrian Sampson' -version = '1.5' -release = '1.5.0' +version = '1.6' +release = '1.6.0' pygments_style = 'sphinx' @@ -32,6 +28,7 @@ r'https://github.com/beetbox/beets/issues/', r'https://github.com/[^/]+$', # ignore user pages r'.*localhost.*', + r'https?://127\.0\.0\.1', r'https://www.musixmatch.com/', # blocks requests r'https://genius.com/', # blocks requests ] @@ -41,14 +38,14 @@ # Options for LaTeX output latex_documents = [ - ('index', 'beets.tex', u'beets Documentation', + ('index', 'beets.tex', 'beets Documentation', AUTHOR, 'manual'), ] # Options for manual page output man_pages = [ - ('reference/cli', 'beet', u'music tagger and library organizer', + ('reference/cli', 'beet', 'music tagger and library organizer', [AUTHOR], 1), - ('reference/config', 'beetsconfig', u'beets configuration file', + ('reference/config', 'beetsconfig', 'beets configuration file', [AUTHOR], 5), ] diff -Nru beets-1.5.0/docs/dev/plugins.rst beets-1.6.0/docs/dev/plugins.rst --- beets-1.5.0/docs/dev/plugins.rst 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/docs/dev/plugins.rst 2021-11-26 20:51:38.000000000 +0000 @@ -143,6 +143,9 @@ command finishes adding an album to the library. Parameters: ``lib``, ``album`` +* `album_removed`: called with an ``Album`` object every time an album is + removed from the library (even when its file is not deleted from disk). + * `item_copied`: called with an ``Item`` object whenever its file is copied. Parameters: ``item``, ``source`` path, ``destination`` path diff -Nru beets-1.5.0/docs/faq.rst beets-1.6.0/docs/faq.rst --- beets-1.5.0/docs/faq.rst 2020-10-27 00:23:46.000000000 +0000 +++ beets-1.6.0/docs/faq.rst 2021-11-26 20:51:38.000000000 +0000 @@ -147,6 +147,12 @@ ``pip install -e git+https://github.com/beetbox/beets#egg=beets`` to clone beets and install it, allowing you to modify the source in-place to try out changes. + - Combine the previous two approaches, cloning the source yourself, + and then installing in editable mode: + ``git clone https://github.com/beetbox/beets.git`` then + ``pip install -e beets``. This approach lets you decide where the + source is stored, with any changes immediately reflected in your + environment. More details about the beets source are available on the :doc:`developer documentation ` pages. diff -Nru beets-1.5.0/docs/guides/main.rst beets-1.6.0/docs/guides/main.rst --- beets-1.5.0/docs/guides/main.rst 2020-09-14 00:55:24.000000000 +0000 +++ beets-1.6.0/docs/guides/main.rst 2021-11-26 20:51:38.000000000 +0000 @@ -10,13 +10,13 @@ ---------- You will need Python. -Beets works on `Python 2.7`_ and Python 3.4 or later. +Beets works on Python 3.6 or later. -.. _Python 2.7: https://www.python.org/download/ - -* **macOS** v10.7 (Lion) and later include Python 2.7 out of the box. - You can opt for Python 3 by installing it via `Homebrew`_: - ``brew install python3`` +* **macOS** 11 (Big Sur) includes Python 3.8 out of the box. + You can opt for a more recent Python installing it via `Homebrew`_ + (``brew install python3``). + There's also a `MacPorts`_ port. Run ``port install beets`` or + ``port install beets-full`` to include many third-party plugins. * On **Debian or Ubuntu**, depending on the version, beets is available as an official package (`Debian details`_, `Ubuntu details`_), so try typing: @@ -40,9 +40,7 @@ * For **Slackware**, there's a `SlackBuild`_ available. -* On **Fedora** 22 or later, there is a `DNF package`_:: - - $ sudo dnf install beets beets-plugins beets-doc +* On **Fedora** 22 or later, there's a `DNF package`_ you can install with ``sudo dnf install beets beets-plugins beets-doc``. * On **Solus**, run ``eopkg install beets``. @@ -57,6 +55,7 @@ .. _OpenBSD: http://openports.se/audio/beets .. _Arch community: https://www.archlinux.org/packages/community/any/beets/ .. _NixOS: https://github.com/NixOS/nixpkgs/tree/master/pkgs/tools/audio/beets +.. _MacPorts: https://www.macports.org If you have `pip`_, just say ``pip install beets`` (or ``pip install --user beets`` if you run into permissions problems). @@ -73,14 +72,14 @@ .. _@b33ts: https://twitter.com/b33ts -Installing on macOS 10.11 and Higher -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Installing by Hand on macOS 10.11 and Higher +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Starting with version 10.11 (El Capitan), macOS has a new security feature called `System Integrity Protection`_ (SIP) that prevents you from modifying -some parts of the system. This means that some ``pip`` commands may fail with -a permissions error. (You probably *won't* run into this if you've installed -Python yourself with `Homebrew`_ or otherwise.) +some parts of the system. This means that some ``pip`` commands may fail with a +permissions error. (You probably *won't* run into this if you've installed +Python yourself with `Homebrew`_ or otherwise. You can also try `MacPorts`_.) If this happens, you can install beets for the current user only by typing ``pip install --user beets``. If you do that, you might want to add diff -Nru beets-1.5.0/docs/plugins/absubmit.rst beets-1.6.0/docs/plugins/absubmit.rst --- beets-1.5.0/docs/plugins/absubmit.rst 2020-09-14 00:55:24.000000000 +0000 +++ beets-1.6.0/docs/plugins/absubmit.rst 2021-11-26 20:51:38.000000000 +0000 @@ -23,13 +23,12 @@ Submitting Data --------------- -Type:: +To run the analysis program and upload its results, type:: beet absubmit [-f] [-d] [QUERY] -To run the analysis program and upload its results. By default, the -command will only look for AcousticBrainz data when the tracks -doesn't already have it; the ``-f`` or ``--force`` switch makes it refetch +By default, the command will only look for AcousticBrainz data when the tracks +don't already have it; the ``-f`` or ``--force`` switch makes it refetch data even when it already exists. You can use the ``-d`` or ``--dry`` swtich to check which files will be analyzed, before you start a longer period of processing. diff -Nru beets-1.5.0/docs/plugins/albumtypes.rst beets-1.6.0/docs/plugins/albumtypes.rst --- beets-1.5.0/docs/plugins/albumtypes.rst 1970-01-01 00:00:00.000000000 +0000 +++ beets-1.6.0/docs/plugins/albumtypes.rst 2021-09-22 14:28:41.000000000 +0000 @@ -0,0 +1,57 @@ +AlbumTypes Plugin +================= + +The ``albumtypes`` plugin adds the ability to format and output album types, +such as "Album", "EP", "Single", etc. For the list of available album types, +see the `MusicBrainz documentation`_. + +To use the ``albumtypes`` plugin, enable it in your configuration +(see :ref:`using-plugins`). The plugin defines a new field ``$atypes``, which +you can use in your path formats or elsewhere. + +.. _MusicBrainz documentation: https://musicbrainz.org/doc/Release_Group/Type + +Configuration +------------- + +To configure the plugin, make a ``albumtypes:`` section in your configuration +file. The available options are: + +- **types**: An ordered list of album type to format mappings. The order of the + mappings determines their order in the output. If a mapping is missing or + blank, it will not be in the output. +- **ignore_va**: A list of types that should not be output for Various Artists + albums. Useful for not adding redundant information - various artist albums + are often compilations. +- **bracket**: Defines the brackets to enclose each album type in the output. + +The default configuration looks like this:: + + albumtypes: + types: + - ep: 'EP' + - single: 'Single' + - soundtrack: 'OST' + - live: 'Live' + - compilation: 'Anthology' + - remix: 'Remix' + ignore_va: compilation + bracket: '[]' + +Examples +-------- +With path formats configured like:: + + paths: + default: $albumartist/[$year]$atypes $album/... + albumtype:soundtrack Various Artists/$album [$year]$atypes/... + comp: Various Artists/$album [$year]$atypes/... + + +The default plugin configuration generates paths that look like this, for example:: + + Aphex Twin/[1993][EP][Remix] On Remixes + Pink Floyd/[1995][Live] p·u·l·s·e + Various Artists/20th Century Lullabies [1999] + Various Artists/Ocean's Eleven [2001][OST] + diff -Nru beets-1.5.0/docs/plugins/export.rst beets-1.6.0/docs/plugins/export.rst --- beets-1.5.0/docs/plugins/export.rst 2020-10-18 10:58:20.000000000 +0000 +++ beets-1.6.0/docs/plugins/export.rst 2021-11-26 20:51:38.000000000 +0000 @@ -34,6 +34,9 @@ * ``--library`` or ``-l``: Show data from the library database instead of the files' tags. +* ``--album`` or ``-a``: Show data from albums instead of tracks (implies + ``--library``). + * ``--output`` or ``-o``: Path for an output file. If not informed, will print the data in the console. diff -Nru beets-1.5.0/docs/plugins/fetchart.rst beets-1.6.0/docs/plugins/fetchart.rst --- beets-1.5.0/docs/plugins/fetchart.rst 2021-03-28 18:23:15.000000000 +0000 +++ beets-1.6.0/docs/plugins/fetchart.rst 2021-11-26 20:51:38.000000000 +0000 @@ -86,6 +86,14 @@ - **high_resolution**: If enabled, fetchart retrieves artwork in the highest resolution it can find (warning: image files can sometimes reach >20MB). Default: ``no``. +- **deinterlace**: If enabled, `Pillow`_ or `ImageMagick`_ backends are + instructed to store cover art as non-progressive JPEG. You might need this if + you use DAPs that don't support progressive images. + Default: ``no``. +- **cover_format**: If enabled, forced the cover image into the specified + format. Most often, this will be either ``JPEG`` or ``PNG`` [#imgformats]_. + Also respects ``deinterlace``. + Default: None (leave unchanged). Note: ``maxwidth`` and ``enforce_ratio`` options require either `ImageMagick`_ or `Pillow`_. @@ -101,6 +109,12 @@ .. _beets custom search engine: https://cse.google.com.au:443/cse/publicurl?cx=001442825323518660753:hrh5ch1gjzm .. _Pillow: https://github.com/python-pillow/Pillow .. _ImageMagick: https://www.imagemagick.org/ +.. [#imgformats] Other image formats are available, though the full list + depends on your system and what backend you are using. If you're using the + ImageMagick backend, you can use ``magick identify -list format`` to get a + full list of all supported formats, and you can use the Python function + PIL.features.pilinfo() to print a list of all supported formats in Pillow + (``python3 -c 'import PIL.features as f; f.pilinfo()'``). Here's an example that makes plugin select only images that contain ``front`` or ``back`` keywords in their filenames and prioritizes the iTunes source over diff -Nru beets-1.5.0/docs/plugins/gmusic.rst beets-1.6.0/docs/plugins/gmusic.rst --- beets-1.5.0/docs/plugins/gmusic.rst 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/docs/plugins/gmusic.rst 2021-11-26 20:51:38.000000000 +0000 @@ -1,87 +1,5 @@ Gmusic Plugin ============= -The ``gmusic`` plugin lets you upload songs to Google Play Music and query -songs in your library. - - -Installation ------------- - -The plugin requires :pypi:`gmusicapi`. You can install it using ``pip``:: - - pip install gmusicapi - -.. _gmusicapi: https://github.com/simon-weber/gmusicapi/ - -Then, you can enable the ``gmusic`` plugin in your configuration (see -:ref:`using-plugins`). - - -Usage ------ -Configuration is required before use. Below is an example configuration:: - - gmusic: - email: user@example.com - password: seekrit - auto: yes - uploader_id: 00:11:22:33:AA:BB - device_id: 00112233AABB - oauth_file: ~/.config/beets/oauth.cred - - -To upload tracks to Google Play Music, use the ``gmusic-upload`` command:: - - beet gmusic-upload [QUERY] - -If you don't include a query, the plugin will upload your entire collection. - -To list your music collection, use the ``gmusic-songs`` command:: - - beet gmusic-songs [-at] [ARGS] - -Use the ``-a`` option to search by artist and ``-t`` to search by track. For -example:: - - beet gmusic-songs -a John Frusciante - beet gmusic-songs -t Black Hole Sun - -For a list of all songs in your library, run ``beet gmusic-songs`` without any -arguments. - - -Configuration -------------- -To configure the plugin, make a ``gmusic:`` section in your configuration file. -The available options are: - -- **email**: Your Google account email address. - Default: none. -- **password**: Password to your Google account. Required to query songs in - your collection. - For accounts with 2-step-verification, an - `app password `__ - will need to be generated. An app password for an account without - 2-step-verification is not required but is recommended. - Default: none. -- **auto**: Set to ``yes`` to automatically upload new imports to Google Play - Music. - Default: ``no`` -- **uploader_id**: Unique id as a MAC address, eg ``00:11:22:33:AA:BB``. - This option should be set before the maximum number of authorized devices is - reached. - If provided, use the same id for all future runs on this, and other, beets - installations as to not reach the maximum number of authorized devices. - Default: device's MAC address. -- **device_id**: Unique device ID for authorized devices. It is usually - the same as your MAC address with the colons removed, eg ``00112233AABB``. - This option only needs to be set if you receive an `InvalidDeviceId` - exception. Below the exception will be a list of valid device IDs. - Default: none. -- **oauth_file**: Filepath for oauth credentials file. - Default: `{user_data_dir} `__/gmusicapi/oauth.cred - -Refer to the `Google Play Music Help -`__ -page for more details on authorized devices. +The ``gmusic`` plugin interfaced beets to Google Play Music. It has been +removed after the shutdown of this service. diff -Nru beets-1.5.0/docs/plugins/index.rst beets-1.6.0/docs/plugins/index.rst --- beets-1.5.0/docs/plugins/index.rst 2021-08-19 18:58:59.000000000 +0000 +++ beets-1.6.0/docs/plugins/index.rst 2021-11-26 20:51:38.000000000 +0000 @@ -61,6 +61,7 @@ absubmit acousticbrainz + albumtypes aura badfiles bareasc @@ -176,6 +177,7 @@ Path Formats ------------ +* :doc:`albumtypes`: Format album type in path formats. * :doc:`bucket`: Group your files into bucket directories that cover different field values ranges. * :doc:`inline`: Use Python snippets to customize path format strings. @@ -229,7 +231,6 @@ * :doc:`filefilter`: Automatically skip files during the import process based on regular expressions. * :doc:`fuzzy`: Search albums and tracks with fuzzy string matching. -* :doc:`gmusic`: Search and upload files to Google Play Music. * :doc:`hook`: Run a command when an event is emitted by beets. * :doc:`ihate`: Automatically skip albums and tracks during the import process. * :doc:`info`: Print music files' tags to the console. @@ -307,6 +308,9 @@ * `beets-ibroadcast`_ uploads tracks to the `iBroadcast`_ cloud service. +* `beets-importreplace`_ lets you perform regex replacements on incoming + metadata. + * `beets-mosaic`_ generates a montage of a mosaic from cover art. * `beets-noimport`_ adds and removes directories from the incremental import skip list. @@ -348,6 +352,7 @@ .. _beets-follow: https://github.com/nolsto/beets-follow .. _beets-ibroadcast: https://github.com/ctrueden/beets-ibroadcast .. _iBroadcast: https://ibroadcast.com/ +.. _beets-importreplace: https://github.com/edgars-supe/beets-importreplace .. _beets-setlister: https://github.com/tomjaspers/beets-setlister .. _beets-noimport: https://gitlab.com/tiago.dias/beets-noimport .. _whatlastgenre: https://github.com/YetAnotherNerd/whatlastgenre/tree/master/plugin/beets diff -Nru beets-1.5.0/docs/plugins/info.rst beets-1.6.0/docs/plugins/info.rst --- beets-1.5.0/docs/plugins/info.rst 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/docs/plugins/info.rst 2021-11-26 20:51:38.000000000 +0000 @@ -31,6 +31,8 @@ * ``--library`` or ``-l``: Show data from the library database instead of the files' tags. +* ``--album`` or ``-a``: Show data from albums instead of tracks (implies + ``--library``). * ``--summarize`` or ``-s``: Merge all the information from multiple files into a single list of values. If the tags differ across the files, print ``[various]``. diff -Nru beets-1.5.0/docs/plugins/spotify.rst beets-1.6.0/docs/plugins/spotify.rst --- beets-1.5.0/docs/plugins/spotify.rst 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/docs/plugins/spotify.rst 2021-11-26 20:51:38.000000000 +0000 @@ -8,9 +8,9 @@ metadata matches for the importer. .. _Spotify: https://www.spotify.com/ -.. _Spotify Search API: https://developer.spotify.com/documentation/web-api/reference/search/search/ -.. _Album: https://developer.spotify.com/documentation/web-api/reference/albums/get-album/ -.. _Track: https://developer.spotify.com/documentation/web-api/reference/tracks/get-track/ +.. _Spotify Search API: https://developer.spotify.com/documentation/web-api/reference/#category-search +.. _Album: https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-album +.. _Track: https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-track Why Use This Plugin? -------------------- diff -Nru beets-1.5.0/docs/plugins/unimported.rst beets-1.6.0/docs/plugins/unimported.rst --- beets-1.5.0/docs/plugins/unimported.rst 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/docs/plugins/unimported.rst 2021-11-26 20:51:38.000000000 +0000 @@ -1,7 +1,7 @@ Unimported Plugin ================= -The ``unimported`` plugin allows to list all files in the library folder which are not listed in the beets library database, including art files. +The ``unimported`` plugin allows one to list all files in the library folder which are not listed in the beets library database, including art files. Command Line Usage ------------------ @@ -9,9 +9,10 @@ To use the ``unimported`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then use it by invoking the ``beet unimported`` command. The command will list all files in the library folder which are not imported. You can -exclude file extensions using the configuration file:: +exclude file extensions or entire subdirectories using the configuration file:: unimported: ignore_extensions: jpg png + ignore_subdirectories: NonMusic data temp -The default configuration list all unimported files, ignoring no extensions. \ No newline at end of file +The default configuration lists all unimported files, ignoring no extensions. diff -Nru beets-1.5.0/docs/reference/cli.rst beets-1.6.0/docs/reference/cli.rst --- beets-1.5.0/docs/reference/cli.rst 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/docs/reference/cli.rst 2021-11-26 20:51:38.000000000 +0000 @@ -316,7 +316,7 @@ beet update [-F] FIELD [-aM] QUERY -Update the library (and, optionally, move files) to reflect out-of-band metadata +Update the library (and, by default, move files) to reflect out-of-band metadata changes and file deletions. This will scan all the matched files and read their tags, populating the diff -Nru beets-1.5.0/docs/reference/config.rst beets-1.6.0/docs/reference/config.rst --- beets-1.5.0/docs/reference/config.rst 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/docs/reference/config.rst 2021-11-26 20:51:38.000000000 +0000 @@ -151,6 +151,25 @@ special characters, you can either add them to the replacement list or use the :ref:`asciify-paths` configuration option below. +.. _path-sep-replace: + +path_sep_replace +~~~~~~~~~~~~~~~~ + +A string that replaces the path separator (for example, the forward slash +``/`` on Linux and MacOS, and the backward slash ``\\`` on Windows) when +generating filenames with beets. +This option is related to :ref:`replace`, but is distict from it for +technical reasons. + +.. warning:: + Changing this option is potentially dangerous. For example, setting + it to the actual path separator could create directories in unexpected + locations. Use caution when changing it and always try it out on a small + number of files before applying it to your whole library. + +Default: ``_``. + .. _asciify-paths: asciify_paths @@ -165,6 +184,10 @@ equivalent to wrapping all your path templates in the ``%asciify{}`` :ref:`template function `. +This uses the `unidecode module`_ which is language agnostic, so some +characters may be transliterated from a different language than expected. +For example, Japanese kanji will usually use their Chinese readings. + Default: ``no``. .. _unidecode module: https://pypi.org/project/Unidecode @@ -753,10 +776,10 @@ genres ~~~~~~ -Use MusicBrainz genre tags to populate the ``genre`` tag. This will make it a -semicolon-separated list of all the genres tagged for the release on -MusicBrainz. - +Use MusicBrainz genre tags to populate (and replace if it's already set) the +``genre`` tag. This will make it a list of all the genres tagged for the +release and the release-group on MusicBrainz, separated by "; " and sorted by +the total number of votes. Default: ``no`` .. _match-config: diff -Nru beets-1.5.0/extra/release.py beets-1.6.0/extra/release.py --- beets-1.5.0/extra/release.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/extra/release.py 2021-11-27 16:37:12.000000000 +0000 @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """A utility script for automating the beets release process. """ @@ -36,7 +35,7 @@ [ ( r'__version__\s*=\s*u[\'"]([0-9\.]+)[\'"]', - "__version__ = u'{version}'", + "__version__ = '{version}'", ) ] ), @@ -110,14 +109,14 @@ out_lines.append(line) if not found: - print("No pattern found in {}".format(filename)) + print(f"No pattern found in {filename}") # Write the file back. with open(filename, 'w') as f: f.write(''.join(out_lines)) # Generate bits to insert into changelog. - header_line = '{} (in development)'.format(version) + header_line = f'{version} (in development)' header = '\n\n' + header_line + '\n' + '-' * len(header_line) + '\n\n' header += 'Changelog goes here!\n' @@ -277,7 +276,7 @@ cur_version = get_version() # Tag. - subprocess.check_output(['git', 'tag', 'v{}'.format(cur_version)]) + subprocess.check_call(['git', 'tag', f'v{cur_version}']) # Build. with chdir(BASE): @@ -292,7 +291,7 @@ # FIXME It should be possible to specify this as an argument. version_parts = [int(n) for n in cur_version.split('.')] version_parts[-1] += 1 - next_version = u'.'.join(map(str, version_parts)) + next_version = '.'.join(map(str, version_parts)) bump_version(next_version) @@ -311,7 +310,7 @@ subprocess.check_call(['git', 'push', '--tags']) # Upload to PyPI. - path = os.path.join(BASE, 'dist', 'beets-{}.tar.gz'.format(version)) + path = os.path.join(BASE, 'dist', f'beets-{version}.tar.gz') subprocess.check_call(['twine', 'upload', path]) @@ -335,12 +334,12 @@ 'github-release', 'release', '-u', GITHUB_USER, '-r', GITHUB_REPO, '--tag', tag, - '--name', '{} {}'.format(GITHUB_REPO, version), + '--name', f'{GITHUB_REPO} {version}', '--description', cl_md, ]) # Attach the release tarball. - tarball = os.path.join(BASE, 'dist', 'beets-{}.tar.gz'.format(version)) + tarball = os.path.join(BASE, 'dist', f'beets-{version}.tar.gz') subprocess.check_call([ 'github-release', 'upload', '-u', GITHUB_USER, '-r', GITHUB_REPO, diff -Nru beets-1.5.0/man/beet.1 beets-1.6.0/man/beet.1 --- beets-1.5.0/man/beet.1 2021-08-19 19:56:50.000000000 +0000 +++ beets-1.6.0/man/beet.1 2021-11-27 16:37:57.000000000 +0000 @@ -1,5 +1,8 @@ .\" Man page generated from reStructuredText. . +.TH "BEET" "1" "Nov 27, 2021" "1.6" "beets" +.SH NAME +beet \- music tagger and library organizer . .nr rst2man-indent-level 0 . @@ -27,9 +30,6 @@ .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BEET" "1" "Aug 19, 2021" "1.5" "beets" -.SH NAME -beet \- music tagger and library organizer .SH SYNOPSIS .nf \fBbeet\fP [\fIargs\fP…] \fIcommand\fP [\fIargs\fP…] @@ -300,7 +300,7 @@ .UNINDENT .UNINDENT .sp -Update the library (and, optionally, move files) to reflect out\-of\-band metadata +Update the library (and, by default, move files) to reflect out\-of\-band metadata changes and file deletions. .sp This will scan all the matched files and read their tags, populating the diff -Nru beets-1.5.0/man/beetsconfig.5 beets-1.6.0/man/beetsconfig.5 --- beets-1.5.0/man/beetsconfig.5 2021-08-19 19:56:50.000000000 +0000 +++ beets-1.6.0/man/beetsconfig.5 2021-11-27 16:37:57.000000000 +0000 @@ -1,5 +1,8 @@ .\" Man page generated from reStructuredText. . +.TH "BEETSCONFIG" "5" "Nov 27, 2021" "1.6" "beets" +.SH NAME +beetsconfig \- beets configuration file . .nr rst2man-indent-level 0 . @@ -27,9 +30,6 @@ .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BEETSCONFIG" "5" "Aug 19, 2021" "1.5" "beets" -.SH NAME -beetsconfig \- beets configuration file .sp Beets has an extensive configuration system that lets you customize nearly every aspect of its operation. To configure beets, you create a file called @@ -97,6 +97,8 @@ .IP \(bu 2 \fI\%replace\fP .IP \(bu 2 +\fI\%path_sep_replace\fP +.IP \(bu 2 \fI\%asciify_paths\fP .IP \(bu 2 \fI\%art_filename\fP @@ -334,6 +336,25 @@ replaced as they don’t match the typewriter quote (\fB"\fP). To also strip these special characters, you can either add them to the replacement list or use the \fI\%asciify_paths\fP configuration option below. +.SS path_sep_replace +.sp +A string that replaces the path separator (for example, the forward slash +\fB/\fP on Linux and MacOS, and the backward slash \fB\e\e\fP on Windows) when +generating filenames with beets. +This option is related to \fI\%replace\fP, but is distict from it for +technical reasons. +.sp +\fBWARNING:\fP +.INDENT 0.0 +.INDENT 3.5 +Changing this option is potentially dangerous. For example, setting +it to the actual path separator could create directories in unexpected +locations. Use caution when changing it and always try it out on a small +number of files before applying it to your whole library. +.UNINDENT +.UNINDENT +.sp +Default: \fB_\fP\&. .SS asciify_paths .sp Convert all non\-ASCII characters in paths to ASCII equivalents. @@ -345,6 +366,10 @@ equivalent to wrapping all your path templates in the \fB%asciify{}\fP template function\&. .sp +This uses the \fI\%unidecode module\fP which is language agnostic, so some +characters may be transliterated from a different language than expected. +For example, Japanese kanji will usually use their Chinese readings. +.sp Default: \fBno\fP\&. .SS art_filename .sp @@ -800,10 +825,10 @@ Default: \fB[]\fP .SS genres .sp -Use MusicBrainz genre tags to populate the \fBgenre\fP tag. This will make it a -semicolon\-separated list of all the genres tagged for the release on -MusicBrainz. -.sp +Use MusicBrainz genre tags to populate (and replace if it’s already set) the +\fBgenre\fP tag. This will make it a list of all the genres tagged for the +release and the release\-group on MusicBrainz, separated by “; ” and sorted by +the total number of votes. Default: \fBno\fP .SH AUTOTAGGER MATCHING OPTIONS .sp diff -Nru beets-1.5.0/PKG-INFO beets-1.6.0/PKG-INFO --- beets-1.5.0/PKG-INFO 2021-08-19 19:56:50.918363800 +0000 +++ beets-1.6.0/PKG-INFO 2021-11-27 16:37:58.540617200 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: beets -Version: 1.5.0 +Version: 1.6.0 Summary: music tagger and library organizer Home-page: https://beets.io/ Author: Adrian Sampson @@ -13,10 +13,7 @@ Classifier: Environment :: Console Classifier: Environment :: Web Environment Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 @@ -29,7 +26,6 @@ Provides-Extra: embedart Provides-Extra: embyupdate Provides-Extra: chroma -Provides-Extra: gmusic Provides-Extra: discogs Provides-Extra: beatport Provides-Extra: kodiupdate diff -Nru beets-1.5.0/setup.cfg beets-1.6.0/setup.cfg --- beets-1.5.0/setup.cfg 2021-08-19 19:56:50.919400700 +0000 +++ beets-1.6.0/setup.cfg 2021-11-27 16:37:58.541482400 +0000 @@ -1,5 +1,5 @@ [flake8] -min-version = 2.7 +min-version = 3.6 accept-encodings = utf-8 docstring-convention = google ignore = @@ -14,12 +14,6 @@ W504, # line break occurred after a binary operator F405, # name be undefined, or defined from star imports: module C901, # function is too complex - FI12, # `__future__` import "with_statement" missing - FI14, # `__future__` import "unicode_literals" missing - FI15, # `__future__` import "generator_stop" missing - FI50, # `__future__` import "division" present - FI51, # `__future__` import "absolute_import" present - FI53, # `__future__` import "print_function" present N818, # Exception subclasses should be named with an Error suffix per-file-ignores = ./beet:D diff -Nru beets-1.5.0/setup.py beets-1.6.0/setup.py --- beets-1.5.0/setup.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/setup.py 2021-11-27 16:16:16.000000000 +0000 @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. @@ -15,7 +14,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import os import sys @@ -56,7 +54,7 @@ setup( name='beets', - version='1.5.0', + version='1.6.0', description='music tagger and library organizer', author='Adrian Sampson', author_email='adrian@radbox.org', @@ -87,24 +85,14 @@ }, install_requires=[ - 'six>=1.9', 'unidecode', 'musicbrainzngs>=0.4', 'pyyaml', 'mediafile>=0.2.0', 'confuse>=1.0.0', - ] + [ - # Avoid a version of munkres incompatible with Python 3. - 'munkres~=1.0.0' if sys.version_info < (3, 5, 0) else - 'munkres!=1.1.0,!=1.1.1' if sys.version_info < (3, 6, 0) else 'munkres>=1.0.0', + 'jellyfish', ] + ( - # Use the backport of Python 3.4's `enum` module. - ['enum34>=1.0.4'] if sys.version_info < (3, 4, 0) else [] - ) + ( - # Pin a Python 2-compatible version of Jellyfish. - ['jellyfish==0.6.0'] if sys.version_info < (3, 4, 0) else ['jellyfish'] - ) + ( # Support for ANSI console colors on Windows. ['colorama'] if (sys.platform == 'win32') else [] ), @@ -122,22 +110,13 @@ 'responses>=0.3.0', 'requests_oauthlib', 'reflink', - ] + ( - # Tests for the thumbnails plugin need pathlib on Python 2 too. - ['pathlib'] if (sys.version_info < (3, 4, 0)) else [] - ) + [ - 'rarfile<4' if sys.version_info < (3, 6, 0) else 'rarfile', - ] + [ - 'discogs-client' if (sys.version_info < (3, 0, 0)) - else 'python3-discogs-client' - ] + ( - ['py7zr'] if (sys.version_info > (3, 5, 0)) else [] - ), + 'rarfile', + 'python3-discogs-client', + 'py7zr', + ], 'lint': [ 'flake8', - 'flake8-coding', 'flake8-docstrings', - 'flake8-future-import', 'pep8-naming', ], @@ -147,11 +126,7 @@ 'embedart': ['Pillow'], 'embyupdate': ['requests'], 'chroma': ['pyacoustid'], - 'gmusic': ['gmusicapi'], - 'discogs': ( - ['discogs-client' if (sys.version_info < (3, 0, 0)) - else 'python3-discogs-client'] - ), + 'discogs': ['python3-discogs-client>=2.3.10'], 'beatport': ['requests-oauthlib>=0.6.1'], 'kodiupdate': ['requests'], 'lastgenre': ['pylast'], @@ -160,11 +135,8 @@ 'mpdstats': ['python-mpd2>=0.4.2'], 'plexupdate': ['requests'], 'web': ['flask', 'flask-cors'], - 'import': ( - ['rarfile<4' if (sys.version_info < (3, 6, 0)) else 'rarfile'] - ), - 'thumbnails': ['pyxdg', 'Pillow'] + - (['pathlib'] if (sys.version_info < (3, 4, 0)) else []), + 'import': ['rarfile', 'py7zr'], + 'thumbnails': ['pyxdg', 'Pillow'], 'metasync': ['dbus-python'], 'sonosupdate': ['soco'], 'scrub': ['mutagen>=1.33'], @@ -193,10 +165,7 @@ 'Environment :: Console', 'Environment :: Web Environment', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', diff -Nru beets-1.5.0/test/_common.py beets-1.6.0/test/_common.py --- beets-1.5.0/test/_common.py 2020-12-15 12:48:01.000000000 +0000 +++ beets-1.6.0/test/_common.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,14 +13,12 @@ # included in all copies or substantial portions of the Software. """Some common functionality for beets' test cases.""" -from __future__ import division, absolute_import, print_function import time import sys import os import tempfile import shutil -import six import unittest from contextlib import contextmanager @@ -64,18 +61,18 @@ global _item_ident _item_ident += 1 i = beets.library.Item( - title=u'the title', - artist=u'the artist', - albumartist=u'the album artist', - album=u'the album', - genre=u'the genre', - lyricist=u'the lyricist', - composer=u'the composer', - arranger=u'the arranger', - grouping=u'the grouping', - work=u'the work title', - mb_workid=u'the work musicbrainz id', - work_disambig=u'the work disambiguation', + title='the title', + artist='the artist', + albumartist='the album artist', + album='the album', + genre='the genre', + lyricist='the lyricist', + composer='the composer', + arranger='the arranger', + grouping='the grouping', + work='the work title', + mb_workid='the work musicbrainz id', + work_disambig='the work disambiguation', year=1, month=2, day=3, @@ -83,11 +80,11 @@ tracktotal=5, disc=6, disctotal=7, - lyrics=u'the lyrics', - comments=u'the comments', + lyrics='the lyrics', + comments='the comments', bpm=8, comp=True, - path='somepath{0}'.format(_item_ident), + path=f'somepath{_item_ident}', length=60.0, bitrate=128000, format='FLAC', @@ -111,11 +108,11 @@ _item_ident += 1 i = beets.library.Album( artpath=None, - albumartist=u'some album artist', - albumartist_sort=u'some sort album artist', - albumartist_credit=u'some album artist credit', - album=u'the album', - genre=u'the genre', + albumartist='some album artist', + albumartist_sort='some sort album artist', + albumartist_credit='some album artist credit', + album='the album', + genre='the genre', year=2014, month=2, day=5, @@ -136,21 +133,21 @@ return cls(lib, loghandler, paths, query) -class Assertions(object): +class Assertions: """A mixin with additional unit test assertions.""" def assertExists(self, path): # noqa self.assertTrue(os.path.exists(util.syspath(path)), - u'file does not exist: {!r}'.format(path)) + f'file does not exist: {path!r}') def assertNotExists(self, path): # noqa self.assertFalse(os.path.exists(util.syspath(path)), - u'file exists: {!r}'.format((path))) + f'file exists: {path!r}') def assert_equal_path(self, a, b): """Check that two paths are equal.""" self.assertEqual(util.normpath(a), util.normpath(b), - u'paths are not equal: {!r} and {!r}'.format(a, b)) + f'paths are not equal: {a!r} and {b!r}') # A test harness for all beets tests. @@ -203,18 +200,18 @@ an item added to the library (`i`). """ def setUp(self): - super(LibTestCase, self).setUp() + super().setUp() self.lib = beets.library.Library(':memory:') self.i = item(self.lib) def tearDown(self): self.lib._connection().close() - super(LibTestCase, self).tearDown() + super().tearDown() # Mock timing. -class Timecop(object): +class Timecop: """Mocks the timing system (namely time() and sleep()) for testing. Inspired by the Ruby timecop library. """ @@ -249,11 +246,11 @@ def __str__(self): msg = "Attempt to read with no input provided." if self.output is not None: - msg += " Output: {!r}".format(self.output) + msg += f" Output: {self.output!r}" return msg -class DummyOut(object): +class DummyOut: encoding = 'utf-8' def __init__(self): @@ -263,10 +260,7 @@ self.buf.append(s) def get(self): - if six.PY2: - return b''.join(self.buf) - else: - return ''.join(self.buf) + return ''.join(self.buf) def flush(self): self.clear() @@ -275,7 +269,7 @@ self.buf = [] -class DummyIn(object): +class DummyIn: encoding = 'utf-8' def __init__(self, out=None): @@ -284,10 +278,7 @@ self.out = out def add(self, s): - if six.PY2: - self.buf.append(s + b'\n') - else: - self.buf.append(s + '\n') + self.buf.append(s + '\n') def close(self): pass @@ -302,7 +293,7 @@ return self.buf.pop(0) -class DummyIO(object): +class DummyIO: """Mocks input and output streams for testing UI code.""" def __init__(self): self.stdout = DummyOut() @@ -334,7 +325,7 @@ open(path, 'a').close() -class Bag(object): +class Bag: """An object that exposes a set of fields given as keyword arguments. Any field not found in the dictionary appears to be None. Used for mocking Album objects and the like. @@ -349,7 +340,7 @@ # Convenience methods for setting up a temporary sandbox directory for tests # that need to interact with the filesystem. -class TempDirMixin(object): +class TempDirMixin: """Text mixin for creating and deleting a temporary directory. """ @@ -408,5 +399,5 @@ def _id(obj): return obj if 'SKIP_SLOW_TESTS' in os.environ: - return unittest.skip(u'test is slow') + return unittest.skip('test is slow') return _id diff -Nru beets-1.5.0/test/helper.py beets-1.6.0/test/helper.py --- beets-1.5.0/test/helper.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/helper.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -31,8 +30,6 @@ """ -from __future__ import division, absolute_import, print_function - import sys import os import os.path @@ -56,7 +53,6 @@ # TODO Move AutotagMock here from test import _common -import six class LogCapture(logging.Handler): @@ -66,7 +62,7 @@ self.messages = [] def emit(self, record): - self.messages.append(six.text_type(record.msg)) + self.messages.append(str(record.msg)) @contextmanager @@ -90,8 +86,6 @@ """ org = sys.stdin sys.stdin = StringIO(input) - if six.PY2: # StringIO encoding attr isn't writable in python >= 3 - sys.stdin.encoding = 'utf-8' try: yield sys.stdin finally: @@ -110,8 +104,6 @@ """ org = sys.stdout sys.stdout = capture = StringIO() - if six.PY2: # StringIO encoding attr isn't writable in python >= 3 - sys.stdout.encoding = 'utf-8' try: yield sys.stdout finally: @@ -124,12 +116,8 @@ on Python 3. """ for i, elem in enumerate(args): - if six.PY2: - if isinstance(elem, six.text_type): - args[i] = elem.encode(util.arg_encoding()) - else: - if isinstance(elem, bytes): - args[i] = elem.decode(util.arg_encoding()) + if isinstance(elem, bytes): + args[i] = elem.decode(util.arg_encoding()) return args @@ -150,7 +138,7 @@ return True -class TestHelper(object): +class TestHelper: """Helper mixin for high-level cli and plugin tests. This mixin provides methods to isolate beets' global state provide @@ -259,7 +247,7 @@ album_no = 0 while album_count: - album = util.bytestring_path(u'album {0}'.format(album_no)) + album = util.bytestring_path(f'album {album_no}') album_dir = os.path.join(import_dir, album) if os.path.exists(album_dir): album_no += 1 @@ -270,9 +258,9 @@ track_no = 0 album_item_count = item_count while album_item_count: - title = u'track {0}'.format(track_no) + title = f'track {track_no}' src = os.path.join(_common.RSRC, b'full.mp3') - title_file = util.bytestring_path('{0}.mp3'.format(title)) + title_file = util.bytestring_path(f'{title}.mp3') dest = os.path.join(album_dir, title_file) if os.path.exists(dest): track_no += 1 @@ -313,9 +301,9 @@ """ item_count = self._get_item_count() values_ = { - 'title': u't\u00eftle {0}', - 'artist': u'the \u00e4rtist', - 'album': u'the \u00e4lbum', + 'title': 't\u00eftle {0}', + 'artist': 'the \u00e4rtist', + 'album': 'the \u00e4lbum', 'track': item_count, 'format': 'MP3', } @@ -375,8 +363,8 @@ path = os.path.join(_common.RSRC, util.bytestring_path('full.' + ext)) for i in range(count): item = Item.from_path(path) - item.album = u'\u00e4lbum {0}'.format(i) # Check unicode paths - item.title = u't\u00eftle {0}'.format(i) + item.album = f'\u00e4lbum {i}' # Check unicode paths + item.title = f't\u00eftle {i}' # mtime needs to be set last since other assignments reset it. item.mtime = 12345 item.add(self.lib) @@ -392,8 +380,8 @@ path = os.path.join(_common.RSRC, util.bytestring_path('full.' + ext)) for i in range(track_count): item = Item.from_path(path) - item.album = u'\u00e4lbum' # Check unicode paths - item.title = u't\u00eftle {0}'.format(i) + item.album = '\u00e4lbum' # Check unicode paths + item.title = f't\u00eftle {i}' # mtime needs to be set last since other assignments reset it. item.mtime = 12345 item.add(self.lib) @@ -422,7 +410,7 @@ mediafile = MediaFile(path) imgs = [] for img_ext in images: - file = util.bytestring_path('image-2x3.{0}'.format(img_ext)) + file = util.bytestring_path(f'image-2x3.{img_ext}') img_path = os.path.join(_common.RSRC, file) with open(img_path, 'rb') as f: imgs.append(Image(f.read())) @@ -517,7 +505,7 @@ """ def __init__(self, *args, **kwargs): - super(ImportSessionFixture, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._choices = [] self._resolutions = [] @@ -577,17 +565,18 @@ """ tracks = [generate_track_info(id, values) for id, values in track_values] album = AlbumInfo( - album_id=u'album info', - album=u'album info', - artist=u'album info', - artist_id=u'album info', + album_id='album info', + album='album info', + artist='album info', + artist_id='album info', tracks=tracks, ) for field in ALBUM_INFO_FIELDS: - setattr(album, field, u'album info') + setattr(album, field, 'album info') return album + ALBUM_INFO_FIELDS = ['album', 'album_id', 'artist', 'artist_id', 'asin', 'albumtype', 'va', 'label', 'artist_sort', 'releasegroup_id', 'catalognum', @@ -603,15 +592,16 @@ string fields are set to "track info". """ track = TrackInfo( - title=u'track info', + title='track info', track_id=track_id, ) for field in TRACK_INFO_FIELDS: - setattr(track, field, u'track info') + setattr(track, field, 'track info') for field, value in values.items(): setattr(track, field, value) return track + TRACK_INFO_FIELDS = ['artist', 'artist_id', 'artist_sort', 'disctitle', 'artist_credit', 'data_source', 'data_url'] diff -Nru beets-1.5.0/test/__init__.py beets-1.6.0/test/__init__.py --- beets-1.5.0/test/__init__.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/__init__.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,5 +1 @@ -# -*- coding: utf-8 -*- - # Make python -m testall.py work. - -from __future__ import division, absolute_import, print_function diff -Nru beets-1.5.0/test/lyrics_download_samples.py beets-1.6.0/test/lyrics_download_samples.py --- beets-1.5.0/test/lyrics_download_samples.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/lyrics_download_samples.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import os import sys @@ -44,7 +42,7 @@ """ if argv is None: argv = sys.argv - print(u'Fetching samples from:') + print('Fetching samples from:') for s in test_lyrics.GOOGLE_SOURCES + test_lyrics.DEFAULT_SOURCES: print(s['url']) url = s['url'] + s['path'] diff -Nru beets-1.5.0/test/rsrc/beetsplug/test.py beets-1.6.0/test/rsrc/beetsplug/test.py --- beets-1.5.0/test/rsrc/beetsplug/test.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/rsrc/beetsplug/test.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,14 +1,10 @@ -# -*- coding: utf-8 -*- - -from __future__ import division, absolute_import, print_function - from beets.plugins import BeetsPlugin from beets import ui class TestPlugin(BeetsPlugin): def __init__(self): - super(TestPlugin, self).__init__() + super().__init__() self.is_test_plugin = True def commands(self): @@ -16,7 +12,7 @@ test.func = lambda *args: None # Used in CompletionTest - test.parser.add_option(u'-o', u'--option', dest='my_opt') + test.parser.add_option('-o', '--option', dest='my_opt') plugin = ui.Subcommand('plugin') plugin.func = lambda *args: None diff -Nru beets-1.5.0/test/rsrc/convert_stub.py beets-1.6.0/test/rsrc/convert_stub.py --- beets-1.5.0/test/rsrc/convert_stub.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/rsrc/convert_stub.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,17 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """A tiny tool used to test the `convert` plugin. It copies a file and appends a specified text tag. """ -from __future__ import division, absolute_import, print_function import sys -import platform import locale -PY2 = sys.version_info[0] == 2 - # From `beets.util`. def arg_encoding(): @@ -24,16 +19,9 @@ def convert(in_file, out_file, tag): """Copy `in_file` to `out_file` and append the string `tag`. """ - # On Python 3, encode the tag argument as bytes. if not isinstance(tag, bytes): tag = tag.encode('utf-8') - # On Windows, use Unicode paths. On Python 3, we get the actual, - # Unicode filenames. On Python 2, we get them as UTF-8 byes. - if platform.system() == 'Windows' and PY2: - in_file = in_file.decode('utf-8') - out_file = out_file.decode('utf-8') - with open(out_file, 'wb') as out_f: with open(in_file, 'rb') as in_f: out_f.write(in_f.read()) diff -Nru beets-1.5.0/test/rsrc/spotify/album_info.json beets-1.6.0/test/rsrc/spotify/album_info.json --- beets-1.5.0/test/rsrc/spotify/album_info.json 1970-01-01 00:00:00.000000000 +0000 +++ beets-1.6.0/test/rsrc/spotify/album_info.json 2021-11-26 20:51:38.000000000 +0000 @@ -0,0 +1,766 @@ +{ + "album_type": "compilation", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/0LyfQWJT6nXafLPZqxe9Of" + }, + "href": "https://api.spotify.com/v1/artists/0LyfQWJT6nXafLPZqxe9Of", + "id": "0LyfQWJT6nXafLPZqxe9Of", + "name": "Various Artists", + "type": "artist", + "uri": "spotify:artist:0LyfQWJT6nXafLPZqxe9Of" + } + ], + "available_markets": [], + "copyrights": [ + { + "text": "2013 Back Lot Music", + "type": "C" + }, + { + "text": "2013 Back Lot Music", + "type": "P" + } + ], + "external_ids": { + "upc": "857970002363" + }, + "external_urls": { + "spotify": "https://open.spotify.com/album/5l3zEmMrOhOzG8d8s83GOL" + }, + "genres": [], + "href": "https://api.spotify.com/v1/albums/5l3zEmMrOhOzG8d8s83GOL", + "id": "5l3zEmMrOhOzG8d8s83GOL", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab67616d0000b27399140a62d43aec760f6172a2", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67616d00001e0299140a62d43aec760f6172a2", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab67616d0000485199140a62d43aec760f6172a2", + "width": 64 + } + ], + "label": "Back Lot Music", + "name": "Despicable Me 2 (Original Motion Picture Soundtrack)", + "popularity": 0, + "release_date": "2013-06-18", + "release_date_precision": "day", + "total_tracks": 24, + "tracks": { + "href": "https://api.spotify.com/v1/albums/5l3zEmMrOhOzG8d8s83GOL/tracks?offset=0&limit=50", + "items": [ + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/5nLYd9ST4Cnwy6NHaCxbj8" + }, + "href": "https://api.spotify.com/v1/artists/5nLYd9ST4Cnwy6NHaCxbj8", + "id": "5nLYd9ST4Cnwy6NHaCxbj8", + "name": "CeeLo Green", + "type": "artist", + "uri": "spotify:artist:5nLYd9ST4Cnwy6NHaCxbj8" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 221805, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/3EiEbQAR44icEkz3rsMI0N" + }, + "href": "https://api.spotify.com/v1/tracks/3EiEbQAR44icEkz3rsMI0N", + "id": "3EiEbQAR44icEkz3rsMI0N", + "is_local": false, + "name": "Scream", + "preview_url": null, + "track_number": 1, + "type": "track", + "uri": "spotify:track:3EiEbQAR44icEkz3rsMI0N" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/3NVrWkcHOtmPbMSvgHmijZ" + }, + "href": "https://api.spotify.com/v1/artists/3NVrWkcHOtmPbMSvgHmijZ", + "id": "3NVrWkcHOtmPbMSvgHmijZ", + "name": "The Minions", + "type": "artist", + "uri": "spotify:artist:3NVrWkcHOtmPbMSvgHmijZ" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 39065, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/1G4Z91vvEGTYd2ZgOD0MuN" + }, + "href": "https://api.spotify.com/v1/tracks/1G4Z91vvEGTYd2ZgOD0MuN", + "id": "1G4Z91vvEGTYd2ZgOD0MuN", + "is_local": false, + "name": "Another Irish Drinking Song", + "preview_url": null, + "track_number": 2, + "type": "track", + "uri": "spotify:track:1G4Z91vvEGTYd2ZgOD0MuN" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8" + }, + "href": "https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8", + "id": "2RdwBSPQiwcmiDo9kixcl8", + "name": "Pharrell Williams", + "type": "artist", + "uri": "spotify:artist:2RdwBSPQiwcmiDo9kixcl8" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 176078, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/7DKqhn3Aa0NT9N9GAcagda" + }, + "href": "https://api.spotify.com/v1/tracks/7DKqhn3Aa0NT9N9GAcagda", + "id": "7DKqhn3Aa0NT9N9GAcagda", + "is_local": false, + "name": "Just a Cloud Away", + "preview_url": null, + "track_number": 3, + "type": "track", + "uri": "spotify:track:7DKqhn3Aa0NT9N9GAcagda" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8" + }, + "href": "https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8", + "id": "2RdwBSPQiwcmiDo9kixcl8", + "name": "Pharrell Williams", + "type": "artist", + "uri": "spotify:artist:2RdwBSPQiwcmiDo9kixcl8" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 233305, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/6NPVjNh8Jhru9xOmyQigds" + }, + "href": "https://api.spotify.com/v1/tracks/6NPVjNh8Jhru9xOmyQigds", + "id": "6NPVjNh8Jhru9xOmyQigds", + "is_local": false, + "name": "Happy", + "preview_url": null, + "track_number": 4, + "type": "track", + "uri": "spotify:track:6NPVjNh8Jhru9xOmyQigds" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/3NVrWkcHOtmPbMSvgHmijZ" + }, + "href": "https://api.spotify.com/v1/artists/3NVrWkcHOtmPbMSvgHmijZ", + "id": "3NVrWkcHOtmPbMSvgHmijZ", + "name": "The Minions", + "type": "artist", + "uri": "spotify:artist:3NVrWkcHOtmPbMSvgHmijZ" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 98211, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/5HSqCeDCn2EEGR5ORwaHA0" + }, + "href": "https://api.spotify.com/v1/tracks/5HSqCeDCn2EEGR5ORwaHA0", + "id": "5HSqCeDCn2EEGR5ORwaHA0", + "is_local": false, + "name": "I Swear", + "preview_url": null, + "track_number": 5, + "type": "track", + "uri": "spotify:track:5HSqCeDCn2EEGR5ORwaHA0" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/3NVrWkcHOtmPbMSvgHmijZ" + }, + "href": "https://api.spotify.com/v1/artists/3NVrWkcHOtmPbMSvgHmijZ", + "id": "3NVrWkcHOtmPbMSvgHmijZ", + "name": "The Minions", + "type": "artist", + "uri": "spotify:artist:3NVrWkcHOtmPbMSvgHmijZ" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 175291, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/2Ls4QknWvBoGSeAlNKw0Xj" + }, + "href": "https://api.spotify.com/v1/tracks/2Ls4QknWvBoGSeAlNKw0Xj", + "id": "2Ls4QknWvBoGSeAlNKw0Xj", + "is_local": false, + "name": "Y.M.C.A.", + "preview_url": null, + "track_number": 6, + "type": "track", + "uri": "spotify:track:2Ls4QknWvBoGSeAlNKw0Xj" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8" + }, + "href": "https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8", + "id": "2RdwBSPQiwcmiDo9kixcl8", + "name": "Pharrell Williams", + "type": "artist", + "uri": "spotify:artist:2RdwBSPQiwcmiDo9kixcl8" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 206105, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/1XkUmKLbm1tzVtrkdj2Ou8" + }, + "href": "https://api.spotify.com/v1/tracks/1XkUmKLbm1tzVtrkdj2Ou8", + "id": "1XkUmKLbm1tzVtrkdj2Ou8", + "is_local": false, + "name": "Fun, Fun, Fun", + "preview_url": null, + "track_number": 7, + "type": "track", + "uri": "spotify:track:1XkUmKLbm1tzVtrkdj2Ou8" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8" + }, + "href": "https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8", + "id": "2RdwBSPQiwcmiDo9kixcl8", + "name": "Pharrell Williams", + "type": "artist", + "uri": "spotify:artist:2RdwBSPQiwcmiDo9kixcl8" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 254705, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/42lHGtAZd6xVLC789afLWt" + }, + "href": "https://api.spotify.com/v1/tracks/42lHGtAZd6xVLC789afLWt", + "id": "42lHGtAZd6xVLC789afLWt", + "is_local": false, + "name": "Despicable Me", + "preview_url": null, + "track_number": 8, + "type": "track", + "uri": "spotify:track:42lHGtAZd6xVLC789afLWt" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 126825, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/7uAC260NViRKyYW4st4vri" + }, + "href": "https://api.spotify.com/v1/tracks/7uAC260NViRKyYW4st4vri", + "id": "7uAC260NViRKyYW4st4vri", + "is_local": false, + "name": "PX-41 Labs", + "preview_url": null, + "track_number": 9, + "type": "track", + "uri": "spotify:track:7uAC260NViRKyYW4st4vri" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 87118, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/6YLmc6yT7OGiNwbShHuEN2" + }, + "href": "https://api.spotify.com/v1/tracks/6YLmc6yT7OGiNwbShHuEN2", + "id": "6YLmc6yT7OGiNwbShHuEN2", + "is_local": false, + "name": "The Fairy Party", + "preview_url": null, + "track_number": 10, + "type": "track", + "uri": "spotify:track:6YLmc6yT7OGiNwbShHuEN2" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 339478, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/5lwsXhSXKFoxoGOFLZdQX6" + }, + "href": "https://api.spotify.com/v1/tracks/5lwsXhSXKFoxoGOFLZdQX6", + "id": "5lwsXhSXKFoxoGOFLZdQX6", + "is_local": false, + "name": "Lucy And The AVL", + "preview_url": null, + "track_number": 11, + "type": "track", + "uri": "spotify:track:5lwsXhSXKFoxoGOFLZdQX6" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 87478, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/2FlWtPuBMGo0a0X7LGETyk" + }, + "href": "https://api.spotify.com/v1/tracks/2FlWtPuBMGo0a0X7LGETyk", + "id": "2FlWtPuBMGo0a0X7LGETyk", + "is_local": false, + "name": "Goodbye Nefario", + "preview_url": null, + "track_number": 12, + "type": "track", + "uri": "spotify:track:2FlWtPuBMGo0a0X7LGETyk" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 86998, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/3YnhGNADeUaoBTjB1uGUjh" + }, + "href": "https://api.spotify.com/v1/tracks/3YnhGNADeUaoBTjB1uGUjh", + "id": "3YnhGNADeUaoBTjB1uGUjh", + "is_local": false, + "name": "Time for Bed", + "preview_url": null, + "track_number": 13, + "type": "track", + "uri": "spotify:track:3YnhGNADeUaoBTjB1uGUjh" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 180265, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/6npUKThV4XI20VLW5ryr5O" + }, + "href": "https://api.spotify.com/v1/tracks/6npUKThV4XI20VLW5ryr5O", + "id": "6npUKThV4XI20VLW5ryr5O", + "is_local": false, + "name": "Break-In", + "preview_url": null, + "track_number": 14, + "type": "track", + "uri": "spotify:track:6npUKThV4XI20VLW5ryr5O" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 95011, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/1qyFlqVfbgyiM7tQ2Jy9vC" + }, + "href": "https://api.spotify.com/v1/tracks/1qyFlqVfbgyiM7tQ2Jy9vC", + "id": "1qyFlqVfbgyiM7tQ2Jy9vC", + "is_local": false, + "name": "Stalking Floyd Eaglesan", + "preview_url": null, + "track_number": 15, + "type": "track", + "uri": "spotify:track:1qyFlqVfbgyiM7tQ2Jy9vC" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 189771, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/4DRQctGiqjJkbFa7iTK4pb" + }, + "href": "https://api.spotify.com/v1/tracks/4DRQctGiqjJkbFa7iTK4pb", + "id": "4DRQctGiqjJkbFa7iTK4pb", + "is_local": false, + "name": "Moving to Australia", + "preview_url": null, + "track_number": 16, + "type": "track", + "uri": "spotify:track:4DRQctGiqjJkbFa7iTK4pb" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 85878, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/1TSjM9GY2oN6RO6aYGN25n" + }, + "href": "https://api.spotify.com/v1/tracks/1TSjM9GY2oN6RO6aYGN25n", + "id": "1TSjM9GY2oN6RO6aYGN25n", + "is_local": false, + "name": "Going to Save the World", + "preview_url": null, + "track_number": 17, + "type": "track", + "uri": "spotify:track:1TSjM9GY2oN6RO6aYGN25n" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 87158, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/3AEMuoglM1myQ8ouIyh8LG" + }, + "href": "https://api.spotify.com/v1/tracks/3AEMuoglM1myQ8ouIyh8LG", + "id": "3AEMuoglM1myQ8ouIyh8LG", + "is_local": false, + "name": "El Macho", + "preview_url": null, + "track_number": 18, + "type": "track", + "uri": "spotify:track:3AEMuoglM1myQ8ouIyh8LG" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 47438, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/2d7fEVYdZnjlya3MPEma21" + }, + "href": "https://api.spotify.com/v1/tracks/2d7fEVYdZnjlya3MPEma21", + "id": "2d7fEVYdZnjlya3MPEma21", + "is_local": false, + "name": "Jillian", + "preview_url": null, + "track_number": 19, + "type": "track", + "uri": "spotify:track:2d7fEVYdZnjlya3MPEma21" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 89398, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/7h8WnOo4Fh6NvfTUnR7nOa" + }, + "href": "https://api.spotify.com/v1/tracks/7h8WnOo4Fh6NvfTUnR7nOa", + "id": "7h8WnOo4Fh6NvfTUnR7nOa", + "is_local": false, + "name": "Take Her Home", + "preview_url": null, + "track_number": 20, + "type": "track", + "uri": "spotify:track:7h8WnOo4Fh6NvfTUnR7nOa" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 212691, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/25A9ZlegjJ0z2fI1PgTqy2" + }, + "href": "https://api.spotify.com/v1/tracks/25A9ZlegjJ0z2fI1PgTqy2", + "id": "25A9ZlegjJ0z2fI1PgTqy2", + "is_local": false, + "name": "El Macho's Lair", + "preview_url": null, + "track_number": 21, + "type": "track", + "uri": "spotify:track:25A9ZlegjJ0z2fI1PgTqy2" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 117745, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/48GwOCuPhWKDktq3efmfRg" + }, + "href": "https://api.spotify.com/v1/tracks/48GwOCuPhWKDktq3efmfRg", + "id": "48GwOCuPhWKDktq3efmfRg", + "is_local": false, + "name": "Home Invasion", + "preview_url": null, + "track_number": 22, + "type": "track", + "uri": "spotify:track:48GwOCuPhWKDktq3efmfRg" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz" + }, + "href": "https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz", + "id": "2RaHCHhZWBXn460JpMaicz", + "name": "Heitor Pereira", + "type": "artist", + "uri": "spotify:artist:2RaHCHhZWBXn460JpMaicz" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 443251, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/6dZkl2egcKVm8rO9W7pPWa" + }, + "href": "https://api.spotify.com/v1/tracks/6dZkl2egcKVm8rO9W7pPWa", + "id": "6dZkl2egcKVm8rO9W7pPWa", + "is_local": false, + "name": "The Big Battle", + "preview_url": null, + "track_number": 23, + "type": "track", + "uri": "spotify:track:6dZkl2egcKVm8rO9W7pPWa" + }, + { + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/3NVrWkcHOtmPbMSvgHmijZ" + }, + "href": "https://api.spotify.com/v1/artists/3NVrWkcHOtmPbMSvgHmijZ", + "id": "3NVrWkcHOtmPbMSvgHmijZ", + "name": "The Minions", + "type": "artist", + "uri": "spotify:artist:3NVrWkcHOtmPbMSvgHmijZ" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 13886, + "explicit": false, + "external_urls": { + "spotify": "https://open.spotify.com/track/2L0OyiAepqAbKvUZfWovOJ" + }, + "href": "https://api.spotify.com/v1/tracks/2L0OyiAepqAbKvUZfWovOJ", + "id": "2L0OyiAepqAbKvUZfWovOJ", + "is_local": false, + "name": "Ba Do Bleep", + "preview_url": null, + "track_number": 24, + "type": "track", + "uri": "spotify:track:2L0OyiAepqAbKvUZfWovOJ" + } + ], + "limit": 50, + "next": null, + "offset": 0, + "previous": null, + "total": 24 + }, + "type": "album", + "uri": "spotify:album:5l3zEmMrOhOzG8d8s83GOL" +} \ No newline at end of file diff -Nru beets-1.5.0/test/rsrc/spotify/track_info.json beets-1.6.0/test/rsrc/spotify/track_info.json --- beets-1.5.0/test/rsrc/spotify/track_info.json 1970-01-01 00:00:00.000000000 +0000 +++ beets-1.6.0/test/rsrc/spotify/track_info.json 2021-11-26 20:51:38.000000000 +0000 @@ -0,0 +1,77 @@ +{ + "album": { + "album_type": "compilation", + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/0LyfQWJT6nXafLPZqxe9Of" + }, + "href": "https://api.spotify.com/v1/artists/0LyfQWJT6nXafLPZqxe9Of", + "id": "0LyfQWJT6nXafLPZqxe9Of", + "name": "Various Artists", + "type": "artist", + "uri": "spotify:artist:0LyfQWJT6nXafLPZqxe9Of" + } + ], + "available_markets": [], + "external_urls": { + "spotify": "https://open.spotify.com/album/5l3zEmMrOhOzG8d8s83GOL" + }, + "href": "https://api.spotify.com/v1/albums/5l3zEmMrOhOzG8d8s83GOL", + "id": "5l3zEmMrOhOzG8d8s83GOL", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab67616d0000b27399140a62d43aec760f6172a2", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67616d00001e0299140a62d43aec760f6172a2", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab67616d0000485199140a62d43aec760f6172a2", + "width": 64 + } + ], + "name": "Despicable Me 2 (Original Motion Picture Soundtrack)", + "release_date": "2013-06-18", + "release_date_precision": "day", + "total_tracks": 24, + "type": "album", + "uri": "spotify:album:5l3zEmMrOhOzG8d8s83GOL" + }, + "artists": [ + { + "external_urls": { + "spotify": "https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8" + }, + "href": "https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8", + "id": "2RdwBSPQiwcmiDo9kixcl8", + "name": "Pharrell Williams", + "type": "artist", + "uri": "spotify:artist:2RdwBSPQiwcmiDo9kixcl8" + } + ], + "available_markets": [], + "disc_number": 1, + "duration_ms": 233305, + "explicit": false, + "external_ids": { + "isrc": "USQ4E1300686" + }, + "external_urls": { + "spotify": "https://open.spotify.com/track/6NPVjNh8Jhru9xOmyQigds" + }, + "href": "https://api.spotify.com/v1/tracks/6NPVjNh8Jhru9xOmyQigds", + "id": "6NPVjNh8Jhru9xOmyQigds", + "is_local": false, + "name": "Happy", + "popularity": 1, + "preview_url": null, + "track_number": 4, + "type": "track", + "uri": "spotify:track:6NPVjNh8Jhru9xOmyQigds" +} \ No newline at end of file diff -Nru beets-1.5.0/test/test_acousticbrainz.py beets-1.6.0/test/test_acousticbrainz.py --- beets-1.5.0/test/test_acousticbrainz.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_acousticbrainz.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Nathan Dwek. # @@ -16,7 +15,6 @@ """Tests for the 'acousticbrainz' plugin. """ -from __future__ import division, absolute_import, print_function import json import os.path diff -Nru beets-1.5.0/test/test_albumtypes.py beets-1.6.0/test/test_albumtypes.py --- beets-1.5.0/test/test_albumtypes.py 1970-01-01 00:00:00.000000000 +0000 +++ beets-1.6.0/test/test_albumtypes.py 2021-09-28 19:51:02.000000000 +0000 @@ -0,0 +1,111 @@ +# This file is part of beets. +# Copyright 2021, Edgars Supe. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Tests for the 'albumtypes' plugin.""" + + +import unittest + +from beets.autotag.mb import VARIOUS_ARTISTS_ID +from beetsplug.albumtypes import AlbumTypesPlugin +from test.helper import TestHelper + + +class AlbumTypesPluginTest(unittest.TestCase, TestHelper): + """Tests for albumtypes plugin.""" + + def setUp(self): + """Set up tests.""" + self.setup_beets() + self.load_plugins('albumtypes') + + def tearDown(self): + """Tear down tests.""" + self.unload_plugins() + self.teardown_beets() + + def test_renames_types(self): + """Tests if the plugin correctly renames the specified types.""" + self._set_config( + types=[('ep', 'EP'), ('remix', 'Remix')], + ignore_va=[], + bracket='()' + ) + album = self._create_album(album_types=['ep', 'remix']) + subject = AlbumTypesPlugin() + result = subject._atypes(album) + self.assertEqual('(EP)(Remix)', result) + return + + def test_returns_only_specified_types(self): + """Tests if the plugin returns only non-blank types given in config.""" + self._set_config( + types=[('ep', 'EP'), ('soundtrack', '')], + ignore_va=[], + bracket='()' + ) + album = self._create_album(album_types=['ep', 'remix', 'soundtrack']) + subject = AlbumTypesPlugin() + result = subject._atypes(album) + self.assertEqual('(EP)', result) + + def test_respects_type_order(self): + """Tests if the types are returned in the same order as config.""" + self._set_config( + types=[('remix', 'Remix'), ('ep', 'EP')], + ignore_va=[], + bracket='()' + ) + album = self._create_album(album_types=['ep', 'remix']) + subject = AlbumTypesPlugin() + result = subject._atypes(album) + self.assertEqual('(Remix)(EP)', result) + return + + def test_ignores_va(self): + """Tests if the specified type is ignored for VA albums.""" + self._set_config( + types=[('ep', 'EP'), ('soundtrack', 'OST')], + ignore_va=['ep'], + bracket='()' + ) + album = self._create_album( + album_types=['ep', 'soundtrack'], + artist_id=VARIOUS_ARTISTS_ID + ) + subject = AlbumTypesPlugin() + result = subject._atypes(album) + self.assertEqual('(OST)', result) + + def test_respects_defaults(self): + """Tests if the plugin uses the default values if config not given.""" + album = self._create_album( + album_types=['ep', 'single', 'soundtrack', 'live', 'compilation', + 'remix'], + artist_id=VARIOUS_ARTISTS_ID + ) + subject = AlbumTypesPlugin() + result = subject._atypes(album) + self.assertEqual('[EP][Single][OST][Live][Remix]', result) + + def _set_config(self, types: [(str, str)], ignore_va: [str], bracket: str): + self.config['albumtypes']['types'] = types + self.config['albumtypes']['ignore_va'] = ignore_va + self.config['albumtypes']['bracket'] = bracket + + def _create_album(self, album_types: [str], artist_id: str = 0): + return self.add_album( + albumtypes='; '.join(album_types), + mb_albumartistid=artist_id + ) diff -Nru beets-1.5.0/test/testall.py beets-1.6.0/test/testall.py --- beets-1.5.0/test/testall.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/testall.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. @@ -15,7 +14,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import os import re diff -Nru beets-1.5.0/test/test_art.py beets-1.6.0/test/test_art.py --- beets-1.5.0/test/test_art.py 2021-03-28 18:23:15.000000000 +0000 +++ beets-1.6.0/test/test_art.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,14 +14,13 @@ """Tests for the album art fetchers.""" -from __future__ import division, absolute_import, print_function import os import shutil import unittest import responses -from mock import patch +from unittest.mock import patch from test import _common from test.helper import capture_log @@ -51,7 +49,7 @@ class UseThePlugin(_common.TestCase): def setUp(self): - super(UseThePlugin, self).setUp() + super().setUp() self.plugin = fetchart.FetchArtPlugin() @@ -61,7 +59,7 @@ """ @responses.activate def run(self, *args, **kwargs): - super(FetchImageHelper, self).run(*args, **kwargs) + super().run(*args, **kwargs) IMAGEHEADER = {'image/jpeg': b'\x00' * 6 + b'JFIF', 'image/png': b'\211PNG\r\n\032\n', } @@ -81,17 +79,13 @@ MBID_RELASE = 'rid' MBID_GROUP = 'rgid' - RELEASE_URL = 'coverartarchive.org/release/{0}' \ + RELEASE_URL = 'coverartarchive.org/release/{}' \ .format(MBID_RELASE) - GROUP_URL = 'coverartarchive.org/release-group/{0}' \ + GROUP_URL = 'coverartarchive.org/release-group/{}' \ .format(MBID_GROUP) - if util.SNI_SUPPORTED: - RELEASE_URL = "https://" + RELEASE_URL - GROUP_URL = "https://" + GROUP_URL - else: - RELEASE_URL = "http://" + RELEASE_URL - GROUP_URL = "http://" + GROUP_URL + RELEASE_URL = "https://" + RELEASE_URL + GROUP_URL = "https://" + GROUP_URL RESPONSE_RELEASE = """{ "images": [ @@ -170,7 +164,7 @@ URL = 'http://example.com/test.jpg' def setUp(self): - super(FetchImageTest, self).setUp() + super().setUp() self.dpath = os.path.join(self.temp_dir, b'arttest') self.source = fetchart.RemoteArtSource(logger, self.plugin.config) self.settings = Settings(maxwidth=0) @@ -201,7 +195,7 @@ class FSArtTest(UseThePlugin): def setUp(self): - super(FSArtTest, self).setUp() + super().setUp() self.dpath = os.path.join(self.temp_dir, b'arttest') os.mkdir(self.dpath) @@ -249,13 +243,13 @@ class CombinedTest(FetchImageHelper, UseThePlugin, CAAHelper): ASIN = 'xxxx' MBID = 'releaseid' - AMAZON_URL = 'https://images.amazon.com/images/P/{0}.01.LZZZZZZZ.jpg' \ + AMAZON_URL = 'https://images.amazon.com/images/P/{}.01.LZZZZZZZ.jpg' \ .format(ASIN) - AAO_URL = 'https://www.albumart.org/index_detail.php?asin={0}' \ + AAO_URL = 'https://www.albumart.org/index_detail.php?asin={}' \ .format(ASIN) def setUp(self): - super(CombinedTest, self).setUp() + super().setUp() self.dpath = os.path.join(self.temp_dir, b'arttest') os.mkdir(self.dpath) @@ -330,16 +324,16 @@ class AAOTest(UseThePlugin): ASIN = 'xxxx' - AAO_URL = 'https://www.albumart.org/index_detail.php?asin={0}'.format(ASIN) + AAO_URL = f'https://www.albumart.org/index_detail.php?asin={ASIN}' def setUp(self): - super(AAOTest, self).setUp() + super().setUp() self.source = fetchart.AlbumArtOrg(logger, self.plugin.config) self.settings = Settings() @responses.activate def run(self, *args, **kwargs): - super(AAOTest, self).run(*args, **kwargs) + super().run(*args, **kwargs) def mock_response(self, url, body): responses.add(responses.GET, url, body=body, content_type='text/html', @@ -367,14 +361,14 @@ class ITunesStoreTest(UseThePlugin): def setUp(self): - super(ITunesStoreTest, self).setUp() + super().setUp() self.source = fetchart.ITunesStore(logger, self.plugin.config) self.settings = Settings() self.album = _common.Bag(albumartist="some artist", album="some album") @responses.activate def run(self, *args, **kwargs): - super(ITunesStoreTest, self).run(*args, **kwargs) + super().run(*args, **kwargs) def mock_response(self, url, json): responses.add(responses.GET, url, body=json, @@ -399,7 +393,7 @@ def test_itunesstore_no_result(self): json = '{"results": []}' self.mock_response(fetchart.ITunesStore.API_URL, json) - expected = u"got no results" + expected = "got no results" with capture_log('beets.test_art') as logs: with self.assertRaises(StopIteration): @@ -409,7 +403,7 @@ def test_itunesstore_requestexception(self): responses.add(responses.GET, fetchart.ITunesStore.API_URL, json={'error': 'not found'}, status=404) - expected = u'iTunes search failed: 404 Client Error' + expected = 'iTunes search failed: 404 Client Error' with capture_log('beets.test_art') as logs: with self.assertRaises(StopIteration): @@ -442,7 +436,7 @@ ] }""" self.mock_response(fetchart.ITunesStore.API_URL, json) - expected = u'Malformed itunes candidate' + expected = 'Malformed itunes candidate' with capture_log('beets.test_art') as logs: with self.assertRaises(StopIteration): @@ -452,7 +446,7 @@ def test_itunesstore_returns_no_result_when_error_received(self): json = '{"error": {"errors": [{"reason": "some reason"}]}}' self.mock_response(fetchart.ITunesStore.API_URL, json) - expected = u"not found in json. Fields are" + expected = "not found in json. Fields are" with capture_log('beets.test_art') as logs: with self.assertRaises(StopIteration): @@ -462,7 +456,7 @@ def test_itunesstore_returns_no_result_with_malformed_response(self): json = """bla blup""" self.mock_response(fetchart.ITunesStore.API_URL, json) - expected = u"Could not decode json response:" + expected = "Could not decode json response:" with capture_log('beets.test_art') as logs: with self.assertRaises(StopIteration): @@ -472,13 +466,13 @@ class GoogleImageTest(UseThePlugin): def setUp(self): - super(GoogleImageTest, self).setUp() + super().setUp() self.source = fetchart.GoogleImages(logger, self.plugin.config) self.settings = Settings() @responses.activate def run(self, *args, **kwargs): - super(GoogleImageTest, self).run(*args, **kwargs) + super().run(*args, **kwargs) def mock_response(self, url, json): responses.add(responses.GET, url, body=json, @@ -509,13 +503,13 @@ class CoverArtArchiveTest(UseThePlugin, CAAHelper): def setUp(self): - super(CoverArtArchiveTest, self).setUp() + super().setUp() self.source = fetchart.CoverArtArchive(logger, self.plugin.config) self.settings = Settings(maxwidth=0) @responses.activate def run(self, *args, **kwargs): - super(CoverArtArchiveTest, self).run(*args, **kwargs) + super().run(*args, **kwargs) def test_caa_finds_image(self): album = _common.Bag(mb_albumid=self.MBID_RELASE, @@ -529,7 +523,7 @@ class FanartTVTest(UseThePlugin): - RESPONSE_MULTIPLE = u"""{ + RESPONSE_MULTIPLE = """{ "name": "artistname", "mbid_id": "artistid", "albums": { @@ -563,7 +557,7 @@ } } }""" - RESPONSE_NO_ART = u"""{ + RESPONSE_NO_ART = """{ "name": "artistname", "mbid_id": "artistid", "albums": { @@ -580,50 +574,50 @@ } } }""" - RESPONSE_ERROR = u"""{ + RESPONSE_ERROR = """{ "status": "error", "error message": "the error message" }""" - RESPONSE_MALFORMED = u"bla blup" + RESPONSE_MALFORMED = "bla blup" def setUp(self): - super(FanartTVTest, self).setUp() + super().setUp() self.source = fetchart.FanartTV(logger, self.plugin.config) self.settings = Settings() @responses.activate def run(self, *args, **kwargs): - super(FanartTVTest, self).run(*args, **kwargs) + super().run(*args, **kwargs) def mock_response(self, url, json): responses.add(responses.GET, url, body=json, content_type='application/json') def test_fanarttv_finds_image(self): - album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') - self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', + album = _common.Bag(mb_releasegroupid='thereleasegroupid') + self.mock_response(fetchart.FanartTV.API_ALBUMS + 'thereleasegroupid', self.RESPONSE_MULTIPLE) candidate = next(self.source.get(album, self.settings, [])) self.assertEqual(candidate.url, 'http://example.com/1.jpg') def test_fanarttv_returns_no_result_when_error_received(self): - album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') - self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', + album = _common.Bag(mb_releasegroupid='thereleasegroupid') + self.mock_response(fetchart.FanartTV.API_ALBUMS + 'thereleasegroupid', self.RESPONSE_ERROR) with self.assertRaises(StopIteration): next(self.source.get(album, self.settings, [])) def test_fanarttv_returns_no_result_with_malformed_response(self): - album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') - self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', + album = _common.Bag(mb_releasegroupid='thereleasegroupid') + self.mock_response(fetchart.FanartTV.API_ALBUMS + 'thereleasegroupid', self.RESPONSE_MALFORMED) with self.assertRaises(StopIteration): next(self.source.get(album, self.settings, [])) def test_fanarttv_only_other_images(self): # The source used to fail when there were images present, but no cover - album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') - self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', + album = _common.Bag(mb_releasegroupid='thereleasegroupid') + self.mock_response(fetchart.FanartTV.API_ALBUMS + 'thereleasegroupid', self.RESPONSE_NO_ART) with self.assertRaises(StopIteration): next(self.source.get(album, self.settings, [])) @@ -632,7 +626,7 @@ @_common.slow_test() class ArtImporterTest(UseThePlugin): def setUp(self): - super(ArtImporterTest, self).setUp() + super().setUp() # Mock the album art fetcher to always return our test file. self.art_file = os.path.join(self.temp_dir, b'tmpcover.jpg') @@ -666,17 +660,17 @@ self.task.is_album = True self.task.album = self.album info = AlbumInfo( - album=u'some album', - album_id=u'albumid', - artist=u'some artist', - artist_id=u'artistid', + album='some album', + album_id='albumid', + artist='some artist', + artist_id='artistid', tracks=[], ) self.task.set_choice(AlbumMatch(0, info, {}, set(), set())) def tearDown(self): self.lib._connection().close() - super(ArtImporterTest, self).tearDown() + super().tearDown() self.plugin.art_for_album = self.old_afa def _fetch_art(self, should_exist): @@ -754,7 +748,7 @@ IMG_348x348_SIZE = os.stat(util.syspath(IMG_348x348)).st_size def setUp(self): - super(ArtForAlbumTest, self).setUp() + super().setUp() self.old_fs_source_get = fetchart.FileSystem.get @@ -768,7 +762,7 @@ def tearDown(self): fetchart.FileSystem.get = self.old_fs_source_get - super(ArtForAlbumTest, self).tearDown() + super().tearDown() def _assertImageIsValidArt(self, image_file, should_exist): # noqa self.assertExists(image_file) @@ -794,7 +788,7 @@ PIL (so comparisons and measurements are unavailable). """ if ArtResizer.shared.method[0] == WEBPROXY: - self.skipTest(u"ArtResizer has no local imaging backend available") + self.skipTest("ArtResizer has no local imaging backend available") def test_respect_minwidth(self): self._require_backend() @@ -876,7 +870,7 @@ # overwritten by _common.TestCase or be set after constructing the # plugin object def setUp(self): - super(DeprecatedConfigTest, self).setUp() + super().setUp() config['fetchart']['remote_priority'] = True self.plugin = fetchart.FetchArtPlugin() @@ -899,12 +893,12 @@ fetchart.FetchArtPlugin() def test_px(self): - self._load_with_config(u'0px 4px 12px 123px'.split(), False) - self._load_with_config(u'00px stuff5px'.split(), True) + self._load_with_config('0px 4px 12px 123px'.split(), False) + self._load_with_config('00px stuff5px'.split(), True) def test_percent(self): - self._load_with_config(u'0% 0.00% 5.1% 5% 100%'.split(), False) - self._load_with_config(u'00% 1.234% foo5% 100.1%'.split(), True) + self._load_with_config('0% 0.00% 5.1% 5% 100%'.split(), False) + self._load_with_config('00% 1.234% foo5% 100.1%'.split(), True) def suite(): diff -Nru beets-1.5.0/test/test_art_resize.py beets-1.6.0/test/test_art_resize.py --- beets-1.5.0/test/test_art_resize.py 2021-03-28 18:23:15.000000000 +0000 +++ beets-1.6.0/test/test_art_resize.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2020, David Swarbrick. # @@ -15,20 +14,21 @@ """Tests for image resizing based on filesize.""" -from __future__ import division, absolute_import, print_function - import unittest import os from test import _common from test.helper import TestHelper -from beets.util import syspath +from beets.util import command_output, syspath from beets.util.artresizer import ( pil_resize, im_resize, get_im_version, get_pil_version, + pil_deinterlace, + im_deinterlace, + ArtResizer, ) @@ -100,6 +100,32 @@ """Test IM resize function is lowering file size.""" self._test_img_resize(im_resize) + @unittest.skipUnless(get_pil_version(), "PIL not available") + def test_pil_file_deinterlace(self): + """Test PIL deinterlace function. + + Check if pil_deinterlace function returns images + that are non-progressive + """ + path = pil_deinterlace(self.IMG_225x225) + from PIL import Image + with Image.open(path) as img: + self.assertFalse('progression' in img.info) + + @unittest.skipUnless(get_im_version(), "ImageMagick not available") + def test_im_file_deinterlace(self): + """Test ImageMagick deinterlace function. + + Check if im_deinterlace function returns images + that are non-progressive. + """ + path = im_deinterlace(self.IMG_225x225) + cmd = ArtResizer.shared.im_identify_cmd + [ + '-format', '%[interlace]', syspath(path, prefix=False), + ] + out = command_output(cmd).stdout + self.assertTrue(out == b'None') + def suite(): """Run this suite of tests.""" diff -Nru beets-1.5.0/test/test_autotag.py beets-1.6.0/test/test_autotag.py --- beets-1.5.0/test/test_autotag.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/test/test_autotag.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """Tests for autotagging functionality. """ -from __future__ import division, absolute_import, print_function import re import unittest @@ -86,7 +84,7 @@ fields = ['artist', 'album', 'albumartist', 'year', 'disctotal', 'mb_albumid', 'label', 'catalognum', 'country', 'media', 'albumdisambig'] - items = [Item(**dict((f, '%s_%s' % (f, i or 1)) for f in fields)) + items = [Item(**{f: '{}_{}'.format(f, i or 1) for f in fields}) for i in range(5)] likelies, _ = match.current_metadata(items) for f in fields: @@ -96,20 +94,20 @@ self.assertEqual(likelies[f], '%s_1' % f) -def _make_item(title, track, artist=u'some artist'): +def _make_item(title, track, artist='some artist'): return Item(title=title, track=track, - artist=artist, album=u'some album', + artist=artist, album='some album', length=1, mb_trackid='', mb_albumid='', mb_artistid='') def _make_trackinfo(): return [ - TrackInfo(title=u'one', track_id=None, artist=u'some artist', + TrackInfo(title='one', track_id=None, artist='some artist', length=1, index=1), - TrackInfo(title=u'two', track_id=None, artist=u'some artist', + TrackInfo(title='two', track_id=None, artist='some artist', length=1, index=2), - TrackInfo(title=u'three', track_id=None, artist=u'some artist', + TrackInfo(title='three', track_id=None, artist='some artist', length=1, index=3), ] @@ -123,7 +121,7 @@ class DistanceTest(_common.TestCase): def tearDown(self): - super(DistanceTest, self).tearDown() + super().tearDown() _clear_weights() def test_add(self): @@ -199,8 +197,8 @@ def test_add_string(self): dist = Distance() - sdist = string_dist(u'abc', u'bcd') - dist.add_string('string', u'abc', u'bcd') + sdist = string_dist('abc', 'bcd') + dist.add_string('string', 'abc', 'bcd') self.assertEqual(dist._penalties['string'], [sdist]) self.assertNotEqual(dist._penalties['string'], [0]) @@ -305,27 +303,27 @@ class TrackDistanceTest(_common.TestCase): def test_identical_tracks(self): - item = _make_item(u'one', 1) + item = _make_item('one', 1) info = _make_trackinfo()[0] dist = match.track_distance(item, info, incl_artist=True) self.assertEqual(dist, 0.0) def test_different_title(self): - item = _make_item(u'foo', 1) + item = _make_item('foo', 1) info = _make_trackinfo()[0] dist = match.track_distance(item, info, incl_artist=True) self.assertNotEqual(dist, 0.0) def test_different_artist(self): - item = _make_item(u'one', 1) - item.artist = u'foo' + item = _make_item('one', 1) + item.artist = 'foo' info = _make_trackinfo()[0] dist = match.track_distance(item, info, incl_artist=True) self.assertNotEqual(dist, 0.0) def test_various_artists_tolerated(self): - item = _make_item(u'one', 1) - item.artist = u'Various Artists' + item = _make_item('one', 1) + item.artist = 'Various Artists' info = _make_trackinfo()[0] dist = match.track_distance(item, info, incl_artist=True) self.assertEqual(dist, 0.0) @@ -343,12 +341,12 @@ def test_identical_albums(self): items = [] - items.append(_make_item(u'one', 1)) - items.append(_make_item(u'two', 2)) - items.append(_make_item(u'three', 3)) + items.append(_make_item('one', 1)) + items.append(_make_item('two', 2)) + items.append(_make_item('three', 3)) info = AlbumInfo( - artist=u'some artist', - album=u'some album', + artist='some artist', + album='some album', tracks=_make_trackinfo(), va=False ) @@ -356,11 +354,11 @@ def test_incomplete_album(self): items = [] - items.append(_make_item(u'one', 1)) - items.append(_make_item(u'three', 3)) + items.append(_make_item('one', 1)) + items.append(_make_item('three', 3)) info = AlbumInfo( - artist=u'some artist', - album=u'some album', + artist='some artist', + album='some album', tracks=_make_trackinfo(), va=False ) @@ -371,12 +369,12 @@ def test_global_artists_differ(self): items = [] - items.append(_make_item(u'one', 1)) - items.append(_make_item(u'two', 2)) - items.append(_make_item(u'three', 3)) + items.append(_make_item('one', 1)) + items.append(_make_item('two', 2)) + items.append(_make_item('three', 3)) info = AlbumInfo( - artist=u'someone else', - album=u'some album', + artist='someone else', + album='some album', tracks=_make_trackinfo(), va=False ) @@ -384,12 +382,12 @@ def test_comp_track_artists_match(self): items = [] - items.append(_make_item(u'one', 1)) - items.append(_make_item(u'two', 2)) - items.append(_make_item(u'three', 3)) + items.append(_make_item('one', 1)) + items.append(_make_item('two', 2)) + items.append(_make_item('three', 3)) info = AlbumInfo( - artist=u'should be ignored', - album=u'some album', + artist='should be ignored', + album='some album', tracks=_make_trackinfo(), va=True ) @@ -398,12 +396,12 @@ def test_comp_no_track_artists(self): # Some VA releases don't have track artists (incomplete metadata). items = [] - items.append(_make_item(u'one', 1)) - items.append(_make_item(u'two', 2)) - items.append(_make_item(u'three', 3)) + items.append(_make_item('one', 1)) + items.append(_make_item('two', 2)) + items.append(_make_item('three', 3)) info = AlbumInfo( - artist=u'should be ignored', - album=u'some album', + artist='should be ignored', + album='some album', tracks=_make_trackinfo(), va=True ) @@ -414,12 +412,12 @@ def test_comp_track_artists_do_not_match(self): items = [] - items.append(_make_item(u'one', 1)) - items.append(_make_item(u'two', 2, u'someone else')) - items.append(_make_item(u'three', 3)) + items.append(_make_item('one', 1)) + items.append(_make_item('two', 2, 'someone else')) + items.append(_make_item('three', 3)) info = AlbumInfo( - artist=u'some artist', - album=u'some album', + artist='some artist', + album='some album', tracks=_make_trackinfo(), va=True ) @@ -427,12 +425,12 @@ def test_tracks_out_of_order(self): items = [] - items.append(_make_item(u'one', 1)) - items.append(_make_item(u'three', 2)) - items.append(_make_item(u'two', 3)) + items.append(_make_item('one', 1)) + items.append(_make_item('three', 2)) + items.append(_make_item('two', 3)) info = AlbumInfo( - artist=u'some artist', - album=u'some album', + artist='some artist', + album='some album', tracks=_make_trackinfo(), va=False ) @@ -441,12 +439,12 @@ def test_two_medium_release(self): items = [] - items.append(_make_item(u'one', 1)) - items.append(_make_item(u'two', 2)) - items.append(_make_item(u'three', 3)) + items.append(_make_item('one', 1)) + items.append(_make_item('two', 2)) + items.append(_make_item('three', 3)) info = AlbumInfo( - artist=u'some artist', - album=u'some album', + artist='some artist', + album='some album', tracks=_make_trackinfo(), va=False ) @@ -458,12 +456,12 @@ def test_per_medium_track_numbers(self): items = [] - items.append(_make_item(u'one', 1)) - items.append(_make_item(u'two', 2)) - items.append(_make_item(u'three', 1)) + items.append(_make_item('one', 1)) + items.append(_make_item('two', 2)) + items.append(_make_item('three', 1)) info = AlbumInfo( - artist=u'some artist', - album=u'some album', + artist='some artist', + album='some album', tracks=_make_trackinfo(), va=False ) @@ -483,13 +481,13 @@ def test_reorder_when_track_numbers_incorrect(self): items = [] - items.append(self.item(u'one', 1)) - items.append(self.item(u'three', 2)) - items.append(self.item(u'two', 3)) + items.append(self.item('one', 1)) + items.append(self.item('three', 2)) + items.append(self.item('two', 3)) trackinfo = [] - trackinfo.append(TrackInfo(title=u'one')) - trackinfo.append(TrackInfo(title=u'two')) - trackinfo.append(TrackInfo(title=u'three')) + trackinfo.append(TrackInfo(title='one')) + trackinfo.append(TrackInfo(title='two')) + trackinfo.append(TrackInfo(title='three')) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, []) @@ -502,13 +500,13 @@ def test_order_works_with_invalid_track_numbers(self): items = [] - items.append(self.item(u'one', 1)) - items.append(self.item(u'three', 1)) - items.append(self.item(u'two', 1)) + items.append(self.item('one', 1)) + items.append(self.item('three', 1)) + items.append(self.item('two', 1)) trackinfo = [] - trackinfo.append(TrackInfo(title=u'one')) - trackinfo.append(TrackInfo(title=u'two')) - trackinfo.append(TrackInfo(title=u'three')) + trackinfo.append(TrackInfo(title='one')) + trackinfo.append(TrackInfo(title='two')) + trackinfo.append(TrackInfo(title='three')) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, []) @@ -521,12 +519,12 @@ def test_order_works_with_missing_tracks(self): items = [] - items.append(self.item(u'one', 1)) - items.append(self.item(u'three', 3)) + items.append(self.item('one', 1)) + items.append(self.item('three', 3)) trackinfo = [] - trackinfo.append(TrackInfo(title=u'one')) - trackinfo.append(TrackInfo(title=u'two')) - trackinfo.append(TrackInfo(title=u'three')) + trackinfo.append(TrackInfo(title='one')) + trackinfo.append(TrackInfo(title='two')) + trackinfo.append(TrackInfo(title='three')) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, []) @@ -538,12 +536,12 @@ def test_order_works_with_extra_tracks(self): items = [] - items.append(self.item(u'one', 1)) - items.append(self.item(u'two', 2)) - items.append(self.item(u'three', 3)) + items.append(self.item('one', 1)) + items.append(self.item('two', 2)) + items.append(self.item('three', 3)) trackinfo = [] - trackinfo.append(TrackInfo(title=u'one')) - trackinfo.append(TrackInfo(title=u'three')) + trackinfo.append(TrackInfo(title='one')) + trackinfo.append(TrackInfo(title='three')) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, [items[1]]) @@ -557,9 +555,9 @@ # A real-world test case contributed by a user. def item(i, length): return Item( - artist=u'ben harper', - album=u'burn to shine', - title=u'ben harper - Burn to Shine {0}'.format(i), + artist='ben harper', + album='burn to shine', + title=f'ben harper - Burn to Shine {i}', track=i, length=length, mb_trackid='', mb_albumid='', mb_artistid='', @@ -582,18 +580,18 @@ return TrackInfo(title=title, length=length, index=index) trackinfo = [] - trackinfo.append(info(1, u'Alone', 238.893)) - trackinfo.append(info(2, u'The Woman in You', 341.44)) - trackinfo.append(info(3, u'Less', 245.59999999999999)) - trackinfo.append(info(4, u'Two Hands of a Prayer', 470.49299999999999)) - trackinfo.append(info(5, u'Please Bleed', 277.86599999999999)) - trackinfo.append(info(6, u'Suzie Blue', 269.30599999999998)) - trackinfo.append(info(7, u'Steal My Kisses', 245.36000000000001)) - trackinfo.append(info(8, u'Burn to Shine', 214.90600000000001)) - trackinfo.append(info(9, u'Show Me a Little Shame', 224.0929999999999)) - trackinfo.append(info(10, u'Forgiven', 317.19999999999999)) - trackinfo.append(info(11, u'Beloved One', 243.733)) - trackinfo.append(info(12, u'In the Lord\'s Arms', 186.13300000000001)) + trackinfo.append(info(1, 'Alone', 238.893)) + trackinfo.append(info(2, 'The Woman in You', 341.44)) + trackinfo.append(info(3, 'Less', 245.59999999999999)) + trackinfo.append(info(4, 'Two Hands of a Prayer', 470.49299999999999)) + trackinfo.append(info(5, 'Please Bleed', 277.86599999999999)) + trackinfo.append(info(6, 'Suzie Blue', 269.30599999999998)) + trackinfo.append(info(7, 'Steal My Kisses', 245.36000000000001)) + trackinfo.append(info(8, 'Burn to Shine', 214.90600000000001)) + trackinfo.append(info(9, 'Show Me a Little Shame', 224.0929999999999)) + trackinfo.append(info(10, 'Forgiven', 317.19999999999999)) + trackinfo.append(info(11, 'Beloved One', 243.733)) + trackinfo.append(info(12, 'In the Lord\'s Arms', 186.13300000000001)) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) @@ -603,7 +601,7 @@ self.assertEqual(items.index(item), trackinfo.index(info)) -class ApplyTestUtil(object): +class ApplyTestUtil: def _apply(self, info=None, per_disc_numbering=False, artist_credit=False): info = info or self.info mapping = {} @@ -616,15 +614,15 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def setUp(self): - super(ApplyTest, self).setUp() + super().setUp() self.items = [] self.items.append(Item({})) self.items.append(Item({})) trackinfo = [] trackinfo.append(TrackInfo( - title=u'oneNew', - track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + title='oneNew', + track_id='dfa939ec-118c-4d0f-84a0-60f3d1e6522c', medium=1, medium_index=1, medium_total=1, @@ -633,8 +631,8 @@ artist_sort='trackArtistSort', )) trackinfo.append(TrackInfo( - title=u'twoNew', - track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', + title='twoNew', + track_id='40130ed1-a27c-42fd-a328-1ebefb6caef4', medium=2, medium_index=1, index=2, @@ -642,13 +640,13 @@ )) self.info = AlbumInfo( tracks=trackinfo, - artist=u'artistNew', - album=u'albumNew', + artist='artistNew', + album='albumNew', album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', - artist_credit=u'albumArtistCredit', - artist_sort=u'albumArtistSort', - albumtype=u'album', + artist_credit='albumArtistCredit', + artist_sort='albumArtistSort', + albumtype='album', va=False, mediums=2, ) @@ -806,33 +804,33 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil): def setUp(self): - super(ApplyCompilationTest, self).setUp() + super().setUp() self.items = [] self.items.append(Item({})) self.items.append(Item({})) trackinfo = [] trackinfo.append(TrackInfo( - title=u'oneNew', - track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', - artist=u'artistOneNew', - artist_id=u'a05686fc-9db2-4c23-b99e-77f5db3e5282', + title='oneNew', + track_id='dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + artist='artistOneNew', + artist_id='a05686fc-9db2-4c23-b99e-77f5db3e5282', index=1, )) trackinfo.append(TrackInfo( - title=u'twoNew', - track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', - artist=u'artistTwoNew', - artist_id=u'80b3cf5e-18fe-4c59-98c7-e5bb87210710', + title='twoNew', + track_id='40130ed1-a27c-42fd-a328-1ebefb6caef4', + artist='artistTwoNew', + artist_id='80b3cf5e-18fe-4c59-98c7-e5bb87210710', index=2, )) self.info = AlbumInfo( tracks=trackinfo, - artist=u'variousNew', - album=u'albumNew', + artist='variousNew', + album='albumNew', album_id='3b69ea40-39b8-487f-8818-04b6eff8c21a', artist_id='89ad4ac3-39f7-470e-963a-56509c546377', - albumtype=u'compilation', + albumtype='compilation', ) def test_album_and_track_artists_separate(self): @@ -868,77 +866,77 @@ class StringDistanceTest(unittest.TestCase): def test_equal_strings(self): - dist = string_dist(u'Some String', u'Some String') + dist = string_dist('Some String', 'Some String') self.assertEqual(dist, 0.0) def test_different_strings(self): - dist = string_dist(u'Some String', u'Totally Different') + dist = string_dist('Some String', 'Totally Different') self.assertNotEqual(dist, 0.0) def test_punctuation_ignored(self): - dist = string_dist(u'Some String', u'Some.String!') + dist = string_dist('Some String', 'Some.String!') self.assertEqual(dist, 0.0) def test_case_ignored(self): - dist = string_dist(u'Some String', u'sOME sTring') + dist = string_dist('Some String', 'sOME sTring') self.assertEqual(dist, 0.0) def test_leading_the_has_lower_weight(self): - dist1 = string_dist(u'XXX Band Name', u'Band Name') - dist2 = string_dist(u'The Band Name', u'Band Name') + dist1 = string_dist('XXX Band Name', 'Band Name') + dist2 = string_dist('The Band Name', 'Band Name') self.assertTrue(dist2 < dist1) def test_parens_have_lower_weight(self): - dist1 = string_dist(u'One .Two.', u'One') - dist2 = string_dist(u'One (Two)', u'One') + dist1 = string_dist('One .Two.', 'One') + dist2 = string_dist('One (Two)', 'One') self.assertTrue(dist2 < dist1) def test_brackets_have_lower_weight(self): - dist1 = string_dist(u'One .Two.', u'One') - dist2 = string_dist(u'One [Two]', u'One') + dist1 = string_dist('One .Two.', 'One') + dist2 = string_dist('One [Two]', 'One') self.assertTrue(dist2 < dist1) def test_ep_label_has_zero_weight(self): - dist = string_dist(u'My Song (EP)', u'My Song') + dist = string_dist('My Song (EP)', 'My Song') self.assertEqual(dist, 0.0) def test_featured_has_lower_weight(self): - dist1 = string_dist(u'My Song blah Someone', u'My Song') - dist2 = string_dist(u'My Song feat Someone', u'My Song') + dist1 = string_dist('My Song blah Someone', 'My Song') + dist2 = string_dist('My Song feat Someone', 'My Song') self.assertTrue(dist2 < dist1) def test_postfix_the(self): - dist = string_dist(u'The Song Title', u'Song Title, The') + dist = string_dist('The Song Title', 'Song Title, The') self.assertEqual(dist, 0.0) def test_postfix_a(self): - dist = string_dist(u'A Song Title', u'Song Title, A') + dist = string_dist('A Song Title', 'Song Title, A') self.assertEqual(dist, 0.0) def test_postfix_an(self): - dist = string_dist(u'An Album Title', u'Album Title, An') + dist = string_dist('An Album Title', 'Album Title, An') self.assertEqual(dist, 0.0) def test_empty_strings(self): - dist = string_dist(u'', u'') + dist = string_dist('', '') self.assertEqual(dist, 0.0) def test_solo_pattern(self): # Just make sure these don't crash. - string_dist(u'The ', u'') - string_dist(u'(EP)', u'(EP)') - string_dist(u', An', u'') + string_dist('The ', '') + string_dist('(EP)', '(EP)') + string_dist(', An', '') def test_heuristic_does_not_harm_distance(self): - dist = string_dist(u'Untitled', u'[Untitled]') + dist = string_dist('Untitled', '[Untitled]') self.assertEqual(dist, 0.0) def test_ampersand_expansion(self): - dist = string_dist(u'And', u'&') + dist = string_dist('And', '&') self.assertEqual(dist, 0.0) def test_accented_characters(self): - dist = string_dist(u'\xe9\xe1\xf1', u'ean') + dist = string_dist('\xe9\xe1\xf1', 'ean') self.assertEqual(dist, 0.0) diff -Nru beets-1.5.0/test/test_bareasc.py beets-1.6.0/test/test_bareasc.py --- beets-1.5.0/test/test_bareasc.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/test/test_bareasc.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2021, Graham R. Cobb. """Tests for the 'bareasc' plugin.""" -from __future__ import division, absolute_import, print_function import unittest @@ -20,55 +18,55 @@ """Set up test environment for bare ASCII query matching.""" self.setup_beets() self.log = logging.getLogger('beets.web') - self.config['bareasc']['prefix'] = u'#' + self.config['bareasc']['prefix'] = '#' self.load_plugins('bareasc') # Add library elements. Note that self.lib.add overrides any "id=" # and assigns the next free id number. - self.add_item(title=u'with accents', + self.add_item(title='with accents', album_id=2, - artist=u'Antonín Dvořák') - self.add_item(title=u'without accents', - artist=u'Antonín Dvorak') - self.add_item(title=u'with umlaut', + artist='Antonín Dvořák') + self.add_item(title='without accents', + artist='Antonín Dvorak') + self.add_item(title='with umlaut', album_id=2, - artist=u'Brüggen') - self.add_item(title=u'without umlaut or e', - artist=u'Bruggen') - self.add_item(title=u'without umlaut with e', - artist=u'Brueggen') + artist='Brüggen') + self.add_item(title='without umlaut or e', + artist='Bruggen') + self.add_item(title='without umlaut with e', + artist='Brueggen') def test_search_normal_noaccent(self): """Normal search, no accents, not using bare-ASCII match. Finds just the unaccented entry. """ - items = self.lib.items(u'dvorak') + items = self.lib.items('dvorak') self.assertEqual(len(items), 1) - self.assertEqual([items[0].title], [u'without accents']) + self.assertEqual([items[0].title], ['without accents']) def test_search_normal_accent(self): """Normal search, with accents, not using bare-ASCII match. Finds just the accented entry. """ - items = self.lib.items(u'dvořák') + items = self.lib.items('dvořák') self.assertEqual(len(items), 1) - self.assertEqual([items[0].title], [u'with accents']) + self.assertEqual([items[0].title], ['with accents']) def test_search_bareasc_noaccent(self): """Bare-ASCII search, no accents. Finds both entries. """ - items = self.lib.items(u'#dvorak') + items = self.lib.items('#dvorak') self.assertEqual(len(items), 2) self.assertEqual( {items[0].title, items[1].title}, - {u'without accents', u'with accents'} + {'without accents', 'with accents'} ) def test_search_bareasc_accent(self): @@ -76,12 +74,12 @@ Finds both entries. """ - items = self.lib.items(u'#dvořák') + items = self.lib.items('#dvořák') self.assertEqual(len(items), 2) self.assertEqual( {items[0].title, items[1].title}, - {u'without accents', u'with accents'} + {'without accents', 'with accents'} ) def test_search_bareasc_wrong_accent(self): @@ -89,12 +87,12 @@ Finds both entries. """ - items = self.lib.items(u'#dvořäk') + items = self.lib.items('#dvořäk') self.assertEqual(len(items), 2) self.assertEqual( {items[0].title, items[1].title}, - {u'without accents', u'with accents'} + {'without accents', 'with accents'} ) def test_search_bareasc_noumlaut(self): @@ -105,12 +103,12 @@ This is expected behaviour for this simple plugin. """ - items = self.lib.items(u'#Bruggen') + items = self.lib.items('#Bruggen') self.assertEqual(len(items), 2) self.assertEqual( {items[0].title, items[1].title}, - {u'without umlaut or e', u'with umlaut'} + {'without umlaut or e', 'with umlaut'} ) def test_search_bareasc_umlaut(self): @@ -121,12 +119,12 @@ This is expected behaviour for this simple plugin. """ - items = self.lib.items(u'#Brüggen') + items = self.lib.items('#Brüggen') self.assertEqual(len(items), 2) self.assertEqual( {items[0].title, items[1].title}, - {u'without umlaut or e', u'with umlaut'} + {'without umlaut or e', 'with umlaut'} ) def test_bareasc_list_output(self): diff -Nru beets-1.5.0/test/test_beatport.py beets-1.6.0/test/test_beatport.py --- beets-1.5.0/test/test_beatport.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_beatport.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,12 +14,10 @@ """Tests for the 'beatport' plugin. """ -from __future__ import division, absolute_import, print_function import unittest from test import _common from test.helper import TestHelper -import six from datetime import timedelta from beetsplug import beatport @@ -543,7 +540,7 @@ for track, test_track, id in zip(self.tracks, self.test_tracks, ids): self.assertEqual( track.url, 'https://beatport.com/track/' + - test_track.url + '/' + six.text_type(id)) + test_track.url + '/' + str(id)) def test_bpm_applied(self): for track, test_track in zip(self.tracks, self.test_tracks): diff -Nru beets-1.5.0/test/test_bucket.py beets-1.6.0/test/test_bucket.py --- beets-1.5.0/test/test_bucket.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_bucket.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte. # @@ -15,7 +14,6 @@ """Tests for the 'bucket' plugin.""" -from __future__ import division, absolute_import, print_function import unittest from beetsplug import bucket diff -Nru beets-1.5.0/test/test_config_command.py beets-1.6.0/test/test_config_command.py --- beets-1.5.0/test/test_config_command.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_config_command.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,10 +1,6 @@ -# -*- coding: utf-8 -*- - -from __future__ import division, absolute_import, print_function - import os import yaml -from mock import patch +from unittest.mock import patch from tempfile import mkdtemp from shutil import rmtree import unittest @@ -14,7 +10,6 @@ from test.helper import TestHelper from beets.library import Library -import six class ConfigCommandTest(unittest.TestCase, TestHelper): @@ -116,8 +111,8 @@ execlp.side_effect = OSError('here is problem') self.run_command('config', '-e') self.assertIn('Could not edit configuration', - six.text_type(user_error.exception)) - self.assertIn('here is problem', six.text_type(user_error.exception)) + str(user_error.exception)) + self.assertIn('here is problem', str(user_error.exception)) def test_edit_invalid_config_file(self): with open(self.config_path, 'w') as file: diff -Nru beets-1.5.0/test/test_convert.py beets-1.6.0/test/test_convert.py --- beets-1.5.0/test/test_convert.py 2020-08-10 22:29:53.000000000 +0000 +++ beets-1.6.0/test/test_convert.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import fnmatch import sys @@ -30,12 +28,8 @@ def shell_quote(text): - if sys.version_info[0] < 3: - import pipes - return pipes.quote(text) - else: - import shlex - return shlex.quote(text) + import shlex + return shlex.quote(text) class TestHelper(helper.TestHelper): @@ -45,13 +39,13 @@ `tag` to the copy. """ if re.search('[^a-zA-Z0-9]', tag): - raise ValueError(u"tag '{0}' must only contain letters and digits" + raise ValueError("tag '{}' must only contain letters and digits" .format(tag)) # A Python script that copies the file and appends a tag. stub = os.path.join(_common.RSRC, b'convert_stub.py').decode('utf-8') - return u"{} {} $source $dest {}".format(shell_quote(sys.executable), - shell_quote(stub), tag) + return "{} {} $source $dest {}".format(shell_quote(sys.executable), + shell_quote(stub), tag) def assertFileTag(self, path, tag): # noqa """Assert that the path is a file and the files content ends with `tag`. @@ -59,12 +53,12 @@ display_tag = tag tag = tag.encode('utf-8') self.assertTrue(os.path.isfile(path), - u'{0} is not a file'.format( + '{} is not a file'.format( util.displayable_path(path))) with open(path, 'rb') as f: f.seek(-len(display_tag), os.SEEK_END) self.assertEqual(f.read(), tag, - u'{0} is not tagged with {1}' + '{} is not tagged with {}' .format( util.displayable_path(path), display_tag)) @@ -76,12 +70,12 @@ display_tag = tag tag = tag.encode('utf-8') self.assertTrue(os.path.isfile(path), - u'{0} is not a file'.format( + '{} is not a file'.format( util.displayable_path(path))) with open(path, 'rb') as f: f.seek(-len(tag), os.SEEK_END) self.assertNotEqual(f.read(), tag, - u'{0} is unexpectedly tagged with {1}' + '{} is unexpectedly tagged with {}' .format( util.displayable_path(path), display_tag)) @@ -113,9 +107,10 @@ item = self.lib.items().get() self.assertFileTag(item.path, 'convert') + @unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows def test_import_original_on_convert_error(self): # `false` exits with non-zero code - self.config['convert']['command'] = u'false' + self.config['convert']['command'] = 'false' self.importer.run() item = self.lib.items().get() @@ -128,14 +123,15 @@ for path in self.importer.paths: for root, dirnames, filenames in os.walk(path): self.assertTrue(len(fnmatch.filter(filenames, '*.mp3')) == 0, - u'Non-empty import directory {0}' + 'Non-empty import directory {}' .format(util.displayable_path(path))) -class ConvertCommand(object): +class ConvertCommand: """A mixin providing a utility method to run the `convert`command in tests. """ + def run_convert_path(self, path, *args): """Run the `convert` command on a given path.""" # The path is currently a filesystem bytestring. Convert it to @@ -229,7 +225,7 @@ converted = os.path.join(self.convert_dest, b'converted.mp3') self.touch(converted, content='XXX') self.run_convert('--yes') - with open(converted, 'r') as f: + with open(converted) as f: self.assertEqual(f.read(), 'XXX') def test_pretend(self): @@ -240,7 +236,7 @@ def test_empty_query(self): with capture_log('beets.convert') as logs: self.run_convert('An impossible query') - self.assertEqual(logs[0], u'convert: Empty query result.') + self.assertEqual(logs[0], 'convert: Empty query result.') @_common.slow_test() diff -Nru beets-1.5.0/test/test_datequery.py beets-1.6.0/test/test_datequery.py --- beets-1.5.0/test/test_datequery.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_datequery.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """Test for dbcore's date-based queries. """ -from __future__ import division, absolute_import, print_function from test import _common from datetime import datetime, timedelta @@ -135,7 +133,7 @@ class DateQueryTest(_common.LibTestCase): def setUp(self): - super(DateQueryTest, self).setUp() + super().setUp() self.i.added = _parsetime('2013-03-30 22:21') self.i.store() @@ -170,7 +168,7 @@ class DateQueryTestRelative(_common.LibTestCase): def setUp(self): - super(DateQueryTestRelative, self).setUp() + super().setUp() # We pick a date near a month changeover, which can reveal some time # zone bugs. @@ -213,7 +211,7 @@ class DateQueryTestRelativeMore(_common.LibTestCase): def setUp(self): - super(DateQueryTestRelativeMore, self).setUp() + super().setUp() self.i.added = _parsetime(datetime.now().strftime('%Y-%m-%d %H:%M')) self.i.store() diff -Nru beets-1.5.0/test/test_dbcore.py beets-1.6.0/test/test_dbcore.py --- beets-1.5.0/test/test_dbcore.py 2021-03-08 00:47:22.000000000 +0000 +++ beets-1.6.0/test/test_dbcore.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,18 +14,15 @@ """Tests for the DBCore database abstraction. """ -from __future__ import division, absolute_import, print_function import os import shutil import sqlite3 import unittest -from six import assertRaisesRegex from test import _common from beets import dbcore from tempfile import mkstemp -import six # Fixture: concrete database and model classes. For migration tests, we @@ -236,7 +232,7 @@ old_rev = self.db.revision with self.db.transaction() as tx: tx.mutate( - 'INSERT INTO {0} ' + 'INSERT INTO {} ' '(field_one) ' 'VALUES (?);'.format(ModelFixture1._table), (111,), @@ -385,9 +381,9 @@ self.assertNotIn('flex_field', model2) def test_check_db_fails(self): - with assertRaisesRegex(self, ValueError, 'no database'): + with self.assertRaisesRegex(ValueError, 'no database'): dbcore.Model()._check_db() - with assertRaisesRegex(self, ValueError, 'no id'): + with self.assertRaisesRegex(ValueError, 'no id'): ModelFixture1(self.db)._check_db() dbcore.Model(self.db)._check_db(need_id=False) @@ -399,7 +395,7 @@ def test_computed_field(self): model = ModelFixtureWithGetters() self.assertEqual(model.aComputedField, 'thing') - with assertRaisesRegex(self, KeyError, u'computed field .+ deleted'): + with self.assertRaisesRegex(KeyError, 'computed field .+ deleted'): del model.aComputedField def test_items(self): @@ -415,7 +411,7 @@ model._db def test_parse_nonstring(self): - with assertRaisesRegex(self, TypeError, u"must be a string"): + with self.assertRaisesRegex(TypeError, "must be a string"): dbcore.Model._parse(None, 42) @@ -424,7 +420,7 @@ model = ModelFixture1() model.field_one = 155 value = model.formatted().get('field_one') - self.assertEqual(value, u'155') + self.assertEqual(value, '155') def test_format_fixed_field_integer_normalized(self): """The normalize method of the Integer class rounds floats @@ -432,41 +428,41 @@ model = ModelFixture1() model.field_one = 142.432 value = model.formatted().get('field_one') - self.assertEqual(value, u'142') + self.assertEqual(value, '142') model.field_one = 142.863 value = model.formatted().get('field_one') - self.assertEqual(value, u'143') + self.assertEqual(value, '143') def test_format_fixed_field_string(self): model = ModelFixture1() - model.field_two = u'caf\xe9' + model.field_two = 'caf\xe9' value = model.formatted().get('field_two') - self.assertEqual(value, u'caf\xe9') + self.assertEqual(value, 'caf\xe9') def test_format_flex_field(self): model = ModelFixture1() - model.other_field = u'caf\xe9' + model.other_field = 'caf\xe9' value = model.formatted().get('other_field') - self.assertEqual(value, u'caf\xe9') + self.assertEqual(value, 'caf\xe9') def test_format_flex_field_bytes(self): model = ModelFixture1() - model.other_field = u'caf\xe9'.encode('utf-8') + model.other_field = 'caf\xe9'.encode() value = model.formatted().get('other_field') - self.assertTrue(isinstance(value, six.text_type)) - self.assertEqual(value, u'caf\xe9') + self.assertTrue(isinstance(value, str)) + self.assertEqual(value, 'caf\xe9') def test_format_unset_field(self): model = ModelFixture1() value = model.formatted().get('other_field') - self.assertEqual(value, u'') + self.assertEqual(value, '') def test_format_typed_flex_field(self): model = ModelFixture1() model.some_float_field = 3.14159265358979 value = model.formatted().get('some_float_field') - self.assertEqual(value, u'3.1') + self.assertEqual(value, '3.1') class FormattedMappingTest(unittest.TestCase): @@ -484,7 +480,7 @@ def test_get_method_with_default(self): model = ModelFixture1() formatted = model.formatted() - self.assertEqual(formatted.get('other_field'), u'') + self.assertEqual(formatted.get('other_field'), '') def test_get_method_with_specified_default(self): model = ModelFixture1() @@ -494,18 +490,18 @@ class ParseTest(unittest.TestCase): def test_parse_fixed_field(self): - value = ModelFixture1._parse('field_one', u'2') + value = ModelFixture1._parse('field_one', '2') self.assertIsInstance(value, int) self.assertEqual(value, 2) def test_parse_flex_field(self): - value = ModelFixture1._parse('some_float_field', u'2') + value = ModelFixture1._parse('some_float_field', '2') self.assertIsInstance(value, float) self.assertEqual(value, 2.0) def test_parse_untyped_field(self): - value = ModelFixture1._parse('field_nine', u'2') - self.assertEqual(value, u'2') + value = ModelFixture1._parse('field_nine', '2') + self.assertEqual(value, '2') class QueryParseTest(unittest.TestCase): diff -Nru beets-1.5.0/test/test_discogs.py beets-1.6.0/test/test_discogs.py --- beets-1.5.0/test/test_discogs.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_discogs.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """Tests for discogs plugin. """ -from __future__ import division, absolute_import, print_function import unittest from test import _common @@ -174,16 +172,16 @@ """Test the conversion of discogs `position` to medium, medium_index and subtrack_index.""" # List of tuples (discogs_position, (medium, medium_index, subindex) - positions = [('1', (None, '1', None)), - ('A12', ('A', '12', None)), - ('12-34', ('12-', '34', None)), - ('CD1-1', ('CD1-', '1', None)), - ('1.12', (None, '1', '12')), - ('12.a', (None, '12', 'A')), - ('12.34', (None, '12', '34')), - ('1ab', (None, '1', 'AB')), + positions = [('1', (None, '1', None)), + ('A12', ('A', '12', None)), + ('12-34', ('12-', '34', None)), + ('CD1-1', ('CD1-', '1', None)), + ('1.12', (None, '1', '12')), + ('12.a', (None, '12', 'A')), + ('12.34', (None, '12', '34')), + ('1ab', (None, '1', 'AB')), # Non-standard - ('IV', ('IV', None, None)), + ('IV', ('IV', None, None)), ] d = DiscogsPlugin() @@ -357,9 +355,28 @@ self.assertEqual(d, None) self.assertIn('Release does not contain the required fields', logs[0]) + def test_album_for_id(self): + """Test parsing for a valid Discogs release_id""" + test_patterns = [('http://www.discogs.com/G%C3%BCnther-Lause-Meru-Ep/release/4354798', 4354798), # NOQA E501 + ('http://www.discogs.com/release/4354798-G%C3%BCnther-Lause-Meru-Ep', 4354798), # NOQA E501 + ('http://www.discogs.com/G%C3%BCnther-4354798Lause-Meru-Ep/release/4354798', 4354798), # NOQA E501 + ('http://www.discogs.com/release/4354798-G%C3%BCnther-4354798Lause-Meru-Ep/', 4354798), # NOQA E501 + ('[r4354798]', 4354798), + ('r4354798', 4354798), + ('4354798', 4354798), + ('yet-another-metadata-provider.org/foo/12345', ''), + ('005b84a0-ecd6-39f1-b2f6-6eb48756b268', ''), + ] + for test_pattern, expected in test_patterns: + match = DiscogsPlugin.extract_release_id_regex(test_pattern) + if not match: + match = '' + self.assertEqual(match, expected) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == '__main__': unittest.main(defaultTest='suite') diff -Nru beets-1.5.0/test/test_edit.py beets-1.6.0/test/test_edit.py --- beets-1.5.0/test/test_edit.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_edit.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson and Diego Moreda. # @@ -13,11 +12,10 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import codecs import unittest -from mock import patch +from unittest.mock import patch from test import _common from test.helper import TestHelper, control_stdin from test.test_ui_importer import TerminalImportSessionSetup @@ -27,7 +25,7 @@ from beetsplug.edit import EditPlugin -class ModifyFileMocker(object): +class ModifyFileMocker: """Helper for modifying a file, replacing or editing its contents. Used for mocking the calls to the external editor during testing. """ @@ -71,7 +69,7 @@ f.write(contents) -class EditMixin(object): +class EditMixin: """Helper containing some common functionality used for the Edit tests.""" def assertItemFieldsModified(self, library_items, items, fields=[], # noqa allowed=['path']): @@ -143,33 +141,33 @@ def test_title_edit_discard(self, mock_write): """Edit title for all items in the library, then discard changes.""" # Edit track titles. - self.run_mocked_command({'replacements': {u't\u00eftle': - u'modified t\u00eftle'}}, + self.run_mocked_command({'replacements': {'t\u00eftle': + 'modified t\u00eftle'}}, # Cancel. ['c']) self.assertCounts(mock_write, write_call_count=0, - title_starts_with=u't\u00eftle') + title_starts_with='t\u00eftle') self.assertItemFieldsModified(self.album.items(), self.items_orig, []) def test_title_edit_apply(self, mock_write): """Edit title for all items in the library, then apply changes.""" # Edit track titles. - self.run_mocked_command({'replacements': {u't\u00eftle': - u'modified t\u00eftle'}}, + self.run_mocked_command({'replacements': {'t\u00eftle': + 'modified t\u00eftle'}}, # Apply changes. ['a']) self.assertCounts(mock_write, write_call_count=self.TRACK_COUNT, - title_starts_with=u'modified t\u00eftle') + title_starts_with='modified t\u00eftle') self.assertItemFieldsModified(self.album.items(), self.items_orig, ['title', 'mtime']) def test_single_title_edit_apply(self, mock_write): """Edit title for one item in the library, then apply changes.""" # Edit one track title. - self.run_mocked_command({'replacements': {u't\u00eftle 9': - u'modified t\u00eftle 9'}}, + self.run_mocked_command({'replacements': {'t\u00eftle 9': + 'modified t\u00eftle 9'}}, # Apply changes. ['a']) @@ -178,7 +176,7 @@ self.assertItemFieldsModified(list(self.album.items())[:-1], self.items_orig[:-1], []) self.assertEqual(list(self.album.items())[-1].title, - u'modified t\u00eftle 9') + 'modified t\u00eftle 9') def test_noedit(self, mock_write): """Do not edit anything.""" @@ -188,7 +186,7 @@ []) self.assertCounts(mock_write, write_call_count=0, - title_starts_with=u't\u00eftle') + title_starts_with='t\u00eftle') self.assertItemFieldsModified(self.album.items(), self.items_orig, []) def test_album_edit_apply(self, mock_write): @@ -196,8 +194,8 @@ By design, the album should not be updated."" """ # Edit album. - self.run_mocked_command({'replacements': {u'\u00e4lbum': - u'modified \u00e4lbum'}}, + self.run_mocked_command({'replacements': {'\u00e4lbum': + 'modified \u00e4lbum'}}, # Apply changes. ['a']) @@ -206,50 +204,50 @@ ['album', 'mtime']) # Ensure album is *not* modified. self.album.load() - self.assertEqual(self.album.album, u'\u00e4lbum') + self.assertEqual(self.album.album, '\u00e4lbum') def test_single_edit_add_field(self, mock_write): """Edit the yaml file appending an extra field to the first item, then apply changes.""" # Append "foo: bar" to item with id == 2. ("id: 1" would match both # "id: 1" and "id: 10") - self.run_mocked_command({'replacements': {u"id: 2": - u"id: 2\nfoo: bar"}}, + self.run_mocked_command({'replacements': {"id: 2": + "id: 2\nfoo: bar"}}, # Apply changes. ['a']) - self.assertEqual(self.lib.items(u'id:2')[0].foo, 'bar') + self.assertEqual(self.lib.items('id:2')[0].foo, 'bar') # Even though a flexible attribute was written (which is not directly # written to the tags), write should still be called since templates # might use it. self.assertCounts(mock_write, write_call_count=1, - title_starts_with=u't\u00eftle') + title_starts_with='t\u00eftle') def test_a_album_edit_apply(self, mock_write): """Album query (-a), edit album field, apply changes.""" - self.run_mocked_command({'replacements': {u'\u00e4lbum': - u'modified \u00e4lbum'}}, + self.run_mocked_command({'replacements': {'\u00e4lbum': + 'modified \u00e4lbum'}}, # Apply changes. ['a'], args=['-a']) self.album.load() self.assertCounts(mock_write, write_call_count=self.TRACK_COUNT) - self.assertEqual(self.album.album, u'modified \u00e4lbum') + self.assertEqual(self.album.album, 'modified \u00e4lbum') self.assertItemFieldsModified(self.album.items(), self.items_orig, ['album', 'mtime']) def test_a_albumartist_edit_apply(self, mock_write): """Album query (-a), edit albumartist field, apply changes.""" - self.run_mocked_command({'replacements': {u'album artist': - u'modified album artist'}}, + self.run_mocked_command({'replacements': {'album artist': + 'modified album artist'}}, # Apply changes. ['a'], args=['-a']) self.album.load() self.assertCounts(mock_write, write_call_count=self.TRACK_COUNT) - self.assertEqual(self.album.albumartist, u'the modified album artist') + self.assertEqual(self.album.albumartist, 'the modified album artist') self.assertItemFieldsModified(self.album.items(), self.items_orig, ['albumartist', 'mtime']) @@ -262,18 +260,18 @@ ['n']) self.assertCounts(mock_write, write_call_count=0, - title_starts_with=u't\u00eftle') + title_starts_with='t\u00eftle') def test_invalid_yaml(self, mock_write): """Edit the yaml file incorrectly (resulting in a well-formed but invalid yaml document).""" # Edit the yaml file to an invalid but parseable file. - self.run_mocked_command({'contents': u'wellformed: yes, but invalid'}, + self.run_mocked_command({'contents': 'wellformed: yes, but invalid'}, # No stdin. []) self.assertCounts(mock_write, write_call_count=0, - title_starts_with=u't\u00eftle') + title_starts_with='t\u00eftle') @_common.slow_test() @@ -305,8 +303,8 @@ """ self._setup_import_session() # Edit track titles. - self.run_mocked_interpreter({'replacements': {u'Tag Title': - u'Edited Title'}}, + self.run_mocked_interpreter({'replacements': {'Tag Title': + 'Edited Title'}}, # eDit, Apply changes. ['d', 'a']) @@ -319,7 +317,7 @@ for i in self.lib.items())) # Ensure album is *not* fetched from a candidate. - self.assertEqual(self.lib.albums()[0].mb_albumid, u'') + self.assertEqual(self.lib.albums()[0].mb_albumid, '') def test_edit_discard_asis(self): """Edit the album field for all items in the library, discard changes, @@ -327,8 +325,8 @@ """ self._setup_import_session() # Edit track titles. - self.run_mocked_interpreter({'replacements': {u'Tag Title': - u'Edited Title'}}, + self.run_mocked_interpreter({'replacements': {'Tag Title': + 'Edited Title'}}, # eDit, Cancel, Use as-is. ['d', 'c', 'u']) @@ -341,7 +339,7 @@ for i in self.lib.items())) # Ensure album is *not* fetched from a candidate. - self.assertEqual(self.lib.albums()[0].mb_albumid, u'') + self.assertEqual(self.lib.albums()[0].mb_albumid, '') def test_edit_apply_candidate(self): """Edit the album field for all items in the library, apply changes, @@ -349,8 +347,8 @@ """ self._setup_import_session() # Edit track titles. - self.run_mocked_interpreter({'replacements': {u'Applied Title': - u'Edited Title'}}, + self.run_mocked_interpreter({'replacements': {'Applied Title': + 'Edited Title'}}, # edit Candidates, 1, Apply changes. ['c', '1', 'a']) @@ -377,8 +375,8 @@ # ids but not the db connections. self.importer.paths = [] self.importer.query = TrueQuery() - self.run_mocked_interpreter({'replacements': {u'Applied Title': - u'Edited Title'}}, + self.run_mocked_interpreter({'replacements': {'Applied Title': + 'Edited Title'}}, # eDit, Apply changes. ['d', 'a']) @@ -398,8 +396,8 @@ """ self._setup_import_session() # Edit track titles. - self.run_mocked_interpreter({'replacements': {u'Applied Title': - u'Edited Title'}}, + self.run_mocked_interpreter({'replacements': {'Applied Title': + 'Edited Title'}}, # edit Candidates, 1, Apply changes. ['c', '1', 'a']) @@ -419,8 +417,8 @@ """ self._setup_import_session(singletons=True) # Edit track titles. - self.run_mocked_interpreter({'replacements': {u'Tag Title': - u'Edited Title'}}, + self.run_mocked_interpreter({'replacements': {'Tag Title': + 'Edited Title'}}, # eDit, Apply changes, aBort. ['d', 'a', 'b']) @@ -438,8 +436,8 @@ """ self._setup_import_session() # Edit track titles. - self.run_mocked_interpreter({'replacements': {u'Applied Title': - u'Edited Title'}}, + self.run_mocked_interpreter({'replacements': {'Applied Title': + 'Edited Title'}}, # edit Candidates, 1, Apply changes, aBort. ['c', '1', 'a', 'b']) diff -Nru beets-1.5.0/test/test_embedart.py beets-1.6.0/test/test_embedart.py --- beets-1.5.0/test/test_embedart.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_embedart.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,11 +12,10 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import os.path import shutil -from mock import patch, MagicMock +from unittest.mock import patch, MagicMock import tempfile import unittest @@ -51,7 +49,7 @@ abbey_differentpath = os.path.join(_common.RSRC, b'abbey-different.jpg') def setUp(self): - super(EmbedartCliTest, self).setUp() + super().setUp() self.io.install() self.setup_beets() # Converter is threaded self.load_plugins('embedart') @@ -121,7 +119,7 @@ if os.path.isfile(tmp_path): os.remove(tmp_path) - self.fail(u'Artwork file {0} was not deleted'.format(tmp_path)) + self.fail(f'Artwork file {tmp_path} was not deleted') def test_art_file_missing(self): self.add_album_fixture() @@ -156,7 +154,7 @@ mediafile = MediaFile(syspath(item.path)) self.assertEqual(mediafile.images[0].data, self.image_data, - u'Image written is not {0}'.format( + 'Image written is not {}'.format( displayable_path(self.abbey_artpath))) @require_artresizer_compare @@ -170,7 +168,7 @@ mediafile = MediaFile(syspath(item.path)) self.assertEqual(mediafile.images[0].data, self.image_data, - u'Image written is not {0}'.format( + 'Image written is not {}'.format( displayable_path(self.abbey_similarpath))) def test_non_ascii_album_path(self): diff -Nru beets-1.5.0/test/test_embyupdate.py beets-1.6.0/test/test_embyupdate.py --- beets-1.5.0/test/test_embyupdate.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_embyupdate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- - -from __future__ import division, absolute_import, print_function - from test.helper import TestHelper from beetsplug import embyupdate import unittest @@ -14,10 +10,10 @@ self.load_plugins('embyupdate') self.config['emby'] = { - u'host': u'localhost', - u'port': 8096, - u'username': u'username', - u'password': u'password' + 'host': 'localhost', + 'port': 8096, + 'username': 'username', + 'password': 'password' } def tearDown(self): @@ -34,7 +30,7 @@ def test_api_url_http(self): self.assertEqual( - embyupdate.api_url(u'http://localhost', + embyupdate.api_url('http://localhost', self.config['emby']['port'].get(), '/Library/Refresh'), 'http://localhost:8096/Library/Refresh?format=json' @@ -42,7 +38,7 @@ def test_api_url_https(self): self.assertEqual( - embyupdate.api_url(u'https://localhost', + embyupdate.api_url('https://localhost', self.config['emby']['port'].get(), '/Library/Refresh'), 'https://localhost:8096/Library/Refresh?format=json' diff -Nru beets-1.5.0/test/test_export.py beets-1.6.0/test/test_export.py --- beets-1.5.0/test/test_export.py 2020-10-18 10:58:20.000000000 +0000 +++ beets-1.6.0/test/test_export.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2019, Carl Suster # @@ -16,7 +15,6 @@ """Test the beets.export utilities associated with the export plugin. """ -from __future__ import division, absolute_import, print_function import unittest from test.helper import TestHelper diff -Nru beets-1.5.0/test/test_fetchart.py beets-1.6.0/test/test_fetchart.py --- beets-1.5.0/test/test_fetchart.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_fetchart.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import ctypes import os @@ -39,7 +37,7 @@ def check_cover_is_stored(self): self.assertEqual(self.album['artpath'], self.cover_path) - with open(util.syspath(self.cover_path), 'r') as f: + with open(util.syspath(self.cover_path)) as f: self.assertEqual(f.read(), 'IMAGE') def hide_file_windows(self): diff -Nru beets-1.5.0/test/test_filefilter.py beets-1.6.0/test/test_filefilter.py --- beets-1.5.0/test/test_filefilter.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_filefilter.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Malte Ried. # @@ -16,7 +15,6 @@ """Tests for the `filefilter` plugin. """ -from __future__ import division, absolute_import, print_function import os import shutil diff -Nru beets-1.5.0/test/test_files.py beets-1.6.0/test/test_files.py --- beets-1.5.0/test/test_files.py 2020-12-15 12:48:01.000000000 +0000 +++ beets-1.6.0/test/test_files.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """Test file manipulation functionality of Item. """ -from __future__ import division, absolute_import, print_function import shutil import os @@ -32,7 +30,7 @@ class MoveTest(_common.TestCase): def setUp(self): - super(MoveTest, self).setUp() + super().setUp() # make a temporary file self.path = join(self.temp_dir, b'temp.mp3') @@ -73,7 +71,7 @@ old_path = self.i.path self.assertExists(old_path) - self.i.artist = u'newArtist' + self.i.artist = 'newArtist' self.i.move() self.assertNotExists(old_path) self.assertNotExists(os.path.dirname(old_path)) @@ -121,20 +119,20 @@ self.assertEqual(self.i.path, old_path) def test_move_file_with_colon(self): - self.i.artist = u'C:DOS' + self.i.artist = 'C:DOS' self.i.move() self.assertIn('C_DOS', self.i.path.decode()) def test_move_file_with_multiple_colons(self): print(beets.config['replace']) - self.i.artist = u'COM:DOS' + self.i.artist = 'COM:DOS' self.i.move() self.assertIn('COM_DOS', self.i.path.decode()) def test_move_file_with_colon_alt_separator(self): old = beets.config['drive_sep_replace'] beets.config["drive_sep_replace"] = '0' - self.i.artist = u'C:DOS' + self.i.artist = 'C:DOS' self.i.move() self.assertIn('C0DOS', self.i.path.decode()) beets.config["drive_sep_replace"] = old @@ -240,7 +238,7 @@ class AlbumFileTest(_common.TestCase): def setUp(self): - super(AlbumFileTest, self).setUp() + super().setUp() # Make library and item. self.lib = beets.library.Library(':memory:') @@ -259,7 +257,7 @@ self.otherdir = os.path.join(self.temp_dir, b'testotherdir') def test_albuminfo_move_changes_paths(self): - self.ai.album = u'newAlbumName' + self.ai.album = 'newAlbumName' self.ai.move() self.ai.store() self.i.load() @@ -268,7 +266,7 @@ def test_albuminfo_move_moves_file(self): oldpath = self.i.path - self.ai.album = u'newAlbumName' + self.ai.album = 'newAlbumName' self.ai.move() self.ai.store() self.i.load() @@ -278,7 +276,7 @@ def test_albuminfo_move_copies_file(self): oldpath = self.i.path - self.ai.album = u'newAlbumName' + self.ai.album = 'newAlbumName' self.ai.move(operation=MoveOperation.COPY) self.ai.store() self.i.load() @@ -289,7 +287,7 @@ @unittest.skipUnless(_common.HAVE_REFLINK, "need reflink") def test_albuminfo_move_reflinks_file(self): oldpath = self.i.path - self.ai.album = u'newAlbumName' + self.ai.album = 'newAlbumName' self.ai.move(operation=MoveOperation.REFLINK) self.ai.store() self.i.load() @@ -306,7 +304,7 @@ class ArtFileTest(_common.TestCase): def setUp(self): - super(ArtFileTest, self).setUp() + super().setUp() # Make library and item. self.lib = beets.library.Library(':memory:') @@ -335,7 +333,7 @@ def test_art_moves_with_album(self): self.assertTrue(os.path.exists(self.art)) oldpath = self.i.path - self.ai.album = u'newAlbum' + self.ai.album = 'newAlbum' self.ai.move() self.i.load() @@ -363,7 +361,7 @@ touch(newart) i2 = item() i2.path = self.i.path - i2.artist = u'someArtist' + i2.artist = 'someArtist' ai = self.lib.add_album((i2,)) i2.move(operation=MoveOperation.COPY) @@ -379,7 +377,7 @@ touch(newart) i2 = item() i2.path = self.i.path - i2.artist = u'someArtist' + i2.artist = 'someArtist' ai = self.lib.add_album((i2,)) i2.move(operation=MoveOperation.COPY) ai.set_art(newart) @@ -393,7 +391,7 @@ touch(newart) i2 = item() i2.path = self.i.path - i2.artist = u'someArtist' + i2.artist = 'someArtist' ai = self.lib.add_album((i2,)) i2.move(operation=MoveOperation.COPY) @@ -410,7 +408,7 @@ touch(newart) i2 = item() i2.path = self.i.path - i2.artist = u'someArtist' + i2.artist = 'someArtist' ai = self.lib.add_album((i2,)) i2.move(operation=MoveOperation.COPY) @@ -434,7 +432,7 @@ try: i2 = item() i2.path = self.i.path - i2.artist = u'someArtist' + i2.artist = 'someArtist' ai = self.lib.add_album((i2,)) i2.move(operation=MoveOperation.COPY) ai.set_art(newart) @@ -452,7 +450,7 @@ oldartpath = self.lib.albums()[0].artpath self.assertExists(oldartpath) - self.ai.album = u'different_album' + self.ai.album = 'different_album' self.ai.store() self.ai.items()[0].move() @@ -469,7 +467,7 @@ oldartpath = self.lib.albums()[0].artpath self.assertExists(oldartpath) - self.i.album = u'different_album' + self.i.album = 'different_album' self.i.album_id = None # detach from album self.i.move() @@ -481,7 +479,7 @@ class RemoveTest(_common.TestCase): def setUp(self): - super(RemoveTest, self).setUp() + super().setUp() # Make library and item. self.lib = beets.library.Library(':memory:') @@ -542,7 +540,7 @@ # Tests that we can "delete" nonexistent files. class SoftRemoveTest(_common.TestCase): def setUp(self): - super(SoftRemoveTest, self).setUp() + super().setUp() self.path = os.path.join(self.temp_dir, b'testfile') touch(self.path) @@ -555,12 +553,12 @@ try: util.remove(self.path + b'XXX', True) except OSError: - self.fail(u'OSError when removing path') + self.fail('OSError when removing path') class SafeMoveCopyTest(_common.TestCase): def setUp(self): - super(SafeMoveCopyTest, self).setUp() + super().setUp() self.path = os.path.join(self.temp_dir, b'testfile') touch(self.path) @@ -608,7 +606,7 @@ class PruneTest(_common.TestCase): def setUp(self): - super(PruneTest, self).setUp() + super().setUp() self.base = os.path.join(self.temp_dir, b'testdir') os.mkdir(self.base) @@ -628,7 +626,7 @@ class WalkTest(_common.TestCase): def setUp(self): - super(WalkTest, self).setUp() + super().setUp() self.base = os.path.join(self.temp_dir, b'testdir') os.mkdir(self.base) @@ -668,7 +666,7 @@ class UniquePathTest(_common.TestCase): def setUp(self): - super(UniquePathTest, self).setUp() + super().setUp() self.base = os.path.join(self.temp_dir, b'testdir') os.mkdir(self.base) diff -Nru beets-1.5.0/test/test_ftintitle.py beets-1.6.0/test/test_ftintitle.py --- beets-1.5.0/test/test_ftintitle.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_ftintitle.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte. # @@ -15,7 +14,6 @@ """Tests for the 'ftintitle' plugin.""" -from __future__ import division, absolute_import, print_function import unittest from test.helper import TestHelper @@ -45,41 +43,41 @@ self.config['ftintitle']['auto'] = auto def test_functional_drop(self): - item = self._ft_add_item('/', u'Alice ft Bob', u'Song 1', u'Alice') + item = self._ft_add_item('/', 'Alice ft Bob', 'Song 1', 'Alice') self.run_command('ftintitle', '-d') item.load() - self.assertEqual(item['artist'], u'Alice') - self.assertEqual(item['title'], u'Song 1') + self.assertEqual(item['artist'], 'Alice') + self.assertEqual(item['title'], 'Song 1') def test_functional_not_found(self): - item = self._ft_add_item('/', u'Alice ft Bob', u'Song 1', u'George') + item = self._ft_add_item('/', 'Alice ft Bob', 'Song 1', 'George') self.run_command('ftintitle', '-d') item.load() # item should be unchanged - self.assertEqual(item['artist'], u'Alice ft Bob') - self.assertEqual(item['title'], u'Song 1') + self.assertEqual(item['artist'], 'Alice ft Bob') + self.assertEqual(item['title'], 'Song 1') def test_functional_custom_format(self): self._ft_set_config('feat. {0}') - item = self._ft_add_item('/', u'Alice ft Bob', u'Song 1', u'Alice') + item = self._ft_add_item('/', 'Alice ft Bob', 'Song 1', 'Alice') self.run_command('ftintitle') item.load() - self.assertEqual(item['artist'], u'Alice') - self.assertEqual(item['title'], u'Song 1 feat. Bob') + self.assertEqual(item['artist'], 'Alice') + self.assertEqual(item['title'], 'Song 1 feat. Bob') self._ft_set_config('featuring {0}') - item = self._ft_add_item('/', u'Alice feat. Bob', u'Song 1', u'Alice') + item = self._ft_add_item('/', 'Alice feat. Bob', 'Song 1', 'Alice') self.run_command('ftintitle') item.load() - self.assertEqual(item['artist'], u'Alice') - self.assertEqual(item['title'], u'Song 1 featuring Bob') + self.assertEqual(item['artist'], 'Alice') + self.assertEqual(item['title'], 'Song 1 featuring Bob') self._ft_set_config('with {0}') - item = self._ft_add_item('/', u'Alice feat Bob', u'Song 1', u'Alice') + item = self._ft_add_item('/', 'Alice feat Bob', 'Song 1', 'Alice') self.run_command('ftintitle') item.load() - self.assertEqual(item['artist'], u'Alice') - self.assertEqual(item['title'], u'Song 1 with Bob') + self.assertEqual(item['artist'], 'Alice') + self.assertEqual(item['title'], 'Song 1 with Bob') class FtInTitlePluginTest(unittest.TestCase): @@ -149,33 +147,33 @@ self.assertEqual(feat_part, test_case['feat_part']) def test_split_on_feat(self): - parts = ftintitle.split_on_feat(u'Alice ft. Bob') - self.assertEqual(parts, (u'Alice', u'Bob')) - parts = ftintitle.split_on_feat(u'Alice feat Bob') - self.assertEqual(parts, (u'Alice', u'Bob')) - parts = ftintitle.split_on_feat(u'Alice feat. Bob') - self.assertEqual(parts, (u'Alice', u'Bob')) - parts = ftintitle.split_on_feat(u'Alice featuring Bob') - self.assertEqual(parts, (u'Alice', u'Bob')) - parts = ftintitle.split_on_feat(u'Alice & Bob') - self.assertEqual(parts, (u'Alice', u'Bob')) - parts = ftintitle.split_on_feat(u'Alice and Bob') - self.assertEqual(parts, (u'Alice', u'Bob')) - parts = ftintitle.split_on_feat(u'Alice With Bob') - self.assertEqual(parts, (u'Alice', u'Bob')) - parts = ftintitle.split_on_feat(u'Alice defeat Bob') - self.assertEqual(parts, (u'Alice defeat Bob', None)) + parts = ftintitle.split_on_feat('Alice ft. Bob') + self.assertEqual(parts, ('Alice', 'Bob')) + parts = ftintitle.split_on_feat('Alice feat Bob') + self.assertEqual(parts, ('Alice', 'Bob')) + parts = ftintitle.split_on_feat('Alice feat. Bob') + self.assertEqual(parts, ('Alice', 'Bob')) + parts = ftintitle.split_on_feat('Alice featuring Bob') + self.assertEqual(parts, ('Alice', 'Bob')) + parts = ftintitle.split_on_feat('Alice & Bob') + self.assertEqual(parts, ('Alice', 'Bob')) + parts = ftintitle.split_on_feat('Alice and Bob') + self.assertEqual(parts, ('Alice', 'Bob')) + parts = ftintitle.split_on_feat('Alice With Bob') + self.assertEqual(parts, ('Alice', 'Bob')) + parts = ftintitle.split_on_feat('Alice defeat Bob') + self.assertEqual(parts, ('Alice defeat Bob', None)) def test_contains_feat(self): - self.assertTrue(ftintitle.contains_feat(u'Alice ft. Bob')) - self.assertTrue(ftintitle.contains_feat(u'Alice feat. Bob')) - self.assertTrue(ftintitle.contains_feat(u'Alice feat Bob')) - self.assertTrue(ftintitle.contains_feat(u'Alice featuring Bob')) - self.assertTrue(ftintitle.contains_feat(u'Alice & Bob')) - self.assertTrue(ftintitle.contains_feat(u'Alice and Bob')) - self.assertTrue(ftintitle.contains_feat(u'Alice With Bob')) - self.assertFalse(ftintitle.contains_feat(u'Alice defeat Bob')) - self.assertFalse(ftintitle.contains_feat(u'Aliceft.Bob')) + self.assertTrue(ftintitle.contains_feat('Alice ft. Bob')) + self.assertTrue(ftintitle.contains_feat('Alice feat. Bob')) + self.assertTrue(ftintitle.contains_feat('Alice feat Bob')) + self.assertTrue(ftintitle.contains_feat('Alice featuring Bob')) + self.assertTrue(ftintitle.contains_feat('Alice & Bob')) + self.assertTrue(ftintitle.contains_feat('Alice and Bob')) + self.assertTrue(ftintitle.contains_feat('Alice With Bob')) + self.assertFalse(ftintitle.contains_feat('Alice defeat Bob')) + self.assertFalse(ftintitle.contains_feat('Aliceft.Bob')) def suite(): diff -Nru beets-1.5.0/test/test_hidden.py beets-1.6.0/test/test_hidden.py --- beets-1.5.0/test/test_hidden.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_hidden.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte. # @@ -15,7 +14,6 @@ """Tests for the 'hidden' utility.""" -from __future__ import division, absolute_import, print_function import unittest import sys diff -Nru beets-1.5.0/test/test_hook.py beets-1.6.0/test/test_hook.py --- beets-1.5.0/test/test_hook.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_hook.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2015, Thomas Scholtes. # @@ -13,9 +12,9 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import os.path +import sys import tempfile import unittest @@ -64,6 +63,7 @@ self.assertIn('hook: invalid command ""', logs) + @unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows def test_hook_non_zero_exit(self): self._add_hook('test_event', 'sh -c "exit 1"') @@ -86,32 +86,34 @@ message.startswith("hook: hook for test_event failed: ") for message in logs)) + @unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows def test_hook_no_arguments(self): temporary_paths = [ get_temporary_path() for i in range(self.TEST_HOOK_COUNT) ] for index, path in enumerate(temporary_paths): - self._add_hook('test_no_argument_event_{0}'.format(index), - 'touch "{0}"'.format(path)) + self._add_hook(f'test_no_argument_event_{index}', + f'touch "{path}"') self.load_plugins('hook') for index in range(len(temporary_paths)): - plugins.send('test_no_argument_event_{0}'.format(index)) + plugins.send(f'test_no_argument_event_{index}') for path in temporary_paths: self.assertTrue(os.path.isfile(path)) os.remove(path) + @unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows def test_hook_event_substitution(self): temporary_directory = tempfile._get_default_tempdir() - event_names = ['test_event_event_{0}'.format(i) for i in + event_names = [f'test_event_event_{i}' for i in range(self.TEST_HOOK_COUNT)] for event in event_names: self._add_hook(event, - 'touch "{0}/{{event}}"'.format(temporary_directory)) + f'touch "{temporary_directory}/{{event}}"') self.load_plugins('hook') @@ -124,24 +126,26 @@ self.assertTrue(os.path.isfile(path)) os.remove(path) + @unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows def test_hook_argument_substitution(self): temporary_paths = [ get_temporary_path() for i in range(self.TEST_HOOK_COUNT) ] for index, path in enumerate(temporary_paths): - self._add_hook('test_argument_event_{0}'.format(index), + self._add_hook(f'test_argument_event_{index}', 'touch "{path}"') self.load_plugins('hook') for index, path in enumerate(temporary_paths): - plugins.send('test_argument_event_{0}'.format(index), path=path) + plugins.send(f'test_argument_event_{index}', path=path) for path in temporary_paths: self.assertTrue(os.path.isfile(path)) os.remove(path) + @unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows def test_hook_bytes_interpolation(self): temporary_paths = [ get_temporary_path().encode('utf-8') @@ -149,13 +153,13 @@ ] for index, path in enumerate(temporary_paths): - self._add_hook('test_bytes_event_{0}'.format(index), + self._add_hook(f'test_bytes_event_{index}', 'touch "{path}"') self.load_plugins('hook') for index, path in enumerate(temporary_paths): - plugins.send('test_bytes_event_{0}'.format(index), path=path) + plugins.send(f'test_bytes_event_{index}', path=path) for path in temporary_paths: self.assertTrue(os.path.isfile(path)) diff -Nru beets-1.5.0/test/test_ihate.py beets-1.6.0/test/test_ihate.py --- beets-1.5.0/test/test_ihate.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_ihate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- - """Tests for the 'ihate' plugin""" -from __future__ import division, absolute_import, print_function import unittest from beets import importer @@ -16,34 +13,34 @@ match_pattern = {} test_item = Item( - genre=u'TestGenre', - album=u'TestAlbum', - artist=u'TestArtist') + genre='TestGenre', + album='TestAlbum', + artist='TestArtist') task = importer.SingletonImportTask(None, test_item) # Empty query should let it pass. self.assertFalse(IHatePlugin.do_i_hate_this(task, match_pattern)) # 1 query match. - match_pattern = [u"artist:bad_artist", u"artist:TestArtist"] + match_pattern = ["artist:bad_artist", "artist:TestArtist"] self.assertTrue(IHatePlugin.do_i_hate_this(task, match_pattern)) # 2 query matches, either should trigger. - match_pattern = [u"album:test", u"artist:testartist"] + match_pattern = ["album:test", "artist:testartist"] self.assertTrue(IHatePlugin.do_i_hate_this(task, match_pattern)) # Query is blocked by AND clause. - match_pattern = [u"album:notthis genre:testgenre"] + match_pattern = ["album:notthis genre:testgenre"] self.assertFalse(IHatePlugin.do_i_hate_this(task, match_pattern)) # Both queries are blocked by AND clause with unmatched condition. - match_pattern = [u"album:notthis genre:testgenre", - u"artist:testartist album:notthis"] + match_pattern = ["album:notthis genre:testgenre", + "artist:testartist album:notthis"] self.assertFalse(IHatePlugin.do_i_hate_this(task, match_pattern)) # Only one query should fire. - match_pattern = [u"album:testalbum genre:testgenre", - u"artist:testartist album:notthis"] + match_pattern = ["album:testalbum genre:testgenre", + "artist:testartist album:notthis"] self.assertTrue(IHatePlugin.do_i_hate_this(task, match_pattern)) diff -Nru beets-1.5.0/test/test_importadded.py beets-1.6.0/test/test_importadded.py --- beets-1.5.0/test/test_importadded.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_importadded.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Stig Inge Lea Bjornsen. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function """Tests for the `importadded` plugin.""" @@ -54,7 +52,7 @@ self._create_import_dir(2) # Different mtimes on the files to be imported in order to test the # plugin - modify_mtimes((mfile.path for mfile in self.media_files)) + modify_mtimes(mfile.path for mfile in self.media_files) self.min_mtime = min(os.path.getmtime(mfile.path) for mfile in self.media_files) self.matcher = AutotagStub().install() @@ -72,7 +70,7 @@ for m in self.media_files: if m.title.replace('Tag', 'Applied') == item.title: return m - raise AssertionError(u"No MediaFile found for Item " + + raise AssertionError("No MediaFile found for Item " + util.displayable_path(item.path)) def assertEqualTimes(self, first, second, msg=None): # noqa @@ -113,8 +111,8 @@ self.importer.run() album = self.lib.albums().get() album_added_before = album.added - items_added_before = dict((item.path, item.added) - for item in album.items()) + items_added_before = {item.path: item.added + for item in album.items()} # Newer Item path mtimes as if Beets had modified them modify_mtimes(items_added_before.keys(), offset=10000) # Reimport @@ -123,11 +121,11 @@ # Verify the reimported items album = self.lib.albums().get() self.assertEqualTimes(album.added, album_added_before) - items_added_after = dict((item.path, item.added) - for item in album.items()) + items_added_after = {item.path: item.added + for item in album.items()} for item_path, added_after in items_added_after.items(): self.assertEqualTimes(items_added_before[item_path], added_after, - u"reimport modified Item.added for " + + "reimport modified Item.added for " + util.displayable_path(item_path)) def test_import_singletons_with_added_dates(self): @@ -152,8 +150,8 @@ self.config['import']['singletons'] = True # Import and record the original added dates self.importer.run() - items_added_before = dict((item.path, item.added) - for item in self.lib.items()) + items_added_before = {item.path: item.added + for item in self.lib.items()} # Newer Item path mtimes as if Beets had modified them modify_mtimes(items_added_before.keys(), offset=10000) # Reimport @@ -161,16 +159,17 @@ self._setup_import_session(import_dir=import_dir, singletons=True) self.importer.run() # Verify the reimported items - items_added_after = dict((item.path, item.added) - for item in self.lib.items()) + items_added_after = {item.path: item.added + for item in self.lib.items()} for item_path, added_after in items_added_after.items(): self.assertEqualTimes(items_added_before[item_path], added_after, - u"reimport modified Item.added for " + + "reimport modified Item.added for " + util.displayable_path(item_path)) def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == '__main__': unittest.main(defaultTest='suite') diff -Nru beets-1.5.0/test/test_importer.py beets-1.6.0/test/test_importer.py --- beets-1.5.0/test/test_importer.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/test/test_importer.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function """Tests for the general importer functionality. """ @@ -27,7 +25,7 @@ from tempfile import mkstemp from zipfile import ZipFile from tarfile import TarFile -from mock import patch, Mock +from unittest.mock import patch, Mock import unittest from test import _common @@ -44,7 +42,7 @@ from beets import util -class AutotagStub(object): +class AutotagStub: """Stub out MusicBrainz album and track matcher and control what the autotagger returns. """ @@ -97,9 +95,9 @@ def match_track(self, artist, title): yield TrackInfo( title=title.replace('Tag', 'Applied'), - track_id=u'trackid', + track_id='trackid', artist=artist.replace('Tag', 'Applied'), - artist_id=u'artistid', + artist_id='artistid', length=1, index=0, ) @@ -112,8 +110,8 @@ def _make_track_match(self, artist, album, number): return TrackInfo( - title=u'Applied Title %d' % number, - track_id=u'match %d' % number, + title='Applied Title %d' % number, + track_id='match %d' % number, artist=artist, length=1, index=0, @@ -125,7 +123,7 @@ else: id = '' if artist is None: - artist = u"Various Artists" + artist = "Various Artists" else: artist = artist.replace('Tag', 'Applied') + id album = album.replace('Tag', 'Applied') + id @@ -139,9 +137,9 @@ album=album, tracks=track_infos, va=False, - album_id=u'albumid' + id, - artist_id=u'artistid' + id, - albumtype=u'soundtrack' + album_id='albumid' + id, + artist_id='artistid' + id, + albumtype='soundtrack' ) @@ -152,11 +150,11 @@ """ def setup_beets(self, disk=False): - super(ImportHelper, self).setup_beets(disk) + super().setup_beets(disk) self.lib.path_formats = [ - (u'default', os.path.join('$artist', '$album', '$title')), - (u'singleton:true', os.path.join('singletons', '$title')), - (u'comp:true', os.path.join('compilations', '$album', '$title')), + ('default', os.path.join('$artist', '$album', '$title')), + ('singleton:true', os.path.join('singletons', '$title')), + ('comp:true', os.path.join('compilations', '$album', '$title')), ] def _create_import_dir(self, count=3): @@ -183,8 +181,8 @@ resource_path = os.path.join(_common.RSRC, b'full.mp3') metadata = { - 'artist': u'Tag Artist', - 'album': u'Tag Album', + 'artist': 'Tag Artist', + 'album': 'Tag Album', 'albumartist': None, 'mb_trackid': None, 'mb_albumid': None, @@ -202,7 +200,7 @@ # Set metadata metadata['track'] = i + 1 - metadata['title'] = u'Tag Title %d' % (i + 1) + metadata['title'] = 'Tag Title %d' % (i + 1) for attr in metadata: setattr(medium, attr, metadata[attr]) medium.save() @@ -259,14 +257,14 @@ self.importer.run() albums = self.lib.albums() self.assertEqual(len(albums), 1) - self.assertEqual(albums[0].albumartist, u'Tag Artist') + self.assertEqual(albums[0].albumartist, 'Tag Artist') def test_import_copy_arrives(self): self.importer.run() for mediafile in self.import_media: self.assert_file_in_lib( b'Tag Artist', b'Tag Album', - util.bytestring_path('{0}.mp3'.format(mediafile.title))) + util.bytestring_path(f'{mediafile.title}.mp3')) def test_threaded_import_copy_arrives(self): config['threaded'] = True @@ -275,7 +273,7 @@ for mediafile in self.import_media: self.assert_file_in_lib( b'Tag Artist', b'Tag Album', - util.bytestring_path('{0}.mp3'.format(mediafile.title))) + util.bytestring_path(f'{mediafile.title}.mp3')) def test_import_with_move_deletes_import_files(self): config['import']['move'] = True @@ -311,7 +309,7 @@ for mediafile in self.import_media: self.assert_file_in_lib( b'Tag Artist', b'Tag Album', - util.bytestring_path('{0}.mp3'.format(mediafile.title))) + util.bytestring_path(f'{mediafile.title}.mp3')) def test_threaded_import_move_deletes_import(self): config['import']['move'] = True @@ -348,7 +346,7 @@ filename = os.path.join( self.libdir, b'Tag Artist', b'Tag Album', - util.bytestring_path('{0}.mp3'.format(mediafile.title)) + util.bytestring_path(f'{mediafile.title}.mp3') ) self.assertExists(filename) self.assertTrue(os.path.islink(filename)) @@ -365,7 +363,7 @@ filename = os.path.join( self.libdir, b'Tag Artist', b'Tag Album', - util.bytestring_path('{0}.mp3'.format(mediafile.title)) + util.bytestring_path(f'{mediafile.title}.mp3') ) self.assertExists(filename) s1 = os.stat(mediafile.path) @@ -443,7 +441,7 @@ return path -@unittest.skipIf(not has_program('unrar'), u'unrar program not found') +@unittest.skipIf(not has_program('unrar'), 'unrar program not found') class ImportRarTest(ImportZipTest): def create_archive(self): @@ -484,7 +482,7 @@ self.importer.add_choice(importer.action.ASIS) self.importer.run() - self.assertEqual(self.lib.items().get().title, u'Tag Title 1') + self.assertEqual(self.lib.items().get().title, 'Tag Title 1') def test_apply_asis_does_not_add_album(self): self.assertEqual(self.lib.albums().get(), None) @@ -505,7 +503,7 @@ self.importer.add_choice(importer.action.APPLY) self.importer.run() - self.assertEqual(self.lib.items().get().title, u'Applied Title 1') + self.assertEqual(self.lib.items().get().title, 'Applied Title 1') def test_apply_candidate_does_not_add_album(self): self.importer.add_choice(importer.action.APPLY) @@ -551,12 +549,12 @@ self.assertEqual(len(self.lib.albums()), 2) def test_set_fields(self): - genre = u"\U0001F3B7 Jazz" - collection = u"To Listen" + genre = "\U0001F3B7 Jazz" + collection = "To Listen" config['import']['set_fields'] = { - u'collection': collection, - u'genre': genre + 'collection': collection, + 'genre': genre } # As-is item import. @@ -602,13 +600,13 @@ self.importer.add_choice(importer.action.ASIS) self.importer.run() - self.assertEqual(self.lib.albums().get().album, u'Tag Album') + self.assertEqual(self.lib.albums().get().album, 'Tag Album') def test_apply_asis_adds_tracks(self): self.assertEqual(self.lib.items().get(), None) self.importer.add_choice(importer.action.ASIS) self.importer.run() - self.assertEqual(self.lib.items().get().title, u'Tag Title 1') + self.assertEqual(self.lib.items().get().title, 'Tag Title 1') def test_apply_asis_adds_album_path(self): self.assert_lib_dir_empty() @@ -623,14 +621,14 @@ self.importer.add_choice(importer.action.APPLY) self.importer.run() - self.assertEqual(self.lib.albums().get().album, u'Applied Album') + self.assertEqual(self.lib.albums().get().album, 'Applied Album') def test_apply_candidate_adds_tracks(self): self.assertEqual(self.lib.items().get(), None) self.importer.add_choice(importer.action.APPLY) self.importer.run() - self.assertEqual(self.lib.items().get().title, u'Applied Title 1') + self.assertEqual(self.lib.items().get().title, 'Applied Title 1') def test_apply_candidate_adds_album_path(self): self.assert_lib_dir_empty() @@ -644,19 +642,19 @@ config['import']['from_scratch'] = True for mediafile in self.import_media: - mediafile.genre = u'Tag Genre' + mediafile.genre = 'Tag Genre' mediafile.save() self.importer.add_choice(importer.action.APPLY) self.importer.run() - self.assertEqual(self.lib.items().get().genre, u'') + self.assertEqual(self.lib.items().get().genre, '') def test_apply_from_scratch_keeps_format(self): config['import']['from_scratch'] = True self.importer.add_choice(importer.action.APPLY) self.importer.run() - self.assertEqual(self.lib.items().get().format, u'MP3') + self.assertEqual(self.lib.items().get().format, 'MP3') def test_apply_from_scratch_keeps_bitrate(self): config['import']['from_scratch'] = True @@ -716,7 +714,7 @@ self.importer.run() import_dir = displayable_path(import_dir) - self.assertIn(u'No files imported from {0}'.format(import_dir), logs) + self.assertIn(f'No files imported from {import_dir}', logs) def test_empty_directory_singleton_warning(self): import_dir = os.path.join(self.temp_dir, b'empty') @@ -726,7 +724,7 @@ self.importer.run() import_dir = displayable_path(import_dir) - self.assertIn(u'No files imported from {0}'.format(import_dir), logs) + self.assertIn(f'No files imported from {import_dir}', logs) def test_asis_no_data_source(self): self.assertEqual(self.lib.items().get(), None) @@ -738,14 +736,14 @@ self.lib.items().get().data_source def test_set_fields(self): - genre = u"\U0001F3B7 Jazz" - collection = u"To Listen" - comments = u"managed by beets" + genre = "\U0001F3B7 Jazz" + collection = "To Listen" + comments = "managed by beets" config['import']['set_fields'] = { - u'genre': genre, - u'collection': collection, - u'comments': comments + 'genre': genre, + 'collection': collection, + 'comments': comments } # As-is album import. @@ -813,7 +811,7 @@ self.importer.add_choice(importer.action.APPLY) self.importer.add_choice(importer.action.APPLY) self.importer.run() - self.assertEqual(self.lib.items().get().title, u'Applied Title 1') + self.assertEqual(self.lib.items().get().title, 'Applied Title 1') self.assertEqual(self.lib.albums().get(), None) def test_apply_tracks_adds_singleton_path(self): @@ -842,27 +840,27 @@ def test_asis_homogenous_sets_albumartist(self): self.importer.add_choice(importer.action.ASIS) self.importer.run() - self.assertEqual(self.lib.albums().get().albumartist, u'Tag Artist') + self.assertEqual(self.lib.albums().get().albumartist, 'Tag Artist') for item in self.lib.items(): - self.assertEqual(item.albumartist, u'Tag Artist') + self.assertEqual(item.albumartist, 'Tag Artist') def test_asis_heterogenous_sets_various_albumartist(self): - self.import_media[0].artist = u'Other Artist' + self.import_media[0].artist = 'Other Artist' self.import_media[0].save() - self.import_media[1].artist = u'Another Artist' + self.import_media[1].artist = 'Another Artist' self.import_media[1].save() self.importer.add_choice(importer.action.ASIS) self.importer.run() self.assertEqual(self.lib.albums().get().albumartist, - u'Various Artists') + 'Various Artists') for item in self.lib.items(): - self.assertEqual(item.albumartist, u'Various Artists') + self.assertEqual(item.albumartist, 'Various Artists') def test_asis_heterogenous_sets_sompilation(self): - self.import_media[0].artist = u'Other Artist' + self.import_media[0].artist = 'Other Artist' self.import_media[0].save() - self.import_media[1].artist = u'Another Artist' + self.import_media[1].artist = 'Another Artist' self.import_media[1].save() self.importer.add_choice(importer.action.ASIS) @@ -871,33 +869,33 @@ self.assertTrue(item.comp) def test_asis_sets_majority_albumartist(self): - self.import_media[0].artist = u'Other Artist' + self.import_media[0].artist = 'Other Artist' self.import_media[0].save() - self.import_media[1].artist = u'Other Artist' + self.import_media[1].artist = 'Other Artist' self.import_media[1].save() self.importer.add_choice(importer.action.ASIS) self.importer.run() - self.assertEqual(self.lib.albums().get().albumartist, u'Other Artist') + self.assertEqual(self.lib.albums().get().albumartist, 'Other Artist') for item in self.lib.items(): - self.assertEqual(item.albumartist, u'Other Artist') + self.assertEqual(item.albumartist, 'Other Artist') def test_asis_albumartist_tag_sets_albumartist(self): - self.import_media[0].artist = u'Other Artist' - self.import_media[1].artist = u'Another Artist' + self.import_media[0].artist = 'Other Artist' + self.import_media[1].artist = 'Another Artist' for mediafile in self.import_media: - mediafile.albumartist = u'Album Artist' - mediafile.mb_albumartistid = u'Album Artist ID' + mediafile.albumartist = 'Album Artist' + mediafile.mb_albumartistid = 'Album Artist ID' mediafile.save() self.importer.add_choice(importer.action.ASIS) self.importer.run() - self.assertEqual(self.lib.albums().get().albumartist, u'Album Artist') + self.assertEqual(self.lib.albums().get().albumartist, 'Album Artist') self.assertEqual(self.lib.albums().get().mb_albumartistid, - u'Album Artist ID') + 'Album Artist ID') for item in self.lib.items(): - self.assertEqual(item.albumartist, u'Album Artist') - self.assertEqual(item.mb_albumartistid, u'Album Artist ID') + self.assertEqual(item.albumartist, 'Album Artist') + self.assertEqual(item.mb_albumartistid, 'Album Artist ID') class ImportExistingTest(_common.TestCase, ImportHelper): @@ -920,45 +918,45 @@ def test_does_not_duplicate_item(self): self.setup_importer.run() - self.assertEqual(len((self.lib.items())), 1) + self.assertEqual(len(self.lib.items()), 1) self.importer.add_choice(importer.action.APPLY) self.importer.run() - self.assertEqual(len((self.lib.items())), 1) + self.assertEqual(len(self.lib.items()), 1) def test_does_not_duplicate_album(self): self.setup_importer.run() - self.assertEqual(len((self.lib.albums())), 1) + self.assertEqual(len(self.lib.albums()), 1) self.importer.add_choice(importer.action.APPLY) self.importer.run() - self.assertEqual(len((self.lib.albums())), 1) + self.assertEqual(len(self.lib.albums()), 1) def test_does_not_duplicate_singleton_track(self): self.setup_importer.add_choice(importer.action.TRACKS) self.setup_importer.add_choice(importer.action.APPLY) self.setup_importer.run() - self.assertEqual(len((self.lib.items())), 1) + self.assertEqual(len(self.lib.items()), 1) self.importer.add_choice(importer.action.TRACKS) self.importer.add_choice(importer.action.APPLY) self.importer.run() - self.assertEqual(len((self.lib.items())), 1) + self.assertEqual(len(self.lib.items()), 1) def test_asis_updates_metadata(self): self.setup_importer.run() medium = MediaFile(self.lib.items().get().path) - medium.title = u'New Title' + medium.title = 'New Title' medium.save() self.importer.add_choice(importer.action.ASIS) self.importer.run() - self.assertEqual(self.lib.items().get().title, u'New Title') + self.assertEqual(self.lib.items().get().title, 'New Title') def test_asis_updated_moves_file(self): self.setup_importer.run() medium = MediaFile(self.lib.items().get().path) - medium.title = u'New Title' + medium.title = 'New Title' medium.save() old_path = os.path.join(b'Applied Artist', b'Applied Album', @@ -974,7 +972,7 @@ def test_asis_updated_without_copy_does_not_move_file(self): self.setup_importer.run() medium = MediaFile(self.lib.items().get().path) - medium.title = u'New Title' + medium.title = 'New Title' medium.save() old_path = os.path.join(b'Applied Artist', b'Applied Album', @@ -1035,56 +1033,56 @@ self.matcher.restore() def test_add_album_for_different_artist_and_different_album(self): - self.import_media[0].artist = u"Artist B" - self.import_media[0].album = u"Album B" + self.import_media[0].artist = "Artist B" + self.import_media[0].album = "Album B" self.import_media[0].save() self.importer.run() - albums = set([album.album for album in self.lib.albums()]) - self.assertEqual(albums, set(['Album B', 'Tag Album'])) + albums = {album.album for album in self.lib.albums()} + self.assertEqual(albums, {'Album B', 'Tag Album'}) def test_add_album_for_different_artist_and_same_albumartist(self): - self.import_media[0].artist = u"Artist B" - self.import_media[0].albumartist = u"Album Artist" + self.import_media[0].artist = "Artist B" + self.import_media[0].albumartist = "Album Artist" self.import_media[0].save() - self.import_media[1].artist = u"Artist C" - self.import_media[1].albumartist = u"Album Artist" + self.import_media[1].artist = "Artist C" + self.import_media[1].albumartist = "Album Artist" self.import_media[1].save() self.importer.run() - artists = set([album.albumartist for album in self.lib.albums()]) - self.assertEqual(artists, set(['Album Artist', 'Tag Artist'])) + artists = {album.albumartist for album in self.lib.albums()} + self.assertEqual(artists, {'Album Artist', 'Tag Artist'}) def test_add_album_for_same_artist_and_different_album(self): - self.import_media[0].album = u"Album B" + self.import_media[0].album = "Album B" self.import_media[0].save() self.importer.run() - albums = set([album.album for album in self.lib.albums()]) - self.assertEqual(albums, set(['Album B', 'Tag Album'])) + albums = {album.album for album in self.lib.albums()} + self.assertEqual(albums, {'Album B', 'Tag Album'}) def test_add_album_for_same_album_and_different_artist(self): - self.import_media[0].artist = u"Artist B" + self.import_media[0].artist = "Artist B" self.import_media[0].save() self.importer.run() - artists = set([album.albumartist for album in self.lib.albums()]) - self.assertEqual(artists, set(['Artist B', 'Tag Artist'])) + artists = {album.albumartist for album in self.lib.albums()} + self.assertEqual(artists, {'Artist B', 'Tag Artist'}) def test_incremental(self): config['import']['incremental'] = True - self.import_media[0].album = u"Album B" + self.import_media[0].album = "Album B" self.import_media[0].save() self.importer.run() - albums = set([album.album for album in self.lib.albums()]) - self.assertEqual(albums, set(['Album B', 'Tag Album'])) + albums = {album.album for album in self.lib.albums()} + self.assertEqual(albums, {'Album B', 'Tag Album'}) class GlobalGroupAlbumsImportTest(GroupAlbumsImportTest): def setUp(self): - super(GlobalGroupAlbumsImportTest, self).setUp() + super().setUp() self.importer.clear_choices() self.importer.default_choice = importer.action.ASIS config['import']['group_albums'] = True @@ -1105,24 +1103,24 @@ def test_choose_first_candidate(self): self.importer.add_choice(1) self.importer.run() - self.assertEqual(self.lib.albums().get().album, u'Applied Album M') + self.assertEqual(self.lib.albums().get().album, 'Applied Album M') def test_choose_second_candidate(self): self.importer.add_choice(2) self.importer.run() - self.assertEqual(self.lib.albums().get().album, u'Applied Album MM') + self.assertEqual(self.lib.albums().get().album, 'Applied Album MM') class InferAlbumDataTest(_common.TestCase): def setUp(self): - super(InferAlbumDataTest, self).setUp() + super().setUp() i1 = _common.item() i2 = _common.item() i3 = _common.item() - i1.title = u'first item' - i2.title = u'second item' - i3.title = u'third item' + i1.title = 'first item' + i2.title = 'second item' + i3.title = 'third item' i1.comp = i2.comp = i3.comp = False i1.albumartist = i2.albumartist = i3.albumartist = '' i1.mb_albumartistid = i2.mb_albumartistid = i3.mb_albumartistid = '' @@ -1138,28 +1136,28 @@ self.assertEqual(self.items[0].albumartist, self.items[2].artist) def test_asis_heterogenous_va(self): - self.items[0].artist = u'another artist' - self.items[1].artist = u'some other artist' + self.items[0].artist = 'another artist' + self.items[1].artist = 'some other artist' self.task.set_choice(importer.action.ASIS) self.task.align_album_level_fields() self.assertTrue(self.items[0].comp) - self.assertEqual(self.items[0].albumartist, u'Various Artists') + self.assertEqual(self.items[0].albumartist, 'Various Artists') def test_asis_comp_applied_to_all_items(self): - self.items[0].artist = u'another artist' - self.items[1].artist = u'some other artist' + self.items[0].artist = 'another artist' + self.items[1].artist = 'some other artist' self.task.set_choice(importer.action.ASIS) self.task.align_album_level_fields() for item in self.items: self.assertTrue(item.comp) - self.assertEqual(item.albumartist, u'Various Artists') + self.assertEqual(item.albumartist, 'Various Artists') def test_asis_majority_artist_single_artist(self): - self.items[0].artist = u'another artist' + self.items[0].artist = 'another artist' self.task.set_choice(importer.action.ASIS) self.task.align_album_level_fields() @@ -1168,19 +1166,19 @@ self.assertEqual(self.items[0].albumartist, self.items[2].artist) def test_asis_track_albumartist_override(self): - self.items[0].artist = u'another artist' - self.items[1].artist = u'some other artist' + self.items[0].artist = 'another artist' + self.items[1].artist = 'some other artist' for item in self.items: - item.albumartist = u'some album artist' - item.mb_albumartistid = u'some album artist id' + item.albumartist = 'some album artist' + item.mb_albumartistid = 'some album artist id' self.task.set_choice(importer.action.ASIS) self.task.align_album_level_fields() self.assertEqual(self.items[0].albumartist, - u'some album artist') + 'some album artist') self.assertEqual(self.items[0].mb_albumartistid, - u'some album artist id') + 'some album artist id') def test_apply_gets_artist_and_id(self): self.task.set_choice(AlbumMatch(0, None, {}, set(), set())) # APPLY @@ -1193,16 +1191,16 @@ def test_apply_lets_album_values_override(self): for item in self.items: - item.albumartist = u'some album artist' - item.mb_albumartistid = u'some album artist id' + item.albumartist = 'some album artist' + item.mb_albumartistid = 'some album artist id' self.task.set_choice(AlbumMatch(0, None, {}, set(), set())) # APPLY self.task.align_album_level_fields() self.assertEqual(self.items[0].albumartist, - u'some album artist') + 'some album artist') self.assertEqual(self.items[0].mb_albumartistid, - u'some album artist id') + 'some album artist id') def test_small_single_artist_album(self): self.items = [self.items[0]] @@ -1216,16 +1214,16 @@ """Create an AlbumInfo object for testing. """ track_info = TrackInfo( - title=u'new title', - track_id=u'trackid', + title='new title', + track_id='trackid', index=0, ) album_info = AlbumInfo( - artist=u'artist', - album=u'album', + artist='artist', + album='album', tracks=[track_info], - album_id=u'albumid', - artist_id=u'artistid', + album_id='albumid', + artist_id='artistid', ) return iter([album_info]) @@ -1238,7 +1236,7 @@ self.setup_beets() # Original album - self.add_album_fixture(albumartist=u'artist', album=u'album') + self.add_album_fixture(albumartist='artist', album='album') # Create import session self.importer = self.create_importer() @@ -1249,7 +1247,7 @@ def test_remove_duplicate_album(self): item = self.lib.items().get() - self.assertEqual(item.title, u't\xeftle 0') + self.assertEqual(item.title, 't\xeftle 0') self.assertExists(item.path) self.importer.default_resolution = self.importer.Resolution.REMOVE @@ -1259,12 +1257,12 @@ self.assertEqual(len(self.lib.albums()), 1) self.assertEqual(len(self.lib.items()), 1) item = self.lib.items().get() - self.assertEqual(item.title, u'new title') + self.assertEqual(item.title, 'new title') def test_no_autotag_keeps_duplicate_album(self): config['import']['autotag'] = False item = self.lib.items().get() - self.assertEqual(item.title, u't\xeftle 0') + self.assertEqual(item.title, 't\xeftle 0') self.assertExists(item.path) # Imported item has the same artist and album as the one in the @@ -1293,7 +1291,7 @@ def test_skip_duplicate_album(self): item = self.lib.items().get() - self.assertEqual(item.title, u't\xeftle 0') + self.assertEqual(item.title, 't\xeftle 0') self.importer.default_resolution = self.importer.Resolution.SKIP self.importer.run() @@ -1301,7 +1299,7 @@ self.assertEqual(len(self.lib.albums()), 1) self.assertEqual(len(self.lib.items()), 1) item = self.lib.items().get() - self.assertEqual(item.title, u't\xeftle 0') + self.assertEqual(item.title, 't\xeftle 0') def test_merge_duplicate_album(self): self.importer.default_resolution = self.importer.Resolution.MERGE @@ -1314,7 +1312,7 @@ def add_album_fixture(self, **kwargs): # TODO move this into upstream - album = super(ImportDuplicateAlbumTest, self).add_album_fixture() + album = super().add_album_fixture() album.update(kwargs) album.store() return album @@ -1322,8 +1320,8 @@ def test_track_info(*args, **kwargs): return iter([TrackInfo( - artist=u'artist', title=u'title', - track_id=u'new trackid', index=0,)]) + artist='artist', title='title', + track_id='new trackid', index=0,)]) @patch('beets.autotag.mb.match_track', Mock(side_effect=test_track_info)) @@ -1334,7 +1332,7 @@ self.setup_beets() # Original file in library - self.add_item_fixture(artist=u'artist', title=u'title', + self.add_item_fixture(artist='artist', title='title', mb_trackid='old trackid') # Import session @@ -1347,7 +1345,7 @@ def test_remove_duplicate(self): item = self.lib.items().get() - self.assertEqual(item.mb_trackid, u'old trackid') + self.assertEqual(item.mb_trackid, 'old trackid') self.assertExists(item.path) self.importer.default_resolution = self.importer.Resolution.REMOVE @@ -1356,7 +1354,7 @@ self.assertNotExists(item.path) self.assertEqual(len(self.lib.items()), 1) item = self.lib.items().get() - self.assertEqual(item.mb_trackid, u'new trackid') + self.assertEqual(item.mb_trackid, 'new trackid') def test_keep_duplicate(self): self.assertEqual(len(self.lib.items()), 1) @@ -1368,14 +1366,14 @@ def test_skip_duplicate(self): item = self.lib.items().get() - self.assertEqual(item.mb_trackid, u'old trackid') + self.assertEqual(item.mb_trackid, 'old trackid') self.importer.default_resolution = self.importer.Resolution.SKIP self.importer.run() self.assertEqual(len(self.lib.items()), 1) item = self.lib.items().get() - self.assertEqual(item.mb_trackid, u'old trackid') + self.assertEqual(item.mb_trackid, 'old trackid') def test_twice_in_import_dir(self): self.skipTest('write me') @@ -1400,8 +1398,8 @@ sio = StringIO() handler = logging.StreamHandler(sio) session = _common.import_session(loghandler=handler) - session.tag_log('status', u'caf\xe9') # send unicode - self.assertIn(u'status caf\xe9', sio.getvalue()) + session.tag_log('status', 'caf\xe9') # send unicode + self.assertIn('status caf\xe9', sio.getvalue()) class ResumeImportTest(unittest.TestCase, TestHelper): @@ -1426,11 +1424,11 @@ self.importer.run() self.assertEqual(len(self.lib.albums()), 1) - self.assertIsNotNone(self.lib.albums(u'album:album 0').get()) + self.assertIsNotNone(self.lib.albums('album:album 0').get()) self.importer.run() self.assertEqual(len(self.lib.albums()), 2) - self.assertIsNotNone(self.lib.albums(u'album:album 1').get()) + self.assertIsNotNone(self.lib.albums('album:album 1').get()) @patch('beets.plugins.send') def test_resume_singleton(self, plugins_send): @@ -1447,11 +1445,11 @@ self.importer.run() self.assertEqual(len(self.lib.items()), 1) - self.assertIsNotNone(self.lib.items(u'title:track 0').get()) + self.assertIsNotNone(self.lib.items('title:track 0').get()) self.importer.run() self.assertEqual(len(self.lib.items()), 2) - self.assertIsNotNone(self.lib.items(u'title:track 1').get()) + self.assertIsNotNone(self.lib.items('title:track 1').get()) class IncrementalImportTest(unittest.TestCase, TestHelper): @@ -1506,7 +1504,7 @@ class AlbumsInDirTest(_common.TestCase): def setUp(self): - super(AlbumsInDirTest, self).setUp() + super().setUp() # create a directory structure for testing self.base = os.path.abspath(os.path.join(self.temp_dir, b'tempdir')) @@ -1557,8 +1555,8 @@ self.base = os.path.abspath(os.path.join(self.temp_dir, b'tempdir')) os.mkdir(self.base) - name = b'CAT' if ascii else util.bytestring_path(u'C\xc1T') - name_alt_case = b'CAt' if ascii else util.bytestring_path(u'C\xc1t') + name = b'CAT' if ascii else util.bytestring_path('C\xc1T') + name_alt_case = b'CAt' if ascii else util.bytestring_path('C\xc1t') self.dirs = [ # Nested album, multiple subdirs. @@ -1681,10 +1679,10 @@ # The existing album. album = self.add_album_fixture() album.added = 4242.0 - album.foo = u'bar' # Some flexible attribute. + album.foo = 'bar' # Some flexible attribute. album.store() item = album.items().get() - item.baz = u'qux' + item.baz = 'qux' item.added = 4747.0 item.store() @@ -1708,14 +1706,14 @@ def test_reimported_album_gets_new_metadata(self): self._setup_session() - self.assertEqual(self._album().album, u'\xe4lbum') + self.assertEqual(self._album().album, '\xe4lbum') self.importer.run() - self.assertEqual(self._album().album, u'the album') + self.assertEqual(self._album().album, 'the album') def test_reimported_album_preserves_flexattr(self): self._setup_session() self.importer.run() - self.assertEqual(self._album().foo, u'bar') + self.assertEqual(self._album().foo, 'bar') def test_reimported_album_preserves_added(self): self._setup_session() @@ -1725,7 +1723,7 @@ def test_reimported_album_preserves_item_flexattr(self): self._setup_session() self.importer.run() - self.assertEqual(self._item().baz, u'qux') + self.assertEqual(self._item().baz, 'qux') def test_reimported_album_preserves_item_added(self): self._setup_session() @@ -1734,14 +1732,14 @@ def test_reimported_item_gets_new_metadata(self): self._setup_session(True) - self.assertEqual(self._item().title, u't\xeftle 0') + self.assertEqual(self._item().title, 't\xeftle 0') self.importer.run() - self.assertEqual(self._item().title, u'full') + self.assertEqual(self._item().title, 'full') def test_reimported_item_preserves_flexattr(self): self._setup_session(True) self.importer.run() - self.assertEqual(self._item().baz, u'qux') + self.assertEqual(self._item().baz, 'qux') def test_reimported_item_preserves_added(self): self._setup_session(True) @@ -1769,11 +1767,11 @@ """ def __init__(self, method_name='runTest'): - super(ImportPretendTest, self).__init__(method_name) + super().__init__(method_name) self.matcher = None def setUp(self): - super(ImportPretendTest, self).setUp() + super().setUp() self.setup_beets() self.__create_import_dir() self.__create_empty_import_dir() @@ -1839,7 +1837,7 @@ def test_import_pretend_empty(self): logs = self.__run([self.empty_path]) - self.assertEqual(logs, [u'No files imported from {0}' + self.assertEqual(logs, ['No files imported from {}' .format(displayable_path(self.empty_path))]) # Helpers for ImportMusicBrainzIdTest. @@ -1985,8 +1983,8 @@ 'an invalid and discarded id'] task.lookup_candidates() - self.assertEqual(set(['VALID_RELEASE_0', 'VALID_RELEASE_1']), - set([c.info.album for c in task.candidates])) + self.assertEqual({'VALID_RELEASE_0', 'VALID_RELEASE_1'}, + {c.info.album for c in task.candidates}) def test_candidates_singleton(self): """Test directly SingletonImportTask.lookup_candidates().""" @@ -1997,8 +1995,8 @@ 'an invalid and discarded id'] task.lookup_candidates() - self.assertEqual(set(['VALID_RECORDING_0', 'VALID_RECORDING_1']), - set([c.info.title for c in task.candidates])) + self.assertEqual({'VALID_RECORDING_0', 'VALID_RECORDING_1'}, + {c.info.title for c in task.candidates}) def suite(): diff -Nru beets-1.5.0/test/test_importfeeds.py beets-1.6.0/test/test_importfeeds.py --- beets-1.5.0/test/test_importfeeds.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_importfeeds.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- - -from __future__ import division, absolute_import, print_function - import os import os.path import tempfile diff -Nru beets-1.5.0/test/test_info.py beets-1.6.0/test/test_info.py --- beets-1.5.0/test/test_info.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/test/test_info.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import unittest from test.helper import TestHelper @@ -59,7 +57,7 @@ out = self.run_with_output('info', 'album:yyyy') self.assertIn(displayable_path(item1.path), out) - self.assertIn(u'album: xxxx', out) + self.assertIn('album: xxxx', out) self.assertNotIn(displayable_path(item2.path), out) @@ -70,7 +68,7 @@ out = self.run_with_output('info', '--library', 'album:xxxx') self.assertIn(displayable_path(item.path), out) - self.assertIn(u'album: xxxx', out) + self.assertIn('album: xxxx', out) def test_collect_item_and_path(self): path = self.create_mediafile_fixture() @@ -87,16 +85,16 @@ mediafile.save() out = self.run_with_output('info', '--summarize', 'album:AAA', path) - self.assertIn(u'album: AAA', out) - self.assertIn(u'tracktotal: 5', out) - self.assertIn(u'title: [various]', out) + self.assertIn('album: AAA', out) + self.assertIn('tracktotal: 5', out) + self.assertIn('title: [various]', out) self.remove_mediafile_fixtures() def test_custom_format(self): self.add_item_fixtures() out = self.run_with_output('info', '--library', '--format', '$track. $title - $artist ($length)') - self.assertEqual(u'02. tïtle 0 - the artist (0:01)\n', out) + self.assertEqual('02. tïtle 0 - the artist (0:01)\n', out) def suite(): diff -Nru beets-1.5.0/test/test_ipfs.py beets-1.6.0/test/test_ipfs.py --- beets-1.5.0/test/test_ipfs.py 2021-03-08 00:47:22.000000000 +0000 +++ beets-1.6.0/test/test_ipfs.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # # Permission is hereby granted, free of charge, to any person obtaining @@ -12,9 +11,8 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function -from mock import patch, Mock +from unittest.mock import patch, Mock from beets import library from beets.util import bytestring_path, _fsencoding @@ -53,8 +51,8 @@ ipfs_item = os.path.basename(want_item.path).decode( _fsencoding(), ) - want_path = '/ipfs/{0}/{1}'.format(test_album.ipfs, - ipfs_item) + want_path = '/ipfs/{}/{}'.format(test_album.ipfs, + ipfs_item) want_path = bytestring_path(want_path) self.assertEqual(check_item.path, want_path) self.assertEqual(check_item.get('ipfs', with_album=False), @@ -97,5 +95,6 @@ def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == '__main__': unittest.main(defaultTest='suite') diff -Nru beets-1.5.0/test/test_keyfinder.py beets-1.6.0/test/test_keyfinder.py --- beets-1.5.0/test/test_keyfinder.py 2020-12-15 12:48:01.000000000 +0000 +++ beets-1.6.0/test/test_keyfinder.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,9 +12,8 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function -from mock import patch +from unittest.mock import patch import unittest from test.helper import TestHelper diff -Nru beets-1.5.0/test/test_lastgenre.py beets-1.6.0/test/test_lastgenre.py --- beets-1.5.0/test/test_lastgenre.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_lastgenre.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte. # @@ -15,17 +14,15 @@ """Tests for the 'lastgenre' plugin.""" -from __future__ import division, absolute_import, print_function import unittest -from mock import Mock +from unittest.mock import Mock from test import _common from beetsplug import lastgenre from beets import config from test.helper import TestHelper -import six class LastGenrePluginTest(unittest.TestCase, TestHelper): @@ -41,11 +38,11 @@ config['lastgenre']['canonical'] = canonical config['lastgenre']['count'] = count config['lastgenre']['prefer_specific'] = prefer_specific - if isinstance(whitelist, (bool, six.string_types)): + if isinstance(whitelist, (bool, (str,))): # Filename, default, or disabled. config['lastgenre']['whitelist'] = whitelist self.plugin.setup() - if not isinstance(whitelist, (bool, six.string_types)): + if not isinstance(whitelist, (bool, (str,))): # Explicit list of genres. self.plugin.whitelist = whitelist @@ -54,7 +51,7 @@ """ self._setup_config() self.assertEqual(self.plugin._resolve_genres(['delta blues']), - u'Delta Blues') + 'Delta Blues') def test_c14n_only(self): """Default c14n tree funnels up to most common genre except for *wrong* @@ -62,16 +59,16 @@ """ self._setup_config(canonical=True, count=99) self.assertEqual(self.plugin._resolve_genres(['delta blues']), - u'Blues') + 'Blues') self.assertEqual(self.plugin._resolve_genres(['iota blues']), - u'Iota Blues') + 'Iota Blues') def test_whitelist_only(self): """Default whitelist rejects *wrong* (non existing) genres. """ self._setup_config(whitelist=True) self.assertEqual(self.plugin._resolve_genres(['iota blues']), - u'') + '') def test_whitelist_c14n(self): """Default whitelist and c14n both activated result in all parents @@ -79,48 +76,48 @@ """ self._setup_config(canonical=True, whitelist=True, count=99) self.assertEqual(self.plugin._resolve_genres(['delta blues']), - u'Delta Blues, Blues') + 'Delta Blues, Blues') def test_whitelist_custom(self): """Keep only genres that are in the whitelist. """ - self._setup_config(whitelist=set(['blues', 'rock', 'jazz']), + self._setup_config(whitelist={'blues', 'rock', 'jazz'}, count=2) self.assertEqual(self.plugin._resolve_genres(['pop', 'blues']), - u'Blues') + 'Blues') - self._setup_config(canonical='', whitelist=set(['rock'])) + self._setup_config(canonical='', whitelist={'rock'}) self.assertEqual(self.plugin._resolve_genres(['delta blues']), - u'') + '') def test_count(self): """Keep the n first genres, as we expect them to be sorted from more to less popular. """ - self._setup_config(whitelist=set(['blues', 'rock', 'jazz']), + self._setup_config(whitelist={'blues', 'rock', 'jazz'}, count=2) self.assertEqual(self.plugin._resolve_genres( ['jazz', 'pop', 'rock', 'blues']), - u'Jazz, Rock') + 'Jazz, Rock') def test_count_c14n(self): """Keep the n first genres, after having applied c14n when necessary """ - self._setup_config(whitelist=set(['blues', 'rock', 'jazz']), + self._setup_config(whitelist={'blues', 'rock', 'jazz'}, canonical=True, count=2) # thanks to c14n, 'blues' superseeds 'country blues' and takes the # second slot self.assertEqual(self.plugin._resolve_genres( ['jazz', 'pop', 'country blues', 'rock']), - u'Jazz, Blues') + 'Jazz, Blues') def test_c14n_whitelist(self): """Genres first pass through c14n and are then filtered """ - self._setup_config(canonical=True, whitelist=set(['rock'])) + self._setup_config(canonical=True, whitelist={'rock'}) self.assertEqual(self.plugin._resolve_genres(['delta blues']), - u'') + '') def test_empty_string_enables_canonical(self): """For backwards compatibility, setting the `canonical` option @@ -128,7 +125,7 @@ """ self._setup_config(canonical='', count=99) self.assertEqual(self.plugin._resolve_genres(['delta blues']), - u'Blues') + 'Blues') def test_empty_string_enables_whitelist(self): """Again for backwards compatibility, setting the `whitelist` @@ -136,7 +133,7 @@ """ self._setup_config(whitelist='') self.assertEqual(self.plugin._resolve_genres(['iota blues']), - u'') + '') def test_prefer_specific_loads_tree(self): """When prefer_specific is enabled but canonical is not the @@ -151,41 +148,41 @@ self._setup_config(prefer_specific=True, canonical=False, count=4) self.assertEqual(self.plugin._resolve_genres( ['math rock', 'post-rock']), - u'Post-Rock, Math Rock') + 'Post-Rock, Math Rock') def test_no_duplicate(self): """Remove duplicated genres. """ self._setup_config(count=99) self.assertEqual(self.plugin._resolve_genres(['blues', 'blues']), - u'Blues') + 'Blues') def test_tags_for(self): - class MockPylastElem(object): + class MockPylastElem: def __init__(self, name): self.name = name def get_name(self): return self.name - class MockPylastObj(object): + class MockPylastObj: def get_top_tags(self): tag1 = Mock() tag1.weight = 90 - tag1.item = MockPylastElem(u'Pop') + tag1.item = MockPylastElem('Pop') tag2 = Mock() tag2.weight = 40 - tag2.item = MockPylastElem(u'Rap') + tag2.item = MockPylastElem('Rap') return [tag1, tag2] plugin = lastgenre.LastGenrePlugin() res = plugin._tags_for(MockPylastObj()) - self.assertEqual(res, [u'pop', u'rap']) + self.assertEqual(res, ['pop', 'rap']) res = plugin._tags_for(MockPylastObj(), min_weight=50) - self.assertEqual(res, [u'pop']) + self.assertEqual(res, ['pop']) def test_get_genre(self): - mock_genres = {'track': u'1', 'album': u'2', 'artist': u'3'} + mock_genres = {'track': '1', 'album': '2', 'artist': '3'} def mock_fetch_track_genre(self, obj=None): return mock_genres['track'] @@ -206,29 +203,29 @@ config['lastgenre'] = {'force': False} res = self.plugin._get_genre(item) - self.assertEqual(res, (item.genre, u'keep')) + self.assertEqual(res, (item.genre, 'keep')) - config['lastgenre'] = {'force': True, 'source': u'track'} + config['lastgenre'] = {'force': True, 'source': 'track'} res = self.plugin._get_genre(item) - self.assertEqual(res, (mock_genres['track'], u'track')) + self.assertEqual(res, (mock_genres['track'], 'track')) - config['lastgenre'] = {'source': u'album'} + config['lastgenre'] = {'source': 'album'} res = self.plugin._get_genre(item) - self.assertEqual(res, (mock_genres['album'], u'album')) + self.assertEqual(res, (mock_genres['album'], 'album')) - config['lastgenre'] = {'source': u'artist'} + config['lastgenre'] = {'source': 'artist'} res = self.plugin._get_genre(item) - self.assertEqual(res, (mock_genres['artist'], u'artist')) + self.assertEqual(res, (mock_genres['artist'], 'artist')) mock_genres['artist'] = None res = self.plugin._get_genre(item) - self.assertEqual(res, (item.genre, u'original')) + self.assertEqual(res, (item.genre, 'original')) - config['lastgenre'] = {'fallback': u'rap'} + config['lastgenre'] = {'fallback': 'rap'} item.genre = None res = self.plugin._get_genre(item) self.assertEqual(res, (config['lastgenre']['fallback'].get(), - u'fallback')) + 'fallback')) def test_sort_by_depth(self): self._setup_config(canonical=True) diff -Nru beets-1.5.0/test/test_library.py beets-1.6.0/test/test_library.py --- beets-1.5.0/test/test_library.py 2021-03-08 00:47:22.000000000 +0000 +++ beets-1.6.0/test/test_library.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """Tests for non-query database functions of Item. """ -from __future__ import division, absolute_import, print_function import os import os.path @@ -37,7 +35,6 @@ from mediafile import MediaFile, UnreadableFileError from beets.util import syspath, bytestring_path from test.helper import TestHelper -import six # Shortcut to path normalization. np = util.normpath @@ -46,12 +43,12 @@ class LoadTest(_common.LibTestCase): def test_load_restores_data_from_db(self): original_title = self.i.title - self.i.title = u'something' + self.i.title = 'something' self.i.load() self.assertEqual(original_title, self.i.title) def test_load_clears_dirty_flags(self): - self.i.artist = u'something' + self.i.artist = 'something' self.assertTrue('artist' in self.i._dirty) self.i.load() self.assertTrue('artist' not in self.i._dirty) @@ -68,7 +65,7 @@ def test_store_only_writes_dirty_fields(self): original_genre = self.i.genre - self.i._values_fixed['genre'] = u'beatboxing' # change w/o dirtying + self.i._values_fixed['genre'] = 'beatboxing' # change w/o dirtying self.i.store() new_genre = self.lib._connection().execute( 'select genre from items where ' @@ -76,14 +73,14 @@ self.assertEqual(new_genre, original_genre) def test_store_clears_dirty_flags(self): - self.i.composer = u'tvp' + self.i.composer = 'tvp' self.i.store() self.assertTrue('composer' not in self.i._dirty) class AddTest(_common.TestCase): def setUp(self): - super(AddTest, self).setUp() + super().setUp() self.lib = beets.library.Library(':memory:') self.i = item() @@ -114,7 +111,7 @@ class GetSetTest(_common.TestCase): def setUp(self): - super(GetSetTest, self).setUp() + super().setUp() self.i = item() def test_set_changes_value(self): @@ -130,32 +127,35 @@ self.assertTrue('title' not in self.i._dirty) def test_invalid_field_raises_attributeerror(self): - self.assertRaises(AttributeError, getattr, self.i, u'xyzzy') + self.assertRaises(AttributeError, getattr, self.i, 'xyzzy') def test_album_fallback(self): # integration test of item-album fallback lib = beets.library.Library(':memory:') i = item(lib) album = lib.add_album([i]) - album['flex'] = u'foo' + album['flex'] = 'foo' album.store() self.assertTrue('flex' in i) self.assertFalse('flex' in i.keys(with_album=False)) - self.assertEqual(i['flex'], u'foo') - self.assertEqual(i.get('flex'), u'foo') + self.assertEqual(i['flex'], 'foo') + self.assertEqual(i.get('flex'), 'foo') self.assertEqual(i.get('flex', with_album=False), None) self.assertEqual(i.get('flexx'), None) class DestinationTest(_common.TestCase): def setUp(self): - super(DestinationTest, self).setUp() - self.lib = beets.library.Library(':memory:') + super().setUp() + # default directory is ~/Music and the only reason why it was switched + # to ~/.Music is to confirm that tests works well when path to + # temporary directory contains . + self.lib = beets.library.Library(':memory:', '~/.Music') self.i = item(self.lib) def tearDown(self): - super(DestinationTest, self).tearDown() + super().tearDown() self.lib._connection().close() # Reset config if it was changed in test cases @@ -164,17 +164,17 @@ def test_directory_works_with_trailing_slash(self): self.lib.directory = b'one/' - self.lib.path_formats = [(u'default', u'two')] + self.lib.path_formats = [('default', 'two')] self.assertEqual(self.i.destination(), np('one/two')) def test_directory_works_without_trailing_slash(self): self.lib.directory = b'one' - self.lib.path_formats = [(u'default', u'two')] + self.lib.path_formats = [('default', 'two')] self.assertEqual(self.i.destination(), np('one/two')) def test_destination_substitutes_metadata_values(self): self.lib.directory = b'base' - self.lib.path_formats = [(u'default', u'$album/$artist $title')] + self.lib.path_formats = [('default', '$album/$artist $title')] self.i.title = 'three' self.i.artist = 'two' self.i.album = 'one' @@ -183,22 +183,22 @@ def test_destination_preserves_extension(self): self.lib.directory = b'base' - self.lib.path_formats = [(u'default', u'$title')] + self.lib.path_formats = [('default', '$title')] self.i.path = 'hey.audioformat' self.assertEqual(self.i.destination(), np('base/the title.audioformat')) def test_lower_case_extension(self): self.lib.directory = b'base' - self.lib.path_formats = [(u'default', u'$title')] + self.lib.path_formats = [('default', '$title')] self.i.path = 'hey.MP3' self.assertEqual(self.i.destination(), np('base/the title.mp3')) def test_destination_pads_some_indices(self): self.lib.directory = b'base' - self.lib.path_formats = [(u'default', - u'$track $tracktotal $disc $disctotal $bpm')] + self.lib.path_formats = [('default', + '$track $tracktotal $disc $disctotal $bpm')] self.i.track = 1 self.i.tracktotal = 2 self.i.disc = 3 @@ -209,7 +209,7 @@ def test_destination_pads_date_values(self): self.lib.directory = b'base' - self.lib.path_formats = [(u'default', u'$year-$month-$day')] + self.lib.path_formats = [('default', '$year-$month-$day')] self.i.year = 1 self.i.month = 2 self.i.day = 3 @@ -227,7 +227,7 @@ self.i.album = '.something' dest = self.i.destination() self.assertTrue(b'something' in dest) - self.assertFalse(b'/.' in dest) + self.assertFalse(b'/.something' in dest) def test_destination_preserves_legitimate_slashes(self): self.i.artist = 'one' @@ -236,13 +236,13 @@ self.assertTrue(os.path.join(b'one', b'two') in dest) def test_destination_long_names_truncated(self): - self.i.title = u'X' * 300 - self.i.artist = u'Y' * 300 + self.i.title = 'X' * 300 + self.i.artist = 'Y' * 300 for c in self.i.destination().split(util.PATH_SEP): self.assertTrue(len(c) <= 255) def test_destination_long_names_keep_extension(self): - self.i.title = u'X' * 300 + self.i.title = 'X' * 300 self.i.path = b'something.extn' dest = self.i.destination() self.assertEqual(dest[-5:], b'.extn') @@ -257,7 +257,7 @@ self.assertFalse(b'two / three' in p) def test_path_with_format(self): - self.lib.path_formats = [(u'default', u'$artist/$album ($format)')] + self.lib.path_formats = [('default', '$artist/$album ($format)')] p = self.i.destination() self.assertTrue(b'(FLAC)' in p) @@ -265,7 +265,7 @@ i1, i2 = item(), item() self.lib.add_album([i1, i2]) i1.year, i2.year = 2009, 2010 - self.lib.path_formats = [(u'default', u'$album ($year)/$track $title')] + self.lib.path_formats = [('default', '$album ($year)/$track $title')] dest1, dest2 = i1.destination(), i2.destination() self.assertEqual(os.path.dirname(dest1), os.path.dirname(dest2)) @@ -273,17 +273,17 @@ self.i.comp = False self.lib.add_album([self.i]) self.lib.directory = b'one' - self.lib.path_formats = [(u'default', u'two'), - (u'comp:true', u'three')] + self.lib.path_formats = [('default', 'two'), + ('comp:true', 'three')] self.assertEqual(self.i.destination(), np('one/two')) def test_singleton_path(self): i = item(self.lib) self.lib.directory = b'one' self.lib.path_formats = [ - (u'default', u'two'), - (u'singleton:true', u'four'), - (u'comp:true', u'three'), + ('default', 'two'), + ('singleton:true', 'four'), + ('comp:true', 'three'), ] self.assertEqual(i.destination(), np('one/four')) @@ -292,9 +292,9 @@ i.comp = True self.lib.directory = b'one' self.lib.path_formats = [ - (u'default', u'two'), - (u'comp:true', u'three'), - (u'singleton:true', u'four'), + ('default', 'two'), + ('comp:true', 'three'), + ('singleton:true', 'four'), ] self.assertEqual(i.destination(), np('one/three')) @@ -303,32 +303,32 @@ self.lib.add_album([self.i]) self.lib.directory = b'one' self.lib.path_formats = [ - (u'default', u'two'), - (u'comp:true', u'three'), + ('default', 'two'), + ('comp:true', 'three'), ] self.assertEqual(self.i.destination(), np('one/three')) def test_albumtype_query_path(self): self.i.comp = True self.lib.add_album([self.i]) - self.i.albumtype = u'sometype' + self.i.albumtype = 'sometype' self.lib.directory = b'one' self.lib.path_formats = [ - (u'default', u'two'), - (u'albumtype:sometype', u'four'), - (u'comp:true', u'three'), + ('default', 'two'), + ('albumtype:sometype', 'four'), + ('comp:true', 'three'), ] self.assertEqual(self.i.destination(), np('one/four')) def test_albumtype_path_fallback_to_comp(self): self.i.comp = True self.lib.add_album([self.i]) - self.i.albumtype = u'sometype' + self.i.albumtype = 'sometype' self.lib.directory = b'one' self.lib.path_formats = [ - (u'default', u'two'), - (u'albumtype:anothertype', u'four'), - (u'comp:true', u'three'), + ('default', 'two'), + ('albumtype:anothertype', 'four'), + ('comp:true', 'three'), ] self.assertEqual(self.i.destination(), np('one/three')) @@ -349,13 +349,13 @@ with _common.platform_posix(): self.i.bitrate = 12345 val = self.i.formatted().get('bitrate') - self.assertEqual(val, u'12kbps') + self.assertEqual(val, '12kbps') def test_get_formatted_uses_khz_samplerate(self): with _common.platform_posix(): self.i.samplerate = 12345 val = self.i.formatted().get('samplerate') - self.assertEqual(val, u'12kHz') + self.assertEqual(val, '12kHz') def test_get_formatted_datetime(self): with _common.platform_posix(): @@ -367,45 +367,45 @@ with _common.platform_posix(): self.i.some_other_field = None val = self.i.formatted().get('some_other_field') - self.assertEqual(val, u'') + self.assertEqual(val, '') def test_artist_falls_back_to_albumartist(self): - self.i.artist = u'' - self.i.albumartist = u'something' - self.lib.path_formats = [(u'default', u'$artist')] + self.i.artist = '' + self.i.albumartist = 'something' + self.lib.path_formats = [('default', '$artist')] p = self.i.destination() self.assertEqual(p.rsplit(util.PATH_SEP, 1)[1], b'something') def test_albumartist_falls_back_to_artist(self): - self.i.artist = u'trackartist' - self.i.albumartist = u'' - self.lib.path_formats = [(u'default', u'$albumartist')] + self.i.artist = 'trackartist' + self.i.albumartist = '' + self.lib.path_formats = [('default', '$albumartist')] p = self.i.destination() self.assertEqual(p.rsplit(util.PATH_SEP, 1)[1], b'trackartist') def test_artist_overrides_albumartist(self): - self.i.artist = u'theartist' - self.i.albumartist = u'something' - self.lib.path_formats = [(u'default', u'$artist')] + self.i.artist = 'theartist' + self.i.albumartist = 'something' + self.lib.path_formats = [('default', '$artist')] p = self.i.destination() self.assertEqual(p.rsplit(util.PATH_SEP, 1)[1], b'theartist') def test_albumartist_overrides_artist(self): - self.i.artist = u'theartist' - self.i.albumartist = u'something' - self.lib.path_formats = [(u'default', u'$albumartist')] + self.i.artist = 'theartist' + self.i.albumartist = 'something' + self.lib.path_formats = [('default', '$albumartist')] p = self.i.destination() self.assertEqual(p.rsplit(util.PATH_SEP, 1)[1], b'something') def test_unicode_normalized_nfd_on_mac(self): - instr = unicodedata.normalize('NFC', u'caf\xe9') - self.lib.path_formats = [(u'default', instr)] + instr = unicodedata.normalize('NFC', 'caf\xe9') + self.lib.path_formats = [('default', instr)] dest = self.i.destination(platform='darwin', fragment=True) self.assertEqual(dest, unicodedata.normalize('NFD', instr)) def test_unicode_normalized_nfc_on_linux(self): - instr = unicodedata.normalize('NFD', u'caf\xe9') - self.lib.path_formats = [(u'default', instr)] + instr = unicodedata.normalize('NFD', 'caf\xe9') + self.lib.path_formats = [('default', instr)] dest = self.i.destination(platform='linux', fragment=True) self.assertEqual(dest, unicodedata.normalize('NFC', instr)) @@ -413,64 +413,74 @@ oldfunc = sys.getfilesystemencoding sys.getfilesystemencoding = lambda: 'mbcs' try: - self.i.title = u'h\u0259d' - self.lib.path_formats = [(u'default', u'$title')] + self.i.title = 'h\u0259d' + self.lib.path_formats = [('default', '$title')] p = self.i.destination() self.assertFalse(b'?' in p) # We use UTF-8 to encode Windows paths now. - self.assertTrue(u'h\u0259d'.encode('utf-8') in p) + self.assertTrue('h\u0259d'.encode() in p) finally: sys.getfilesystemencoding = oldfunc def test_unicode_extension_in_fragment(self): - self.lib.path_formats = [(u'default', u'foo')] - self.i.path = util.bytestring_path(u'bar.caf\xe9') + self.lib.path_formats = [('default', 'foo')] + self.i.path = util.bytestring_path('bar.caf\xe9') dest = self.i.destination(platform='linux', fragment=True) - self.assertEqual(dest, u'foo.caf\xe9') + self.assertEqual(dest, 'foo.caf\xe9') def test_asciify_and_replace(self): config['asciify_paths'] = True - self.lib.replacements = [(re.compile(u'"'), u'q')] + self.lib.replacements = [(re.compile('"'), 'q')] self.lib.directory = b'lib' - self.lib.path_formats = [(u'default', u'$title')] - self.i.title = u'\u201c\u00f6\u2014\u00cf\u201d' + self.lib.path_formats = [('default', '$title')] + self.i.title = '\u201c\u00f6\u2014\u00cf\u201d' self.assertEqual(self.i.destination(), np('lib/qo--Iq')) def test_asciify_character_expanding_to_slash(self): config['asciify_paths'] = True self.lib.directory = b'lib' - self.lib.path_formats = [(u'default', u'$title')] - self.i.title = u'ab\xa2\xbdd' + self.lib.path_formats = [('default', '$title')] + self.i.title = 'ab\xa2\xbdd' self.assertEqual(self.i.destination(), np('lib/abC_ 1_2 d')) def test_destination_with_replacements(self): self.lib.directory = b'base' - self.lib.replacements = [(re.compile(r'a'), u'e')] - self.lib.path_formats = [(u'default', u'$album/$title')] - self.i.title = u'foo' - self.i.album = u'bar' + self.lib.replacements = [(re.compile(r'a'), 'e')] + self.lib.path_formats = [('default', '$album/$title')] + self.i.title = 'foo' + self.i.album = 'bar' self.assertEqual(self.i.destination(), np('base/ber/foo')) + def test_destination_with_replacements_argument(self): + self.lib.directory = b'base' + self.lib.replacements = [(re.compile(r'a'), 'f')] + self.lib.path_formats = [('default', '$album/$title')] + self.i.title = 'foo' + self.i.album = 'bar' + replacements = [(re.compile(r'a'), 'e')] + self.assertEqual(self.i.destination(replacements=replacements), + np('base/ber/foo')) + @unittest.skip('unimplemented: #359') def test_destination_with_empty_component(self): self.lib.directory = b'base' - self.lib.replacements = [(re.compile(r'^$'), u'_')] - self.lib.path_formats = [(u'default', u'$album/$artist/$title')] - self.i.title = u'three' - self.i.artist = u'' - self.i.albumartist = u'' - self.i.album = u'one' + self.lib.replacements = [(re.compile(r'^$'), '_')] + self.lib.path_formats = [('default', '$album/$artist/$title')] + self.i.title = 'three' + self.i.artist = '' + self.i.albumartist = '' + self.i.album = 'one' self.assertEqual(self.i.destination(), np('base/one/_/three')) @unittest.skip('unimplemented: #359') def test_destination_with_empty_final_component(self): self.lib.directory = b'base' - self.lib.replacements = [(re.compile(r'^$'), u'_')] - self.lib.path_formats = [(u'default', u'$album/$title')] - self.i.title = u'' - self.i.album = u'one' + self.lib.replacements = [(re.compile(r'^$'), '_')] + self.lib.path_formats = [('default', '$album/$title')] + self.i.title = '' + self.i.album = 'one' self.i.path = 'foo.mp3' self.assertEqual(self.i.destination(), np('base/one/_.mp3')) @@ -479,12 +489,12 @@ # Use a replacement that should always replace the last X in any # path component with a Z. self.lib.replacements = [ - (re.compile(r'X$'), u'Z'), + (re.compile(r'X$'), 'Z'), ] # Construct an item whose untruncated path ends with a Y but whose # truncated version ends with an X. - self.i.title = u'X' * 300 + u'Y' + self.i.title = 'X' * 300 + 'Y' # The final path should reflect the replacement. dest = self.i.destination() @@ -494,12 +504,12 @@ # Use a replacement that should always replace the last X in any # path component with four Zs. self.lib.replacements = [ - (re.compile(r'X$'), u'ZZZZ'), + (re.compile(r'X$'), 'ZZZZ'), ] # Construct an item whose untruncated path ends with a Y but whose # truncated version ends with an X. - self.i.title = u'X' * 300 + u'Y' + self.i.title = 'X' * 300 + 'Y' # The final path should ignore the user replacement and create a path # of the correct length, containing Xs. @@ -508,19 +518,19 @@ def test_album_field_query(self): self.lib.directory = b'one' - self.lib.path_formats = [(u'default', u'two'), - (u'flex:foo', u'three')] + self.lib.path_formats = [('default', 'two'), + ('flex:foo', 'three')] album = self.lib.add_album([self.i]) self.assertEqual(self.i.destination(), np('one/two')) - album['flex'] = u'foo' + album['flex'] = 'foo' album.store() self.assertEqual(self.i.destination(), np('one/three')) def test_album_field_in_template(self): self.lib.directory = b'one' - self.lib.path_formats = [(u'default', u'$flex/two')] + self.lib.path_formats = [('default', '$flex/two')] album = self.lib.add_album([self.i]) - album['flex'] = u'foo' + album['flex'] = 'foo' album.store() self.assertEqual(self.i.destination(), np('one/foo/two')) @@ -528,7 +538,7 @@ class ItemFormattedMappingTest(_common.LibTestCase): def test_formatted_item_value(self): formatted = self.i.formatted() - self.assertEqual(formatted['artist'], u'the artist') + self.assertEqual(formatted['artist'], 'the artist') def test_get_unset_field(self): formatted = self.i.formatted() @@ -537,57 +547,57 @@ def test_get_method_with_default(self): formatted = self.i.formatted() - self.assertEqual(formatted.get('other_field'), u'') + self.assertEqual(formatted.get('other_field'), '') def test_get_method_with_specified_default(self): formatted = self.i.formatted() - self.assertEqual(formatted.get('other_field', u'default'), u'default') + self.assertEqual(formatted.get('other_field', 'default'), 'default') def test_item_precedence(self): album = self.lib.add_album([self.i]) - album['artist'] = u'foo' + album['artist'] = 'foo' album.store() - self.assertNotEqual(u'foo', self.i.formatted().get('artist')) + self.assertNotEqual('foo', self.i.formatted().get('artist')) def test_album_flex_field(self): album = self.lib.add_album([self.i]) - album['flex'] = u'foo' + album['flex'] = 'foo' album.store() - self.assertEqual(u'foo', self.i.formatted().get('flex')) + self.assertEqual('foo', self.i.formatted().get('flex')) def test_album_field_overrides_item_field_for_path(self): # Make the album inconsistent with the item. album = self.lib.add_album([self.i]) - album.album = u'foo' + album.album = 'foo' album.store() - self.i.album = u'bar' + self.i.album = 'bar' self.i.store() # Ensure the album takes precedence. formatted = self.i.formatted(for_path=True) - self.assertEqual(formatted['album'], u'foo') + self.assertEqual(formatted['album'], 'foo') def test_artist_falls_back_to_albumartist(self): - self.i.artist = u'' + self.i.artist = '' formatted = self.i.formatted() - self.assertEqual(formatted['artist'], u'the album artist') + self.assertEqual(formatted['artist'], 'the album artist') def test_albumartist_falls_back_to_artist(self): - self.i.albumartist = u'' + self.i.albumartist = '' formatted = self.i.formatted() - self.assertEqual(formatted['albumartist'], u'the artist') + self.assertEqual(formatted['albumartist'], 'the artist') def test_both_artist_and_albumartist_empty(self): - self.i.artist = u'' - self.i.albumartist = u'' + self.i.artist = '' + self.i.albumartist = '' formatted = self.i.formatted() - self.assertEqual(formatted['albumartist'], u'') + self.assertEqual(formatted['albumartist'], '') -class PathFormattingMixin(object): +class PathFormattingMixin: """Utilities for testing path formatting.""" def _setf(self, fmt): - self.lib.path_formats.insert(0, (u'default', fmt)) + self.lib.path_formats.insert(0, ('default', fmt)) def _assert_dest(self, dest, i=None): if i is None: @@ -599,119 +609,119 @@ class DestinationFunctionTest(_common.TestCase, PathFormattingMixin): def setUp(self): - super(DestinationFunctionTest, self).setUp() + super().setUp() self.lib = beets.library.Library(':memory:') self.lib.directory = b'/base' - self.lib.path_formats = [(u'default', u'path')] + self.lib.path_formats = [('default', 'path')] self.i = item(self.lib) def tearDown(self): - super(DestinationFunctionTest, self).tearDown() + super().tearDown() self.lib._connection().close() def test_upper_case_literal(self): - self._setf(u'%upper{foo}') + self._setf('%upper{foo}') self._assert_dest(b'/base/FOO') def test_upper_case_variable(self): - self._setf(u'%upper{$title}') + self._setf('%upper{$title}') self._assert_dest(b'/base/THE TITLE') def test_title_case_variable(self): - self._setf(u'%title{$title}') + self._setf('%title{$title}') self._assert_dest(b'/base/The Title') def test_title_case_variable_aphostrophe(self): - self._setf(u'%title{I can\'t}') + self._setf('%title{I can\'t}') self._assert_dest(b'/base/I Can\'t') def test_asciify_variable(self): - self._setf(u'%asciify{ab\xa2\xbdd}') + self._setf('%asciify{ab\xa2\xbdd}') self._assert_dest(b'/base/abC_ 1_2 d') def test_left_variable(self): - self._setf(u'%left{$title, 3}') + self._setf('%left{$title, 3}') self._assert_dest(b'/base/the') def test_right_variable(self): - self._setf(u'%right{$title,3}') + self._setf('%right{$title,3}') self._assert_dest(b'/base/tle') def test_if_false(self): - self._setf(u'x%if{,foo}') + self._setf('x%if{,foo}') self._assert_dest(b'/base/x') def test_if_false_value(self): - self._setf(u'x%if{false,foo}') + self._setf('x%if{false,foo}') self._assert_dest(b'/base/x') def test_if_true(self): - self._setf(u'%if{bar,foo}') + self._setf('%if{bar,foo}') self._assert_dest(b'/base/foo') def test_if_else_false(self): - self._setf(u'%if{,foo,baz}') + self._setf('%if{,foo,baz}') self._assert_dest(b'/base/baz') def test_if_else_false_value(self): - self._setf(u'%if{false,foo,baz}') + self._setf('%if{false,foo,baz}') self._assert_dest(b'/base/baz') def test_if_int_value(self): - self._setf(u'%if{0,foo,baz}') + self._setf('%if{0,foo,baz}') self._assert_dest(b'/base/baz') def test_nonexistent_function(self): - self._setf(u'%foo{bar}') + self._setf('%foo{bar}') self._assert_dest(b'/base/%foo{bar}') def test_if_def_field_return_self(self): self.i.bar = 3 - self._setf(u'%ifdef{bar}') + self._setf('%ifdef{bar}') self._assert_dest(b'/base/3') def test_if_def_field_not_defined(self): - self._setf(u' %ifdef{bar}/$artist') + self._setf(' %ifdef{bar}/$artist') self._assert_dest(b'/base/the artist') def test_if_def_field_not_defined_2(self): - self._setf(u'$artist/%ifdef{bar}') + self._setf('$artist/%ifdef{bar}') self._assert_dest(b'/base/the artist') def test_if_def_true(self): - self._setf(u'%ifdef{artist,cool}') + self._setf('%ifdef{artist,cool}') self._assert_dest(b'/base/cool') def test_if_def_true_complete(self): self.i.series = "Now" - self._setf(u'%ifdef{series,$series Series,Albums}/$album') + self._setf('%ifdef{series,$series Series,Albums}/$album') self._assert_dest(b'/base/Now Series/the album') def test_if_def_false_complete(self): - self._setf(u'%ifdef{plays,$plays,not_played}') + self._setf('%ifdef{plays,$plays,not_played}') self._assert_dest(b'/base/not_played') def test_first(self): self.i.genres = "Pop; Rock; Classical Crossover" - self._setf(u'%first{$genres}') + self._setf('%first{$genres}') self._assert_dest(b'/base/Pop') def test_first_skip(self): self.i.genres = "Pop; Rock; Classical Crossover" - self._setf(u'%first{$genres,1,2}') + self._setf('%first{$genres,1,2}') self._assert_dest(b'/base/Classical Crossover') def test_first_different_sep(self): - self._setf(u'%first{Alice / Bob / Eve,2,0, / , & }') + self._setf('%first{Alice / Bob / Eve,2,0, / , & }') self._assert_dest(b'/base/Alice & Bob') class DisambiguationTest(_common.TestCase, PathFormattingMixin): def setUp(self): - super(DisambiguationTest, self).setUp() + super().setUp() self.lib = beets.library.Library(':memory:') self.lib.directory = b'/base' - self.lib.path_formats = [(u'default', u'path')] + self.lib.path_formats = [('default', 'path')] self.i1 = item() self.i1.year = 2001 @@ -721,10 +731,10 @@ self.lib.add_album([self.i2]) self.lib._connection().commit() - self._setf(u'foo%aunique{albumartist album,year}/$title') + self._setf('foo%aunique{albumartist album,year}/$title') def tearDown(self): - super(DisambiguationTest, self).tearDown() + super().tearDown() self.lib._connection().close() def test_unique_expands_to_disambiguating_year(self): @@ -732,14 +742,14 @@ def test_unique_with_default_arguments_uses_albumtype(self): album2 = self.lib.get_album(self.i1) - album2.albumtype = u'bar' + album2.albumtype = 'bar' album2.store() - self._setf(u'foo%aunique{}/$title') + self._setf('foo%aunique{}/$title') self._assert_dest(b'/base/foo [bar]/the title', self.i1) def test_unique_expands_to_nothing_for_distinct_albums(self): album2 = self.lib.get_album(self.i2) - album2.album = u'different album' + album2.album = 'different album' album2.store() self._assert_dest(b'/base/foo/the title', self.i1) @@ -753,41 +763,51 @@ self._assert_dest(b'/base/foo [2]/the title', self.i2) def test_unique_falls_back_to_second_distinguishing_field(self): - self._setf(u'foo%aunique{albumartist album,month year}/$title') + self._setf('foo%aunique{albumartist album,month year}/$title') self._assert_dest(b'/base/foo [2001]/the title', self.i1) def test_unique_sanitized(self): album2 = self.lib.get_album(self.i2) album2.year = 2001 album1 = self.lib.get_album(self.i1) - album1.albumtype = u'foo/bar' + album1.albumtype = 'foo/bar' album2.store() album1.store() - self._setf(u'foo%aunique{albumartist album,albumtype}/$title') + self._setf('foo%aunique{albumartist album,albumtype}/$title') self._assert_dest(b'/base/foo [foo_bar]/the title', self.i1) def test_drop_empty_disambig_string(self): album1 = self.lib.get_album(self.i1) album1.albumdisambig = None album2 = self.lib.get_album(self.i2) - album2.albumdisambig = u'foo' + album2.albumdisambig = 'foo' album1.store() album2.store() - self._setf(u'foo%aunique{albumartist album,albumdisambig}/$title') + self._setf('foo%aunique{albumartist album,albumdisambig}/$title') self._assert_dest(b'/base/foo/the title', self.i1) def test_change_brackets(self): - self._setf(u'foo%aunique{albumartist album,year,()}/$title') + self._setf('foo%aunique{albumartist album,year,()}/$title') self._assert_dest(b'/base/foo (2001)/the title', self.i1) def test_remove_brackets(self): - self._setf(u'foo%aunique{albumartist album,year,}/$title') + self._setf('foo%aunique{albumartist album,year,}/$title') self._assert_dest(b'/base/foo 2001/the title', self.i1) + def test_key_flexible_attribute(self): + album1 = self.lib.get_album(self.i1) + album1.flex = 'flex1' + album2 = self.lib.get_album(self.i2) + album2.flex = 'flex2' + album1.store() + album2.store() + self._setf('foo%aunique{albumartist album flex,year}/$title') + self._assert_dest(b'/base/foo/the title', self.i1) + class PluginDestinationTest(_common.TestCase): def setUp(self): - super(PluginDestinationTest, self).setUp() + super().setUp() # Mock beets.plugins.item_field_getters. self._tv_map = {} @@ -803,11 +823,11 @@ self.lib = beets.library.Library(':memory:') self.lib.directory = b'/base' - self.lib.path_formats = [(u'default', u'$artist $foo')] + self.lib.path_formats = [('default', '$artist $foo')] self.i = item(self.lib) def tearDown(self): - super(PluginDestinationTest, self).tearDown() + super().tearDown() plugins.item_field_getters = self.old_field_getters def _assert_dest(self, dest): @@ -839,7 +859,7 @@ class AlbumInfoTest(_common.TestCase): def setUp(self): - super(AlbumInfoTest, self).setUp() + super().setUp() self.lib = beets.library.Library(':memory:') self.i = item() self.lib.add_album((self.i,)) @@ -871,7 +891,7 @@ def test_individual_tracks_have_no_albuminfo(self): i2 = item() - i2.album = u'aTotallyDifferentAlbum' + i2.album = 'aTotallyDifferentAlbum' self.lib.add(i2) ai = self.lib.get_album(i2) self.assertEqual(ai, None) @@ -887,29 +907,29 @@ if i.id == self.i.id: break else: - self.fail(u"item not found") + self.fail("item not found") def test_albuminfo_changes_affect_items(self): ai = self.lib.get_album(self.i) - ai.album = u'myNewAlbum' + ai.album = 'myNewAlbum' ai.store() i = self.lib.items()[0] - self.assertEqual(i.album, u'myNewAlbum') + self.assertEqual(i.album, 'myNewAlbum') def test_albuminfo_change_albumartist_changes_items(self): ai = self.lib.get_album(self.i) - ai.albumartist = u'myNewArtist' + ai.albumartist = 'myNewArtist' ai.store() i = self.lib.items()[0] - self.assertEqual(i.albumartist, u'myNewArtist') - self.assertNotEqual(i.artist, u'myNewArtist') + self.assertEqual(i.albumartist, 'myNewArtist') + self.assertNotEqual(i.artist, 'myNewArtist') def test_albuminfo_change_artist_does_not_change_items(self): ai = self.lib.get_album(self.i) - ai.artist = u'myNewArtist' + ai.artist = 'myNewArtist' ai.store() i = self.lib.items()[0] - self.assertNotEqual(i.artist, u'myNewArtist') + self.assertNotEqual(i.artist, 'myNewArtist') def test_albuminfo_remove_removes_items(self): item_id = self.i.id @@ -926,7 +946,7 @@ def test_noop_albuminfo_changes_affect_items(self): i = self.lib.items()[0] - i.album = u'foobar' + i.album = 'foobar' i.store() ai = self.lib.get_album(self.i) ai.album = ai.album @@ -937,11 +957,11 @@ class ArtDestinationTest(_common.TestCase): def setUp(self): - super(ArtDestinationTest, self).setUp() - config['art_filename'] = u'artimage' - config['replace'] = {u'X': u'Y'} + super().setUp() + config['art_filename'] = 'artimage' + config['replace'] = {'X': 'Y'} self.lib = beets.library.Library( - ':memory:', replacements=[(re.compile(u'X'), u'Y')] + ':memory:', replacements=[(re.compile('X'), 'Y')] ) self.i = item(self.lib) self.i.path = self.i.destination() @@ -958,14 +978,14 @@ self.assertEqual(os.path.dirname(art), os.path.dirname(track)) def test_art_path_sanitized(self): - config['art_filename'] = u'artXimage' + config['art_filename'] = 'artXimage' art = self.ai.art_destination('something.jpg') self.assertTrue(b'artYimage' in art) class PathStringTest(_common.TestCase): def setUp(self): - super(PathStringTest, self).setUp() + super().setUp() self.lib = beets.library.Library(':memory:') self.i = item(self.lib) @@ -977,18 +997,18 @@ self.assertTrue(isinstance(i.path, bytes)) def test_unicode_path_becomes_bytestring(self): - self.i.path = u'unicodepath' + self.i.path = 'unicodepath' self.assertTrue(isinstance(self.i.path, bytes)) def test_unicode_in_database_becomes_bytestring(self): self.lib._connection().execute(""" update items set path=? where id=? - """, (self.i.id, u'somepath')) + """, (self.i.id, 'somepath')) i = list(self.lib.items())[0] self.assertTrue(isinstance(i.path, bytes)) def test_special_chars_preserved_in_database(self): - path = u'b\xe1r'.encode('utf-8') + path = 'b\xe1r'.encode() self.i.path = path self.i.store() i = list(self.lib.items())[0] @@ -996,7 +1016,7 @@ def test_special_char_path_added_to_database(self): self.i.remove() - path = u'b\xe1r'.encode('utf-8') + path = 'b\xe1r'.encode() i = item() i.path = path self.lib.add(i) @@ -1004,14 +1024,14 @@ self.assertEqual(i.path, path) def test_destination_returns_bytestring(self): - self.i.artist = u'b\xe1r' + self.i.artist = 'b\xe1r' dest = self.i.destination() self.assertTrue(isinstance(dest, bytes)) def test_art_destination_returns_bytestring(self): - self.i.artist = u'b\xe1r' + self.i.artist = 'b\xe1r' alb = self.lib.add_album([self.i]) - dest = alb.art_destination(u'image.jpg') + dest = alb.art_destination('image.jpg') self.assertTrue(isinstance(dest, bytes)) def test_artpath_stores_special_chars(self): @@ -1023,25 +1043,25 @@ self.assertEqual(path, alb.artpath) def test_sanitize_path_with_special_chars(self): - path = u'b\xe1r?' + path = 'b\xe1r?' new_path = util.sanitize_path(path) - self.assertTrue(new_path.startswith(u'b\xe1r')) + self.assertTrue(new_path.startswith('b\xe1r')) def test_sanitize_path_returns_unicode(self): - path = u'b\xe1r?' + path = 'b\xe1r?' new_path = util.sanitize_path(path) - self.assertTrue(isinstance(new_path, six.text_type)) + self.assertTrue(isinstance(new_path, str)) def test_unicode_artpath_becomes_bytestring(self): alb = self.lib.add_album([self.i]) - alb.artpath = u'somep\xe1th' + alb.artpath = 'somep\xe1th' self.assertTrue(isinstance(alb.artpath, bytes)) def test_unicode_artpath_in_database_decoded(self): alb = self.lib.add_album([self.i]) self.lib._connection().execute( "update albums set artpath=? where id=?", - (u'somep\xe1th', alb.id) + ('somep\xe1th', alb.id) ) alb = self.lib.get_album(alb.id) self.assertTrue(isinstance(alb.artpath, bytes)) @@ -1049,7 +1069,7 @@ class MtimeTest(_common.TestCase): def setUp(self): - super(MtimeTest, self).setUp() + super().setUp() self.ipath = os.path.join(self.temp_dir, b'testfile.mp3') shutil.copy(os.path.join(_common.RSRC, b'full.mp3'), self.ipath) self.i = beets.library.Item.from_path(self.ipath) @@ -1057,7 +1077,7 @@ self.lib.add(self.i) def tearDown(self): - super(MtimeTest, self).tearDown() + super().tearDown() if os.path.exists(self.ipath): os.remove(self.ipath) @@ -1068,23 +1088,23 @@ self.assertGreaterEqual(self.i.mtime, self._mtime()) def test_mtime_reset_on_db_modify(self): - self.i.title = u'something else' + self.i.title = 'something else' self.assertLess(self.i.mtime, self._mtime()) def test_mtime_up_to_date_after_write(self): - self.i.title = u'something else' + self.i.title = 'something else' self.i.write() self.assertGreaterEqual(self.i.mtime, self._mtime()) def test_mtime_up_to_date_after_read(self): - self.i.title = u'something else' + self.i.title = 'something else' self.i.read() self.assertGreaterEqual(self.i.mtime, self._mtime()) class ImportTimeTest(_common.TestCase): def setUp(self): - super(ImportTimeTest, self).setUp() + super().setUp() self.lib = beets.library.Library(':memory:') def added(self): @@ -1102,36 +1122,36 @@ def test_year_formatted_in_template(self): self.i.year = 123 self.i.store() - self.assertEqual(self.i.evaluate_template('$year'), u'0123') + self.assertEqual(self.i.evaluate_template('$year'), '0123') def test_album_flexattr_appears_in_item_template(self): self.album = self.lib.add_album([self.i]) - self.album.foo = u'baz' + self.album.foo = 'baz' self.album.store() - self.assertEqual(self.i.evaluate_template('$foo'), u'baz') + self.assertEqual(self.i.evaluate_template('$foo'), 'baz') def test_album_and_item_format(self): - config['format_album'] = u'foö $foo' + config['format_album'] = 'foö $foo' album = beets.library.Album() - album.foo = u'bar' - album.tagada = u'togodo' - self.assertEqual(u"{0}".format(album), u"foö bar") - self.assertEqual(u"{0:$tagada}".format(album), u"togodo") - self.assertEqual(six.text_type(album), u"foö bar") + album.foo = 'bar' + album.tagada = 'togodo' + self.assertEqual(f"{album}", "foö bar") + self.assertEqual(f"{album:$tagada}", "togodo") + self.assertEqual(str(album), "foö bar") self.assertEqual(bytes(album), b"fo\xc3\xb6 bar") - config['format_item'] = u'bar $foo' + config['format_item'] = 'bar $foo' item = beets.library.Item() - item.foo = u'bar' - item.tagada = u'togodo' - self.assertEqual(u"{0}".format(item), u"bar bar") - self.assertEqual(u"{0:$tagada}".format(item), u"togodo") + item.foo = 'bar' + item.tagada = 'togodo' + self.assertEqual(f"{item}", "bar bar") + self.assertEqual(f"{item:$tagada}", "togodo") class UnicodePathTest(_common.LibTestCase): def test_unicode_path(self): self.i.path = os.path.join(_common.RSRC, - u'unicode\u2019d.mp3'.encode('utf-8')) + 'unicode\u2019d.mp3'.encode()) # If there are any problems with unicode paths, we will raise # here and fail. self.i.read() @@ -1188,7 +1208,7 @@ # Since `date` is not a MediaField, this should do nothing. item = self.add_item_fixture() clean_year = item.year - item.date = u'foo' + item.date = 'foo' item.write() self.assertEqual(MediaFile(syspath(item.path)).year, clean_year) @@ -1228,7 +1248,7 @@ class ParseQueryTest(unittest.TestCase): def test_parse_invalid_query_string(self): with self.assertRaises(beets.dbcore.InvalidQueryError) as raised: - beets.library.parse_query_string(u'foo"', None) + beets.library.parse_query_string('foo"', None) self.assertIsInstance(raised.exception, beets.dbcore.query.ParsingError) @@ -1249,42 +1269,42 @@ self.assertEqual(time_local, t.format(123456789)) # parse self.assertEqual(123456789.0, t.parse(time_local)) - self.assertEqual(123456789.0, t.parse(u'123456789.0')) - self.assertEqual(t.null, t.parse(u'not123456789.0')) - self.assertEqual(t.null, t.parse(u'1973-11-29')) + self.assertEqual(123456789.0, t.parse('123456789.0')) + self.assertEqual(t.null, t.parse('not123456789.0')) + self.assertEqual(t.null, t.parse('1973-11-29')) def test_pathtype(self): t = beets.library.PathType() # format self.assertEqual('/tmp', t.format('/tmp')) - self.assertEqual(u'/tmp/\xe4lbum', t.format(u'/tmp/\u00e4lbum')) + self.assertEqual('/tmp/\xe4lbum', t.format('/tmp/\u00e4lbum')) # parse self.assertEqual(np(b'/tmp'), t.parse('/tmp')) self.assertEqual(np(b'/tmp/\xc3\xa4lbum'), - t.parse(u'/tmp/\u00e4lbum/')) + t.parse('/tmp/\u00e4lbum/')) def test_musicalkey(self): t = beets.library.MusicalKey() # parse - self.assertEqual(u'C#m', t.parse(u'c#m')) - self.assertEqual(u'Gm', t.parse(u'g minor')) - self.assertEqual(u'Not c#m', t.parse(u'not C#m')) + self.assertEqual('C#m', t.parse('c#m')) + self.assertEqual('Gm', t.parse('g minor')) + self.assertEqual('Not c#m', t.parse('not C#m')) def test_durationtype(self): t = beets.library.DurationType() # format - self.assertEqual(u'1:01', t.format(61.23)) - self.assertEqual(u'60:01', t.format(3601.23)) - self.assertEqual(u'0:00', t.format(None)) + self.assertEqual('1:01', t.format(61.23)) + self.assertEqual('60:01', t.format(3601.23)) + self.assertEqual('0:00', t.format(None)) # parse - self.assertEqual(61.0, t.parse(u'1:01')) - self.assertEqual(61.23, t.parse(u'61.23')) - self.assertEqual(3601.0, t.parse(u'60:01')) - self.assertEqual(t.null, t.parse(u'1:00:01')) - self.assertEqual(t.null, t.parse(u'not61.23')) + self.assertEqual(61.0, t.parse('1:01')) + self.assertEqual(61.23, t.parse('61.23')) + self.assertEqual(3601.0, t.parse('60:01')) + self.assertEqual(t.null, t.parse('1:00:01')) + self.assertEqual(t.null, t.parse('not61.23')) # config format_raw_length beets.config['format_raw_length'] = True self.assertEqual(61.23, t.format(61.23)) diff -Nru beets-1.5.0/test/test_logging.py beets-1.6.0/test/test_logging.py --- beets-1.5.0/test/test_logging.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_logging.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- - """Stupid tests that ensure logging works as expected""" -from __future__ import division, absolute_import, print_function import sys import threading @@ -15,7 +12,6 @@ from test import _common from test._common import TestCase from test import helper -import six class LoggingTest(TestCase): @@ -46,13 +42,13 @@ l.addHandler(handler) l.propagate = False - l.warning(u"foo {0} {bar}", "oof", bar=u"baz") + l.warning("foo {0} {bar}", "oof", bar="baz") handler.flush() - self.assertTrue(stream.getvalue(), u"foo oof baz") + self.assertTrue(stream.getvalue(), "foo oof baz") class LoggingLevelTest(unittest.TestCase, helper.TestHelper): - class DummyModule(object): + class DummyModule: class DummyPlugin(plugins.BeetsPlugin): def __init__(self): plugins.BeetsPlugin.__init__(self, 'dummy') @@ -60,9 +56,9 @@ self.register_listener('dummy_event', self.listener) def log_all(self, name): - self._log.debug(u'debug ' + name) - self._log.info(u'info ' + name) - self._log.warning(u'warning ' + name) + self._log.debug('debug ' + name) + self._log.info('info ' + name) + self._log.warning('warning ' + name) def commands(self): cmd = ui.Subcommand('dummy') @@ -93,76 +89,76 @@ self.config['verbose'] = 0 with helper.capture_log() as logs: self.run_command('dummy') - self.assertIn(u'dummy: warning cmd', logs) - self.assertIn(u'dummy: info cmd', logs) - self.assertNotIn(u'dummy: debug cmd', logs) + self.assertIn('dummy: warning cmd', logs) + self.assertIn('dummy: info cmd', logs) + self.assertNotIn('dummy: debug cmd', logs) def test_command_level1(self): self.config['verbose'] = 1 with helper.capture_log() as logs: self.run_command('dummy') - self.assertIn(u'dummy: warning cmd', logs) - self.assertIn(u'dummy: info cmd', logs) - self.assertIn(u'dummy: debug cmd', logs) + self.assertIn('dummy: warning cmd', logs) + self.assertIn('dummy: info cmd', logs) + self.assertIn('dummy: debug cmd', logs) def test_command_level2(self): self.config['verbose'] = 2 with helper.capture_log() as logs: self.run_command('dummy') - self.assertIn(u'dummy: warning cmd', logs) - self.assertIn(u'dummy: info cmd', logs) - self.assertIn(u'dummy: debug cmd', logs) + self.assertIn('dummy: warning cmd', logs) + self.assertIn('dummy: info cmd', logs) + self.assertIn('dummy: debug cmd', logs) def test_listener_level0(self): self.config['verbose'] = 0 with helper.capture_log() as logs: plugins.send('dummy_event') - self.assertIn(u'dummy: warning listener', logs) - self.assertNotIn(u'dummy: info listener', logs) - self.assertNotIn(u'dummy: debug listener', logs) + self.assertIn('dummy: warning listener', logs) + self.assertNotIn('dummy: info listener', logs) + self.assertNotIn('dummy: debug listener', logs) def test_listener_level1(self): self.config['verbose'] = 1 with helper.capture_log() as logs: plugins.send('dummy_event') - self.assertIn(u'dummy: warning listener', logs) - self.assertIn(u'dummy: info listener', logs) - self.assertNotIn(u'dummy: debug listener', logs) + self.assertIn('dummy: warning listener', logs) + self.assertIn('dummy: info listener', logs) + self.assertNotIn('dummy: debug listener', logs) def test_listener_level2(self): self.config['verbose'] = 2 with helper.capture_log() as logs: plugins.send('dummy_event') - self.assertIn(u'dummy: warning listener', logs) - self.assertIn(u'dummy: info listener', logs) - self.assertIn(u'dummy: debug listener', logs) + self.assertIn('dummy: warning listener', logs) + self.assertIn('dummy: info listener', logs) + self.assertIn('dummy: debug listener', logs) def test_import_stage_level0(self): self.config['verbose'] = 0 with helper.capture_log() as logs: importer = self.create_importer() importer.run() - self.assertIn(u'dummy: warning import_stage', logs) - self.assertNotIn(u'dummy: info import_stage', logs) - self.assertNotIn(u'dummy: debug import_stage', logs) + self.assertIn('dummy: warning import_stage', logs) + self.assertNotIn('dummy: info import_stage', logs) + self.assertNotIn('dummy: debug import_stage', logs) def test_import_stage_level1(self): self.config['verbose'] = 1 with helper.capture_log() as logs: importer = self.create_importer() importer.run() - self.assertIn(u'dummy: warning import_stage', logs) - self.assertIn(u'dummy: info import_stage', logs) - self.assertNotIn(u'dummy: debug import_stage', logs) + self.assertIn('dummy: warning import_stage', logs) + self.assertIn('dummy: info import_stage', logs) + self.assertNotIn('dummy: debug import_stage', logs) def test_import_stage_level2(self): self.config['verbose'] = 2 with helper.capture_log() as logs: importer = self.create_importer() importer.run() - self.assertIn(u'dummy: warning import_stage', logs) - self.assertIn(u'dummy: info import_stage', logs) - self.assertIn(u'dummy: debug import_stage', logs) + self.assertIn('dummy: warning import_stage', logs) + self.assertIn('dummy: info import_stage', logs) + self.assertIn('dummy: debug import_stage', logs) @_common.slow_test() @@ -183,9 +179,9 @@ self.t1_step = self.t2_step = 0 def log_all(self, name): - self._log.debug(u'debug ' + name) - self._log.info(u'info ' + name) - self._log.warning(u'warning ' + name) + self._log.debug('debug ' + name) + self._log.info('info ' + name) + self._log.warning('warning ' + name) def listener1(self): try: @@ -220,7 +216,7 @@ def check_dp_exc(): if dp.exc_info: - six.reraise(dp.exc_info[1], None, dp.exc_info[2]) + raise None.with_traceback(dp.exc_info[2]) try: dp.lock1.acquire() @@ -258,14 +254,14 @@ self.assertFalse(t2.is_alive()) except Exception: - print(u"Alive threads:", threading.enumerate()) + print("Alive threads:", threading.enumerate()) if dp.lock1.locked(): - print(u"Releasing lock1 after exception in test") + print("Releasing lock1 after exception in test") dp.lock1.release() if dp.lock2.locked(): - print(u"Releasing lock2 after exception in test") + print("Releasing lock2 after exception in test") dp.lock2.release() - print(u"Alive threads:", threading.enumerate()) + print("Alive threads:", threading.enumerate()) raise def test_root_logger_levels(self): @@ -284,14 +280,14 @@ importer = self.create_importer() importer.run() for l in logs: - self.assertIn(u"import", l) - self.assertIn(u"album", l) + self.assertIn("import", l) + self.assertIn("album", l) blog.getLogger('beets').set_global_level(blog.DEBUG) with helper.capture_log() as logs: importer = self.create_importer() importer.run() - self.assertIn(u"Sending event: database_change", logs) + self.assertIn("Sending event: database_change", logs) def suite(): diff -Nru beets-1.5.0/test/test_lyrics.py beets-1.6.0/test/test_lyrics.py --- beets-1.5.0/test/test_lyrics.py 2021-03-28 18:23:23.000000000 +0000 +++ beets-1.6.0/test/test_lyrics.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte. # @@ -15,18 +14,14 @@ """Tests for the 'lyrics' plugin.""" -from __future__ import absolute_import, division, print_function import itertools -from io import open import os import re -import six -import sys import unittest import confuse -from mock import MagicMock, patch +from unittest.mock import MagicMock, patch from beets import logging from beets.library import Item @@ -48,72 +43,72 @@ lyrics.LyricsPlugin() def test_search_artist(self): - item = Item(artist=u'Alice ft. Bob', title=u'song') - self.assertIn((u'Alice ft. Bob', [u'song']), + item = Item(artist='Alice ft. Bob', title='song') + self.assertIn(('Alice ft. Bob', ['song']), lyrics.search_pairs(item)) - self.assertIn((u'Alice', [u'song']), + self.assertIn(('Alice', ['song']), lyrics.search_pairs(item)) - item = Item(artist=u'Alice feat Bob', title=u'song') - self.assertIn((u'Alice feat Bob', [u'song']), + item = Item(artist='Alice feat Bob', title='song') + self.assertIn(('Alice feat Bob', ['song']), lyrics.search_pairs(item)) - self.assertIn((u'Alice', [u'song']), + self.assertIn(('Alice', ['song']), lyrics.search_pairs(item)) - item = Item(artist=u'Alice feat. Bob', title=u'song') - self.assertIn((u'Alice feat. Bob', [u'song']), + item = Item(artist='Alice feat. Bob', title='song') + self.assertIn(('Alice feat. Bob', ['song']), lyrics.search_pairs(item)) - self.assertIn((u'Alice', [u'song']), + self.assertIn(('Alice', ['song']), lyrics.search_pairs(item)) - item = Item(artist=u'Alice feats Bob', title=u'song') - self.assertIn((u'Alice feats Bob', [u'song']), + item = Item(artist='Alice feats Bob', title='song') + self.assertIn(('Alice feats Bob', ['song']), lyrics.search_pairs(item)) - self.assertNotIn((u'Alice', [u'song']), + self.assertNotIn(('Alice', ['song']), lyrics.search_pairs(item)) - item = Item(artist=u'Alice featuring Bob', title=u'song') - self.assertIn((u'Alice featuring Bob', [u'song']), + item = Item(artist='Alice featuring Bob', title='song') + self.assertIn(('Alice featuring Bob', ['song']), lyrics.search_pairs(item)) - self.assertIn((u'Alice', [u'song']), + self.assertIn(('Alice', ['song']), lyrics.search_pairs(item)) - item = Item(artist=u'Alice & Bob', title=u'song') - self.assertIn((u'Alice & Bob', [u'song']), + item = Item(artist='Alice & Bob', title='song') + self.assertIn(('Alice & Bob', ['song']), lyrics.search_pairs(item)) - self.assertIn((u'Alice', [u'song']), + self.assertIn(('Alice', ['song']), lyrics.search_pairs(item)) - item = Item(artist=u'Alice and Bob', title=u'song') - self.assertIn((u'Alice and Bob', [u'song']), + item = Item(artist='Alice and Bob', title='song') + self.assertIn(('Alice and Bob', ['song']), lyrics.search_pairs(item)) - self.assertIn((u'Alice', [u'song']), + self.assertIn(('Alice', ['song']), lyrics.search_pairs(item)) - item = Item(artist=u'Alice and Bob', title=u'song') - self.assertEqual((u'Alice and Bob', [u'song']), + item = Item(artist='Alice and Bob', title='song') + self.assertEqual(('Alice and Bob', ['song']), list(lyrics.search_pairs(item))[0]) def test_search_artist_sort(self): - item = Item(artist=u'CHVRCHΞS', title=u'song', artist_sort=u'CHVRCHES') - self.assertIn((u'CHVRCHΞS', [u'song']), + item = Item(artist='CHVRCHΞS', title='song', artist_sort='CHVRCHES') + self.assertIn(('CHVRCHΞS', ['song']), lyrics.search_pairs(item)) - self.assertIn((u'CHVRCHES', [u'song']), + self.assertIn(('CHVRCHES', ['song']), lyrics.search_pairs(item)) # Make sure that the original artist name is still the first entry - self.assertEqual((u'CHVRCHΞS', [u'song']), + self.assertEqual(('CHVRCHΞS', ['song']), list(lyrics.search_pairs(item))[0]) - item = Item(artist=u'横山克', title=u'song', - artist_sort=u'Masaru Yokoyama') - self.assertIn((u'横山克', [u'song']), + item = Item(artist='横山克', title='song', + artist_sort='Masaru Yokoyama') + self.assertIn(('横山克', ['song']), lyrics.search_pairs(item)) - self.assertIn((u'Masaru Yokoyama', [u'song']), + self.assertIn(('Masaru Yokoyama', ['song']), lyrics.search_pairs(item)) # Make sure that the original artist name is still the first entry - self.assertEqual((u'横山克', [u'song']), + self.assertEqual(('横山克', ['song']), list(lyrics.search_pairs(item))[0]) def test_search_pairs_multi_titles(self): @@ -179,12 +174,12 @@ self.assertFalse(google.is_lyrics(t)) def test_slugify(self): - text = u"http://site.com/\xe7afe-au_lait(boisson)" + text = "http://site.com/\xe7afe-au_lait(boisson)" self.assertEqual(google.slugify(text), 'http://site.com/cafe_au_lait') def test_scrape_strip_cruft(self): - text = u""" + text = """  one
two ! @@ -194,17 +189,17 @@ "one\ntwo !\n\nfour") def test_scrape_strip_scripts(self): - text = u"""foobaz""" + text = """foobaz""" self.assertEqual(lyrics._scrape_strip_cruft(text, True), "foobaz") def test_scrape_strip_tag_in_comment(self): - text = u"""fooqux""" + text = """fooqux""" self.assertEqual(lyrics._scrape_strip_cruft(text, True), "fooqux") def test_scrape_merge_paragraphs(self): - text = u"one

two

three" + text = "one

two

three" self.assertEqual(lyrics._scrape_merge_paragraphs(text), "one\ntwo\nthree") @@ -222,7 +217,7 @@ return fn -class MockFetchUrl(object): +class MockFetchUrl: def __init__(self, pathval='fetched_path'): self.pathval = pathval @@ -231,7 +226,7 @@ def __call__(self, url, filename=None): self.fetched = url fn = url_to_filename(url) - with open(fn, 'r', encoding="utf8") as f: + with open(fn, encoding="utf8") as f: content = f.read() return content @@ -241,9 +236,10 @@ if not text: return keywords = set(LYRICS_TEXTS[google.slugify(title)].split()) - words = set(x.strip(".?, ") for x in text.lower().split()) + words = {x.strip(".?, ") for x in text.lower().split()} return keywords <= words + LYRICS_ROOT_DIR = os.path.join(_common.RSRC, b'lyrics') yaml_path = os.path.join(_common.RSRC, b'lyricstext.yaml') LYRICS_TEXTS = confuse.load_yaml(yaml_path) @@ -257,8 +253,6 @@ __import__('bs4') except ImportError: self.skipTest('Beautiful Soup 4 not available') - if sys.version_info[:3] < (2, 7, 3): - self.skipTest("Python's built-in HTML parser is not good enough") class LyricsPluginSourcesTest(LyricsGoogleBaseTest): @@ -266,7 +260,7 @@ scraped. """ - DEFAULT_SONG = dict(artist=u'The Beatles', title=u'Lady Madonna') + DEFAULT_SONG = dict(artist='The Beatles', title='Lady Madonna') DEFAULT_SOURCES = [ # dict(artist=u'Santana', title=u'Black magic woman', @@ -274,53 +268,53 @@ dict(DEFAULT_SONG, backend=lyrics.Genius, # GitHub actions is on some form of Cloudflare blacklist. skip=os.environ.get('GITHUB_ACTIONS') == 'true'), - dict(artist=u'Boy In Space', title=u'u n eye', + dict(artist='Boy In Space', title='u n eye', backend=lyrics.Tekstowo), ] GOOGLE_SOURCES = [ dict(DEFAULT_SONG, - url=u'http://www.absolutelyrics.com', - path=u'/lyrics/view/the_beatles/lady_madonna'), + url='http://www.absolutelyrics.com', + path='/lyrics/view/the_beatles/lady_madonna'), dict(DEFAULT_SONG, - url=u'http://www.azlyrics.com', - path=u'/lyrics/beatles/ladymadonna.html', + url='http://www.azlyrics.com', + path='/lyrics/beatles/ladymadonna.html', # AZLyrics returns a 403 on GitHub actions. skip=os.environ.get('GITHUB_ACTIONS') == 'true'), dict(DEFAULT_SONG, - url=u'http://www.chartlyrics.com', - path=u'/_LsLsZ7P4EK-F-LD4dJgDQ/Lady+Madonna.aspx'), + url='http://www.chartlyrics.com', + path='/_LsLsZ7P4EK-F-LD4dJgDQ/Lady+Madonna.aspx'), # dict(DEFAULT_SONG, # url=u'http://www.elyricsworld.com', # path=u'/lady_madonna_lyrics_beatles.html'), - dict(url=u'http://www.lacoccinelle.net', - artist=u'Jacques Brel', title=u"Amsterdam", - path=u'/paroles-officielles/275679.html'), + dict(url='http://www.lacoccinelle.net', + artist='Jacques Brel', title="Amsterdam", + path='/paroles-officielles/275679.html'), dict(DEFAULT_SONG, - url=u'http://letras.mus.br/', path=u'the-beatles/275/'), + url='http://letras.mus.br/', path='the-beatles/275/'), dict(DEFAULT_SONG, url='http://www.lyricsmania.com/', path='lady_madonna_lyrics_the_beatles.html'), dict(DEFAULT_SONG, - url=u'http://www.lyricsmode.com', - path=u'/lyrics/b/beatles/lady_madonna.html'), - dict(url=u'http://www.lyricsontop.com', - artist=u'Amy Winehouse', title=u"Jazz'n'blues", - path=u'/amy-winehouse-songs/jazz-n-blues-lyrics.html'), + url='http://www.lyricsmode.com', + path='/lyrics/b/beatles/lady_madonna.html'), + dict(url='http://www.lyricsontop.com', + artist='Amy Winehouse', title="Jazz'n'blues", + path='/amy-winehouse-songs/jazz-n-blues-lyrics.html'), # dict(DEFAULT_SONG, # url='http://www.metrolyrics.com/', # path='lady-madonna-lyrics-beatles.html'), # dict(url='http://www.musica.com/', path='letras.asp?letra=2738', # artist=u'Santana', title=u'Black magic woman'), - dict(url=u'http://www.paroles.net/', - artist=u'Lilly Wood & the prick', title=u"Hey it's ok", - path=u'lilly-wood-the-prick/paroles-hey-it-s-ok'), + dict(url='http://www.paroles.net/', + artist='Lilly Wood & the prick', title="Hey it's ok", + path='lilly-wood-the-prick/paroles-hey-it-s-ok'), dict(DEFAULT_SONG, url='http://www.songlyrics.com', - path=u'/the-beatles/lady-madonna-lyrics'), + path='/the-beatles/lady-madonna-lyrics'), dict(DEFAULT_SONG, - url=u'http://www.sweetslyrics.com', - path=u'/761696.The%20Beatles%20-%20Lady%20Madonna.html') + url='http://www.sweetslyrics.com', + path='/761696.The%20Beatles%20-%20Lady%20Madonna.html') ] def setUp(self): @@ -364,8 +358,8 @@ """Test scraping heuristics on a fake html page. """ - source = dict(url=u'http://www.example.com', artist=u'John Doe', - title=u'Beets song', path=u'/lyrics/beetssong') + source = dict(url='http://www.example.com', artist='John Doe', + title='Beets song', path='/lyrics/beetssong') def setUp(self): """Set up configuration""" @@ -388,7 +382,7 @@ """ from bs4 import SoupStrainer, BeautifulSoup s = self.source - url = six.text_type(s['url'] + s['path']) + url = str(s['url'] + s['path']) html = raw_backend.fetch_url(url) soup = BeautifulSoup(html, "html.parser", parse_only=SoupStrainer('title')) @@ -402,13 +396,13 @@ """ s = self.source url = s['url'] + s['path'] - url_title = u'example.com | Beats song by John doe' + url_title = 'example.com | Beats song by John doe' # very small diffs (typo) are ok eg 'beats' vs 'beets' with same artist self.assertEqual(google.is_page_candidate(url, url_title, s['title'], s['artist']), True, url) # reject different title - url_title = u'example.com | seets bong lyrics by John doe' + url_title = 'example.com | seets bong lyrics by John doe' self.assertEqual(google.is_page_candidate(url, url_title, s['title'], s['artist']), False, url) @@ -419,9 +413,9 @@ # https://github.com/beetbox/beets/issues/1673 s = self.source url = s['url'] + s['path'] - url_title = u'foo' + url_title = 'foo' - google.is_page_candidate(url, url_title, s['title'], u'Sunn O)))') + google.is_page_candidate(url, url_title, s['title'], 'Sunn O)))') # test Genius backend @@ -433,8 +427,6 @@ __import__('bs4') except ImportError: self.skipTest('Beautiful Soup 4 not available') - if sys.version_info[:3] < (2, 7, 3): - self.skipTest("Python's built-in HTML parser is not good enough") class GeniusScrapeLyricsFromHtmlTest(GeniusBaseTest): @@ -483,18 +475,18 @@ { "result": { "primary_artist": { - "name": u"\u200Bblackbear", - }, + "name": "\u200Bblackbear", + }, "url": "blackbear_url" - } + } }, { "result": { "primary_artist": { - "name": u"El\u002Dp" - }, + "name": "El\u002Dp" + }, "url": "El-p_url" - } + } } ] } @@ -527,28 +519,28 @@ def test_slug(self): # plain ascii passthrough - text = u"test" + text = "test" self.assertEqual(lyrics.slug(text), 'test') # german unicode and capitals - text = u"Mørdag" + text = "Mørdag" self.assertEqual(lyrics.slug(text), 'mordag') # more accents and quotes - text = u"l'été c'est fait pour jouer" + text = "l'été c'est fait pour jouer" self.assertEqual(lyrics.slug(text), 'l-ete-c-est-fait-pour-jouer') # accents, parens and spaces - text = u"\xe7afe au lait (boisson)" + text = "\xe7afe au lait (boisson)" self.assertEqual(lyrics.slug(text), 'cafe-au-lait-boisson') - text = u"Multiple spaces -- and symbols! -- merged" + text = "Multiple spaces -- and symbols! -- merged" self.assertEqual(lyrics.slug(text), 'multiple-spaces-and-symbols-merged') - text = u"\u200Bno-width-space" + text = "\u200Bno-width-space" self.assertEqual(lyrics.slug(text), 'no-width-space') # variations of dashes should get standardized - dashes = [u'\u200D', u'\u2010'] + dashes = ['\u200D', '\u2010'] for dash1, dash2 in itertools.combinations(dashes, 2): self.assertEqual(lyrics.slug(dash1), lyrics.slug(dash2)) @@ -556,5 +548,6 @@ def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == '__main__': unittest.main(defaultTest='suite') diff -Nru beets-1.5.0/test/test_mb.py beets-1.6.0/test/test_mb.py --- beets-1.5.0/test/test_mb.py 2021-03-06 21:56:33.000000000 +0000 +++ beets-1.6.0/test/test_mb.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,14 +14,13 @@ """Tests for MusicBrainz API wrapper. """ -from __future__ import division, absolute_import, print_function from test import _common from beets.autotag import mb from beets import config import unittest -import mock +from unittest import mock class MBAlbumInfoTest(_common.TestCase): diff -Nru beets-1.5.0/test/test_mbsubmit.py beets-1.6.0/test/test_mbsubmit.py --- beets-1.5.0/test/test_mbsubmit.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_mbsubmit.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson and Diego Moreda. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import unittest from test.helper import capture_stdout, control_stdin, TestHelper @@ -45,9 +43,9 @@ self.importer.run() # Manually build the string for comparing the output. - tracklist = (u'Print tracks? ' - u'01. Tag Title 1 - Tag Artist (0:01)\n' - u'02. Tag Title 2 - Tag Artist (0:01)') + tracklist = ('Print tracks? ' + '01. Tag Title 1 - Tag Artist (0:01)\n' + '02. Tag Title 2 - Tag Artist (0:01)') self.assertIn(tracklist, output.getvalue()) def test_print_tracks_output_as_tracks(self): @@ -60,8 +58,8 @@ self.importer.run() # Manually build the string for comparing the output. - tracklist = (u'Print tracks? ' - u'02. Tag Title 2 - Tag Artist (0:01)') + tracklist = ('Print tracks? ' + '02. Tag Title 2 - Tag Artist (0:01)') self.assertIn(tracklist, output.getvalue()) diff -Nru beets-1.5.0/test/test_mbsync.py beets-1.6.0/test/test_mbsync.py --- beets-1.5.0/test/test_mbsync.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_mbsync.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,10 +12,9 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import unittest -from mock import patch +from unittest.mock import patch from test.helper import TestHelper,\ generate_album_info, \ @@ -43,24 +41,24 @@ album_for_id.return_value = \ generate_album_info( 'album id', - [('track id', {'release_track_id': u'release track id'})] + [('track id', {'release_track_id': 'release track id'})] ) track_for_id.return_value = \ - generate_track_info(u'singleton track id', - {'title': u'singleton info'}) + generate_track_info('singleton track id', + {'title': 'singleton info'}) album_item = Item( - album=u'old title', - mb_albumid=u'81ae60d4-5b75-38df-903a-db2cfa51c2c6', - mb_trackid=u'old track id', - mb_releasetrackid=u'release track id', + album='old title', + mb_albumid='81ae60d4-5b75-38df-903a-db2cfa51c2c6', + mb_trackid='old track id', + mb_releasetrackid='release track id', path='' ) album = self.lib.add_album([album_item]) item = Item( - title=u'old title', - mb_trackid=u'b8c2cf90-83f9-3b5f-8ccd-31fb866fcf37', + title='old title', + mb_trackid='b8c2cf90-83f9-3b5f-8ccd-31fb866fcf37', path='', ) self.lib.add(item) @@ -72,25 +70,25 @@ self.assertIn('Sending event: trackinfo_received', logs) item.load() - self.assertEqual(item.title, u'singleton info') + self.assertEqual(item.title, 'singleton info') album_item.load() - self.assertEqual(album_item.title, u'track info') - self.assertEqual(album_item.mb_trackid, u'track id') + self.assertEqual(album_item.title, 'track info') + self.assertEqual(album_item.mb_trackid, 'track id') album.load() - self.assertEqual(album.album, u'album info') + self.assertEqual(album.album, 'album info') def test_message_when_skipping(self): - config['format_item'] = u'$artist - $album - $title' - config['format_album'] = u'$albumartist - $album' + config['format_item'] = '$artist - $album - $title' + config['format_album'] = '$albumartist - $album' # Test album with no mb_albumid. # The default format for an album include $albumartist so # set that here, too. album_invalid = Item( - albumartist=u'album info', - album=u'album info', + albumartist='album info', + album='album info', path='' ) self.lib.add_album([album_invalid]) @@ -98,27 +96,27 @@ # default format with capture_log('beets.mbsync') as logs: self.run_command('mbsync') - e = u'mbsync: Skipping album with no mb_albumid: ' + \ - u'album info - album info' + e = 'mbsync: Skipping album with no mb_albumid: ' + \ + 'album info - album info' self.assertEqual(e, logs[0]) # custom format with capture_log('beets.mbsync') as logs: self.run_command('mbsync', '-f', "'$album'") - e = u"mbsync: Skipping album with no mb_albumid: 'album info'" + e = "mbsync: Skipping album with no mb_albumid: 'album info'" self.assertEqual(e, logs[0]) # restore the config - config['format_item'] = u'$artist - $album - $title' - config['format_album'] = u'$albumartist - $album' + config['format_item'] = '$artist - $album - $title' + config['format_album'] = '$albumartist - $album' # Test singleton with no mb_trackid. # The default singleton format includes $artist and $album # so we need to stub them here item_invalid = Item( - artist=u'album info', - album=u'album info', - title=u'old title', + artist='album info', + album='album info', + title='old title', path='', ) self.lib.add(item_invalid) @@ -126,27 +124,27 @@ # default format with capture_log('beets.mbsync') as logs: self.run_command('mbsync') - e = u'mbsync: Skipping singleton with no mb_trackid: ' + \ - u'album info - album info - old title' + e = 'mbsync: Skipping singleton with no mb_trackid: ' + \ + 'album info - album info - old title' self.assertEqual(e, logs[0]) # custom format with capture_log('beets.mbsync') as logs: self.run_command('mbsync', '-f', "'$title'") - e = u"mbsync: Skipping singleton with no mb_trackid: 'old title'" + e = "mbsync: Skipping singleton with no mb_trackid: 'old title'" self.assertEqual(e, logs[0]) def test_message_when_invalid(self): - config['format_item'] = u'$artist - $album - $title' - config['format_album'] = u'$albumartist - $album' + config['format_item'] = '$artist - $album - $title' + config['format_album'] = '$albumartist - $album' # Test album with invalid mb_albumid. # The default format for an album include $albumartist so # set that here, too. album_invalid = Item( - albumartist=u'album info', - album=u'album info', - mb_albumid=u'a1b2c3d4', + albumartist='album info', + album='album info', + mb_albumid='a1b2c3d4', path='' ) self.lib.add_album([album_invalid]) @@ -154,28 +152,28 @@ # default format with capture_log('beets.mbsync') as logs: self.run_command('mbsync') - e = u'mbsync: Skipping album with invalid mb_albumid: ' + \ - u'album info - album info' + e = 'mbsync: Skipping album with invalid mb_albumid: ' + \ + 'album info - album info' self.assertEqual(e, logs[0]) # custom format with capture_log('beets.mbsync') as logs: self.run_command('mbsync', '-f', "'$album'") - e = u"mbsync: Skipping album with invalid mb_albumid: 'album info'" + e = "mbsync: Skipping album with invalid mb_albumid: 'album info'" self.assertEqual(e, logs[0]) # restore the config - config['format_item'] = u'$artist - $album - $title' - config['format_album'] = u'$albumartist - $album' + config['format_item'] = '$artist - $album - $title' + config['format_album'] = '$albumartist - $album' # Test singleton with invalid mb_trackid. # The default singleton format includes $artist and $album # so we need to stub them here item_invalid = Item( - artist=u'album info', - album=u'album info', - title=u'old title', - mb_trackid=u'a1b2c3d4', + artist='album info', + album='album info', + title='old title', + mb_trackid='a1b2c3d4', path='', ) self.lib.add(item_invalid) @@ -183,14 +181,14 @@ # default format with capture_log('beets.mbsync') as logs: self.run_command('mbsync') - e = u'mbsync: Skipping singleton with invalid mb_trackid: ' + \ - u'album info - album info - old title' + e = 'mbsync: Skipping singleton with invalid mb_trackid: ' + \ + 'album info - album info - old title' self.assertEqual(e, logs[0]) # custom format with capture_log('beets.mbsync') as logs: self.run_command('mbsync', '-f', "'$title'") - e = u"mbsync: Skipping singleton with invalid mb_trackid: 'old title'" + e = "mbsync: Skipping singleton with invalid mb_trackid: 'old title'" self.assertEqual(e, logs[0]) diff -Nru beets-1.5.0/test/test_metasync.py beets-1.6.0/test/test_metasync.py --- beets-1.5.0/test/test_metasync.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/test/test_metasync.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Tom Jaspers. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import os import platform @@ -72,12 +70,12 @@ if _is_windows(): items[0].path = \ - u'G:\\Music\\Alt-J\\An Awesome Wave\\03 Tessellate.mp3' + 'G:\\Music\\Alt-J\\An Awesome Wave\\03 Tessellate.mp3' items[1].path = \ - u'G:\\Music\\Alt-J\\An Awesome Wave\\04 Breezeblocks.mp3' + 'G:\\Music\\Alt-J\\An Awesome Wave\\04 Breezeblocks.mp3' else: - items[0].path = u'/Music/Alt-J/An Awesome Wave/03 Tessellate.mp3' - items[1].path = u'/Music/Alt-J/An Awesome Wave/04 Breezeblocks.mp3' + items[0].path = '/Music/Alt-J/An Awesome Wave/03 Tessellate.mp3' + items[1].path = '/Music/Alt-J/An Awesome Wave/04 Breezeblocks.mp3' for item in items: self.lib.add(item) diff -Nru beets-1.5.0/test/test_mpdstats.py beets-1.6.0/test/test_mpdstats.py --- beets-1.5.0/test/test_mpdstats.py 2020-08-10 22:29:53.000000000 +0000 +++ beets-1.6.0/test/test_mpdstats.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016 # @@ -13,10 +12,9 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import unittest -from mock import Mock, patch, call, ANY +from unittest.mock import Mock, patch, call, ANY from test.helper import TestHelper from beets.library import Item @@ -34,7 +32,7 @@ self.unload_plugins() def test_update_rating(self): - item = Item(title=u'title', path='', id=1) + item = Item(title='title', path='', id=1) item.add(self.lib) log = Mock() @@ -45,7 +43,7 @@ def test_get_item(self): item_path = util.normpath('/foo/bar.flac') - item = Item(title=u'title', path=item_path, id=1) + item = Item(title='title', path=item_path, id=1) item.add(self.lib) log = Mock() @@ -53,13 +51,13 @@ self.assertEqual(str(mpdstats.get_item(item_path)), str(item)) self.assertIsNone(mpdstats.get_item('/some/non-existing/path')) - self.assertIn(u'item not found:', log.info.call_args[0][0]) + self.assertIn('item not found:', log.info.call_args[0][0]) FAKE_UNKNOWN_STATE = 'some-unknown-one' STATUSES = [{'state': FAKE_UNKNOWN_STATE}, - {'state': u'pause'}, - {'state': u'play', 'songid': 1, 'time': u'0:1'}, - {'state': u'stop'}] + {'state': 'pause'}, + {'state': 'play', 'songid': 1, 'time': '0:1'}, + {'state': 'stop'}] EVENTS = [["player"]] * (len(STATUSES) - 1) + [KeyboardInterrupt] item_path = util.normpath('/foo/bar.flac') songid = 1 @@ -68,7 +66,7 @@ "events.side_effect": EVENTS, "status.side_effect": STATUSES, "currentsong.return_value": (item_path, songid)})) def test_run_mpdstats(self, mpd_mock): - item = Item(title=u'title', path=self.item_path, id=1) + item = Item(title='title', path=self.item_path, id=1) item.add(self.lib) log = Mock() @@ -78,9 +76,9 @@ pass log.debug.assert_has_calls( - [call(u'unhandled status "{0}"', ANY)]) + [call('unhandled status "{0}"', ANY)]) log.info.assert_has_calls( - [call(u'pause'), call(u'playing {0}', ANY), call(u'stop')]) + [call('pause'), call('playing {0}', ANY), call('stop')]) def suite(): diff -Nru beets-1.5.0/test/test_parentwork.py beets-1.6.0/test/test_parentwork.py --- beets-1.5.0/test/test_parentwork.py 2021-01-08 18:07:39.000000000 +0000 +++ beets-1.6.0/test/test_parentwork.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2017, Dorian Soergel # @@ -15,12 +14,11 @@ """Tests for the 'parentwork' plugin.""" -from __future__ import division, absolute_import, print_function import os import unittest from test.helper import TestHelper -from mock import patch +from unittest.mock import patch from beets.library import Item from beetsplug import parentwork @@ -81,8 +79,8 @@ 'integration testing not enabled') def test_normal_case_real(self): item = Item(path='/file', - mb_workid=u'e27bda6e-531e-36d3-9cd7-b8ebc18e8c53', - parentwork_workid_current=u'e27bda6e-531e-36d3-9cd7-\ + mb_workid='e27bda6e-531e-36d3-9cd7-b8ebc18e8c53', + parentwork_workid_current='e27bda6e-531e-36d3-9cd7-\ b8ebc18e8c53') item.add(self.lib) @@ -90,7 +88,7 @@ item.load() self.assertEqual(item['mb_parentworkid'], - u'32c8943f-1b27-3a23-8660-4567f4847c94') + '32c8943f-1b27-3a23-8660-4567f4847c94') @unittest.skipUnless( os.environ.get('INTEGRATION_TEST', '0') == '1', @@ -98,9 +96,9 @@ def test_force_real(self): self.config['parentwork']['force'] = True item = Item(path='/file', - mb_workid=u'e27bda6e-531e-36d3-9cd7-b8ebc18e8c53', - mb_parentworkid=u'XXX', - parentwork_workid_current=u'e27bda6e-531e-36d3-9cd7-\ + mb_workid='e27bda6e-531e-36d3-9cd7-b8ebc18e8c53', + mb_parentworkid='XXX', + parentwork_workid_current='e27bda6e-531e-36d3-9cd7-\ b8ebc18e8c53', parentwork='whatever') item.add(self.lib) @@ -108,23 +106,23 @@ item.load() self.assertEqual(item['mb_parentworkid'], - u'32c8943f-1b27-3a23-8660-4567f4847c94') + '32c8943f-1b27-3a23-8660-4567f4847c94') @unittest.skipUnless( os.environ.get('INTEGRATION_TEST', '0') == '1', 'integration testing not enabled') def test_no_force_real(self): self.config['parentwork']['force'] = False - item = Item(path='/file', mb_workid=u'e27bda6e-531e-36d3-9cd7-\ - b8ebc18e8c53', mb_parentworkid=u'XXX', - parentwork_workid_current=u'e27bda6e-531e-36d3-9cd7-\ + item = Item(path='/file', mb_workid='e27bda6e-531e-36d3-9cd7-\ + b8ebc18e8c53', mb_parentworkid='XXX', + parentwork_workid_current='e27bda6e-531e-36d3-9cd7-\ b8ebc18e8c53', parentwork='whatever') item.add(self.lib) self.run_command('parentwork') item.load() - self.assertEqual(item['mb_parentworkid'], u'XXX') + self.assertEqual(item['mb_parentworkid'], 'XXX') # test different cases, still with Matthew Passion Ouverture or Mozart # requiem @@ -133,10 +131,10 @@ os.environ.get('INTEGRATION_TEST', '0') == '1', 'integration testing not enabled') def test_direct_parent_work_real(self): - mb_workid = u'2e4a3668-458d-3b2a-8be2-0b08e0d8243a' - self.assertEqual(u'f04b42df-7251-4d86-a5ee-67cfa49580d1', + mb_workid = '2e4a3668-458d-3b2a-8be2-0b08e0d8243a' + self.assertEqual('f04b42df-7251-4d86-a5ee-67cfa49580d1', parentwork.direct_parent_id(mb_workid)[0]) - self.assertEqual(u'45afb3b2-18ac-4187-bc72-beb1b1c194ba', + self.assertEqual('45afb3b2-18ac-4187-bc72-beb1b1c194ba', parentwork.work_parent_id(mb_workid)[0]) @@ -165,7 +163,7 @@ def test_force(self): self.config['parentwork']['force'] = True - item = Item(path='/file', mb_workid='1', mb_parentworkid=u'XXX', + item = Item(path='/file', mb_workid='1', mb_parentworkid='XXX', parentwork_workid_current='1', parentwork='parentwork') item.add(self.lib) @@ -176,14 +174,14 @@ def test_no_force(self): self.config['parentwork']['force'] = False - item = Item(path='/file', mb_workid='1', mb_parentworkid=u'XXX', + item = Item(path='/file', mb_workid='1', mb_parentworkid='XXX', parentwork_workid_current='1', parentwork='parentwork') item.add(self.lib) self.run_command('parentwork') item.load() - self.assertEqual(item['mb_parentworkid'], u'XXX') + self.assertEqual(item['mb_parentworkid'], 'XXX') def test_direct_parent_work(self): self.assertEqual('2', parentwork.direct_parent_id('1')[0]) diff -Nru beets-1.5.0/test/test_permissions.py beets-1.6.0/test/test_permissions.py --- beets-1.5.0/test/test_permissions.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_permissions.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,15 +1,13 @@ -# -*- coding: utf-8 -*- - """Tests for the 'permissions' plugin. """ -from __future__ import division, absolute_import, print_function import os import platform import unittest -from mock import patch, Mock +from unittest.mock import patch, Mock from test.helper import TestHelper +from test._common import touch from beets.util import displayable_path from beetsplug.permissions import (check_permissions, convert_perm, @@ -68,7 +66,7 @@ def assertPerms(self, path, typ, expect_success): # noqa for x in [(True, self.exp_perms[expect_success][typ], '!='), (False, self.exp_perms[not expect_success][typ], '==')]: - msg = u'{} : {} {} {}'.format( + msg = '{} : {} {} {}'.format( displayable_path(path), oct(os.stat(path).st_mode), x[2], @@ -82,6 +80,25 @@ def test_convert_perm_from_int(self): self.assertEqual(convert_perm(10), 8) + def test_permissions_on_set_art(self): + self.do_set_art(True) + + @patch("os.chmod", Mock()) + def test_failing_permissions_on_set_art(self): + self.do_set_art(False) + + def do_set_art(self, expect_success): + if platform.system() == 'Windows': + self.skipTest('permissions not available on Windows') + self.importer = self.create_importer() + self.importer.run() + album = self.lib.albums().get() + artpath = os.path.join(self.temp_dir, b'cover.jpg') + touch(artpath) + album.set_art(artpath) + self.assertEqual(expect_success, + check_permissions(album.artpath, 0o777)) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff -Nru beets-1.5.0/test/test_pipeline.py beets-1.6.0/test/test_pipeline.py --- beets-1.5.0/test/test_pipeline.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_pipeline.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,9 +14,7 @@ """Test the "pipeline.py" restricted parallel programming library. """ -from __future__ import division, absolute_import, print_function -import six import unittest from beets.util import pipeline @@ -25,8 +22,7 @@ # Some simple pipeline stages for testing. def _produce(num=5): - for i in range(num): - yield i + yield from range(num) def _work(): @@ -112,7 +108,7 @@ def test_run_parallel(self): self.pl.run_parallel() # Order possibly not preserved; use set equality. - self.assertEqual(set(self.l), set([0, 2, 4, 6, 8])) + self.assertEqual(set(self.l), {0, 2, 4, 6, 8}) def test_pull(self): pl = pipeline.Pipeline((_produce(), (_work(), _work()))) @@ -136,10 +132,7 @@ pull = pl.pull() for i in range(3): next(pull) - if six.PY2: - self.assertRaises(ExceptionFixture, pull.next) - else: - self.assertRaises(ExceptionFixture, pull.__next__) + self.assertRaises(ExceptionFixture, pull.__next__) class ParallelExceptionTest(unittest.TestCase): @@ -174,7 +167,7 @@ _produce(1000), (_work(), _work()), _consume(l) )) pl.run_parallel(1) - self.assertEqual(set(l), set(i * 2 for i in range(1000))) + self.assertEqual(set(l), {i * 2 for i in range(1000)}) class BubbleTest(unittest.TestCase): diff -Nru beets-1.5.0/test/test_player.py beets-1.6.0/test/test_player.py --- beets-1.5.0/test/test_player.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/test/test_player.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """Tests for BPD's implementation of the MPD protocol. """ -from __future__ import division, absolute_import, print_function import unittest from test.helper import TestHelper @@ -36,7 +34,7 @@ # Mock GstPlayer so that the forked process doesn't attempt to import gi: -import mock +from unittest import mock import imp gstplayer = imp.new_module("beetsplug.bpd.gstplayer") def _gstplayer_play(*_): # noqa: 42 @@ -63,45 +61,45 @@ def test_no_args(self): s = r'command' c = bpd.Command(s) - self.assertEqual(c.name, u'command') + self.assertEqual(c.name, 'command') self.assertEqual(c.args, []) def test_one_unquoted_arg(self): s = r'command hello' c = bpd.Command(s) - self.assertEqual(c.name, u'command') - self.assertEqual(c.args, [u'hello']) + self.assertEqual(c.name, 'command') + self.assertEqual(c.args, ['hello']) def test_two_unquoted_args(self): s = r'command hello there' c = bpd.Command(s) - self.assertEqual(c.name, u'command') - self.assertEqual(c.args, [u'hello', u'there']) + self.assertEqual(c.name, 'command') + self.assertEqual(c.args, ['hello', 'there']) def test_one_quoted_arg(self): s = r'command "hello there"' c = bpd.Command(s) - self.assertEqual(c.name, u'command') - self.assertEqual(c.args, [u'hello there']) + self.assertEqual(c.name, 'command') + self.assertEqual(c.args, ['hello there']) def test_heterogenous_args(self): s = r'command "hello there" sir' c = bpd.Command(s) - self.assertEqual(c.name, u'command') - self.assertEqual(c.args, [u'hello there', u'sir']) + self.assertEqual(c.name, 'command') + self.assertEqual(c.args, ['hello there', 'sir']) def test_quote_in_arg(self): s = r'command "hello \" there"' c = bpd.Command(s) - self.assertEqual(c.args, [u'hello " there']) + self.assertEqual(c.args, ['hello " there']) def test_backslash_in_arg(self): s = r'command "hello \\ there"' c = bpd.Command(s) - self.assertEqual(c.args, [u'hello \\ there']) + self.assertEqual(c.args, ['hello \\ there']) -class MPCResponse(object): +class MPCResponse: def __init__(self, raw_response): body = b'\n'.join(raw_response.split(b'\n')[:-2]).decode('utf-8') self.data = self._parse_body(body) @@ -119,7 +117,7 @@ cmd, rest = rest[2:].split('}') return False, (int(code), int(pos), cmd, rest[1:]) else: - raise RuntimeError('Unexpected status: {!r}'.format(status)) + raise RuntimeError(f'Unexpected status: {status!r}') def _parse_body(self, body): """ Messages are generally in the format "header: content". @@ -132,7 +130,7 @@ if not line: continue if ':' not in line: - raise RuntimeError('Unexpected line: {!r}'.format(line)) + raise RuntimeError(f'Unexpected line: {line!r}') header, content = line.split(':', 1) content = content.lstrip() if header in repeated_headers: @@ -145,7 +143,7 @@ return data -class MPCClient(object): +class MPCClient: def __init__(self, sock, do_hello=True): self.sock = sock self.buf = b'' @@ -178,7 +176,7 @@ responses.append(MPCResponse(response)) response = b'' elif not line: - raise RuntimeError('Unexpected response: {!r}'.format(line)) + raise RuntimeError(f'Unexpected response: {line!r}') def serialise_command(self, command, *args): cmd = [command.encode('utf-8')] diff -Nru beets-1.5.0/test/test_playlist.py beets-1.6.0/test/test_playlist.py --- beets-1.5.0/test/test_playlist.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/test/test_playlist.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,14 +12,13 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function -from six.moves import shlex_quote import os import shutil import tempfile import unittest +from shlex import quote from test import _common from test import helper @@ -39,8 +37,8 @@ self.music_dir, 'a', 'b', 'c.mp3', )) - i1.title = u'some item' - i1.album = u'some album' + i1.title = 'some item' + i1.album = 'some album' self.lib.add(i1) self.lib.add_album([i1]) @@ -82,50 +80,50 @@ class PlaylistQueryTestHelper(PlaylistTestHelper): def test_name_query_with_absolute_paths_in_playlist(self): - q = u'playlist:absolute' + q = 'playlist:absolute' results = self.lib.items(q) - self.assertEqual(set([i.title for i in results]), set([ - u'some item', - u'another item', - ])) + self.assertEqual({i.title for i in results}, { + 'some item', + 'another item', + }) def test_path_query_with_absolute_paths_in_playlist(self): - q = u'playlist:{0}'.format(shlex_quote(os.path.join( + q = 'playlist:{}'.format(quote(os.path.join( self.playlist_dir, 'absolute.m3u', ))) results = self.lib.items(q) - self.assertEqual(set([i.title for i in results]), set([ - u'some item', - u'another item', - ])) + self.assertEqual({i.title for i in results}, { + 'some item', + 'another item', + }) def test_name_query_with_relative_paths_in_playlist(self): - q = u'playlist:relative' + q = 'playlist:relative' results = self.lib.items(q) - self.assertEqual(set([i.title for i in results]), set([ - u'some item', - u'another item', - ])) + self.assertEqual({i.title for i in results}, { + 'some item', + 'another item', + }) def test_path_query_with_relative_paths_in_playlist(self): - q = u'playlist:{0}'.format(shlex_quote(os.path.join( + q = 'playlist:{}'.format(quote(os.path.join( self.playlist_dir, 'relative.m3u', ))) results = self.lib.items(q) - self.assertEqual(set([i.title for i in results]), set([ - u'some item', - u'another item', - ])) + self.assertEqual({i.title for i in results}, { + 'some item', + 'another item', + }) def test_name_query_with_nonexisting_playlist(self): - q = u'playlist:nonexisting' + q = 'playlist:nonexisting' results = self.lib.items(q) self.assertEqual(set(results), set()) def test_path_query_with_nonexisting_playlist(self): - q = u'playlist:{0}'.format(shlex_quote(os.path.join( + q = 'playlist:{}'.format(quote(os.path.join( self.playlist_dir, self.playlist_dir, 'nonexisting.m3u', @@ -137,17 +135,17 @@ class PlaylistTestRelativeToLib(PlaylistQueryTestHelper, unittest.TestCase): def setup_test(self): with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'a', 'b', 'c.mp3'))) - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'd', 'e', 'f.mp3'))) - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'nonexisting.mp3'))) with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: - f.write('{0}\n'.format(os.path.join('a', 'b', 'c.mp3'))) - f.write('{0}\n'.format(os.path.join('d', 'e', 'f.mp3'))) - f.write('{0}\n'.format('nonexisting.mp3')) + f.write('{}\n'.format(os.path.join('a', 'b', 'c.mp3'))) + f.write('{}\n'.format(os.path.join('d', 'e', 'f.mp3'))) + f.write('{}\n'.format('nonexisting.mp3')) self.config['playlist']['relative_to'] = 'library' @@ -155,17 +153,17 @@ class PlaylistTestRelativeToDir(PlaylistQueryTestHelper, unittest.TestCase): def setup_test(self): with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'a', 'b', 'c.mp3'))) - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'd', 'e', 'f.mp3'))) - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'nonexisting.mp3'))) with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: - f.write('{0}\n'.format(os.path.join('a', 'b', 'c.mp3'))) - f.write('{0}\n'.format(os.path.join('d', 'e', 'f.mp3'))) - f.write('{0}\n'.format('nonexisting.mp3')) + f.write('{}\n'.format(os.path.join('a', 'b', 'c.mp3'))) + f.write('{}\n'.format(os.path.join('d', 'e', 'f.mp3'))) + f.write('{}\n'.format('nonexisting.mp3')) self.config['playlist']['relative_to'] = self.music_dir @@ -173,23 +171,23 @@ class PlaylistTestRelativeToPls(PlaylistQueryTestHelper, unittest.TestCase): def setup_test(self): with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'a', 'b', 'c.mp3'))) - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'd', 'e', 'f.mp3'))) - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'nonexisting.mp3'))) with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: - f.write('{0}\n'.format(os.path.relpath( + f.write('{}\n'.format(os.path.relpath( os.path.join(self.music_dir, 'a', 'b', 'c.mp3'), start=self.playlist_dir, ))) - f.write('{0}\n'.format(os.path.relpath( + f.write('{}\n'.format(os.path.relpath( os.path.join(self.music_dir, 'd', 'e', 'f.mp3'), start=self.playlist_dir, ))) - f.write('{0}\n'.format(os.path.relpath( + f.write('{}\n'.format(os.path.relpath( os.path.join(self.music_dir, 'nonexisting.mp3'), start=self.playlist_dir, ))) @@ -201,17 +199,17 @@ class PlaylistUpdateTestHelper(PlaylistTestHelper): def setup_test(self): with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'a', 'b', 'c.mp3'))) - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'd', 'e', 'f.mp3'))) - f.write('{0}\n'.format(os.path.join( + f.write('{}\n'.format(os.path.join( self.music_dir, 'nonexisting.mp3'))) with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: - f.write('{0}\n'.format(os.path.join('a', 'b', 'c.mp3'))) - f.write('{0}\n'.format(os.path.join('d', 'e', 'f.mp3'))) - f.write('{0}\n'.format('nonexisting.mp3')) + f.write('{}\n'.format(os.path.join('a', 'b', 'c.mp3'))) + f.write('{}\n'.format(os.path.join('d', 'e', 'f.mp3'))) + f.write('{}\n'.format('nonexisting.mp3')) self.config['playlist']['auto'] = True self.config['playlist']['relative_to'] = 'library' @@ -220,7 +218,7 @@ class PlaylistTestItemMoved(PlaylistUpdateTestHelper, unittest.TestCase): def test_item_moved(self): # Emit item_moved event for an item that is in a playlist - results = self.lib.items(u'path:{0}'.format(shlex_quote( + results = self.lib.items('path:{}'.format(quote( os.path.join(self.music_dir, 'd', 'e', 'f.mp3')))) item = results[0] beets.plugins.send( @@ -229,7 +227,7 @@ os.path.join(self.music_dir, 'g', 'h', 'i.mp3'))) # Emit item_moved event for an item that is not in a playlist - results = self.lib.items(u'path:{0}'.format(shlex_quote( + results = self.lib.items('path:{}'.format(quote( os.path.join(self.music_dir, 'x', 'y', 'z.mp3')))) item = results[0] beets.plugins.send( @@ -242,7 +240,7 @@ # Check playlist with absolute paths playlist_path = os.path.join(self.playlist_dir, 'absolute.m3u') - with open(playlist_path, 'r') as f: + with open(playlist_path) as f: lines = [line.strip() for line in f.readlines()] self.assertEqual(lines, [ @@ -253,7 +251,7 @@ # Check playlist with relative paths playlist_path = os.path.join(self.playlist_dir, 'relative.m3u') - with open(playlist_path, 'r') as f: + with open(playlist_path) as f: lines = [line.strip() for line in f.readlines()] self.assertEqual(lines, [ @@ -266,13 +264,13 @@ class PlaylistTestItemRemoved(PlaylistUpdateTestHelper, unittest.TestCase): def test_item_removed(self): # Emit item_removed event for an item that is in a playlist - results = self.lib.items(u'path:{0}'.format(shlex_quote( + results = self.lib.items('path:{}'.format(quote( os.path.join(self.music_dir, 'd', 'e', 'f.mp3')))) item = results[0] beets.plugins.send('item_removed', item=item) # Emit item_removed event for an item that is not in a playlist - results = self.lib.items(u'path:{0}'.format(shlex_quote( + results = self.lib.items('path:{}'.format(quote( os.path.join(self.music_dir, 'x', 'y', 'z.mp3')))) item = results[0] beets.plugins.send('item_removed', item=item) @@ -282,7 +280,7 @@ # Check playlist with absolute paths playlist_path = os.path.join(self.playlist_dir, 'absolute.m3u') - with open(playlist_path, 'r') as f: + with open(playlist_path) as f: lines = [line.strip() for line in f.readlines()] self.assertEqual(lines, [ @@ -292,7 +290,7 @@ # Check playlist with relative paths playlist_path = os.path.join(self.playlist_dir, 'relative.m3u') - with open(playlist_path, 'r') as f: + with open(playlist_path) as f: lines = [line.strip() for line in f.readlines()] self.assertEqual(lines, [ @@ -304,5 +302,6 @@ def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == '__main__': unittest.main(defaultTest='suite') diff -Nru beets-1.5.0/test/test_play.py beets-1.6.0/test/test_play.py --- beets-1.5.0/test/test_play.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_play.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Jesse Weinstein # @@ -15,12 +14,12 @@ """Tests for the play plugin""" -from __future__ import division, absolute_import, print_function import os +import sys import unittest -from mock import patch, ANY +from unittest.mock import patch, ANY from test.helper import TestHelper, control_stdin @@ -33,7 +32,7 @@ def setUp(self): self.setup_beets() self.load_plugins('play') - self.item = self.add_item(album=u'a nice älbum', title=u'aNiceTitle') + self.item = self.add_item(album='a nice älbum', title='aNiceTitle') self.lib.add_album([self.item]) self.config['play']['command'] = 'echo' @@ -47,7 +46,7 @@ open_mock.assert_called_once_with(ANY, expected_cmd) expected_playlist = expected_playlist or self.item.path.decode('utf-8') - exp_playlist = expected_playlist + u'\n' + exp_playlist = expected_playlist + '\n' with open(open_mock.call_args[0][0][0], 'rb') as playlist: self.assertEqual(exp_playlist, playlist.read().decode('utf-8')) @@ -55,24 +54,25 @@ self.run_and_assert(open_mock) def test_album_option(self, open_mock): - self.run_and_assert(open_mock, [u'-a', u'nice']) + self.run_and_assert(open_mock, ['-a', 'nice']) def test_args_option(self, open_mock): self.run_and_assert( - open_mock, [u'-A', u'foo', u'title:aNiceTitle'], u'echo foo') + open_mock, ['-A', 'foo', 'title:aNiceTitle'], 'echo foo') def test_args_option_in_middle(self, open_mock): self.config['play']['command'] = 'echo $args other' self.run_and_assert( - open_mock, [u'-A', u'foo', u'title:aNiceTitle'], u'echo foo other') + open_mock, ['-A', 'foo', 'title:aNiceTitle'], 'echo foo other') def test_unset_args_option_in_middle(self, open_mock): self.config['play']['command'] = 'echo $args other' self.run_and_assert( - open_mock, [u'title:aNiceTitle'], u'echo other') + open_mock, ['title:aNiceTitle'], 'echo other') + @unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows def test_relative_to(self, open_mock): self.config['play']['command'] = 'echo' self.config['play']['relative_to'] = '/something' @@ -90,19 +90,19 @@ open_mock.assert_called_once_with(ANY, open_anything()) with open(open_mock.call_args[0][0][0], 'rb') as f: playlist = f.read().decode('utf-8') - self.assertEqual(u'{}\n'.format( + self.assertEqual('{}\n'.format( os.path.dirname(self.item.path.decode('utf-8'))), playlist) def test_raw(self, open_mock): self.config['play']['raw'] = True - self.run_command(u'play', u'nice') + self.run_command('play', 'nice') open_mock.assert_called_once_with([self.item.path], 'echo') def test_not_found(self, open_mock): - self.run_command(u'play', u'not found') + self.run_command('play', 'not found') open_mock.assert_not_called() @@ -111,7 +111,7 @@ self.add_item(title='another NiceTitle') with control_stdin("a"): - self.run_command(u'play', u'nice') + self.run_command('play', 'nice') open_mock.assert_not_called() @@ -119,21 +119,21 @@ self.config['play']['warning_threshold'] = 1 self.other_item = self.add_item(title='another NiceTitle') - expected_playlist = u'{0}\n{1}'.format( + expected_playlist = '{}\n{}'.format( self.item.path.decode('utf-8'), self.other_item.path.decode('utf-8')) with control_stdin("a"): self.run_and_assert( open_mock, - [u'-y', u'NiceTitle'], + ['-y', 'NiceTitle'], expected_playlist=expected_playlist) def test_command_failed(self, open_mock): - open_mock.side_effect = OSError(u"some reason") + open_mock.side_effect = OSError("some reason") with self.assertRaises(UserError): - self.run_command(u'play', u'title:aNiceTitle') + self.run_command('play', 'title:aNiceTitle') def suite(): diff -Nru beets-1.5.0/test/test_plexupdate.py beets-1.6.0/test/test_plexupdate.py --- beets-1.5.0/test/test_plexupdate.py 2020-07-14 10:55:55.000000000 +0000 +++ beets-1.6.0/test/test_plexupdate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- - -from __future__ import division, absolute_import, print_function - from test.helper import TestHelper from beetsplug.plexupdate import get_music_section, update_plex import unittest @@ -76,8 +72,8 @@ self.load_plugins('plexupdate') self.config['plex'] = { - u'host': u'localhost', - u'port': 32400} + 'host': 'localhost', + 'port': 32400} def tearDown(self): self.teardown_beets() diff -Nru beets-1.5.0/test/test_plugin_mediafield.py beets-1.6.0/test/test_plugin_mediafield.py --- beets-1.5.0/test/test_plugin_mediafield.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_plugin_mediafield.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,10 +14,8 @@ """Tests the facility that lets plugins add custom field to MediaFile. """ -from __future__ import division, absolute_import, print_function import os -import six import shutil import unittest @@ -30,7 +27,7 @@ field_extension = mediafile.MediaField( - mediafile.MP3DescStorageStyle(u'customtag'), + mediafile.MP3DescStorageStyle('customtag'), mediafile.MP4StorageStyle('----:com.apple.iTunes:customtag'), mediafile.StorageStyle('customtag'), mediafile.ASFStorageStyle('customtag'), @@ -52,11 +49,11 @@ try: mf = self._mediafile_fixture('empty') - mf.customtag = u'F#' + mf.customtag = 'F#' mf.save() mf = mediafile.MediaFile(mf.path) - self.assertEqual(mf.customtag, u'F#') + self.assertEqual(mf.customtag, 'F#') finally: delattr(mediafile.MediaFile, 'customtag') @@ -70,10 +67,10 @@ mf = self._mediafile_fixture('empty') self.assertIsNone(mf.customtag) - item = Item(path=mf.path, customtag=u'Gb') + item = Item(path=mf.path, customtag='Gb') item.write() mf = mediafile.MediaFile(mf.path) - self.assertEqual(mf.customtag, u'Gb') + self.assertEqual(mf.customtag, 'Gb') finally: delattr(mediafile.MediaFile, 'customtag') @@ -85,11 +82,11 @@ try: mf = self._mediafile_fixture('empty') - mf.update({'customtag': u'F#'}) + mf.update({'customtag': 'F#'}) mf.save() item = Item.from_path(mf.path) - self.assertEqual(item['customtag'], u'F#') + self.assertEqual(item['customtag'], 'F#') finally: delattr(mediafile.MediaFile, 'customtag') @@ -98,14 +95,14 @@ def test_invalid_descriptor(self): with self.assertRaises(ValueError) as cm: mediafile.MediaFile.add_field('somekey', True) - self.assertIn(u'must be an instance of MediaField', - six.text_type(cm.exception)) + self.assertIn('must be an instance of MediaField', + str(cm.exception)) def test_overwrite_property(self): with self.assertRaises(ValueError) as cm: mediafile.MediaFile.add_field('artist', mediafile.MediaField()) - self.assertIn(u'property "artist" already exists', - six.text_type(cm.exception)) + self.assertIn('property "artist" already exists', + str(cm.exception)) def suite(): diff -Nru beets-1.5.0/test/test_plugins.py beets-1.6.0/test/test_plugins.py --- beets-1.5.0/test/test_plugins.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_plugins.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,10 +12,9 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import os -from mock import patch, Mock, ANY +from unittest.mock import patch, Mock, ANY import shutil import itertools import unittest @@ -74,22 +72,22 @@ self.register_plugin(RatingPlugin) self.config['plugins'] = 'rating' - item = Item(path=u'apath', artist=u'aaa') + item = Item(path='apath', artist='aaa') item.add(self.lib) # Do not match unset values - out = self.run_with_output(u'ls', u'rating:1..3') - self.assertNotIn(u'aaa', out) + out = self.run_with_output('ls', 'rating:1..3') + self.assertNotIn('aaa', out) - self.run_command(u'modify', u'rating=2', u'--yes') + self.run_command('modify', 'rating=2', '--yes') # Match in range - out = self.run_with_output(u'ls', u'rating:1..3') - self.assertIn(u'aaa', out) + out = self.run_with_output('ls', 'rating:1..3') + self.assertIn('aaa', out) # Don't match out of range - out = self.run_with_output(u'ls', u'rating:3..5') - self.assertNotIn(u'aaa', out) + out = self.run_with_output('ls', 'rating:3..5') + self.assertNotIn('aaa', out) class ItemWriteTest(unittest.TestCase, TestHelper): @@ -110,16 +108,16 @@ def test_change_tags(self): def on_write(item=None, path=None, tags=None): - if tags['artist'] == u'XXX': - tags['artist'] = u'YYY' + if tags['artist'] == 'XXX': + tags['artist'] = 'YYY' self.register_listener('write', on_write) - item = self.add_item_fixture(artist=u'XXX') + item = self.add_item_fixture(artist='XXX') item.write() mediafile = MediaFile(syspath(item.path)) - self.assertEqual(mediafile.artist, u'YYY') + self.assertEqual(mediafile.artist, 'YYY') def register_listener(self, event, func): self.event_listener_plugin.register_listener(event, func) @@ -195,8 +193,8 @@ os.makedirs(self.album_path) metadata = { - 'artist': u'Tag Artist', - 'album': u'Tag Album', + 'artist': 'Tag Artist', + 'album': 'Tag Album', 'albumartist': None, 'mb_trackid': None, 'mb_albumid': None, @@ -205,7 +203,7 @@ self.file_paths = [] for i in range(count): metadata['track'] = i + 1 - metadata['title'] = u'Tag Title Album %d' % (i + 1) + metadata['title'] = 'Tag Title Album %d' % (i + 1) track_file = bytestring_path('%02d - track.mp3' % (i + 1)) dest_path = os.path.join(self.album_path, track_file) self.__copy_file(dest_path, metadata) @@ -222,21 +220,21 @@ # Exactly one event should have been imported (for the album). # Sentinels do not get emitted. - self.assertEqual(logs.count(u'Sending event: import_task_created'), 1) + self.assertEqual(logs.count('Sending event: import_task_created'), 1) logs = [line for line in logs if not line.startswith( - u'Sending event:')] + 'Sending event:')] self.assertEqual(logs, [ - u'Album: {0}'.format(displayable_path( + 'Album: {}'.format(displayable_path( os.path.join(self.import_dir, b'album'))), - u' {0}'.format(displayable_path(self.file_paths[0])), - u' {0}'.format(displayable_path(self.file_paths[1])), + ' {}'.format(displayable_path(self.file_paths[0])), + ' {}'.format(displayable_path(self.file_paths[1])), ]) def test_import_task_created_with_plugin(self): class ToSingletonPlugin(plugins.BeetsPlugin): def __init__(self): - super(ToSingletonPlugin, self).__init__() + super().__init__() self.register_listener('import_task_created', self.import_task_created_event) @@ -266,13 +264,13 @@ # Exactly one event should have been imported (for the album). # Sentinels do not get emitted. - self.assertEqual(logs.count(u'Sending event: import_task_created'), 1) + self.assertEqual(logs.count('Sending event: import_task_created'), 1) logs = [line for line in logs if not line.startswith( - u'Sending event:')] + 'Sending event:')] self.assertEqual(logs, [ - u'Singleton: {0}'.format(displayable_path(self.file_paths[0])), - u'Singleton: {0}'.format(displayable_path(self.file_paths[1])), + 'Singleton: {}'.format(displayable_path(self.file_paths[0])), + 'Singleton: {}'.format(displayable_path(self.file_paths[1])), ]) @@ -280,13 +278,13 @@ def test_sanitize_choices(self): self.assertEqual( - plugins.sanitize_choices([u'A', u'Z'], (u'A', u'B')), [u'A']) + plugins.sanitize_choices(['A', 'Z'], ('A', 'B')), ['A']) self.assertEqual( - plugins.sanitize_choices([u'A', u'A'], (u'A')), [u'A']) + plugins.sanitize_choices(['A', 'A'], ('A')), ['A']) self.assertEqual( - plugins.sanitize_choices([u'D', u'*', u'A'], - (u'A', u'B', u'C', u'D')), - [u'D', u'B', u'C', u'A']) + plugins.sanitize_choices(['D', '*', 'A'], + ('A', 'B', 'C', 'D')), + ['D', 'B', 'C', 'A']) class ListenersTest(unittest.TestCase, TestHelper): @@ -301,7 +299,7 @@ class DummyPlugin(plugins.BeetsPlugin): def __init__(self): - super(DummyPlugin, self).__init__() + super().__init__() self.register_listener('cli_exit', self.dummy) self.register_listener('cli_exit', self.dummy) @@ -326,7 +324,7 @@ class DummyPlugin(plugins.BeetsPlugin): def __init__(self): - super(DummyPlugin, self).__init__() + super().__init__() self.foo = Mock(__name__='foo') self.register_listener('event_foo', self.foo) self.bar = Mock(__name__='bar') @@ -339,8 +337,8 @@ d.foo.assert_has_calls([]) d.bar.assert_has_calls([]) - plugins.send('event_foo', var=u"tagada") - d.foo.assert_called_once_with(var=u"tagada") + plugins.send('event_foo', var="tagada") + d.foo.assert_called_once_with(var="tagada") d.bar.assert_has_calls([]) @patch('beets.plugins.find_plugins') @@ -349,13 +347,13 @@ class DummyPlugin(plugins.BeetsPlugin): def __init__(self): - super(DummyPlugin, self).__init__() + super().__init__() for i in itertools.count(1): try: - meth = getattr(self, 'dummy{0}'.format(i)) + meth = getattr(self, f'dummy{i}') except AttributeError: break - self.register_listener('event{0}'.format(i), meth) + self.register_listener(f'event{i}', meth) def dummy1(self, foo): test.assertEqual(foo, 5) @@ -433,19 +431,19 @@ """Test the presence of plugin choices on the prompt (album).""" class DummyPlugin(plugins.BeetsPlugin): def __init__(self): - super(DummyPlugin, self).__init__() + super().__init__() self.register_listener('before_choose_candidate', self.return_choices) def return_choices(self, session, task): - return [ui.commands.PromptChoice('f', u'Foo', None), - ui.commands.PromptChoice('r', u'baR', None)] + return [ui.commands.PromptChoice('f', 'Foo', None), + ui.commands.PromptChoice('r', 'baR', None)] self.register_plugin(DummyPlugin) # Default options + extra choices by the plugin ('Foo', 'Bar') - opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is', - u'as Tracks', u'Group albums', u'Enter search', - u'enter Id', u'aBort') + (u'Foo', u'baR') + opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', + 'as Tracks', 'Group albums', 'Enter search', + 'enter Id', 'aBort') + ('Foo', 'baR') self.importer.add_choice(action.SKIP) self.importer.run() @@ -456,19 +454,19 @@ """Test the presence of plugin choices on the prompt (singleton).""" class DummyPlugin(plugins.BeetsPlugin): def __init__(self): - super(DummyPlugin, self).__init__() + super().__init__() self.register_listener('before_choose_candidate', self.return_choices) def return_choices(self, session, task): - return [ui.commands.PromptChoice('f', u'Foo', None), - ui.commands.PromptChoice('r', u'baR', None)] + return [ui.commands.PromptChoice('f', 'Foo', None), + ui.commands.PromptChoice('r', 'baR', None)] self.register_plugin(DummyPlugin) # Default options + extra choices by the plugin ('Foo', 'Bar') - opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is', - u'Enter search', - u'enter Id', u'aBort') + (u'Foo', u'baR') + opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', + 'Enter search', + 'enter Id', 'aBort') + ('Foo', 'baR') config['import']['singletons'] = True self.importer.add_choice(action.SKIP) @@ -480,21 +478,21 @@ """Test the short letter conflict solving.""" class DummyPlugin(plugins.BeetsPlugin): def __init__(self): - super(DummyPlugin, self).__init__() + super().__init__() self.register_listener('before_choose_candidate', self.return_choices) def return_choices(self, session, task): - return [ui.commands.PromptChoice('a', u'A foo', None), # dupe - ui.commands.PromptChoice('z', u'baZ', None), # ok - ui.commands.PromptChoice('z', u'Zupe', None), # dupe - ui.commands.PromptChoice('z', u'Zoo', None)] # dupe + return [ui.commands.PromptChoice('a', 'A foo', None), # dupe + ui.commands.PromptChoice('z', 'baZ', None), # ok + ui.commands.PromptChoice('z', 'Zupe', None), # dupe + ui.commands.PromptChoice('z', 'Zoo', None)] # dupe self.register_plugin(DummyPlugin) # Default options + not dupe extra choices by the plugin ('baZ') - opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is', - u'as Tracks', u'Group albums', u'Enter search', - u'enter Id', u'aBort') + (u'baZ',) + opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', + 'as Tracks', 'Group albums', 'Enter search', + 'enter Id', 'aBort') + ('baZ',) self.importer.add_choice(action.SKIP) self.importer.run() self.mock_input_options.assert_called_once_with(opts, default='a', @@ -504,21 +502,21 @@ """Test that plugin callbacks are being called upon user choice.""" class DummyPlugin(plugins.BeetsPlugin): def __init__(self): - super(DummyPlugin, self).__init__() + super().__init__() self.register_listener('before_choose_candidate', self.return_choices) def return_choices(self, session, task): - return [ui.commands.PromptChoice('f', u'Foo', self.foo)] + return [ui.commands.PromptChoice('f', 'Foo', self.foo)] def foo(self, session, task): pass self.register_plugin(DummyPlugin) # Default options + extra choices by the plugin ('Foo', 'Bar') - opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is', - u'as Tracks', u'Group albums', u'Enter search', - u'enter Id', u'aBort') + (u'Foo',) + opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', + 'as Tracks', 'Group albums', 'Enter search', + 'enter Id', 'aBort') + ('Foo',) # DummyPlugin.foo() should be called once with patch.object(DummyPlugin, 'foo', autospec=True) as mock_foo: @@ -535,21 +533,21 @@ """Test that plugin callbacks that return a value exit the loop.""" class DummyPlugin(plugins.BeetsPlugin): def __init__(self): - super(DummyPlugin, self).__init__() + super().__init__() self.register_listener('before_choose_candidate', self.return_choices) def return_choices(self, session, task): - return [ui.commands.PromptChoice('f', u'Foo', self.foo)] + return [ui.commands.PromptChoice('f', 'Foo', self.foo)] def foo(self, session, task): return action.SKIP self.register_plugin(DummyPlugin) # Default options + extra choices by the plugin ('Foo', 'Bar') - opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is', - u'as Tracks', u'Group albums', u'Enter search', - u'enter Id', u'aBort') + (u'Foo',) + opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', + 'as Tracks', 'Group albums', 'Enter search', + 'enter Id', 'aBort') + ('Foo',) # DummyPlugin.foo() should be called once with helper.control_stdin('f\n'): diff -Nru beets-1.5.0/test/test_query.py beets-1.6.0/test/test_query.py --- beets-1.5.0/test/test_query.py 2021-03-08 00:47:22.000000000 +0000 +++ beets-1.6.0/test/test_query.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,10 +14,9 @@ """Various tests for querying the library database. """ -from __future__ import division, absolute_import, print_function from functools import partial -from mock import patch +from unittest.mock import patch import os import sys import unittest @@ -34,7 +32,6 @@ from beets.library import Library, Item from beets import util import platform -import six class TestHelper(helper.TestHelper): @@ -57,19 +54,19 @@ self.assertEqual(self.lib.items(q).get().title, 'the title') def test_restriction_completeness(self): - q = dbcore.query.AnyFieldQuery('title', [u'title'], + q = dbcore.query.AnyFieldQuery('title', ['title'], dbcore.query.SubstringQuery) - self.assertEqual(self.lib.items(q).get().title, u'the title') + self.assertEqual(self.lib.items(q).get().title, 'the title') def test_restriction_soundness(self): - q = dbcore.query.AnyFieldQuery('title', [u'artist'], + q = dbcore.query.AnyFieldQuery('title', ['artist'], dbcore.query.SubstringQuery) self.assertEqual(self.lib.items(q).get(), None) def test_eq(self): - q1 = dbcore.query.AnyFieldQuery('foo', [u'bar'], + q1 = dbcore.query.AnyFieldQuery('foo', ['bar'], dbcore.query.SubstringQuery) - q2 = dbcore.query.AnyFieldQuery('foo', [u'bar'], + q2 = dbcore.query.AnyFieldQuery('foo', ['bar'], dbcore.query.SubstringQuery) self.assertEqual(q1, q2) @@ -77,34 +74,34 @@ self.assertNotEqual(q1, q2) -class AssertsMixin(object): +class AssertsMixin: def assert_items_matched(self, results, titles): - self.assertEqual(set([i.title for i in results]), set(titles)) + self.assertEqual({i.title for i in results}, set(titles)) def assert_albums_matched(self, results, albums): - self.assertEqual(set([a.album for a in results]), set(albums)) + self.assertEqual({a.album for a in results}, set(albums)) # A test case class providing a library with some dummy data and some # assertions involving that data. class DummyDataTestCase(_common.TestCase, AssertsMixin): def setUp(self): - super(DummyDataTestCase, self).setUp() + super().setUp() self.lib = beets.library.Library(':memory:') items = [_common.item() for _ in range(3)] - items[0].title = u'foo bar' - items[0].artist = u'one' - items[0].album = u'baz' + items[0].title = 'foo bar' + items[0].artist = 'one' + items[0].album = 'baz' items[0].year = 2001 items[0].comp = True - items[1].title = u'baz qux' - items[1].artist = u'two' - items[1].album = u'baz' + items[1].title = 'baz qux' + items[1].artist = 'two' + items[1].album = 'baz' items[1].year = 2002 items[1].comp = True - items[2].title = u'beets 4 eva' - items[2].artist = u'three' - items[2].album = u'foo' + items[2].title = 'beets 4 eva' + items[2].artist = 'three' + items[2].album = 'foo' items[2].year = 2003 items[2].comp = False for item in items: @@ -113,15 +110,15 @@ def assert_items_matched_all(self, results): self.assert_items_matched(results, [ - u'foo bar', - u'baz qux', - u'beets 4 eva', + 'foo bar', + 'baz qux', + 'beets 4 eva', ]) class GetTest(DummyDataTestCase): def test_get_empty(self): - q = u'' + q = '' results = self.lib.items(q) self.assert_items_matched_all(results) @@ -131,255 +128,252 @@ self.assert_items_matched_all(results) def test_get_one_keyed_term(self): - q = u'title:qux' + q = 'title:qux' results = self.lib.items(q) - self.assert_items_matched(results, [u'baz qux']) + self.assert_items_matched(results, ['baz qux']) def test_get_one_keyed_regexp(self): - q = u'artist::t.+r' + q = 'artist::t.+r' results = self.lib.items(q) - self.assert_items_matched(results, [u'beets 4 eva']) + self.assert_items_matched(results, ['beets 4 eva']) def test_get_one_unkeyed_term(self): - q = u'three' + q = 'three' results = self.lib.items(q) - self.assert_items_matched(results, [u'beets 4 eva']) + self.assert_items_matched(results, ['beets 4 eva']) def test_get_one_unkeyed_regexp(self): - q = u':x$' + q = ':x$' results = self.lib.items(q) - self.assert_items_matched(results, [u'baz qux']) + self.assert_items_matched(results, ['baz qux']) def test_get_no_matches(self): - q = u'popebear' + q = 'popebear' results = self.lib.items(q) self.assert_items_matched(results, []) def test_invalid_key(self): - q = u'pope:bear' + q = 'pope:bear' results = self.lib.items(q) # Matches nothing since the flexattr is not present on the # objects. self.assert_items_matched(results, []) def test_term_case_insensitive(self): - q = u'oNE' + q = 'oNE' results = self.lib.items(q) - self.assert_items_matched(results, [u'foo bar']) + self.assert_items_matched(results, ['foo bar']) def test_regexp_case_sensitive(self): - q = u':oNE' + q = ':oNE' results = self.lib.items(q) self.assert_items_matched(results, []) - q = u':one' + q = ':one' results = self.lib.items(q) - self.assert_items_matched(results, [u'foo bar']) + self.assert_items_matched(results, ['foo bar']) def test_term_case_insensitive_with_key(self): - q = u'artist:thrEE' + q = 'artist:thrEE' results = self.lib.items(q) - self.assert_items_matched(results, [u'beets 4 eva']) + self.assert_items_matched(results, ['beets 4 eva']) def test_key_case_insensitive(self): - q = u'ArTiST:three' + q = 'ArTiST:three' results = self.lib.items(q) - self.assert_items_matched(results, [u'beets 4 eva']) + self.assert_items_matched(results, ['beets 4 eva']) def test_unkeyed_term_matches_multiple_columns(self): - q = u'baz' + q = 'baz' results = self.lib.items(q) self.assert_items_matched(results, [ - u'foo bar', - u'baz qux', + 'foo bar', + 'baz qux', ]) def test_unkeyed_regexp_matches_multiple_columns(self): - q = u':z$' + q = ':z$' results = self.lib.items(q) self.assert_items_matched(results, [ - u'foo bar', - u'baz qux', + 'foo bar', + 'baz qux', ]) def test_keyed_term_matches_only_one_column(self): - q = u'title:baz' + q = 'title:baz' results = self.lib.items(q) - self.assert_items_matched(results, [u'baz qux']) + self.assert_items_matched(results, ['baz qux']) def test_keyed_regexp_matches_only_one_column(self): - q = u'title::baz' + q = 'title::baz' results = self.lib.items(q) self.assert_items_matched(results, [ - u'baz qux', + 'baz qux', ]) def test_multiple_terms_narrow_search(self): - q = u'qux baz' + q = 'qux baz' results = self.lib.items(q) self.assert_items_matched(results, [ - u'baz qux', + 'baz qux', ]) def test_multiple_regexps_narrow_search(self): - q = u':baz :qux' + q = ':baz :qux' results = self.lib.items(q) - self.assert_items_matched(results, [u'baz qux']) + self.assert_items_matched(results, ['baz qux']) def test_mixed_terms_regexps_narrow_search(self): - q = u':baz qux' + q = ':baz qux' results = self.lib.items(q) - self.assert_items_matched(results, [u'baz qux']) + self.assert_items_matched(results, ['baz qux']) def test_single_year(self): - q = u'year:2001' + q = 'year:2001' results = self.lib.items(q) - self.assert_items_matched(results, [u'foo bar']) + self.assert_items_matched(results, ['foo bar']) def test_year_range(self): - q = u'year:2000..2002' + q = 'year:2000..2002' results = self.lib.items(q) self.assert_items_matched(results, [ - u'foo bar', - u'baz qux', + 'foo bar', + 'baz qux', ]) def test_singleton_true(self): - q = u'singleton:true' + q = 'singleton:true' results = self.lib.items(q) - self.assert_items_matched(results, [u'beets 4 eva']) + self.assert_items_matched(results, ['beets 4 eva']) def test_singleton_false(self): - q = u'singleton:false' + q = 'singleton:false' results = self.lib.items(q) - self.assert_items_matched(results, [u'foo bar', u'baz qux']) + self.assert_items_matched(results, ['foo bar', 'baz qux']) def test_compilation_true(self): - q = u'comp:true' + q = 'comp:true' results = self.lib.items(q) - self.assert_items_matched(results, [u'foo bar', u'baz qux']) + self.assert_items_matched(results, ['foo bar', 'baz qux']) def test_compilation_false(self): - q = u'comp:false' + q = 'comp:false' results = self.lib.items(q) - self.assert_items_matched(results, [u'beets 4 eva']) + self.assert_items_matched(results, ['beets 4 eva']) def test_unknown_field_name_no_results(self): - q = u'xyzzy:nonsense' + q = 'xyzzy:nonsense' results = self.lib.items(q) titles = [i.title for i in results] self.assertEqual(titles, []) def test_unknown_field_name_no_results_in_album_query(self): - q = u'xyzzy:nonsense' + q = 'xyzzy:nonsense' results = self.lib.albums(q) names = [a.album for a in results] self.assertEqual(names, []) def test_item_field_name_matches_nothing_in_album_query(self): - q = u'format:nonsense' + q = 'format:nonsense' results = self.lib.albums(q) names = [a.album for a in results] self.assertEqual(names, []) def test_unicode_query(self): item = self.lib.items().get() - item.title = u'caf\xe9' + item.title = 'caf\xe9' item.store() - q = u'title:caf\xe9' + q = 'title:caf\xe9' results = self.lib.items(q) - self.assert_items_matched(results, [u'caf\xe9']) + self.assert_items_matched(results, ['caf\xe9']) def test_numeric_search_positive(self): - q = dbcore.query.NumericQuery('year', u'2001') + q = dbcore.query.NumericQuery('year', '2001') results = self.lib.items(q) self.assertTrue(results) def test_numeric_search_negative(self): - q = dbcore.query.NumericQuery('year', u'1999') + q = dbcore.query.NumericQuery('year', '1999') results = self.lib.items(q) self.assertFalse(results) def test_album_field_fallback(self): - self.album['albumflex'] = u'foo' + self.album['albumflex'] = 'foo' self.album.store() - q = u'albumflex:foo' + q = 'albumflex:foo' results = self.lib.items(q) self.assert_items_matched(results, [ - u'foo bar', - u'baz qux', + 'foo bar', + 'baz qux', ]) def test_invalid_query(self): with self.assertRaises(InvalidQueryArgumentValueError) as raised: - dbcore.query.NumericQuery('year', u'199a') - self.assertIn(u'not an int', six.text_type(raised.exception)) + dbcore.query.NumericQuery('year', '199a') + self.assertIn('not an int', str(raised.exception)) with self.assertRaises(InvalidQueryArgumentValueError) as raised: - dbcore.query.RegexpQuery('year', u'199(') - exception_text = six.text_type(raised.exception) - self.assertIn(u'not a regular expression', exception_text) - if sys.version_info >= (3, 5): - self.assertIn(u'unterminated subpattern', exception_text) - else: - self.assertIn(u'unbalanced parenthesis', exception_text) + dbcore.query.RegexpQuery('year', '199(') + exception_text = str(raised.exception) + self.assertIn('not a regular expression', exception_text) + self.assertIn('unterminated subpattern', exception_text) self.assertIsInstance(raised.exception, ParsingError) class MatchTest(_common.TestCase): def setUp(self): - super(MatchTest, self).setUp() + super().setUp() self.item = _common.item() def test_regex_match_positive(self): - q = dbcore.query.RegexpQuery('album', u'^the album$') + q = dbcore.query.RegexpQuery('album', '^the album$') self.assertTrue(q.match(self.item)) def test_regex_match_negative(self): - q = dbcore.query.RegexpQuery('album', u'^album$') + q = dbcore.query.RegexpQuery('album', '^album$') self.assertFalse(q.match(self.item)) def test_regex_match_non_string_value(self): - q = dbcore.query.RegexpQuery('disc', u'^6$') + q = dbcore.query.RegexpQuery('disc', '^6$') self.assertTrue(q.match(self.item)) def test_substring_match_positive(self): - q = dbcore.query.SubstringQuery('album', u'album') + q = dbcore.query.SubstringQuery('album', 'album') self.assertTrue(q.match(self.item)) def test_substring_match_negative(self): - q = dbcore.query.SubstringQuery('album', u'ablum') + q = dbcore.query.SubstringQuery('album', 'ablum') self.assertFalse(q.match(self.item)) def test_substring_match_non_string_value(self): - q = dbcore.query.SubstringQuery('disc', u'6') + q = dbcore.query.SubstringQuery('disc', '6') self.assertTrue(q.match(self.item)) def test_year_match_positive(self): - q = dbcore.query.NumericQuery('year', u'1') + q = dbcore.query.NumericQuery('year', '1') self.assertTrue(q.match(self.item)) def test_year_match_negative(self): - q = dbcore.query.NumericQuery('year', u'10') + q = dbcore.query.NumericQuery('year', '10') self.assertFalse(q.match(self.item)) def test_bitrate_range_positive(self): - q = dbcore.query.NumericQuery('bitrate', u'100000..200000') + q = dbcore.query.NumericQuery('bitrate', '100000..200000') self.assertTrue(q.match(self.item)) def test_bitrate_range_negative(self): - q = dbcore.query.NumericQuery('bitrate', u'200000..300000') + q = dbcore.query.NumericQuery('bitrate', '200000..300000') self.assertFalse(q.match(self.item)) def test_open_range(self): - dbcore.query.NumericQuery('bitrate', u'100000..') + dbcore.query.NumericQuery('bitrate', '100000..') def test_eq(self): - q1 = dbcore.query.MatchQuery('foo', u'bar') - q2 = dbcore.query.MatchQuery('foo', u'bar') - q3 = dbcore.query.MatchQuery('foo', u'baz') - q4 = dbcore.query.StringFieldQuery('foo', u'bar') + q1 = dbcore.query.MatchQuery('foo', 'bar') + q2 = dbcore.query.MatchQuery('foo', 'bar') + q3 = dbcore.query.MatchQuery('foo', 'baz') + q4 = dbcore.query.StringFieldQuery('foo', 'bar') self.assertEqual(q1, q2) self.assertNotEqual(q1, q3) self.assertNotEqual(q1, q4) @@ -388,12 +382,12 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin): def setUp(self): - super(PathQueryTest, self).setUp() + super().setUp() # This is the item we'll try to match. self.i.path = util.normpath('/a/b/c.mp3') - self.i.title = u'path item' - self.i.album = u'path album' + self.i.title = 'path item' + self.i.album = 'path album' self.i.store() self.lib.add_album([self.i]) @@ -418,37 +412,39 @@ self.patcher_samefile.start().return_value = True def tearDown(self): - super(PathQueryTest, self).tearDown() + super().tearDown() self.patcher_samefile.stop() self.patcher_exists.stop() def test_path_exact_match(self): - q = u'path:/a/b/c.mp3' + q = 'path:/a/b/c.mp3' results = self.lib.items(q) - self.assert_items_matched(results, [u'path item']) + self.assert_items_matched(results, ['path item']) results = self.lib.albums(q) self.assert_albums_matched(results, []) + @unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows def test_parent_directory_no_slash(self): - q = u'path:/a' + q = 'path:/a' results = self.lib.items(q) - self.assert_items_matched(results, [u'path item']) + self.assert_items_matched(results, ['path item']) results = self.lib.albums(q) - self.assert_albums_matched(results, [u'path album']) + self.assert_albums_matched(results, ['path album']) + @unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows def test_parent_directory_with_slash(self): - q = u'path:/a/' + q = 'path:/a/' results = self.lib.items(q) - self.assert_items_matched(results, [u'path item']) + self.assert_items_matched(results, ['path item']) results = self.lib.albums(q) - self.assert_albums_matched(results, [u'path album']) + self.assert_albums_matched(results, ['path album']) def test_no_match(self): - q = u'path:/xyzzy/' + q = 'path:/xyzzy/' results = self.lib.items(q) self.assert_items_matched(results, []) @@ -456,7 +452,7 @@ self.assert_albums_matched(results, []) def test_fragment_no_match(self): - q = u'path:/b/' + q = 'path:/b/' results = self.lib.items(q) self.assert_items_matched(results, []) @@ -464,20 +460,20 @@ self.assert_albums_matched(results, []) def test_nonnorm_path(self): - q = u'path:/x/../a/b' + q = 'path:/x/../a/b' results = self.lib.items(q) - self.assert_items_matched(results, [u'path item']) + self.assert_items_matched(results, ['path item']) results = self.lib.albums(q) - self.assert_albums_matched(results, [u'path album']) + self.assert_albums_matched(results, ['path album']) def test_slashed_query_matches_path(self): - q = u'/a/b' + q = '/a/b' results = self.lib.items(q) - self.assert_items_matched(results, [u'path item']) + self.assert_items_matched(results, ['path item']) results = self.lib.albums(q) - self.assert_albums_matched(results, [u'path album']) + self.assert_albums_matched(results, ['path album']) @unittest.skip('unfixed (#1865)') def test_path_query_in_or_query(self): @@ -486,7 +482,7 @@ self.assert_items_matched(results, ['path item']) def test_non_slashed_does_not_match_path(self): - q = u'c.mp3' + q = 'c.mp3' results = self.lib.items(q) self.assert_items_matched(results, []) @@ -494,60 +490,60 @@ self.assert_albums_matched(results, []) def test_slashes_in_explicit_field_does_not_match_path(self): - q = u'title:/a/b' + q = 'title:/a/b' results = self.lib.items(q) self.assert_items_matched(results, []) def test_path_item_regex(self): - q = u'path::c\\.mp3$' + q = 'path::c\\.mp3$' results = self.lib.items(q) - self.assert_items_matched(results, [u'path item']) + self.assert_items_matched(results, ['path item']) def test_path_album_regex(self): - q = u'path::b' + q = 'path::b' results = self.lib.albums(q) - self.assert_albums_matched(results, [u'path album']) + self.assert_albums_matched(results, ['path album']) def test_escape_underscore(self): - self.add_album(path=b'/a/_/title.mp3', title=u'with underscore', - album=u'album with underscore') - q = u'path:/a/_' + self.add_album(path=b'/a/_/title.mp3', title='with underscore', + album='album with underscore') + q = 'path:/a/_' results = self.lib.items(q) - self.assert_items_matched(results, [u'with underscore']) + self.assert_items_matched(results, ['with underscore']) results = self.lib.albums(q) - self.assert_albums_matched(results, [u'album with underscore']) + self.assert_albums_matched(results, ['album with underscore']) def test_escape_percent(self): - self.add_album(path=b'/a/%/title.mp3', title=u'with percent', - album=u'album with percent') - q = u'path:/a/%' + self.add_album(path=b'/a/%/title.mp3', title='with percent', + album='album with percent') + q = 'path:/a/%' results = self.lib.items(q) - self.assert_items_matched(results, [u'with percent']) + self.assert_items_matched(results, ['with percent']) results = self.lib.albums(q) - self.assert_albums_matched(results, [u'album with percent']) + self.assert_albums_matched(results, ['album with percent']) def test_escape_backslash(self): - self.add_album(path=br'/a/\x/title.mp3', title=u'with backslash', - album=u'album with backslash') - q = u'path:/a/\\\\x' + self.add_album(path=br'/a/\x/title.mp3', title='with backslash', + album='album with backslash') + q = 'path:/a/\\\\x' results = self.lib.items(q) - self.assert_items_matched(results, [u'with backslash']) + self.assert_items_matched(results, ['with backslash']) results = self.lib.albums(q) - self.assert_albums_matched(results, [u'album with backslash']) + self.assert_albums_matched(results, ['album with backslash']) def test_case_sensitivity(self): - self.add_album(path=b'/A/B/C2.mp3', title=u'caps path') + self.add_album(path=b'/A/B/C2.mp3', title='caps path') - makeq = partial(beets.library.PathQuery, u'path', '/A/B') + makeq = partial(beets.library.PathQuery, 'path', '/A/B') results = self.lib.items(makeq(case_sensitive=True)) - self.assert_items_matched(results, [u'caps path']) + self.assert_items_matched(results, ['caps path']) results = self.lib.items(makeq(case_sensitive=False)) - self.assert_items_matched(results, [u'path item', u'caps path']) + self.assert_items_matched(results, ['path item', 'caps path']) # Check for correct case sensitivity selection (this check # only works on non-Windows OSes). @@ -622,7 +618,7 @@ self.assertTrue(is_path(parent)) # Some non-existent path. - self.assertFalse(is_path(path + u'baz')) + self.assertFalse(is_path(path + 'baz')) finally: # Restart the `os.path.exists` patch. @@ -639,10 +635,10 @@ cur_dir = os.getcwd() try: os.chdir(self.temp_dir) - self.assertTrue(is_path(u'foo/')) - self.assertTrue(is_path(u'foo/bar')) - self.assertTrue(is_path(u'foo/bar:tagada')) - self.assertFalse(is_path(u'bar')) + self.assertTrue(is_path('foo/')) + self.assertTrue(is_path('foo/bar')) + self.assertTrue(is_path('foo/bar:tagada')) + self.assertFalse(is_path('bar')) finally: os.chdir(cur_dir) @@ -660,32 +656,32 @@ def test_exact_value_match(self): item = self.add_item(bpm=120) - matched = self.lib.items(u'bpm:120').get() + matched = self.lib.items('bpm:120').get() self.assertEqual(item.id, matched.id) def test_range_match(self): item = self.add_item(bpm=120) self.add_item(bpm=130) - matched = self.lib.items(u'bpm:110..125') + matched = self.lib.items('bpm:110..125') self.assertEqual(1, len(matched)) self.assertEqual(item.id, matched.get().id) def test_flex_range_match(self): Item._types = {'myint': types.Integer()} item = self.add_item(myint=2) - matched = self.lib.items(u'myint:2').get() + matched = self.lib.items('myint:2').get() self.assertEqual(item.id, matched.id) def test_flex_dont_match_missing(self): Item._types = {'myint': types.Integer()} self.add_item() - matched = self.lib.items(u'myint:2').get() + matched = self.lib.items('myint:2').get() self.assertIsNone(matched) def test_no_substring_match(self): self.add_item(bpm=120) - matched = self.lib.items(u'bpm:12').get() + matched = self.lib.items('bpm:12').get() self.assertIsNone(matched) @@ -701,35 +697,35 @@ def test_parse_true(self): item_true = self.add_item(comp=True) item_false = self.add_item(comp=False) - matched = self.lib.items(u'comp:true') + matched = self.lib.items('comp:true') self.assertInResult(item_true, matched) self.assertNotInResult(item_false, matched) def test_flex_parse_true(self): item_true = self.add_item(flexbool=True) item_false = self.add_item(flexbool=False) - matched = self.lib.items(u'flexbool:true') + matched = self.lib.items('flexbool:true') self.assertInResult(item_true, matched) self.assertNotInResult(item_false, matched) def test_flex_parse_false(self): item_true = self.add_item(flexbool=True) item_false = self.add_item(flexbool=False) - matched = self.lib.items(u'flexbool:false') + matched = self.lib.items('flexbool:false') self.assertInResult(item_false, matched) self.assertNotInResult(item_true, matched) def test_flex_parse_1(self): item_true = self.add_item(flexbool=True) item_false = self.add_item(flexbool=False) - matched = self.lib.items(u'flexbool:1') + matched = self.lib.items('flexbool:1') self.assertInResult(item_true, matched) self.assertNotInResult(item_false, matched) def test_flex_parse_0(self): item_true = self.add_item(flexbool=True) item_false = self.add_item(flexbool=False) - matched = self.lib.items(u'flexbool:0') + matched = self.lib.items('flexbool:0') self.assertInResult(item_false, matched) self.assertNotInResult(item_true, matched) @@ -737,26 +733,26 @@ # TODO this should be the other way around item_true = self.add_item(flexbool=True) item_false = self.add_item(flexbool=False) - matched = self.lib.items(u'flexbool:something') + matched = self.lib.items('flexbool:something') self.assertInResult(item_false, matched) self.assertNotInResult(item_true, matched) class DefaultSearchFieldsTest(DummyDataTestCase): def test_albums_matches_album(self): - albums = list(self.lib.albums(u'baz')) + albums = list(self.lib.albums('baz')) self.assertEqual(len(albums), 1) def test_albums_matches_albumartist(self): - albums = list(self.lib.albums([u'album artist'])) + albums = list(self.lib.albums(['album artist'])) self.assertEqual(len(albums), 1) def test_items_matches_title(self): - items = self.lib.items(u'beets') - self.assert_items_matched(items, [u'beets 4 eva']) + items = self.lib.items('beets') + self.assert_items_matched(items, ['beets 4 eva']) def test_items_does_not_match_year(self): - items = self.lib.items(u'2001') + items = self.lib.items('2001') self.assert_items_matched(items, []) @@ -769,33 +765,33 @@ singleton = self.add_item() album_item = self.add_album().items().get() - matched = self.lib.items(NoneQuery(u'album_id')) + matched = self.lib.items(NoneQuery('album_id')) self.assertInResult(singleton, matched) self.assertNotInResult(album_item, matched) def test_match_after_set_none(self): item = self.add_item(rg_track_gain=0) - matched = self.lib.items(NoneQuery(u'rg_track_gain')) + matched = self.lib.items(NoneQuery('rg_track_gain')) self.assertNotInResult(item, matched) item['rg_track_gain'] = None item.store() - matched = self.lib.items(NoneQuery(u'rg_track_gain')) + matched = self.lib.items(NoneQuery('rg_track_gain')) self.assertInResult(item, matched) def test_match_slow(self): item = self.add_item() - matched = self.lib.items(NoneQuery(u'rg_track_peak', fast=False)) + matched = self.lib.items(NoneQuery('rg_track_peak', fast=False)) self.assertInResult(item, matched) def test_match_slow_after_set_none(self): item = self.add_item(rg_track_gain=0) - matched = self.lib.items(NoneQuery(u'rg_track_gain', fast=False)) + matched = self.lib.items(NoneQuery('rg_track_gain', fast=False)) self.assertNotInResult(item, matched) item['rg_track_gain'] = None item.store() - matched = self.lib.items(NoneQuery(u'rg_track_gain', fast=False)) + matched = self.lib.items(NoneQuery('rg_track_gain', fast=False)) self.assertInResult(item, matched) @@ -804,62 +800,63 @@ cases and assertions as on `MatchTest`, plus assertion on the negated queries (ie. assertTrue(q) -> assertFalse(NotQuery(q))). """ + def setUp(self): - super(NotQueryMatchTest, self).setUp() + super().setUp() self.item = _common.item() def test_regex_match_positive(self): - q = dbcore.query.RegexpQuery(u'album', u'^the album$') + q = dbcore.query.RegexpQuery('album', '^the album$') self.assertTrue(q.match(self.item)) self.assertFalse(dbcore.query.NotQuery(q).match(self.item)) def test_regex_match_negative(self): - q = dbcore.query.RegexpQuery(u'album', u'^album$') + q = dbcore.query.RegexpQuery('album', '^album$') self.assertFalse(q.match(self.item)) self.assertTrue(dbcore.query.NotQuery(q).match(self.item)) def test_regex_match_non_string_value(self): - q = dbcore.query.RegexpQuery(u'disc', u'^6$') + q = dbcore.query.RegexpQuery('disc', '^6$') self.assertTrue(q.match(self.item)) self.assertFalse(dbcore.query.NotQuery(q).match(self.item)) def test_substring_match_positive(self): - q = dbcore.query.SubstringQuery(u'album', u'album') + q = dbcore.query.SubstringQuery('album', 'album') self.assertTrue(q.match(self.item)) self.assertFalse(dbcore.query.NotQuery(q).match(self.item)) def test_substring_match_negative(self): - q = dbcore.query.SubstringQuery(u'album', u'ablum') + q = dbcore.query.SubstringQuery('album', 'ablum') self.assertFalse(q.match(self.item)) self.assertTrue(dbcore.query.NotQuery(q).match(self.item)) def test_substring_match_non_string_value(self): - q = dbcore.query.SubstringQuery(u'disc', u'6') + q = dbcore.query.SubstringQuery('disc', '6') self.assertTrue(q.match(self.item)) self.assertFalse(dbcore.query.NotQuery(q).match(self.item)) def test_year_match_positive(self): - q = dbcore.query.NumericQuery(u'year', u'1') + q = dbcore.query.NumericQuery('year', '1') self.assertTrue(q.match(self.item)) self.assertFalse(dbcore.query.NotQuery(q).match(self.item)) def test_year_match_negative(self): - q = dbcore.query.NumericQuery(u'year', u'10') + q = dbcore.query.NumericQuery('year', '10') self.assertFalse(q.match(self.item)) self.assertTrue(dbcore.query.NotQuery(q).match(self.item)) def test_bitrate_range_positive(self): - q = dbcore.query.NumericQuery(u'bitrate', u'100000..200000') + q = dbcore.query.NumericQuery('bitrate', '100000..200000') self.assertTrue(q.match(self.item)) self.assertFalse(dbcore.query.NotQuery(q).match(self.item)) def test_bitrate_range_negative(self): - q = dbcore.query.NumericQuery(u'bitrate', u'200000..300000') + q = dbcore.query.NumericQuery('bitrate', '200000..300000') self.assertFalse(q.match(self.item)) self.assertTrue(dbcore.query.NotQuery(q).match(self.item)) def test_open_range(self): - q = dbcore.query.NumericQuery(u'bitrate', u'100000..') + q = dbcore.query.NumericQuery('bitrate', '100000..') dbcore.query.NotQuery(q) @@ -868,6 +865,7 @@ - `test_type_xxx`: tests for the negation of a particular XxxQuery class. - `test_get_yyy`: tests on query strings (similar to `GetTest`) """ + def assertNegationProperties(self, q): # noqa """Given a Query `q`, assert that: - q OR not(q) == all items @@ -882,47 +880,47 @@ self.assert_items_matched(self.lib.items(q_and), []) # assert manually checking the item titles - all_titles = set([i.title for i in self.lib.items()]) - q_results = set([i.title for i in self.lib.items(q)]) - not_q_results = set([i.title for i in self.lib.items(not_q)]) + all_titles = {i.title for i in self.lib.items()} + q_results = {i.title for i in self.lib.items(q)} + not_q_results = {i.title for i in self.lib.items(not_q)} self.assertEqual(q_results.union(not_q_results), all_titles) self.assertEqual(q_results.intersection(not_q_results), set()) # round trip not_not_q = dbcore.query.NotQuery(not_q) - self.assertEqual(set([i.title for i in self.lib.items(q)]), - set([i.title for i in self.lib.items(not_not_q)])) + self.assertEqual({i.title for i in self.lib.items(q)}, + {i.title for i in self.lib.items(not_not_q)}) def test_type_and(self): # not(a and b) <-> not(a) or not(b) q = dbcore.query.AndQuery([ - dbcore.query.BooleanQuery(u'comp', True), - dbcore.query.NumericQuery(u'year', u'2002')], + dbcore.query.BooleanQuery('comp', True), + dbcore.query.NumericQuery('year', '2002')], ) not_results = self.lib.items(dbcore.query.NotQuery(q)) - self.assert_items_matched(not_results, [u'foo bar', u'beets 4 eva']) + self.assert_items_matched(not_results, ['foo bar', 'beets 4 eva']) self.assertNegationProperties(q) def test_type_anyfield(self): - q = dbcore.query.AnyFieldQuery(u'foo', [u'title', u'artist', u'album'], + q = dbcore.query.AnyFieldQuery('foo', ['title', 'artist', 'album'], dbcore.query.SubstringQuery) not_results = self.lib.items(dbcore.query.NotQuery(q)) - self.assert_items_matched(not_results, [u'baz qux']) + self.assert_items_matched(not_results, ['baz qux']) self.assertNegationProperties(q) def test_type_boolean(self): - q = dbcore.query.BooleanQuery(u'comp', True) + q = dbcore.query.BooleanQuery('comp', True) not_results = self.lib.items(dbcore.query.NotQuery(q)) - self.assert_items_matched(not_results, [u'beets 4 eva']) + self.assert_items_matched(not_results, ['beets 4 eva']) self.assertNegationProperties(q) def test_type_date(self): - q = dbcore.query.DateQuery(u'added', u'2000-01-01') + q = dbcore.query.DateQuery('added', '2000-01-01') not_results = self.lib.items(dbcore.query.NotQuery(q)) # query date is in the past, thus the 'not' results should contain all # items - self.assert_items_matched(not_results, [u'foo bar', u'baz qux', - u'beets 4 eva']) + self.assert_items_matched(not_results, ['foo bar', 'baz qux', + 'beets 4 eva']) self.assertNegationProperties(q) def test_type_false(self): @@ -932,41 +930,41 @@ self.assertNegationProperties(q) def test_type_match(self): - q = dbcore.query.MatchQuery(u'year', u'2003') + q = dbcore.query.MatchQuery('year', '2003') not_results = self.lib.items(dbcore.query.NotQuery(q)) - self.assert_items_matched(not_results, [u'foo bar', u'baz qux']) + self.assert_items_matched(not_results, ['foo bar', 'baz qux']) self.assertNegationProperties(q) def test_type_none(self): - q = dbcore.query.NoneQuery(u'rg_track_gain') + q = dbcore.query.NoneQuery('rg_track_gain') not_results = self.lib.items(dbcore.query.NotQuery(q)) self.assert_items_matched(not_results, []) self.assertNegationProperties(q) def test_type_numeric(self): - q = dbcore.query.NumericQuery(u'year', u'2001..2002') + q = dbcore.query.NumericQuery('year', '2001..2002') not_results = self.lib.items(dbcore.query.NotQuery(q)) - self.assert_items_matched(not_results, [u'beets 4 eva']) + self.assert_items_matched(not_results, ['beets 4 eva']) self.assertNegationProperties(q) def test_type_or(self): # not(a or b) <-> not(a) and not(b) - q = dbcore.query.OrQuery([dbcore.query.BooleanQuery(u'comp', True), - dbcore.query.NumericQuery(u'year', u'2002')]) + q = dbcore.query.OrQuery([dbcore.query.BooleanQuery('comp', True), + dbcore.query.NumericQuery('year', '2002')]) not_results = self.lib.items(dbcore.query.NotQuery(q)) - self.assert_items_matched(not_results, [u'beets 4 eva']) + self.assert_items_matched(not_results, ['beets 4 eva']) self.assertNegationProperties(q) def test_type_regexp(self): - q = dbcore.query.RegexpQuery(u'artist', u'^t') + q = dbcore.query.RegexpQuery('artist', '^t') not_results = self.lib.items(dbcore.query.NotQuery(q)) - self.assert_items_matched(not_results, [u'foo bar']) + self.assert_items_matched(not_results, ['foo bar']) self.assertNegationProperties(q) def test_type_substring(self): - q = dbcore.query.SubstringQuery(u'album', u'ba') + q = dbcore.query.SubstringQuery('album', 'ba') not_results = self.lib.items(dbcore.query.NotQuery(q)) - self.assert_items_matched(not_results, [u'beets 4 eva']) + self.assert_items_matched(not_results, ['beets 4 eva']) self.assertNegationProperties(q) def test_type_true(self): @@ -977,41 +975,41 @@ def test_get_prefixes_keyed(self): """Test both negation prefixes on a keyed query.""" - q0 = u'-title:qux' - q1 = u'^title:qux' + q0 = '-title:qux' + q1 = '^title:qux' results0 = self.lib.items(q0) results1 = self.lib.items(q1) - self.assert_items_matched(results0, [u'foo bar', u'beets 4 eva']) - self.assert_items_matched(results1, [u'foo bar', u'beets 4 eva']) + self.assert_items_matched(results0, ['foo bar', 'beets 4 eva']) + self.assert_items_matched(results1, ['foo bar', 'beets 4 eva']) def test_get_prefixes_unkeyed(self): """Test both negation prefixes on an unkeyed query.""" - q0 = u'-qux' - q1 = u'^qux' + q0 = '-qux' + q1 = '^qux' results0 = self.lib.items(q0) results1 = self.lib.items(q1) - self.assert_items_matched(results0, [u'foo bar', u'beets 4 eva']) - self.assert_items_matched(results1, [u'foo bar', u'beets 4 eva']) + self.assert_items_matched(results0, ['foo bar', 'beets 4 eva']) + self.assert_items_matched(results1, ['foo bar', 'beets 4 eva']) def test_get_one_keyed_regexp(self): - q = u'-artist::t.+r' + q = '-artist::t.+r' results = self.lib.items(q) - self.assert_items_matched(results, [u'foo bar', u'baz qux']) + self.assert_items_matched(results, ['foo bar', 'baz qux']) def test_get_one_unkeyed_regexp(self): - q = u'-:x$' + q = '-:x$' results = self.lib.items(q) - self.assert_items_matched(results, [u'foo bar', u'beets 4 eva']) + self.assert_items_matched(results, ['foo bar', 'beets 4 eva']) def test_get_multiple_terms(self): - q = u'baz -bar' + q = 'baz -bar' results = self.lib.items(q) - self.assert_items_matched(results, [u'baz qux']) + self.assert_items_matched(results, ['baz qux']) def test_get_mixed_terms(self): - q = u'baz -title:bar' + q = 'baz -title:bar' results = self.lib.items(q) - self.assert_items_matched(results, [u'baz qux']) + self.assert_items_matched(results, ['baz qux']) def test_fast_vs_slow(self): """Test that the results are the same regardless of the `fast` flag @@ -1021,13 +1019,13 @@ AttributeError: type object 'NoneQuery' has no attribute 'field' at NoneQuery.match() (due to being @classmethod, and no self?) """ - classes = [(dbcore.query.DateQuery, [u'added', u'2001-01-01']), - (dbcore.query.MatchQuery, [u'artist', u'one']), + classes = [(dbcore.query.DateQuery, ['added', '2001-01-01']), + (dbcore.query.MatchQuery, ['artist', 'one']), # (dbcore.query.NoneQuery, ['rg_track_gain']), - (dbcore.query.NumericQuery, [u'year', u'2002']), - (dbcore.query.StringFieldQuery, [u'year', u'2001']), - (dbcore.query.RegexpQuery, [u'album', u'^.a']), - (dbcore.query.SubstringQuery, [u'title', u'x'])] + (dbcore.query.NumericQuery, ['year', '2002']), + (dbcore.query.StringFieldQuery, ['year', '2001']), + (dbcore.query.RegexpQuery, ['album', '^.a']), + (dbcore.query.SubstringQuery, ['title', 'x'])] for klass, args in classes: q_fast = dbcore.query.NotQuery(klass(*(args + [True]))) diff -Nru beets-1.5.0/test/test_random.py beets-1.6.0/test/test_random.py --- beets-1.5.0/test/test_random.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_random.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2019, Carl Suster # @@ -16,7 +15,6 @@ """Test the beets.random utilities associated with the random plugin. """ -from __future__ import division, absolute_import, print_function import unittest from test.helper import TestHelper diff -Nru beets-1.5.0/test/test_replaygain.py beets-1.6.0/test/test_replaygain.py --- beets-1.5.0/test/test_replaygain.py 2021-03-06 21:56:33.000000000 +0000 +++ beets-1.6.0/test/test_replaygain.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes # @@ -14,9 +13,6 @@ # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function - -import six import unittest from mediafile import MediaFile @@ -70,7 +66,7 @@ # teardown operations may fail. In particular # {Item,Album} # may not have the _original_types attribute in unload_plugins pass - six.reraise(exc_info[1], None, exc_info[2]) + raise None.with_traceback(exc_info[2]) album = self.add_album_fixture(2) for item in album.items(): @@ -104,7 +100,7 @@ # that it could only happen if the decoder plugins are missing. if all(i.rg_track_peak is None and i.rg_track_gain is None for i in self.lib.items()): - self.skipTest(u'decoder plugins could not be loaded.') + self.skipTest('decoder plugins could not be loaded.') for item in self.lib.items(): self.assertIsNotNone(item.rg_track_peak) @@ -116,11 +112,11 @@ mediafile.rg_track_gain, item.rg_track_gain, places=2) def test_cli_skips_calculated_tracks(self): - self.run_command(u'replaygain') + self.run_command('replaygain') item = self.lib.items()[0] peak = item.rg_track_peak item.rg_track_gain = 0.0 - self.run_command(u'replaygain') + self.run_command('replaygain') self.assertEqual(item.rg_track_gain, 0.0) self.assertEqual(item.rg_track_peak, peak) @@ -130,7 +126,7 @@ self.assertIsNone(mediafile.rg_album_peak) self.assertIsNone(mediafile.rg_album_gain) - self.run_command(u'replaygain', u'-a') + self.run_command('replaygain', '-a') peaks = [] gains = [] @@ -155,7 +151,7 @@ for item in album.items(): self._reset_replaygain(item) - self.run_command(u'replaygain', u'-a') + self.run_command('replaygain', '-a') for item in album.items(): mediafile = MediaFile(item.path) @@ -172,7 +168,7 @@ def analyse(target_level): self.config['replaygain']['targetlevel'] = target_level self._reset_replaygain(item) - self.run_command(u'replaygain', '-f') + self.run_command('replaygain', '-f') mediafile = MediaFile(item.path) return mediafile.rg_track_gain @@ -186,9 +182,9 @@ self.assertNotEqual(gain_relative_to_84, gain_relative_to_89) -@unittest.skipIf(not GST_AVAILABLE, u'gstreamer cannot be found') +@unittest.skipIf(not GST_AVAILABLE, 'gstreamer cannot be found') class ReplayGainGstCliTest(ReplayGainCliTestBase, unittest.TestCase): - backend = u'gstreamer' + backend = 'gstreamer' def setUp(self): try: @@ -200,21 +196,22 @@ # Skip the test if plugins could not be loaded. self.skipTest(str(e)) - super(ReplayGainGstCliTest, self).setUp() + super().setUp() -@unittest.skipIf(not GAIN_PROG_AVAILABLE, u'no *gain command found') +@unittest.skipIf(not GAIN_PROG_AVAILABLE, 'no *gain command found') class ReplayGainCmdCliTest(ReplayGainCliTestBase, unittest.TestCase): - backend = u'command' + backend = 'command' -@unittest.skipIf(not FFMPEG_AVAILABLE, u'ffmpeg cannot be found') +@unittest.skipIf(not FFMPEG_AVAILABLE, 'ffmpeg cannot be found') class ReplayGainFfmpegTest(ReplayGainCliTestBase, unittest.TestCase): - backend = u'ffmpeg' + backend = 'ffmpeg' def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == '__main__': unittest.main(defaultTest='suite') diff -Nru beets-1.5.0/test/test_smartplaylist.py beets-1.6.0/test/test_smartplaylist.py --- beets-1.5.0/test/test_smartplaylist.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_smartplaylist.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Bruno Cauet. # @@ -13,14 +12,13 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function from os import path, remove from tempfile import mkdtemp from shutil import rmtree import unittest -from mock import Mock, MagicMock +from unittest.mock import Mock, MagicMock from beetsplug.smartplaylist import SmartPlaylistPlugin from beets.library import Item, Album, parse_query_string @@ -45,58 +43,58 @@ self.assertEqual(spl._unmatched_playlists, set()) config['smartplaylist']['playlists'].set([ - {'name': u'foo', - 'query': u'FOO foo'}, - {'name': u'bar', - 'album_query': [u'BAR bar1', u'BAR bar2']}, - {'name': u'baz', - 'query': u'BAZ baz', - 'album_query': u'BAZ baz'} + {'name': 'foo', + 'query': 'FOO foo'}, + {'name': 'bar', + 'album_query': ['BAR bar1', 'BAR bar2']}, + {'name': 'baz', + 'query': 'BAZ baz', + 'album_query': 'BAZ baz'} ]) spl.build_queries() self.assertEqual(spl._matched_playlists, set()) - foo_foo = parse_query_string(u'FOO foo', Item) - baz_baz = parse_query_string(u'BAZ baz', Item) - baz_baz2 = parse_query_string(u'BAZ baz', Album) - bar_bar = OrQuery((parse_query_string(u'BAR bar1', Album)[0], - parse_query_string(u'BAR bar2', Album)[0])) - self.assertEqual(spl._unmatched_playlists, set([ - (u'foo', foo_foo, (None, None)), - (u'baz', baz_baz, baz_baz2), - (u'bar', (None, None), (bar_bar, None)), - ])) + foo_foo = parse_query_string('FOO foo', Item) + baz_baz = parse_query_string('BAZ baz', Item) + baz_baz2 = parse_query_string('BAZ baz', Album) + bar_bar = OrQuery((parse_query_string('BAR bar1', Album)[0], + parse_query_string('BAR bar2', Album)[0])) + self.assertEqual(spl._unmatched_playlists, { + ('foo', foo_foo, (None, None)), + ('baz', baz_baz, baz_baz2), + ('bar', (None, None), (bar_bar, None)), + }) def test_build_queries_with_sorts(self): spl = SmartPlaylistPlugin() config['smartplaylist']['playlists'].set([ - {'name': u'no_sort', - 'query': u'foo'}, - {'name': u'one_sort', - 'query': u'foo year+'}, - {'name': u'only_empty_sorts', - 'query': [u'foo', u'bar']}, - {'name': u'one_non_empty_sort', - 'query': [u'foo year+', u'bar']}, - {'name': u'multiple_sorts', - 'query': [u'foo year+', u'bar genre-']}, - {'name': u'mixed', - 'query': [u'foo year+', u'bar', u'baz genre+ id-']} + {'name': 'no_sort', + 'query': 'foo'}, + {'name': 'one_sort', + 'query': 'foo year+'}, + {'name': 'only_empty_sorts', + 'query': ['foo', 'bar']}, + {'name': 'one_non_empty_sort', + 'query': ['foo year+', 'bar']}, + {'name': 'multiple_sorts', + 'query': ['foo year+', 'bar genre-']}, + {'name': 'mixed', + 'query': ['foo year+', 'bar', 'baz genre+ id-']} ]) spl.build_queries() - sorts = dict((name, sort) - for name, (_, sort), _ in spl._unmatched_playlists) + sorts = {name: sort + for name, (_, sort), _ in spl._unmatched_playlists} asseq = self.assertEqual # less cluttered code sort = FixedFieldSort # short cut since we're only dealing with this asseq(sorts["no_sort"], NullSort()) - asseq(sorts["one_sort"], sort(u'year')) + asseq(sorts["one_sort"], sort('year')) asseq(sorts["only_empty_sorts"], None) - asseq(sorts["one_non_empty_sort"], sort(u'year')) + asseq(sorts["one_non_empty_sort"], sort('year')) asseq(sorts["multiple_sorts"], - MultipleSort([sort('year'), sort(u'genre', False)])) + MultipleSort([sort('year'), sort('genre', False)])) asseq(sorts["mixed"], - MultipleSort([sort('year'), sort(u'genre'), sort(u'id', False)])) + MultipleSort([sort('year'), sort('genre'), sort('id', False)])) def test_matches(self): spl = SmartPlaylistPlugin() @@ -124,27 +122,27 @@ spl = SmartPlaylistPlugin() nones = None, None - pl1 = '1', (u'q1', None), nones - pl2 = '2', (u'q2', None), nones - pl3 = '3', (u'q3', None), nones + pl1 = '1', ('q1', None), nones + pl2 = '2', ('q2', None), nones + pl3 = '3', ('q3', None), nones - spl._unmatched_playlists = set([pl1, pl2, pl3]) + spl._unmatched_playlists = {pl1, pl2, pl3} spl._matched_playlists = set() spl.matches = Mock(return_value=False) - spl.db_change(None, u"nothing") - self.assertEqual(spl._unmatched_playlists, set([pl1, pl2, pl3])) + spl.db_change(None, "nothing") + self.assertEqual(spl._unmatched_playlists, {pl1, pl2, pl3}) self.assertEqual(spl._matched_playlists, set()) - spl.matches.side_effect = lambda _, q, __: q == u'q3' - spl.db_change(None, u"matches 3") - self.assertEqual(spl._unmatched_playlists, set([pl1, pl2])) - self.assertEqual(spl._matched_playlists, set([pl3])) - - spl.matches.side_effect = lambda _, q, __: q == u'q1' - spl.db_change(None, u"matches 3") - self.assertEqual(spl._matched_playlists, set([pl1, pl3])) - self.assertEqual(spl._unmatched_playlists, set([pl2])) + spl.matches.side_effect = lambda _, q, __: q == 'q3' + spl.db_change(None, "matches 3") + self.assertEqual(spl._unmatched_playlists, {pl1, pl2}) + self.assertEqual(spl._matched_playlists, {pl3}) + + spl.matches.side_effect = lambda _, q, __: q == 'q1' + spl.db_change(None, "matches 3") + self.assertEqual(spl._matched_playlists, {pl1, pl3}) + self.assertEqual(spl._unmatched_playlists, {pl2}) def test_playlist_update(self): spl = SmartPlaylistPlugin() @@ -193,7 +191,7 @@ {'name': 'my_playlist.m3u', 'query': self.item.title}, {'name': 'all.m3u', - 'query': u''} + 'query': ''} ]) config['smartplaylist']['playlist_dir'].set(py3_path(self.temp_dir)) self.load_plugins('smartplaylist') @@ -204,21 +202,21 @@ def test_splupdate(self): with self.assertRaises(UserError): - self.run_with_output(u'splupdate', u'tagada') + self.run_with_output('splupdate', 'tagada') - self.run_with_output(u'splupdate', u'my_playlist') + self.run_with_output('splupdate', 'my_playlist') m3u_path = path.join(self.temp_dir, b'my_playlist.m3u') self.assertTrue(path.exists(m3u_path)) with open(m3u_path, 'rb') as f: self.assertEqual(f.read(), self.item.path + b"\n") remove(m3u_path) - self.run_with_output(u'splupdate', u'my_playlist.m3u') + self.run_with_output('splupdate', 'my_playlist.m3u') with open(m3u_path, 'rb') as f: self.assertEqual(f.read(), self.item.path + b"\n") remove(m3u_path) - self.run_with_output(u'splupdate') + self.run_with_output('splupdate') for name in (b'my_playlist.m3u', b'all.m3u'): with open(path.join(self.temp_dir, name), 'rb') as f: self.assertEqual(f.read(), self.item.path + b"\n") diff -Nru beets-1.5.0/test/test_sort.py beets-1.6.0/test/test_sort.py --- beets-1.5.0/test/test_sort.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_sort.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """Various tests for querying the library database. """ -from __future__ import division, absolute_import, print_function import unittest from test import _common @@ -28,75 +26,75 @@ # assertions involving that data. class DummyDataTestCase(_common.TestCase): def setUp(self): - super(DummyDataTestCase, self).setUp() + super().setUp() self.lib = beets.library.Library(':memory:') albums = [_common.album() for _ in range(3)] - albums[0].album = u"Album A" - albums[0].genre = u"Rock" + albums[0].album = "Album A" + albums[0].genre = "Rock" albums[0].year = 2001 - albums[0].flex1 = u"Flex1-1" - albums[0].flex2 = u"Flex2-A" - albums[0].albumartist = u"Foo" + albums[0].flex1 = "Flex1-1" + albums[0].flex2 = "Flex2-A" + albums[0].albumartist = "Foo" albums[0].albumartist_sort = None - albums[1].album = u"Album B" - albums[1].genre = u"Rock" + albums[1].album = "Album B" + albums[1].genre = "Rock" albums[1].year = 2001 - albums[1].flex1 = u"Flex1-2" - albums[1].flex2 = u"Flex2-A" - albums[1].albumartist = u"Bar" + albums[1].flex1 = "Flex1-2" + albums[1].flex2 = "Flex2-A" + albums[1].albumartist = "Bar" albums[1].albumartist_sort = None - albums[2].album = u"Album C" - albums[2].genre = u"Jazz" + albums[2].album = "Album C" + albums[2].genre = "Jazz" albums[2].year = 2005 - albums[2].flex1 = u"Flex1-1" - albums[2].flex2 = u"Flex2-B" - albums[2].albumartist = u"Baz" + albums[2].flex1 = "Flex1-1" + albums[2].flex2 = "Flex2-B" + albums[2].albumartist = "Baz" albums[2].albumartist_sort = None for album in albums: self.lib.add(album) items = [_common.item() for _ in range(4)] - items[0].title = u'Foo bar' - items[0].artist = u'One' - items[0].album = u'Baz' + items[0].title = 'Foo bar' + items[0].artist = 'One' + items[0].album = 'Baz' items[0].year = 2001 items[0].comp = True - items[0].flex1 = u"Flex1-0" - items[0].flex2 = u"Flex2-A" + items[0].flex1 = "Flex1-0" + items[0].flex2 = "Flex2-A" items[0].album_id = albums[0].id items[0].artist_sort = None items[0].path = "/path0.mp3" items[0].track = 1 - items[1].title = u'Baz qux' - items[1].artist = u'Two' - items[1].album = u'Baz' + items[1].title = 'Baz qux' + items[1].artist = 'Two' + items[1].album = 'Baz' items[1].year = 2002 items[1].comp = True - items[1].flex1 = u"Flex1-1" - items[1].flex2 = u"Flex2-A" + items[1].flex1 = "Flex1-1" + items[1].flex2 = "Flex2-A" items[1].album_id = albums[0].id items[1].artist_sort = None items[1].path = "/patH1.mp3" items[1].track = 2 - items[2].title = u'Beets 4 eva' - items[2].artist = u'Three' - items[2].album = u'Foo' + items[2].title = 'Beets 4 eva' + items[2].artist = 'Three' + items[2].album = 'Foo' items[2].year = 2003 items[2].comp = False - items[2].flex1 = u"Flex1-2" - items[2].flex2 = u"Flex1-B" + items[2].flex1 = "Flex1-2" + items[2].flex2 = "Flex1-B" items[2].album_id = albums[1].id items[2].artist_sort = None items[2].path = "/paTH2.mp3" items[2].track = 3 - items[3].title = u'Beets 4 eva' - items[3].artist = u'Three' - items[3].album = u'Foo2' + items[3].title = 'Beets 4 eva' + items[3].artist = 'Three' + items[3].album = 'Foo2' items[3].year = 2004 items[3].comp = False - items[3].flex1 = u"Flex1-2" - items[3].flex2 = u"Flex1-C" + items[3].flex1 = "Flex1-2" + items[3].flex2 = "Flex1-C" items[3].album_id = albums[2].id items[3].artist_sort = None items[3].path = "/PATH3.mp3" @@ -107,50 +105,50 @@ class SortFixedFieldTest(DummyDataTestCase): def test_sort_asc(self): - q = u'' - sort = dbcore.query.FixedFieldSort(u"year", True) + q = '' + sort = dbcore.query.FixedFieldSort("year", True) results = self.lib.items(q, sort) self.assertLessEqual(results[0]['year'], results[1]['year']) self.assertEqual(results[0]['year'], 2001) # same thing with query string - q = u'year+' + q = 'year+' results2 = self.lib.items(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_sort_desc(self): - q = u'' - sort = dbcore.query.FixedFieldSort(u"year", False) + q = '' + sort = dbcore.query.FixedFieldSort("year", False) results = self.lib.items(q, sort) self.assertGreaterEqual(results[0]['year'], results[1]['year']) self.assertEqual(results[0]['year'], 2004) # same thing with query string - q = u'year-' + q = 'year-' results2 = self.lib.items(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_sort_two_field_asc(self): - q = u'' - s1 = dbcore.query.FixedFieldSort(u"album", True) - s2 = dbcore.query.FixedFieldSort(u"year", True) + q = '' + s1 = dbcore.query.FixedFieldSort("album", True) + s2 = dbcore.query.FixedFieldSort("year", True) sort = dbcore.query.MultipleSort() sort.add_sort(s1) sort.add_sort(s2) results = self.lib.items(q, sort) self.assertLessEqual(results[0]['album'], results[1]['album']) self.assertLessEqual(results[1]['album'], results[2]['album']) - self.assertEqual(results[0]['album'], u'Baz') - self.assertEqual(results[1]['album'], u'Baz') + self.assertEqual(results[0]['album'], 'Baz') + self.assertEqual(results[1]['album'], 'Baz') self.assertLessEqual(results[0]['year'], results[1]['year']) # same thing with query string - q = u'album+ year+' + q = 'album+ year+' results2 = self.lib.items(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_sort_path_field(self): - q = u'' + q = '' sort = dbcore.query.FixedFieldSort('path', True) results = self.lib.items(q, sort) self.assertEqual(results[0]['path'], b'/path0.mp3') @@ -161,46 +159,46 @@ class SortFlexFieldTest(DummyDataTestCase): def test_sort_asc(self): - q = u'' - sort = dbcore.query.SlowFieldSort(u"flex1", True) + q = '' + sort = dbcore.query.SlowFieldSort("flex1", True) results = self.lib.items(q, sort) self.assertLessEqual(results[0]['flex1'], results[1]['flex1']) - self.assertEqual(results[0]['flex1'], u'Flex1-0') + self.assertEqual(results[0]['flex1'], 'Flex1-0') # same thing with query string - q = u'flex1+' + q = 'flex1+' results2 = self.lib.items(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_sort_desc(self): - q = u'' - sort = dbcore.query.SlowFieldSort(u"flex1", False) + q = '' + sort = dbcore.query.SlowFieldSort("flex1", False) results = self.lib.items(q, sort) self.assertGreaterEqual(results[0]['flex1'], results[1]['flex1']) self.assertGreaterEqual(results[1]['flex1'], results[2]['flex1']) self.assertGreaterEqual(results[2]['flex1'], results[3]['flex1']) - self.assertEqual(results[0]['flex1'], u'Flex1-2') + self.assertEqual(results[0]['flex1'], 'Flex1-2') # same thing with query string - q = u'flex1-' + q = 'flex1-' results2 = self.lib.items(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_sort_two_field(self): - q = u'' - s1 = dbcore.query.SlowFieldSort(u"flex2", False) - s2 = dbcore.query.SlowFieldSort(u"flex1", True) + q = '' + s1 = dbcore.query.SlowFieldSort("flex2", False) + s2 = dbcore.query.SlowFieldSort("flex1", True) sort = dbcore.query.MultipleSort() sort.add_sort(s1) sort.add_sort(s2) results = self.lib.items(q, sort) self.assertGreaterEqual(results[0]['flex2'], results[1]['flex2']) self.assertGreaterEqual(results[1]['flex2'], results[2]['flex2']) - self.assertEqual(results[0]['flex2'], u'Flex2-A') - self.assertEqual(results[1]['flex2'], u'Flex2-A') + self.assertEqual(results[0]['flex2'], 'Flex2-A') + self.assertEqual(results[1]['flex2'], 'Flex2-A') self.assertLessEqual(results[0]['flex1'], results[1]['flex1']) # same thing with query string - q = u'flex2- flex1+' + q = 'flex2- flex1+' results2 = self.lib.items(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) @@ -208,44 +206,44 @@ class SortAlbumFixedFieldTest(DummyDataTestCase): def test_sort_asc(self): - q = u'' - sort = dbcore.query.FixedFieldSort(u"year", True) + q = '' + sort = dbcore.query.FixedFieldSort("year", True) results = self.lib.albums(q, sort) self.assertLessEqual(results[0]['year'], results[1]['year']) self.assertEqual(results[0]['year'], 2001) # same thing with query string - q = u'year+' + q = 'year+' results2 = self.lib.albums(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_sort_desc(self): - q = u'' - sort = dbcore.query.FixedFieldSort(u"year", False) + q = '' + sort = dbcore.query.FixedFieldSort("year", False) results = self.lib.albums(q, sort) self.assertGreaterEqual(results[0]['year'], results[1]['year']) self.assertEqual(results[0]['year'], 2005) # same thing with query string - q = u'year-' + q = 'year-' results2 = self.lib.albums(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_sort_two_field_asc(self): - q = u'' - s1 = dbcore.query.FixedFieldSort(u"genre", True) - s2 = dbcore.query.FixedFieldSort(u"album", True) + q = '' + s1 = dbcore.query.FixedFieldSort("genre", True) + s2 = dbcore.query.FixedFieldSort("album", True) sort = dbcore.query.MultipleSort() sort.add_sort(s1) sort.add_sort(s2) results = self.lib.albums(q, sort) self.assertLessEqual(results[0]['genre'], results[1]['genre']) self.assertLessEqual(results[1]['genre'], results[2]['genre']) - self.assertEqual(results[1]['genre'], u'Rock') - self.assertEqual(results[2]['genre'], u'Rock') + self.assertEqual(results[1]['genre'], 'Rock') + self.assertEqual(results[2]['genre'], 'Rock') self.assertLessEqual(results[1]['album'], results[2]['album']) # same thing with query string - q = u'genre+ album+' + q = 'genre+ album+' results2 = self.lib.albums(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) @@ -253,44 +251,44 @@ class SortAlbumFlexFieldTest(DummyDataTestCase): def test_sort_asc(self): - q = u'' - sort = dbcore.query.SlowFieldSort(u"flex1", True) + q = '' + sort = dbcore.query.SlowFieldSort("flex1", True) results = self.lib.albums(q, sort) self.assertLessEqual(results[0]['flex1'], results[1]['flex1']) self.assertLessEqual(results[1]['flex1'], results[2]['flex1']) # same thing with query string - q = u'flex1+' + q = 'flex1+' results2 = self.lib.albums(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_sort_desc(self): - q = u'' - sort = dbcore.query.SlowFieldSort(u"flex1", False) + q = '' + sort = dbcore.query.SlowFieldSort("flex1", False) results = self.lib.albums(q, sort) self.assertGreaterEqual(results[0]['flex1'], results[1]['flex1']) self.assertGreaterEqual(results[1]['flex1'], results[2]['flex1']) # same thing with query string - q = u'flex1-' + q = 'flex1-' results2 = self.lib.albums(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_sort_two_field_asc(self): - q = u'' - s1 = dbcore.query.SlowFieldSort(u"flex2", True) - s2 = dbcore.query.SlowFieldSort(u"flex1", True) + q = '' + s1 = dbcore.query.SlowFieldSort("flex2", True) + s2 = dbcore.query.SlowFieldSort("flex1", True) sort = dbcore.query.MultipleSort() sort.add_sort(s1) sort.add_sort(s2) results = self.lib.albums(q, sort) self.assertLessEqual(results[0]['flex2'], results[1]['flex2']) self.assertLessEqual(results[1]['flex2'], results[2]['flex2']) - self.assertEqual(results[0]['flex2'], u'Flex2-A') - self.assertEqual(results[1]['flex2'], u'Flex2-A') + self.assertEqual(results[0]['flex2'], 'Flex2-A') + self.assertEqual(results[1]['flex2'], 'Flex2-A') self.assertLessEqual(results[0]['flex1'], results[1]['flex1']) # same thing with query string - q = u'flex2+ flex1+' + q = 'flex2+ flex1+' results2 = self.lib.albums(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) @@ -298,25 +296,25 @@ class SortAlbumComputedFieldTest(DummyDataTestCase): def test_sort_asc(self): - q = u'' - sort = dbcore.query.SlowFieldSort(u"path", True) + q = '' + sort = dbcore.query.SlowFieldSort("path", True) results = self.lib.albums(q, sort) self.assertLessEqual(results[0]['path'], results[1]['path']) self.assertLessEqual(results[1]['path'], results[2]['path']) # same thing with query string - q = u'path+' + q = 'path+' results2 = self.lib.albums(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_sort_desc(self): - q = u'' - sort = dbcore.query.SlowFieldSort(u"path", False) + q = '' + sort = dbcore.query.SlowFieldSort("path", False) results = self.lib.albums(q, sort) self.assertGreaterEqual(results[0]['path'], results[1]['path']) self.assertGreaterEqual(results[1]['path'], results[2]['path']) # same thing with query string - q = u'path-' + q = 'path-' results2 = self.lib.albums(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) @@ -324,24 +322,24 @@ class SortCombinedFieldTest(DummyDataTestCase): def test_computed_first(self): - q = u'' - s1 = dbcore.query.SlowFieldSort(u"path", True) - s2 = dbcore.query.FixedFieldSort(u"year", True) + q = '' + s1 = dbcore.query.SlowFieldSort("path", True) + s2 = dbcore.query.FixedFieldSort("year", True) sort = dbcore.query.MultipleSort() sort.add_sort(s1) sort.add_sort(s2) results = self.lib.albums(q, sort) self.assertLessEqual(results[0]['path'], results[1]['path']) self.assertLessEqual(results[1]['path'], results[2]['path']) - q = u'path+ year+' + q = 'path+ year+' results2 = self.lib.albums(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) def test_computed_second(self): - q = u'' - s1 = dbcore.query.FixedFieldSort(u"year", True) - s2 = dbcore.query.SlowFieldSort(u"path", True) + q = '' + s1 = dbcore.query.FixedFieldSort("year", True) + s2 = dbcore.query.SlowFieldSort("path", True) sort = dbcore.query.MultipleSort() sort.add_sort(s1) sort.add_sort(s2) @@ -349,7 +347,7 @@ self.assertLessEqual(results[0]['year'], results[1]['year']) self.assertLessEqual(results[1]['year'], results[2]['year']) self.assertLessEqual(results[0]['path'], results[1]['path']) - q = u'year+ path+' + q = 'year+ path+' results2 = self.lib.albums(q) for r1, r2 in zip(results, results2): self.assertEqual(r1.id, r2.id) @@ -381,26 +379,26 @@ """ def setUp(self): - super(CaseSensitivityTest, self).setUp() + super().setUp() album = _common.album() - album.album = u"album" - album.genre = u"alternative" - album.year = u"2001" - album.flex1 = u"flex1" - album.flex2 = u"flex2-A" - album.albumartist = u"bar" + album.album = "album" + album.genre = "alternative" + album.year = "2001" + album.flex1 = "flex1" + album.flex2 = "flex2-A" + album.albumartist = "bar" album.albumartist_sort = None self.lib.add(album) item = _common.item() - item.title = u'another' - item.artist = u'lowercase' - item.album = u'album' + item.title = 'another' + item.artist = 'lowercase' + item.album = 'album' item.year = 2001 item.comp = True - item.flex1 = u"flex1" - item.flex2 = u"flex2-A" + item.flex1 = "flex1" + item.flex2 = "flex2-A" item.album_id = album.id item.artist_sort = None item.track = 10 @@ -412,53 +410,53 @@ def tearDown(self): self.new_item.remove(delete=True) self.new_album.remove(delete=True) - super(CaseSensitivityTest, self).tearDown() + super().tearDown() def test_smart_artist_case_insensitive(self): config['sort_case_insensitive'] = True - q = u'artist+' + q = 'artist+' results = list(self.lib.items(q)) - self.assertEqual(results[0].artist, u'lowercase') - self.assertEqual(results[1].artist, u'One') + self.assertEqual(results[0].artist, 'lowercase') + self.assertEqual(results[1].artist, 'One') def test_smart_artist_case_sensitive(self): config['sort_case_insensitive'] = False - q = u'artist+' + q = 'artist+' results = list(self.lib.items(q)) - self.assertEqual(results[0].artist, u'One') - self.assertEqual(results[-1].artist, u'lowercase') + self.assertEqual(results[0].artist, 'One') + self.assertEqual(results[-1].artist, 'lowercase') def test_fixed_field_case_insensitive(self): config['sort_case_insensitive'] = True - q = u'album+' + q = 'album+' results = list(self.lib.albums(q)) - self.assertEqual(results[0].album, u'album') - self.assertEqual(results[1].album, u'Album A') + self.assertEqual(results[0].album, 'album') + self.assertEqual(results[1].album, 'Album A') def test_fixed_field_case_sensitive(self): config['sort_case_insensitive'] = False - q = u'album+' + q = 'album+' results = list(self.lib.albums(q)) - self.assertEqual(results[0].album, u'Album A') - self.assertEqual(results[-1].album, u'album') + self.assertEqual(results[0].album, 'Album A') + self.assertEqual(results[-1].album, 'album') def test_flex_field_case_insensitive(self): config['sort_case_insensitive'] = True - q = u'flex1+' + q = 'flex1+' results = list(self.lib.items(q)) - self.assertEqual(results[0].flex1, u'flex1') - self.assertEqual(results[1].flex1, u'Flex1-0') + self.assertEqual(results[0].flex1, 'flex1') + self.assertEqual(results[1].flex1, 'Flex1-0') def test_flex_field_case_sensitive(self): config['sort_case_insensitive'] = False - q = u'flex1+' + q = 'flex1+' results = list(self.lib.items(q)) - self.assertEqual(results[0].flex1, u'Flex1-0') - self.assertEqual(results[-1].flex1, u'flex1') + self.assertEqual(results[0].flex1, 'Flex1-0') + self.assertEqual(results[-1].flex1, 'flex1') def test_case_sensitive_only_affects_text(self): config['sort_case_insensitive'] = True - q = u'track+' + q = 'track+' results = list(self.lib.items(q)) # If the numerical values were sorted as strings, # then ['1', '10', '2'] would be valid. @@ -472,10 +470,10 @@ """Test sorting by non-existing fields""" def test_non_existing_fields_not_fail(self): - qs = [u'foo+', u'foo-', u'--', u'-+', u'+-', - u'++', u'-foo-', u'-foo+', u'---'] + qs = ['foo+', 'foo-', '--', '-+', '+-', + '++', '-foo-', '-foo+', '---'] - q0 = u'foo+' + q0 = 'foo+' results0 = list(self.lib.items(q0)) for q1 in qs: results1 = list(self.lib.items(q1)) @@ -483,16 +481,16 @@ self.assertEqual(r1.id, r2.id) def test_combined_non_existing_field_asc(self): - all_results = list(self.lib.items(u'id+')) - q = u'foo+ id+' + all_results = list(self.lib.items('id+')) + q = 'foo+ id+' results = list(self.lib.items(q)) self.assertEqual(len(all_results), len(results)) for r1, r2 in zip(all_results, results): self.assertEqual(r1.id, r2.id) def test_combined_non_existing_field_desc(self): - all_results = list(self.lib.items(u'id+')) - q = u'foo- id+' + all_results = list(self.lib.items('id+')) + q = 'foo- id+' results = list(self.lib.items(q)) self.assertEqual(len(all_results), len(results)) for r1, r2 in zip(all_results, results): @@ -501,18 +499,18 @@ def test_field_present_in_some_items(self): """Test ordering by a field not present on all items.""" # append 'foo' to two to items (1,2) - items = self.lib.items(u'id+') + items = self.lib.items('id+') ids = [i.id for i in items] - items[1].foo = u'bar1' - items[2].foo = u'bar2' + items[1].foo = 'bar1' + items[2].foo = 'bar2' items[1].store() items[2].store() - results_asc = list(self.lib.items(u'foo+ id+')) + results_asc = list(self.lib.items('foo+ id+')) self.assertEqual([i.id for i in results_asc], # items without field first [ids[0], ids[3], ids[1], ids[2]]) - results_desc = list(self.lib.items(u'foo- id+')) + results_desc = list(self.lib.items('foo- id+')) self.assertEqual([i.id for i in results_desc], # items without field last [ids[2], ids[1], ids[0], ids[3]]) @@ -523,13 +521,13 @@ If a string ends with a sorting suffix, it takes precedence over the NotQuery parsing. """ - query, sort = beets.library.parse_query_string(u'-bar+', + query, sort = beets.library.parse_query_string('-bar+', beets.library.Item) self.assertEqual(len(query.subqueries), 1) self.assertTrue(isinstance(query.subqueries[0], dbcore.query.TrueQuery)) self.assertTrue(isinstance(sort, dbcore.query.SlowFieldSort)) - self.assertEqual(sort.field, u'-bar') + self.assertEqual(sort.field, '-bar') def suite(): diff -Nru beets-1.5.0/test/test_spotify.py beets-1.6.0/test/test_spotify.py --- beets-1.5.0/test/test_spotify.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_spotify.py 2021-11-26 20:51:38.000000000 +0000 @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- - """Tests for the 'spotify' plugin""" -from __future__ import division, absolute_import, print_function import os import responses @@ -16,7 +13,7 @@ from six.moves.urllib.parse import parse_qs, urlparse -class ArgumentsMock(object): +class ArgumentsMock: def __init__(self, mode, show_failures): self.mode = mode self.show_failures = show_failures @@ -60,7 +57,7 @@ def test_empty_query(self): self.assertEqual( - None, self.spotify._match_library_tracks(self.lib, u"1=2") + None, self.spotify._match_library_tracks(self.lib, "1=2") ) @responses.activate @@ -79,21 +76,21 @@ content_type='application/json', ) item = Item( - mb_trackid=u'01234', - album=u'lkajsdflakjsd', - albumartist=u'ujydfsuihse', - title=u'duifhjslkef', + mb_trackid='01234', + album='lkajsdflakjsd', + albumartist='ujydfsuihse', + title='duifhjslkef', length=10, ) item.add(self.lib) - self.assertEqual([], self.spotify._match_library_tracks(self.lib, u"")) + self.assertEqual([], self.spotify._match_library_tracks(self.lib, "")) params = _params(responses.calls[0].request.url) query = params['q'][0] - self.assertIn(u'duifhjslkef', query) - self.assertIn(u'artist:ujydfsuihse', query) - self.assertIn(u'album:lkajsdflakjsd', query) - self.assertEqual(params['type'], [u'track']) + self.assertIn('duifhjslkef', query) + self.assertIn('artist:ujydfsuihse', query) + self.assertIn('album:lkajsdflakjsd', query) + self.assertEqual(params['type'], ['track']) @responses.activate def test_track_request(self): @@ -111,24 +108,86 @@ content_type='application/json', ) item = Item( - mb_trackid=u'01234', - album=u'Despicable Me 2', - albumartist=u'Pharrell Williams', - title=u'Happy', + mb_trackid='01234', + album='Despicable Me 2', + albumartist='Pharrell Williams', + title='Happy', length=10, ) item.add(self.lib) - results = self.spotify._match_library_tracks(self.lib, u"Happy") + results = self.spotify._match_library_tracks(self.lib, "Happy") self.assertEqual(1, len(results)) - self.assertEqual(u"6NPVjNh8Jhru9xOmyQigds", results[0]['id']) + self.assertEqual("6NPVjNh8Jhru9xOmyQigds", results[0]['id']) self.spotify._output_match_results(results) params = _params(responses.calls[0].request.url) query = params['q'][0] - self.assertIn(u'Happy', query) - self.assertIn(u'artist:Pharrell Williams', query) - self.assertIn(u'album:Despicable Me 2', query) - self.assertEqual(params['type'], [u'track']) + self.assertIn('Happy', query) + self.assertIn('artist:Pharrell Williams', query) + self.assertIn('album:Despicable Me 2', query) + self.assertEqual(params['type'], ['track']) + + @responses.activate + def test_track_for_id(self): + """Tests if plugin is able to fetch a track by its Spotify ID""" + + # Mock the Spotify 'Get Track' call + json_file = os.path.join( + _common.RSRC, b'spotify', b'track_info.json' + ) + with open(json_file, 'rb') as f: + response_body = f.read() + + responses.add( + responses.GET, + spotify.SpotifyPlugin.track_url + '6NPVjNh8Jhru9xOmyQigds', + body=response_body, + status=200, + content_type='application/json', + ) + + # Mock the Spotify 'Get Album' call + json_file = os.path.join( + _common.RSRC, b'spotify', b'album_info.json' + ) + with open(json_file, 'rb') as f: + response_body = f.read() + + responses.add( + responses.GET, + spotify.SpotifyPlugin.album_url + '5l3zEmMrOhOzG8d8s83GOL', + body=response_body, + status=200, + content_type='application/json', + ) + + # Mock the Spotify 'Search' call + json_file = os.path.join( + _common.RSRC, b'spotify', b'track_request.json' + ) + with open(json_file, 'rb') as f: + response_body = f.read() + + responses.add( + responses.GET, + spotify.SpotifyPlugin.search_url, + body=response_body, + status=200, + content_type='application/json', + ) + + track_info = self.spotify.track_for_id('6NPVjNh8Jhru9xOmyQigds') + item = Item( + mb_trackid=track_info.track_id, + albumartist=track_info.artist, + title=track_info.title, + length=track_info.length + ) + item.add(self.lib) + + results = self.spotify._match_library_tracks(self.lib, "Happy") + self.assertEqual(1, len(results)) + self.assertEqual("6NPVjNh8Jhru9xOmyQigds", results[0]['id']) def suite(): diff -Nru beets-1.5.0/test/test_subsonicupdate.py beets-1.6.0/test/test_subsonicupdate.py --- beets-1.5.0/test/test_subsonicupdate.py 2021-03-06 21:56:33.000000000 +0000 +++ beets-1.6.0/test/test_subsonicupdate.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- - """Tests for the 'subsonic' plugin.""" -from __future__ import division, absolute_import, print_function import responses import unittest @@ -11,11 +8,12 @@ from beets import config from beetsplug import subsonicupdate from test.helper import TestHelper -from six.moves.urllib.parse import parse_qs, urlparse +from urllib.parse import parse_qs, urlparse -class ArgumentsMock(object): +class ArgumentsMock: """Argument mocks for tests.""" + def __init__(self, mode, show_failures): """Constructs ArgumentsMock.""" self.mode = mode diff -Nru beets-1.5.0/test/test_template.py beets-1.6.0/test/test_template.py --- beets-1.5.0/test/test_template.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_template.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,10 +14,8 @@ """Tests for template engine. """ -from __future__ import division, absolute_import, print_function import unittest -import six from beets.util import functemplate @@ -29,17 +26,17 @@ """ textbuf = [] for part in expr.parts: - if isinstance(part, six.string_types): + if isinstance(part, str): textbuf.append(part) else: if textbuf: - text = u''.join(textbuf) + text = ''.join(textbuf) if text: yield text textbuf = [] yield part if textbuf: - text = u''.join(textbuf) + text = ''.join(textbuf) if text: yield text @@ -51,15 +48,15 @@ class ParseTest(unittest.TestCase): def test_empty_string(self): - self.assertEqual(list(_normparse(u'')), []) + self.assertEqual(list(_normparse('')), []) def _assert_symbol(self, obj, ident): """Assert that an object is a Symbol with the given identifier. """ self.assertTrue(isinstance(obj, functemplate.Symbol), - u"not a Symbol: %s" % repr(obj)) + "not a Symbol: %s" % repr(obj)) self.assertEqual(obj.ident, ident, - u"wrong identifier: %s vs. %s" % + "wrong identifier: %s vs. %s" % (repr(obj.ident), repr(ident))) def _assert_call(self, obj, ident, numargs): @@ -67,223 +64,223 @@ argument count. """ self.assertTrue(isinstance(obj, functemplate.Call), - u"not a Call: %s" % repr(obj)) + "not a Call: %s" % repr(obj)) self.assertEqual(obj.ident, ident, - u"wrong identifier: %s vs. %s" % + "wrong identifier: %s vs. %s" % (repr(obj.ident), repr(ident))) self.assertEqual(len(obj.args), numargs, - u"wrong argument count in %s: %i vs. %i" % + "wrong argument count in %s: %i vs. %i" % (repr(obj.ident), len(obj.args), numargs)) def test_plain_text(self): - self.assertEqual(list(_normparse(u'hello world')), [u'hello world']) + self.assertEqual(list(_normparse('hello world')), ['hello world']) def test_escaped_character_only(self): - self.assertEqual(list(_normparse(u'$$')), [u'$']) + self.assertEqual(list(_normparse('$$')), ['$']) def test_escaped_character_in_text(self): - self.assertEqual(list(_normparse(u'a $$ b')), [u'a $ b']) + self.assertEqual(list(_normparse('a $$ b')), ['a $ b']) def test_escaped_character_at_start(self): - self.assertEqual(list(_normparse(u'$$ hello')), [u'$ hello']) + self.assertEqual(list(_normparse('$$ hello')), ['$ hello']) def test_escaped_character_at_end(self): - self.assertEqual(list(_normparse(u'hello $$')), [u'hello $']) + self.assertEqual(list(_normparse('hello $$')), ['hello $']) def test_escaped_function_delim(self): - self.assertEqual(list(_normparse(u'a $% b')), [u'a % b']) + self.assertEqual(list(_normparse('a $% b')), ['a % b']) def test_escaped_sep(self): - self.assertEqual(list(_normparse(u'a $, b')), [u'a , b']) + self.assertEqual(list(_normparse('a $, b')), ['a , b']) def test_escaped_close_brace(self): - self.assertEqual(list(_normparse(u'a $} b')), [u'a } b']) + self.assertEqual(list(_normparse('a $} b')), ['a } b']) def test_bare_value_delim_kept_intact(self): - self.assertEqual(list(_normparse(u'a $ b')), [u'a $ b']) + self.assertEqual(list(_normparse('a $ b')), ['a $ b']) def test_bare_function_delim_kept_intact(self): - self.assertEqual(list(_normparse(u'a % b')), [u'a % b']) + self.assertEqual(list(_normparse('a % b')), ['a % b']) def test_bare_opener_kept_intact(self): - self.assertEqual(list(_normparse(u'a { b')), [u'a { b']) + self.assertEqual(list(_normparse('a { b')), ['a { b']) def test_bare_closer_kept_intact(self): - self.assertEqual(list(_normparse(u'a } b')), [u'a } b']) + self.assertEqual(list(_normparse('a } b')), ['a } b']) def test_bare_sep_kept_intact(self): - self.assertEqual(list(_normparse(u'a , b')), [u'a , b']) + self.assertEqual(list(_normparse('a , b')), ['a , b']) def test_symbol_alone(self): - parts = list(_normparse(u'$foo')) + parts = list(_normparse('$foo')) self.assertEqual(len(parts), 1) - self._assert_symbol(parts[0], u"foo") + self._assert_symbol(parts[0], "foo") def test_symbol_in_text(self): - parts = list(_normparse(u'hello $foo world')) + parts = list(_normparse('hello $foo world')) self.assertEqual(len(parts), 3) - self.assertEqual(parts[0], u'hello ') - self._assert_symbol(parts[1], u"foo") - self.assertEqual(parts[2], u' world') + self.assertEqual(parts[0], 'hello ') + self._assert_symbol(parts[1], "foo") + self.assertEqual(parts[2], ' world') def test_symbol_with_braces(self): - parts = list(_normparse(u'hello${foo}world')) + parts = list(_normparse('hello${foo}world')) self.assertEqual(len(parts), 3) - self.assertEqual(parts[0], u'hello') - self._assert_symbol(parts[1], u"foo") - self.assertEqual(parts[2], u'world') + self.assertEqual(parts[0], 'hello') + self._assert_symbol(parts[1], "foo") + self.assertEqual(parts[2], 'world') def test_unclosed_braces_symbol(self): - self.assertEqual(list(_normparse(u'a ${ b')), [u'a ${ b']) + self.assertEqual(list(_normparse('a ${ b')), ['a ${ b']) def test_empty_braces_symbol(self): - self.assertEqual(list(_normparse(u'a ${} b')), [u'a ${} b']) + self.assertEqual(list(_normparse('a ${} b')), ['a ${} b']) def test_call_without_args_at_end(self): - self.assertEqual(list(_normparse(u'foo %bar')), [u'foo %bar']) + self.assertEqual(list(_normparse('foo %bar')), ['foo %bar']) def test_call_without_args(self): - self.assertEqual(list(_normparse(u'foo %bar baz')), [u'foo %bar baz']) + self.assertEqual(list(_normparse('foo %bar baz')), ['foo %bar baz']) def test_call_with_unclosed_args(self): - self.assertEqual(list(_normparse(u'foo %bar{ baz')), - [u'foo %bar{ baz']) + self.assertEqual(list(_normparse('foo %bar{ baz')), + ['foo %bar{ baz']) def test_call_with_unclosed_multiple_args(self): - self.assertEqual(list(_normparse(u'foo %bar{bar,bar baz')), - [u'foo %bar{bar,bar baz']) + self.assertEqual(list(_normparse('foo %bar{bar,bar baz')), + ['foo %bar{bar,bar baz']) def test_call_empty_arg(self): - parts = list(_normparse(u'%foo{}')) + parts = list(_normparse('%foo{}')) self.assertEqual(len(parts), 1) - self._assert_call(parts[0], u"foo", 1) + self._assert_call(parts[0], "foo", 1) self.assertEqual(list(_normexpr(parts[0].args[0])), []) def test_call_single_arg(self): - parts = list(_normparse(u'%foo{bar}')) + parts = list(_normparse('%foo{bar}')) self.assertEqual(len(parts), 1) - self._assert_call(parts[0], u"foo", 1) - self.assertEqual(list(_normexpr(parts[0].args[0])), [u'bar']) + self._assert_call(parts[0], "foo", 1) + self.assertEqual(list(_normexpr(parts[0].args[0])), ['bar']) def test_call_two_args(self): - parts = list(_normparse(u'%foo{bar,baz}')) + parts = list(_normparse('%foo{bar,baz}')) self.assertEqual(len(parts), 1) - self._assert_call(parts[0], u"foo", 2) - self.assertEqual(list(_normexpr(parts[0].args[0])), [u'bar']) - self.assertEqual(list(_normexpr(parts[0].args[1])), [u'baz']) + self._assert_call(parts[0], "foo", 2) + self.assertEqual(list(_normexpr(parts[0].args[0])), ['bar']) + self.assertEqual(list(_normexpr(parts[0].args[1])), ['baz']) def test_call_with_escaped_sep(self): - parts = list(_normparse(u'%foo{bar$,baz}')) + parts = list(_normparse('%foo{bar$,baz}')) self.assertEqual(len(parts), 1) - self._assert_call(parts[0], u"foo", 1) - self.assertEqual(list(_normexpr(parts[0].args[0])), [u'bar,baz']) + self._assert_call(parts[0], "foo", 1) + self.assertEqual(list(_normexpr(parts[0].args[0])), ['bar,baz']) def test_call_with_escaped_close(self): - parts = list(_normparse(u'%foo{bar$}baz}')) + parts = list(_normparse('%foo{bar$}baz}')) self.assertEqual(len(parts), 1) - self._assert_call(parts[0], u"foo", 1) - self.assertEqual(list(_normexpr(parts[0].args[0])), [u'bar}baz']) + self._assert_call(parts[0], "foo", 1) + self.assertEqual(list(_normexpr(parts[0].args[0])), ['bar}baz']) def test_call_with_symbol_argument(self): - parts = list(_normparse(u'%foo{$bar,baz}')) + parts = list(_normparse('%foo{$bar,baz}')) self.assertEqual(len(parts), 1) - self._assert_call(parts[0], u"foo", 2) + self._assert_call(parts[0], "foo", 2) arg_parts = list(_normexpr(parts[0].args[0])) self.assertEqual(len(arg_parts), 1) - self._assert_symbol(arg_parts[0], u"bar") - self.assertEqual(list(_normexpr(parts[0].args[1])), [u"baz"]) + self._assert_symbol(arg_parts[0], "bar") + self.assertEqual(list(_normexpr(parts[0].args[1])), ["baz"]) def test_call_with_nested_call_argument(self): - parts = list(_normparse(u'%foo{%bar{},baz}')) + parts = list(_normparse('%foo{%bar{},baz}')) self.assertEqual(len(parts), 1) - self._assert_call(parts[0], u"foo", 2) + self._assert_call(parts[0], "foo", 2) arg_parts = list(_normexpr(parts[0].args[0])) self.assertEqual(len(arg_parts), 1) - self._assert_call(arg_parts[0], u"bar", 1) - self.assertEqual(list(_normexpr(parts[0].args[1])), [u"baz"]) + self._assert_call(arg_parts[0], "bar", 1) + self.assertEqual(list(_normexpr(parts[0].args[1])), ["baz"]) def test_nested_call_with_argument(self): - parts = list(_normparse(u'%foo{%bar{baz}}')) + parts = list(_normparse('%foo{%bar{baz}}')) self.assertEqual(len(parts), 1) - self._assert_call(parts[0], u"foo", 1) + self._assert_call(parts[0], "foo", 1) arg_parts = list(_normexpr(parts[0].args[0])) self.assertEqual(len(arg_parts), 1) - self._assert_call(arg_parts[0], u"bar", 1) - self.assertEqual(list(_normexpr(arg_parts[0].args[0])), [u'baz']) + self._assert_call(arg_parts[0], "bar", 1) + self.assertEqual(list(_normexpr(arg_parts[0].args[0])), ['baz']) def test_sep_before_call_two_args(self): - parts = list(_normparse(u'hello, %foo{bar,baz}')) + parts = list(_normparse('hello, %foo{bar,baz}')) self.assertEqual(len(parts), 2) - self.assertEqual(parts[0], u'hello, ') - self._assert_call(parts[1], u"foo", 2) - self.assertEqual(list(_normexpr(parts[1].args[0])), [u'bar']) - self.assertEqual(list(_normexpr(parts[1].args[1])), [u'baz']) + self.assertEqual(parts[0], 'hello, ') + self._assert_call(parts[1], "foo", 2) + self.assertEqual(list(_normexpr(parts[1].args[0])), ['bar']) + self.assertEqual(list(_normexpr(parts[1].args[1])), ['baz']) def test_sep_with_symbols(self): - parts = list(_normparse(u'hello,$foo,$bar')) + parts = list(_normparse('hello,$foo,$bar')) self.assertEqual(len(parts), 4) - self.assertEqual(parts[0], u'hello,') - self._assert_symbol(parts[1], u"foo") - self.assertEqual(parts[2], u',') - self._assert_symbol(parts[3], u"bar") + self.assertEqual(parts[0], 'hello,') + self._assert_symbol(parts[1], "foo") + self.assertEqual(parts[2], ',') + self._assert_symbol(parts[3], "bar") def test_newline_at_end(self): - parts = list(_normparse(u'foo\n')) + parts = list(_normparse('foo\n')) self.assertEqual(len(parts), 1) - self.assertEqual(parts[0], u'foo\n') + self.assertEqual(parts[0], 'foo\n') class EvalTest(unittest.TestCase): def _eval(self, template): values = { - u'foo': u'bar', - u'baz': u'BaR', + 'foo': 'bar', + 'baz': 'BaR', } functions = { - u'lower': six.text_type.lower, - u'len': len, + 'lower': str.lower, + 'len': len, } return functemplate.Template(template).substitute(values, functions) def test_plain_text(self): - self.assertEqual(self._eval(u"foo"), u"foo") + self.assertEqual(self._eval("foo"), "foo") def test_subtitute_value(self): - self.assertEqual(self._eval(u"$foo"), u"bar") + self.assertEqual(self._eval("$foo"), "bar") def test_subtitute_value_in_text(self): - self.assertEqual(self._eval(u"hello $foo world"), u"hello bar world") + self.assertEqual(self._eval("hello $foo world"), "hello bar world") def test_not_subtitute_undefined_value(self): - self.assertEqual(self._eval(u"$bar"), u"$bar") + self.assertEqual(self._eval("$bar"), "$bar") def test_function_call(self): - self.assertEqual(self._eval(u"%lower{FOO}"), u"foo") + self.assertEqual(self._eval("%lower{FOO}"), "foo") def test_function_call_with_text(self): - self.assertEqual(self._eval(u"A %lower{FOO} B"), u"A foo B") + self.assertEqual(self._eval("A %lower{FOO} B"), "A foo B") def test_nested_function_call(self): - self.assertEqual(self._eval(u"%lower{%lower{FOO}}"), u"foo") + self.assertEqual(self._eval("%lower{%lower{FOO}}"), "foo") def test_symbol_in_argument(self): - self.assertEqual(self._eval(u"%lower{$baz}"), u"bar") + self.assertEqual(self._eval("%lower{$baz}"), "bar") def test_function_call_exception(self): - res = self._eval(u"%lower{a,b,c,d,e}") - self.assertTrue(isinstance(res, six.string_types)) + res = self._eval("%lower{a,b,c,d,e}") + self.assertTrue(isinstance(res, str)) def test_function_returning_integer(self): - self.assertEqual(self._eval(u"%len{foo}"), u"3") + self.assertEqual(self._eval("%len{foo}"), "3") def test_not_subtitute_undefined_func(self): - self.assertEqual(self._eval(u"%bar{}"), u"%bar{}") + self.assertEqual(self._eval("%bar{}"), "%bar{}") def test_not_subtitute_func_with_no_args(self): - self.assertEqual(self._eval(u"%lower"), u"%lower") + self.assertEqual(self._eval("%lower"), "%lower") def test_function_call_with_empty_arg(self): - self.assertEqual(self._eval(u"%len{}"), u"0") + self.assertEqual(self._eval("%len{}"), "0") def suite(): diff -Nru beets-1.5.0/test/test_the.py beets-1.6.0/test/test_the.py --- beets-1.5.0/test/test_the.py 2020-08-10 22:29:53.000000000 +0000 +++ beets-1.6.0/test/test_the.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- - """Tests for the 'the' plugin""" -from __future__ import division, absolute_import, print_function import unittest from test import _common @@ -13,54 +10,54 @@ class ThePluginTest(_common.TestCase): def test_unthe_with_default_patterns(self): - self.assertEqual(ThePlugin().unthe(u'', PATTERN_THE), '') - self.assertEqual(ThePlugin().unthe(u'The Something', PATTERN_THE), - u'Something, The') - self.assertEqual(ThePlugin().unthe(u'The The', PATTERN_THE), - u'The, The') - self.assertEqual(ThePlugin().unthe(u'The The', PATTERN_THE), - u'The, The') - self.assertEqual(ThePlugin().unthe(u'The The X', PATTERN_THE), - u'The X, The') - self.assertEqual(ThePlugin().unthe(u'the The', PATTERN_THE), - u'The, the') - self.assertEqual(ThePlugin().unthe(u'Protected The', PATTERN_THE), - u'Protected The') - self.assertEqual(ThePlugin().unthe(u'A Boy', PATTERN_A), - u'Boy, A') - self.assertEqual(ThePlugin().unthe(u'a girl', PATTERN_A), - u'girl, a') - self.assertEqual(ThePlugin().unthe(u'An Apple', PATTERN_A), - u'Apple, An') - self.assertEqual(ThePlugin().unthe(u'An A Thing', PATTERN_A), - u'A Thing, An') - self.assertEqual(ThePlugin().unthe(u'the An Arse', PATTERN_A), - u'the An Arse') - self.assertEqual(ThePlugin().unthe(u'TET - Travailleur', PATTERN_THE), - u'TET - Travailleur') + self.assertEqual(ThePlugin().unthe('', PATTERN_THE), '') + self.assertEqual(ThePlugin().unthe('The Something', PATTERN_THE), + 'Something, The') + self.assertEqual(ThePlugin().unthe('The The', PATTERN_THE), + 'The, The') + self.assertEqual(ThePlugin().unthe('The The', PATTERN_THE), + 'The, The') + self.assertEqual(ThePlugin().unthe('The The X', PATTERN_THE), + 'The X, The') + self.assertEqual(ThePlugin().unthe('the The', PATTERN_THE), + 'The, the') + self.assertEqual(ThePlugin().unthe('Protected The', PATTERN_THE), + 'Protected The') + self.assertEqual(ThePlugin().unthe('A Boy', PATTERN_A), + 'Boy, A') + self.assertEqual(ThePlugin().unthe('a girl', PATTERN_A), + 'girl, a') + self.assertEqual(ThePlugin().unthe('An Apple', PATTERN_A), + 'Apple, An') + self.assertEqual(ThePlugin().unthe('An A Thing', PATTERN_A), + 'A Thing, An') + self.assertEqual(ThePlugin().unthe('the An Arse', PATTERN_A), + 'the An Arse') + self.assertEqual(ThePlugin().unthe('TET - Travailleur', PATTERN_THE), + 'TET - Travailleur') def test_unthe_with_strip(self): config['the']['strip'] = True - self.assertEqual(ThePlugin().unthe(u'The Something', PATTERN_THE), - u'Something') - self.assertEqual(ThePlugin().unthe(u'An A', PATTERN_A), u'A') + self.assertEqual(ThePlugin().unthe('The Something', PATTERN_THE), + 'Something') + self.assertEqual(ThePlugin().unthe('An A', PATTERN_A), 'A') def test_template_function_with_defaults(self): ThePlugin().patterns = [PATTERN_THE, PATTERN_A] - self.assertEqual(ThePlugin().the_template_func(u'The The'), - u'The, The') - self.assertEqual(ThePlugin().the_template_func(u'An A'), u'A, An') + self.assertEqual(ThePlugin().the_template_func('The The'), + 'The, The') + self.assertEqual(ThePlugin().the_template_func('An A'), 'A, An') def test_custom_pattern(self): - config['the']['patterns'] = [u'^test\\s'] + config['the']['patterns'] = ['^test\\s'] config['the']['format'] = FORMAT - self.assertEqual(ThePlugin().the_template_func(u'test passed'), - u'passed, test') + self.assertEqual(ThePlugin().the_template_func('test passed'), + 'passed, test') def test_custom_format(self): config['the']['patterns'] = [PATTERN_THE, PATTERN_A] - config['the']['format'] = u'{1} ({0})' - self.assertEqual(ThePlugin().the_template_func(u'The A'), u'The (A)') + config['the']['format'] = '{1} ({0})' + self.assertEqual(ThePlugin().the_template_func('The A'), 'The (A)') def suite(): diff -Nru beets-1.5.0/test/test_thumbnails.py beets-1.6.0/test/test_thumbnails.py --- beets-1.5.0/test/test_thumbnails.py 2020-08-10 22:29:51.000000000 +0000 +++ beets-1.6.0/test/test_thumbnails.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Bruno Cauet # @@ -13,10 +12,9 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import os.path -from mock import Mock, patch, call +from unittest.mock import Mock, patch, call from tempfile import mkdtemp from shutil import rmtree import unittest @@ -38,13 +36,13 @@ @patch('beetsplug.thumbnails.util') def test_write_metadata_im(self, mock_util): - metadata = {"a": u"A", "b": u"B"} + metadata = {"a": "A", "b": "B"} write_metadata_im("foo", metadata) try: - command = u"convert foo -set a A -set b B foo".split(' ') + command = "convert foo -set a A -set b B foo".split(' ') mock_util.command_output.assert_called_once_with(command) except AssertionError: - command = u"convert foo -set b B -set a A foo".split(' ') + command = "convert foo -set b B -set a A foo".split(' ') mock_util.command_output.assert_called_once_with(command) @patch('beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok') @@ -60,7 +58,7 @@ plugin.add_tags(album, b"/path/to/thumbnail") metadata = {"Thumb::URI": "COVER_URI", - "Thumb::MTime": u"12345"} + "Thumb::MTime": "12345"} plugin.write_metadata.assert_called_once_with(b"/path/to/thumbnail", metadata) mock_stat.assert_called_once_with(album.artpath) @@ -85,7 +83,7 @@ return False if path == LARGE_DIR: return True - raise ValueError(u"unexpected path {0!r}".format(path)) + raise ValueError(f"unexpected path {path!r}") mock_os.path.exists = exists plugin = ThumbnailsPlugin() mock_os.makedirs.assert_called_once_with(NORMAL_DIR) @@ -144,7 +142,7 @@ elif target == path_to_art: return Mock(st_mtime=2) else: - raise ValueError(u"invalid target {0}".format(target)) + raise ValueError(f"invalid target {target}") mock_os.stat.side_effect = os_stat plugin.make_cover_thumbnail(album, 12345, thumbnail_dir) @@ -170,7 +168,7 @@ elif target == path_to_art: return Mock(st_mtime=2) else: - raise ValueError(u"invalid target {0}".format(target)) + raise ValueError(f"invalid target {target}") mock_os.stat.side_effect = os_stat plugin.make_cover_thumbnail(album, 12345, thumbnail_dir) @@ -267,21 +265,21 @@ @patch('beetsplug.thumbnails.BaseDirectory') def test_thumbnail_file_name(self, mock_basedir): plug = ThumbnailsPlugin() - plug.get_uri = Mock(return_value=u"file:///my/uri") + plug.get_uri = Mock(return_value="file:///my/uri") self.assertEqual(plug.thumbnail_file_name(b'idontcare'), b"9488f5797fbe12ffb316d607dfd93d04.png") def test_uri(self): gio = GioURI() if not gio.available: - self.skipTest(u"GIO library not found") + self.skipTest("GIO library not found") - self.assertEqual(gio.uri(u"/foo"), u"file:///") # silent fail - self.assertEqual(gio.uri(b"/foo"), u"file:///foo") - self.assertEqual(gio.uri(b"/foo!"), u"file:///foo!") + self.assertEqual(gio.uri("/foo"), "file:///") # silent fail + self.assertEqual(gio.uri(b"/foo"), "file:///foo") + self.assertEqual(gio.uri(b"/foo!"), "file:///foo!") self.assertEqual( gio.uri(b'/music/\xec\x8b\xb8\xec\x9d\xb4'), - u'file:///music/%EC%8B%B8%EC%9D%B4') + 'file:///music/%EC%8B%B8%EC%9D%B4') class TestPathlibURI(): diff -Nru beets-1.5.0/test/test_types_plugin.py beets-1.6.0/test/test_types_plugin.py --- beets-1.5.0/test/test_types_plugin.py 2021-06-28 21:31:11.000000000 +0000 +++ beets-1.6.0/test/test_types_plugin.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import time from datetime import datetime @@ -36,77 +34,77 @@ def test_integer_modify_and_query(self): self.config['types'] = {'myint': 'int'} - item = self.add_item(artist=u'aaa') + item = self.add_item(artist='aaa') # Do not match unset values - out = self.list(u'myint:1..3') - self.assertEqual(u'', out) + out = self.list('myint:1..3') + self.assertEqual('', out) - self.modify(u'myint=2') + self.modify('myint=2') item.load() self.assertEqual(item['myint'], 2) # Match in range - out = self.list(u'myint:1..3') + out = self.list('myint:1..3') self.assertIn('aaa', out) def test_album_integer_modify_and_query(self): - self.config['types'] = {'myint': u'int'} - album = self.add_album(albumartist=u'aaa') + self.config['types'] = {'myint': 'int'} + album = self.add_album(albumartist='aaa') # Do not match unset values - out = self.list_album(u'myint:1..3') - self.assertEqual(u'', out) + out = self.list_album('myint:1..3') + self.assertEqual('', out) - self.modify(u'-a', u'myint=2') + self.modify('-a', 'myint=2') album.load() self.assertEqual(album['myint'], 2) # Match in range - out = self.list_album(u'myint:1..3') + out = self.list_album('myint:1..3') self.assertIn('aaa', out) def test_float_modify_and_query(self): - self.config['types'] = {'myfloat': u'float'} - item = self.add_item(artist=u'aaa') + self.config['types'] = {'myfloat': 'float'} + item = self.add_item(artist='aaa') # Do not match unset values - out = self.list(u'myfloat:10..0') - self.assertEqual(u'', out) + out = self.list('myfloat:10..0') + self.assertEqual('', out) - self.modify(u'myfloat=-9.1') + self.modify('myfloat=-9.1') item.load() self.assertEqual(item['myfloat'], -9.1) # Match in range - out = self.list(u'myfloat:-10..0') + out = self.list('myfloat:-10..0') self.assertIn('aaa', out) def test_bool_modify_and_query(self): - self.config['types'] = {'mybool': u'bool'} - true = self.add_item(artist=u'true') - false = self.add_item(artist=u'false') - self.add_item(artist=u'unset') + self.config['types'] = {'mybool': 'bool'} + true = self.add_item(artist='true') + false = self.add_item(artist='false') + self.add_item(artist='unset') # Do not match unset values - out = self.list(u'mybool:true, mybool:false') - self.assertEqual(u'', out) + out = self.list('mybool:true, mybool:false') + self.assertEqual('', out) # Set true - self.modify(u'mybool=1', u'artist:true') + self.modify('mybool=1', 'artist:true') true.load() self.assertEqual(true['mybool'], True) # Set false - self.modify(u'mybool=false', u'artist:false') + self.modify('mybool=false', 'artist:false') false.load() self.assertEqual(false['mybool'], False) # Query bools - out = self.list(u'mybool:true', u'$artist $mybool') - self.assertEqual(u'true True', out) + out = self.list('mybool:true', '$artist $mybool') + self.assertEqual('true True', out) - out = self.list(u'mybool:false', u'$artist $mybool') + out = self.list('mybool:false', '$artist $mybool') # Dealing with unset fields? # self.assertEqual('false False', out) @@ -114,27 +112,27 @@ # self.assertIn('unset $mybool', out) def test_date_modify_and_query(self): - self.config['types'] = {'mydate': u'date'} + self.config['types'] = {'mydate': 'date'} # FIXME parsing should also work with default time format self.config['time_format'] = '%Y-%m-%d' - old = self.add_item(artist=u'prince') - new = self.add_item(artist=u'britney') + old = self.add_item(artist='prince') + new = self.add_item(artist='britney') # Do not match unset values - out = self.list(u'mydate:..2000') - self.assertEqual(u'', out) + out = self.list('mydate:..2000') + self.assertEqual('', out) - self.modify(u'mydate=1999-01-01', u'artist:prince') + self.modify('mydate=1999-01-01', 'artist:prince') old.load() self.assertEqual(old['mydate'], mktime(1999, 1, 1)) - self.modify(u'mydate=1999-12-30', u'artist:britney') + self.modify('mydate=1999-12-30', 'artist:britney') new.load() self.assertEqual(new['mydate'], mktime(1999, 12, 30)) # Match in range - out = self.list(u'mydate:..1999-07', u'$artist $mydate') - self.assertEqual(u'prince 1999-01-01', out) + out = self.list('mydate:..1999-07', '$artist $mydate') + self.assertEqual('prince 1999-01-01', out) # FIXME some sort of timezone issue here # out = self.list('mydate:1999-12-30', '$artist $mydate') @@ -143,50 +141,50 @@ def test_unknown_type_error(self): self.config['types'] = {'flex': 'unkown type'} with self.assertRaises(ConfigValueError): - self.run_command(u'ls') + self.run_command('ls') def test_template_if_def(self): # Tests for a subtle bug when using %ifdef in templates along with # types that have truthy default values (e.g. '0', '0.0', 'False') # https://github.com/beetbox/beets/issues/3852 - self.config['types'] = {'playcount': u'int', 'rating': u'float', - 'starred': u'bool'} + self.config['types'] = {'playcount': 'int', 'rating': 'float', + 'starred': 'bool'} - with_fields = self.add_item(artist=u'prince') - self.modify(u'playcount=10', u'artist=prince') - self.modify(u'rating=5.0', u'artist=prince') - self.modify(u'starred=yes', u'artist=prince') + with_fields = self.add_item(artist='prince') + self.modify('playcount=10', 'artist=prince') + self.modify('rating=5.0', 'artist=prince') + self.modify('starred=yes', 'artist=prince') with_fields.load() - without_fields = self.add_item(artist=u'britney') + without_fields = self.add_item(artist='britney') - int_template = u'%ifdef{playcount,Play count: $playcount,Not played}' + int_template = '%ifdef{playcount,Play count: $playcount,Not played}' self.assertEqual(with_fields.evaluate_template(int_template), - u'Play count: 10') + 'Play count: 10') self.assertEqual(without_fields.evaluate_template(int_template), - u'Not played') + 'Not played') - float_template = u'%ifdef{rating,Rating: $rating,Not rated}' + float_template = '%ifdef{rating,Rating: $rating,Not rated}' self.assertEqual(with_fields.evaluate_template(float_template), - u'Rating: 5.0') + 'Rating: 5.0') self.assertEqual(without_fields.evaluate_template(float_template), - u'Not rated') + 'Not rated') - bool_template = u'%ifdef{starred,Starred: $starred,Not starred}' + bool_template = '%ifdef{starred,Starred: $starred,Not starred}' self.assertIn(with_fields.evaluate_template(bool_template).lower(), - (u'starred: true', u'starred: yes', u'starred: y')) + ('starred: true', 'starred: yes', 'starred: y')) self.assertEqual(without_fields.evaluate_template(bool_template), - u'Not starred') + 'Not starred') def modify(self, *args): - return self.run_with_output(u'modify', u'--yes', u'--nowrite', - u'--nomove', *args) + return self.run_with_output('modify', '--yes', '--nowrite', + '--nomove', *args) - def list(self, query, fmt=u'$artist - $album - $title'): - return self.run_with_output(u'ls', u'-f', fmt, query).strip() + def list(self, query, fmt='$artist - $album - $title'): + return self.run_with_output('ls', '-f', fmt, query).strip() - def list_album(self, query, fmt=u'$albumartist - $album - $title'): - return self.run_with_output(u'ls', u'-a', u'-f', fmt, query).strip() + def list_album(self, query, fmt='$albumartist - $album - $title'): + return self.run_with_output('ls', '-a', '-f', fmt, query).strip() def mktime(*args): diff -Nru beets-1.5.0/test/test_ui_commands.py beets-1.6.0/test/test_ui_commands.py --- beets-1.5.0/test/test_ui_commands.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_ui_commands.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Test module for file ui/commands.py """ -from __future__ import division, absolute_import, print_function import os import shutil @@ -31,7 +29,7 @@ class QueryTest(_common.TestCase): def setUp(self): - super(QueryTest, self).setUp() + super().setUp() self.libdir = os.path.join(self.temp_dir, b'testlibdir') os.mkdir(self.libdir) @@ -89,7 +87,7 @@ class FieldsTest(_common.LibTestCase): def setUp(self): - super(FieldsTest, self).setUp() + super().setUp() self.io.install() diff -Nru beets-1.5.0/test/test_ui_importer.py beets-1.6.0/test/test_ui_importer.py --- beets-1.5.0/test/test_ui_importer.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_ui_importer.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -19,7 +18,6 @@ ``TerminalImportSession``. So we test this class, too. """ -from __future__ import division, absolute_import, print_function import unittest from test._common import DummyIO @@ -27,14 +25,13 @@ from beets.ui.commands import TerminalImportSession from beets import importer from beets import config -import six class TerminalImportSessionFixture(TerminalImportSession): def __init__(self, *args, **kwargs): self.io = kwargs.pop('io') - super(TerminalImportSessionFixture, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._choices = [] default_choice = importer.action.APPLY @@ -47,11 +44,11 @@ def choose_match(self, task): self._add_choice_input() - return super(TerminalImportSessionFixture, self).choose_match(task) + return super().choose_match(task) def choose_item(self, task): self._add_choice_input() - return super(TerminalImportSessionFixture, self).choose_item(task) + return super().choose_item(task) def _add_choice_input(self): try: @@ -60,24 +57,24 @@ choice = self.default_choice if choice == importer.action.APPLY: - self.io.addinput(u'A') + self.io.addinput('A') elif choice == importer.action.ASIS: - self.io.addinput(u'U') + self.io.addinput('U') elif choice == importer.action.ALBUMS: - self.io.addinput(u'G') + self.io.addinput('G') elif choice == importer.action.TRACKS: - self.io.addinput(u'T') + self.io.addinput('T') elif choice == importer.action.SKIP: - self.io.addinput(u'S') + self.io.addinput('S') elif isinstance(choice, int): - self.io.addinput(u'M') - self.io.addinput(six.text_type(choice)) + self.io.addinput('M') + self.io.addinput(str(choice)) self._add_choice_input() else: - raise Exception(u'Unknown choice %s' % choice) + raise Exception('Unknown choice %s' % choice) -class TerminalImportSessionSetup(object): +class TerminalImportSessionSetup: """Overwrites test_importer.ImportHelper to provide a terminal importer """ diff -Nru beets-1.5.0/test/test_ui_init.py beets-1.6.0/test/test_ui_init.py --- beets-1.5.0/test/test_ui_init.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_ui_init.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Test module for file ui/__init__.py """ -from __future__ import division, absolute_import, print_function import unittest from test import _common @@ -26,7 +24,7 @@ class InputMethodsTest(_common.TestCase): def setUp(self): - super(InputMethodsTest, self).setUp() + super().setUp() self.io.install() def _print_helper(self, s): @@ -86,7 +84,7 @@ class InitTest(_common.LibTestCase): def setUp(self): - super(InitTest, self).setUp() + super().setUp() def test_human_bytes(self): tests = [ diff -Nru beets-1.5.0/test/test_ui.py beets-1.6.0/test/test_ui.py --- beets-1.5.0/test/test_ui.py 2020-09-14 00:55:34.000000000 +0000 +++ beets-1.6.0/test/test_ui.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,17 +14,16 @@ """Tests for the command-line interface. """ -from __future__ import division, absolute_import, print_function import os import shutil import re import subprocess import platform -import six +import sys import unittest -from mock import patch, Mock +from unittest.mock import patch, Mock from test import _common from test.helper import capture_stdout, has_program, TestHelper, control_stdin @@ -50,70 +48,68 @@ self.lib.add(self.item) self.lib.add_album([self.item]) - def _run_list(self, query=u'', album=False, path=False, fmt=u''): + def _run_list(self, query='', album=False, path=False, fmt=''): with capture_stdout() as stdout: commands.list_items(self.lib, query, album, fmt) return stdout def test_list_outputs_item(self): stdout = self._run_list() - self.assertIn(u'the title', stdout.getvalue()) + self.assertIn('the title', stdout.getvalue()) def test_list_unicode_query(self): - self.item.title = u'na\xefve' + self.item.title = 'na\xefve' self.item.store() self.lib._connection().commit() - stdout = self._run_list([u'na\xefve']) + stdout = self._run_list(['na\xefve']) out = stdout.getvalue() - if six.PY2: - out = out.decode(stdout.encoding) - self.assertTrue(u'na\xefve' in out) + self.assertTrue('na\xefve' in out) def test_list_item_path(self): - stdout = self._run_list(fmt=u'$path') - self.assertEqual(stdout.getvalue().strip(), u'xxx/yyy') + stdout = self._run_list(fmt='$path') + self.assertEqual(stdout.getvalue().strip(), 'xxx/yyy') def test_list_album_outputs_something(self): stdout = self._run_list(album=True) self.assertGreater(len(stdout.getvalue()), 0) def test_list_album_path(self): - stdout = self._run_list(album=True, fmt=u'$path') - self.assertEqual(stdout.getvalue().strip(), u'xxx') + stdout = self._run_list(album=True, fmt='$path') + self.assertEqual(stdout.getvalue().strip(), 'xxx') def test_list_album_omits_title(self): stdout = self._run_list(album=True) - self.assertNotIn(u'the title', stdout.getvalue()) + self.assertNotIn('the title', stdout.getvalue()) def test_list_uses_track_artist(self): stdout = self._run_list() - self.assertIn(u'the artist', stdout.getvalue()) - self.assertNotIn(u'the album artist', stdout.getvalue()) + self.assertIn('the artist', stdout.getvalue()) + self.assertNotIn('the album artist', stdout.getvalue()) def test_list_album_uses_album_artist(self): stdout = self._run_list(album=True) - self.assertNotIn(u'the artist', stdout.getvalue()) - self.assertIn(u'the album artist', stdout.getvalue()) + self.assertNotIn('the artist', stdout.getvalue()) + self.assertIn('the album artist', stdout.getvalue()) def test_list_item_format_artist(self): - stdout = self._run_list(fmt=u'$artist') - self.assertIn(u'the artist', stdout.getvalue()) + stdout = self._run_list(fmt='$artist') + self.assertIn('the artist', stdout.getvalue()) def test_list_item_format_multiple(self): - stdout = self._run_list(fmt=u'$artist - $album - $year') - self.assertEqual(u'the artist - the album - 0001', + stdout = self._run_list(fmt='$artist - $album - $year') + self.assertEqual('the artist - the album - 0001', stdout.getvalue().strip()) def test_list_album_format(self): - stdout = self._run_list(album=True, fmt=u'$genre') - self.assertIn(u'the genre', stdout.getvalue()) - self.assertNotIn(u'the album', stdout.getvalue()) + stdout = self._run_list(album=True, fmt='$genre') + self.assertIn('the genre', stdout.getvalue()) + self.assertNotIn('the album', stdout.getvalue()) class RemoveTest(_common.TestCase, TestHelper): def setUp(self): - super(RemoveTest, self).setUp() + super().setUp() self.io.install() @@ -129,26 +125,26 @@ def test_remove_items_no_delete(self): self.io.addinput('y') - commands.remove_items(self.lib, u'', False, False, False) + commands.remove_items(self.lib, '', False, False, False) items = self.lib.items() self.assertEqual(len(list(items)), 0) self.assertTrue(os.path.exists(self.i.path)) def test_remove_items_with_delete(self): self.io.addinput('y') - commands.remove_items(self.lib, u'', False, True, False) + commands.remove_items(self.lib, '', False, True, False) items = self.lib.items() self.assertEqual(len(list(items)), 0) self.assertFalse(os.path.exists(self.i.path)) def test_remove_items_with_force_no_delete(self): - commands.remove_items(self.lib, u'', False, False, True) + commands.remove_items(self.lib, '', False, False, True) items = self.lib.items() self.assertEqual(len(list(items)), 0) self.assertTrue(os.path.exists(self.i.path)) def test_remove_items_with_force_delete(self): - commands.remove_items(self.lib, u'', False, True, True) + commands.remove_items(self.lib, '', False, True, True) items = self.lib.items() self.assertEqual(len(list(items)), 0) self.assertFalse(os.path.exists(self.i.path)) @@ -160,7 +156,7 @@ for s in ('s', 'y', 'n'): self.io.addinput(s) - commands.remove_items(self.lib, u'', False, True, False) + commands.remove_items(self.lib, '', False, True, False) items = self.lib.items() self.assertEqual(len(list(items)), 1) # There is probably no guarantee that the items are queried in any @@ -182,7 +178,7 @@ for s in ('s', 'y', 'n'): self.io.addinput(s) - commands.remove_items(self.lib, u'', True, True, False) + commands.remove_items(self.lib, '', True, True, False) items = self.lib.items() self.assertEqual(len(list(items)), 2) # incl. the item from setUp() # See test_remove_items_select_with_delete() @@ -212,58 +208,58 @@ # Item tests def test_modify_item(self): - self.modify(u"title=newTitle") + self.modify("title=newTitle") item = self.lib.items().get() - self.assertEqual(item.title, u'newTitle') + self.assertEqual(item.title, 'newTitle') def test_modify_item_abort(self): item = self.lib.items().get() title = item.title - self.modify_inp('n', u"title=newTitle") + self.modify_inp('n', "title=newTitle") item = self.lib.items().get() self.assertEqual(item.title, title) def test_modify_item_no_change(self): - title = u"Tracktitle" + title = "Tracktitle" item = self.add_item_fixture(title=title) - self.modify_inp('y', u"title", u"title={0}".format(title)) + self.modify_inp('y', "title", f"title={title}") item = self.lib.items(title).get() self.assertEqual(item.title, title) def test_modify_write_tags(self): - self.modify(u"title=newTitle") + self.modify("title=newTitle") item = self.lib.items().get() item.read() - self.assertEqual(item.title, u'newTitle') + self.assertEqual(item.title, 'newTitle') def test_modify_dont_write_tags(self): - self.modify(u"--nowrite", u"title=newTitle") + self.modify("--nowrite", "title=newTitle") item = self.lib.items().get() item.read() self.assertNotEqual(item.title, 'newTitle') def test_move(self): - self.modify(u"title=newTitle") + self.modify("title=newTitle") item = self.lib.items().get() self.assertIn(b'newTitle', item.path) def test_not_move(self): - self.modify(u"--nomove", u"title=newTitle") + self.modify("--nomove", "title=newTitle") item = self.lib.items().get() self.assertNotIn(b'newTitle', item.path) def test_no_write_no_move(self): - self.modify(u"--nomove", u"--nowrite", u"title=newTitle") + self.modify("--nomove", "--nowrite", "title=newTitle") item = self.lib.items().get() item.read() self.assertNotIn(b'newTitle', item.path) - self.assertNotEqual(item.title, u'newTitle') + self.assertNotEqual(item.title, 'newTitle') def test_update_mtime(self): item = self.item old_mtime = item.mtime - self.modify(u"title=newTitle") + self.modify("title=newTitle") item.load() self.assertNotEqual(old_mtime, item.mtime) self.assertEqual(item.current_mtime(), item.mtime) @@ -271,53 +267,53 @@ def test_reset_mtime_with_no_write(self): item = self.item - self.modify(u"--nowrite", u"title=newTitle") + self.modify("--nowrite", "title=newTitle") item.load() self.assertEqual(0, item.mtime) def test_selective_modify(self): - title = u"Tracktitle" - album = u"album" - original_artist = u"composer" - new_artist = u"coverArtist" + title = "Tracktitle" + album = "album" + original_artist = "composer" + new_artist = "coverArtist" for i in range(0, 10): - self.add_item_fixture(title=u"{0}{1}".format(title, i), + self.add_item_fixture(title=f"{title}{i}", artist=original_artist, album=album) self.modify_inp('s\ny\ny\ny\nn\nn\ny\ny\ny\ny\nn', - title, u"artist={0}".format(new_artist)) - original_items = self.lib.items(u"artist:{0}".format(original_artist)) - new_items = self.lib.items(u"artist:{0}".format(new_artist)) + title, f"artist={new_artist}") + original_items = self.lib.items(f"artist:{original_artist}") + new_items = self.lib.items(f"artist:{new_artist}") self.assertEqual(len(list(original_items)), 3) self.assertEqual(len(list(new_items)), 7) # Album Tests def test_modify_album(self): - self.modify(u"--album", u"album=newAlbum") + self.modify("--album", "album=newAlbum") album = self.lib.albums().get() - self.assertEqual(album.album, u'newAlbum') + self.assertEqual(album.album, 'newAlbum') def test_modify_album_write_tags(self): - self.modify(u"--album", u"album=newAlbum") + self.modify("--album", "album=newAlbum") item = self.lib.items().get() item.read() - self.assertEqual(item.album, u'newAlbum') + self.assertEqual(item.album, 'newAlbum') def test_modify_album_dont_write_tags(self): - self.modify(u"--album", u"--nowrite", u"album=newAlbum") + self.modify("--album", "--nowrite", "album=newAlbum") item = self.lib.items().get() item.read() - self.assertEqual(item.album, u'the album') + self.assertEqual(item.album, 'the album') def test_album_move(self): - self.modify(u"--album", u"album=newAlbum") + self.modify("--album", "album=newAlbum") item = self.lib.items().get() item.read() self.assertIn(b'newAlbum', item.path) def test_album_not_move(self): - self.modify(u"--nomove", u"--album", u"album=newAlbum") + self.modify("--nomove", "--album", "album=newAlbum") item = self.lib.items().get() item.read() self.assertNotIn(b'newAlbum', item.path) @@ -325,62 +321,62 @@ # Misc def test_write_initial_key_tag(self): - self.modify(u"initial_key=C#m") + self.modify("initial_key=C#m") item = self.lib.items().get() mediafile = MediaFile(syspath(item.path)) - self.assertEqual(mediafile.initial_key, u'C#m') + self.assertEqual(mediafile.initial_key, 'C#m') def test_set_flexattr(self): - self.modify(u"flexattr=testAttr") + self.modify("flexattr=testAttr") item = self.lib.items().get() - self.assertEqual(item.flexattr, u'testAttr') + self.assertEqual(item.flexattr, 'testAttr') def test_remove_flexattr(self): item = self.lib.items().get() - item.flexattr = u'testAttr' + item.flexattr = 'testAttr' item.store() - self.modify(u"flexattr!") + self.modify("flexattr!") item = self.lib.items().get() - self.assertNotIn(u"flexattr", item) + self.assertNotIn("flexattr", item) - @unittest.skip(u'not yet implemented') + @unittest.skip('not yet implemented') def test_delete_initial_key_tag(self): item = self.lib.items().get() - item.initial_key = u'C#m' + item.initial_key = 'C#m' item.write() item.store() mediafile = MediaFile(syspath(item.path)) - self.assertEqual(mediafile.initial_key, u'C#m') + self.assertEqual(mediafile.initial_key, 'C#m') - self.modify(u"initial_key!") + self.modify("initial_key!") mediafile = MediaFile(syspath(item.path)) self.assertIsNone(mediafile.initial_key) def test_arg_parsing_colon_query(self): - (query, mods, dels) = commands.modify_parse_args([u"title:oldTitle", - u"title=newTitle"]) - self.assertEqual(query, [u"title:oldTitle"]) - self.assertEqual(mods, {"title": u"newTitle"}) + (query, mods, dels) = commands.modify_parse_args(["title:oldTitle", + "title=newTitle"]) + self.assertEqual(query, ["title:oldTitle"]) + self.assertEqual(mods, {"title": "newTitle"}) def test_arg_parsing_delete(self): - (query, mods, dels) = commands.modify_parse_args([u"title:oldTitle", - u"title!"]) - self.assertEqual(query, [u"title:oldTitle"]) + (query, mods, dels) = commands.modify_parse_args(["title:oldTitle", + "title!"]) + self.assertEqual(query, ["title:oldTitle"]) self.assertEqual(dels, ["title"]) def test_arg_parsing_query_with_exclaimation(self): - (query, mods, dels) = commands.modify_parse_args([u"title:oldTitle!", - u"title=newTitle!"]) - self.assertEqual(query, [u"title:oldTitle!"]) - self.assertEqual(mods, {"title": u"newTitle!"}) + (query, mods, dels) = commands.modify_parse_args(["title:oldTitle!", + "title=newTitle!"]) + self.assertEqual(query, ["title:oldTitle!"]) + self.assertEqual(mods, {"title": "newTitle!"}) def test_arg_parsing_equals_in_value(self): - (query, mods, dels) = commands.modify_parse_args([u"title:foo=bar", - u"title=newTitle"]) - self.assertEqual(query, [u"title:foo=bar"]) - self.assertEqual(mods, {"title": u"newTitle"}) + (query, mods, dels) = commands.modify_parse_args(["title:foo=bar", + "title=newTitle"]) + self.assertEqual(query, ["title:foo=bar"]) + self.assertEqual(mods, {"title": "newTitle"}) class WriteTest(unittest.TestCase, TestHelper): @@ -396,7 +392,7 @@ def test_update_mtime(self): item = self.add_item_fixture() - item['title'] = u'a new title' + item['title'] = 'a new title' item.store() item = self.lib.items().get() @@ -427,18 +423,18 @@ item.read() old_title = item.title - item.title = u'new title' + item.title = 'new title' item.store() output = self.write_cmd() - self.assertTrue(u'{0} -> new title'.format(old_title) + self.assertTrue(f'{old_title} -> new title' in output) class MoveTest(_common.TestCase): def setUp(self): - super(MoveTest, self).setUp() + super().setUp() self.io.install() @@ -535,7 +531,7 @@ class UpdateTest(_common.TestCase): def setUp(self): - super(UpdateTest, self).setUp() + super().setUp() self.io.install() @@ -593,15 +589,15 @@ def test_modified_metadata_detected(self): mf = MediaFile(syspath(self.i.path)) - mf.title = u'differentTitle' + mf.title = 'differentTitle' mf.save() self._update() item = self.lib.items().get() - self.assertEqual(item.title, u'differentTitle') + self.assertEqual(item.title, 'differentTitle') def test_modified_metadata_moved(self): mf = MediaFile(syspath(self.i.path)) - mf.title = u'differentTitle' + mf.title = 'differentTitle' mf.save() self._update(move=True) item = self.lib.items().get() @@ -609,7 +605,7 @@ def test_modified_metadata_not_moved(self): mf = MediaFile(syspath(self.i.path)) - mf.title = u'differentTitle' + mf.title = 'differentTitle' mf.save() self._update(move=False) item = self.lib.items().get() @@ -617,27 +613,27 @@ def test_selective_modified_metadata_moved(self): mf = MediaFile(syspath(self.i.path)) - mf.title = u'differentTitle' - mf.genre = u'differentGenre' + mf.title = 'differentTitle' + mf.genre = 'differentGenre' mf.save() self._update(move=True, fields=['title']) item = self.lib.items().get() self.assertTrue(b'differentTitle' in item.path) - self.assertNotEqual(item.genre, u'differentGenre') + self.assertNotEqual(item.genre, 'differentGenre') def test_selective_modified_metadata_not_moved(self): mf = MediaFile(syspath(self.i.path)) - mf.title = u'differentTitle' - mf.genre = u'differentGenre' + mf.title = 'differentTitle' + mf.genre = 'differentGenre' mf.save() self._update(move=False, fields=['title']) item = self.lib.items().get() self.assertTrue(b'differentTitle' not in item.path) - self.assertNotEqual(item.genre, u'differentGenre') + self.assertNotEqual(item.genre, 'differentGenre') def test_modified_album_metadata_moved(self): mf = MediaFile(syspath(self.i.path)) - mf.album = u'differentAlbum' + mf.album = 'differentAlbum' mf.save() self._update(move=True) item = self.lib.items().get() @@ -646,7 +642,7 @@ def test_modified_album_metadata_art_moved(self): artpath = self.album.artpath mf = MediaFile(syspath(self.i.path)) - mf.album = u'differentAlbum' + mf.album = 'differentAlbum' mf.save() self._update(move=True) album = self.lib.albums()[0] @@ -655,27 +651,27 @@ def test_selective_modified_album_metadata_moved(self): mf = MediaFile(syspath(self.i.path)) - mf.album = u'differentAlbum' - mf.genre = u'differentGenre' + mf.album = 'differentAlbum' + mf.genre = 'differentGenre' mf.save() self._update(move=True, fields=['album']) item = self.lib.items().get() self.assertTrue(b'differentAlbum' in item.path) - self.assertNotEqual(item.genre, u'differentGenre') + self.assertNotEqual(item.genre, 'differentGenre') def test_selective_modified_album_metadata_not_moved(self): mf = MediaFile(syspath(self.i.path)) - mf.album = u'differentAlbum' - mf.genre = u'differentGenre' + mf.album = 'differentAlbum' + mf.genre = 'differentGenre' mf.save() self._update(move=True, fields=['genre']) item = self.lib.items().get() self.assertTrue(b'differentAlbum' not in item.path) - self.assertEqual(item.genre, u'differentGenre') + self.assertEqual(item.genre, 'differentGenre') def test_mtime_match_skips_update(self): mf = MediaFile(syspath(self.i.path)) - mf.title = u'differentTitle' + mf.title = 'differentTitle' mf.save() # Make in-memory mtime match on-disk mtime. @@ -684,12 +680,12 @@ self._update(reset_mtime=False) item = self.lib.items().get() - self.assertEqual(item.title, u'full') + self.assertEqual(item.title, 'full') class PrintTest(_common.TestCase): def setUp(self): - super(PrintTest, self).setUp() + super().setUp() self.io.install() def test_print_without_locale(self): @@ -698,9 +694,9 @@ del os.environ['LANG'] try: - ui.print_(u'something') + ui.print_('something') except TypeError: - self.fail(u'TypeError during print') + self.fail('TypeError during print') finally: if lang: os.environ['LANG'] = lang @@ -712,9 +708,9 @@ os.environ['LC_CTYPE'] = 'UTF-8' try: - ui.print_(u'something') + ui.print_('something') except ValueError: - self.fail(u'ValueError during print') + self.fail('ValueError during print') finally: if old_lang: os.environ['LANG'] = old_lang @@ -772,6 +768,7 @@ os.makedirs(self.beetsdir) self._reset_config() + self.load_plugins() def tearDown(self): commands.default_commands.pop() @@ -782,10 +779,11 @@ del os.environ['APPDATA'] else: os.environ['APPDATA'] = self._old_appdata + self.unload_plugins() self.teardown_beets() def _make_test_cmd(self): - test_cmd = ui.Subcommand('test', help=u'test') + test_cmd = ui.Subcommand('test', help='test') def run(lib, options, args): test_cmd.lib = lib @@ -846,7 +844,7 @@ self.run_command('test', lib=None) replacements = self.test_cmd.lib.replacements repls = [(p.pattern, s) for p, s in replacements] # Compare patterns. - self.assertEqual(repls, [(u'[xy]', 'z')]) + self.assertEqual(repls, [('[xy]', 'z')]) def test_multiple_replacements_parsed(self): with self.write_config_file() as config: @@ -855,8 +853,8 @@ replacements = self.test_cmd.lib.replacements repls = [(p.pattern, s) for p, s in replacements] self.assertEqual(repls, [ - (u'[xy]', u'z'), - (u'foo', u'bar'), + ('[xy]', 'z'), + ('foo', 'bar'), ]) def test_cli_config_option(self): @@ -920,6 +918,7 @@ # '--config', cli_overwrite_config_path, 'test') # self.assertEqual(config['anoption'].get(), 'cli overwrite') + @unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows def test_cli_config_paths_resolve_relative_to_user_dir(self): cli_config_path = os.path.join(self.temp_dir, b'config.yaml') with open(cli_config_path, 'w') as file: @@ -1029,7 +1028,7 @@ class ShowModelChangeTest(_common.TestCase): def setUp(self): - super(ShowModelChangeTest, self).setUp() + super().setUp() self.io.install() self.a = _common.item() self.b = _common.item() @@ -1049,54 +1048,54 @@ self.b.title = 'x' change, out = self._show() self.assertTrue(change) - self.assertTrue(u'title' in out) + self.assertTrue('title' in out) def test_int_fixed_field_change(self): self.b.track = 9 change, out = self._show() self.assertTrue(change) - self.assertTrue(u'track' in out) + self.assertTrue('track' in out) def test_floats_close_to_identical(self): self.a.length = 1.00001 self.b.length = 1.00005 change, out = self._show() self.assertFalse(change) - self.assertEqual(out, u'') + self.assertEqual(out, '') def test_floats_different(self): self.a.length = 1.00001 self.b.length = 2.00001 change, out = self._show() self.assertTrue(change) - self.assertTrue(u'length' in out) + self.assertTrue('length' in out) def test_both_values_shown(self): - self.a.title = u'foo' - self.b.title = u'bar' + self.a.title = 'foo' + self.b.title = 'bar' change, out = self._show() - self.assertTrue(u'foo' in out) - self.assertTrue(u'bar' in out) + self.assertTrue('foo' in out) + self.assertTrue('bar' in out) class ShowChangeTest(_common.TestCase): def setUp(self): - super(ShowChangeTest, self).setUp() + super().setUp() self.io.install() self.items = [_common.item()] self.items[0].track = 1 self.items[0].path = b'/path/to/file.mp3' self.info = autotag.AlbumInfo( - album=u'the album', album_id=u'album id', artist=u'the artist', - artist_id=u'artist id', tracks=[ - autotag.TrackInfo(title=u'the title', track_id=u'track id', + album='the album', album_id='album id', artist='the artist', + artist_id='artist id', tracks=[ + autotag.TrackInfo(title='the title', track_id='track id', index=1) ] ) def _show_change(self, items=None, info=None, - cur_artist=u'the artist', cur_album=u'the album', + cur_artist='the artist', cur_album='the album', dist=0.1): """Return an unicode string representing the changes""" items = items or self.items @@ -1124,37 +1123,37 @@ self.assertTrue('correcting tags from:' in msg) def test_item_data_change(self): - self.items[0].title = u'different' + self.items[0].title = 'different' msg = self._show_change() self.assertTrue('different -> the title' in msg) def test_item_data_change_with_unicode(self): - self.items[0].title = u'caf\xe9' + self.items[0].title = 'caf\xe9' msg = self._show_change() - self.assertTrue(u'caf\xe9 -> the title' in msg) + self.assertTrue('caf\xe9 -> the title' in msg) def test_album_data_change_with_unicode(self): - msg = self._show_change(cur_artist=u'caf\xe9', - cur_album=u'another album') - self.assertTrue(u'correcting tags from:' in msg) + msg = self._show_change(cur_artist='caf\xe9', + cur_album='another album') + self.assertTrue('correcting tags from:' in msg) def test_item_data_change_title_missing(self): - self.items[0].title = u'' + self.items[0].title = '' msg = re.sub(r' +', ' ', self._show_change()) - self.assertTrue(u'file.mp3 -> the title' in msg) + self.assertTrue('file.mp3 -> the title' in msg) def test_item_data_change_title_missing_with_unicode_filename(self): - self.items[0].title = u'' - self.items[0].path = u'/path/to/caf\xe9.mp3'.encode('utf-8') + self.items[0].title = '' + self.items[0].path = '/path/to/caf\xe9.mp3'.encode() msg = re.sub(r' +', ' ', self._show_change()) - self.assertTrue(u'caf\xe9.mp3 -> the title' in msg or - u'caf.mp3 ->' in msg) + self.assertTrue('caf\xe9.mp3 -> the title' in msg or + 'caf.mp3 ->' in msg) @patch('beets.library.Item.try_filesize', Mock(return_value=987)) class SummarizeItemsTest(_common.TestCase): def setUp(self): - super(SummarizeItemsTest, self).setUp() + super().setUp() item = library.Item() item.bitrate = 4321 item.length = 10 * 60 + 54 @@ -1163,41 +1162,41 @@ def test_summarize_item(self): summary = commands.summarize_items([], True) - self.assertEqual(summary, u"") + self.assertEqual(summary, "") summary = commands.summarize_items([self.item], True) - self.assertEqual(summary, u"F, 4kbps, 10:54, 987.0 B") + self.assertEqual(summary, "F, 4kbps, 10:54, 987.0 B") def test_summarize_items(self): summary = commands.summarize_items([], False) - self.assertEqual(summary, u"0 items") + self.assertEqual(summary, "0 items") summary = commands.summarize_items([self.item], False) - self.assertEqual(summary, u"1 items, F, 4kbps, 10:54, 987.0 B") + self.assertEqual(summary, "1 items, F, 4kbps, 10:54, 987.0 B") # make a copy of self.item i2 = self.item.copy() summary = commands.summarize_items([self.item, i2], False) - self.assertEqual(summary, u"2 items, F, 4kbps, 21:48, 1.9 KiB") + self.assertEqual(summary, "2 items, F, 4kbps, 21:48, 1.9 KiB") i2.format = "G" summary = commands.summarize_items([self.item, i2], False) - self.assertEqual(summary, u"2 items, F 1, G 1, 4kbps, 21:48, 1.9 KiB") + self.assertEqual(summary, "2 items, F 1, G 1, 4kbps, 21:48, 1.9 KiB") summary = commands.summarize_items([self.item, i2, i2], False) - self.assertEqual(summary, u"3 items, G 2, F 1, 4kbps, 32:42, 2.9 KiB") + self.assertEqual(summary, "3 items, G 2, F 1, 4kbps, 32:42, 2.9 KiB") class PathFormatTest(_common.TestCase): def test_custom_paths_prepend(self): default_formats = ui.get_path_formats() - config['paths'] = {u'foo': u'bar'} + config['paths'] = {'foo': 'bar'} pf = ui.get_path_formats() key, tmpl = pf[0] - self.assertEqual(key, u'foo') - self.assertEqual(tmpl.original, u'bar') + self.assertEqual(key, 'foo') + self.assertEqual(tmpl.original, 'bar') self.assertEqual(pf[1:], default_formats) @@ -1225,7 +1224,7 @@ # commands via stdin. cmd = os.environ.get('BEETS_TEST_SHELL', '/bin/bash --norc').split() if not has_program(cmd[0]): - self.skipTest(u'bash not available') + self.skipTest('bash not available') tester = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=env) @@ -1235,12 +1234,12 @@ bash_completion = path break else: - self.skipTest(u'bash-completion script not found') + self.skipTest('bash-completion script not found') try: with open(util.syspath(bash_completion), 'rb') as f: tester.stdin.writelines(f) - except IOError: - self.skipTest(u'could not read bash-completion script') + except OSError: + self.skipTest('could not read bash-completion script') # Load completion script. self.io.install() @@ -1256,7 +1255,7 @@ out, err = tester.communicate() if tester.returncode != 0 or out != b'completion tests passed\n': print(out.decode('utf-8')) - self.fail(u'test/test_completion.sh did not execute properly') + self.fail('test/test_completion.sh did not execute properly') class CommonOptionsParserCliTest(unittest.TestCase, TestHelper): @@ -1276,63 +1275,63 @@ self.teardown_beets() def test_base(self): - l = self.run_with_output(u'ls') - self.assertEqual(l, u'the artist - the album - the title\n') + l = self.run_with_output('ls') + self.assertEqual(l, 'the artist - the album - the title\n') - l = self.run_with_output(u'ls', u'-a') - self.assertEqual(l, u'the album artist - the album\n') + l = self.run_with_output('ls', '-a') + self.assertEqual(l, 'the album artist - the album\n') def test_path_option(self): - l = self.run_with_output(u'ls', u'-p') - self.assertEqual(l, u'xxx/yyy\n') + l = self.run_with_output('ls', '-p') + self.assertEqual(l, 'xxx/yyy\n') - l = self.run_with_output(u'ls', u'-a', u'-p') - self.assertEqual(l, u'xxx\n') + l = self.run_with_output('ls', '-a', '-p') + self.assertEqual(l, 'xxx\n') def test_format_option(self): - l = self.run_with_output(u'ls', u'-f', u'$artist') - self.assertEqual(l, u'the artist\n') + l = self.run_with_output('ls', '-f', '$artist') + self.assertEqual(l, 'the artist\n') - l = self.run_with_output(u'ls', u'-a', u'-f', u'$albumartist') - self.assertEqual(l, u'the album artist\n') + l = self.run_with_output('ls', '-a', '-f', '$albumartist') + self.assertEqual(l, 'the album artist\n') def test_format_option_unicode(self): l = self.run_with_output(b'ls', b'-f', - u'caf\xe9'.encode(util.arg_encoding())) - self.assertEqual(l, u'caf\xe9\n') + 'caf\xe9'.encode(util.arg_encoding())) + self.assertEqual(l, 'caf\xe9\n') def test_root_format_option(self): - l = self.run_with_output(u'--format-item', u'$artist', - u'--format-album', u'foo', u'ls') - self.assertEqual(l, u'the artist\n') - - l = self.run_with_output(u'--format-item', u'foo', - u'--format-album', u'$albumartist', - u'ls', u'-a') - self.assertEqual(l, u'the album artist\n') + l = self.run_with_output('--format-item', '$artist', + '--format-album', 'foo', 'ls') + self.assertEqual(l, 'the artist\n') + + l = self.run_with_output('--format-item', 'foo', + '--format-album', '$albumartist', + 'ls', '-a') + self.assertEqual(l, 'the album artist\n') def test_help(self): - l = self.run_with_output(u'help') - self.assertIn(u'Usage:', l) + l = self.run_with_output('help') + self.assertIn('Usage:', l) - l = self.run_with_output(u'help', u'list') - self.assertIn(u'Usage:', l) + l = self.run_with_output('help', 'list') + self.assertIn('Usage:', l) with self.assertRaises(ui.UserError): - self.run_command(u'help', u'this.is.not.a.real.command') + self.run_command('help', 'this.is.not.a.real.command') def test_stats(self): - l = self.run_with_output(u'stats') - self.assertIn(u'Approximate total size:', l) + l = self.run_with_output('stats') + self.assertIn('Approximate total size:', l) # # Need to have more realistic library setup for this to work # l = self.run_with_output('stats', '-e') # self.assertIn('Total size:', l) def test_version(self): - l = self.run_with_output(u'version') - self.assertIn(u'Python version', l) - self.assertIn(u'no plugins loaded', l) + l = self.run_with_output('version') + self.assertIn('Python version', l) + self.assertIn('no plugins loaded', l) # # Need to have plugin loaded # l = self.run_with_output('version') @@ -1353,8 +1352,8 @@ self.assertTrue(bool(parser._album_flags)) self.assertEqual(parser.parse_args([]), ({'album': None}, [])) - self.assertEqual(parser.parse_args([u'-a']), ({'album': True}, [])) - self.assertEqual(parser.parse_args([u'--album']), + self.assertEqual(parser.parse_args(['-a']), ({'album': True}, [])) + self.assertEqual(parser.parse_args(['--album']), ({'album': True}, [])) def test_path_option(self): @@ -1364,15 +1363,15 @@ config['format_item'].set('$foo') self.assertEqual(parser.parse_args([]), ({'path': None}, [])) - self.assertEqual(config['format_item'].as_str(), u'$foo') + self.assertEqual(config['format_item'].as_str(), '$foo') - self.assertEqual(parser.parse_args([u'-p']), - ({'path': True, 'format': u'$path'}, [])) + self.assertEqual(parser.parse_args(['-p']), + ({'path': True, 'format': '$path'}, [])) self.assertEqual(parser.parse_args(['--path']), - ({'path': True, 'format': u'$path'}, [])) + ({'path': True, 'format': '$path'}, [])) - self.assertEqual(config['format_item'].as_str(), u'$path') - self.assertEqual(config['format_album'].as_str(), u'$path') + self.assertEqual(config['format_item'].as_str(), '$path') + self.assertEqual(config['format_album'].as_str(), '$path') def test_format_option(self): parser = ui.CommonOptionsParser() @@ -1381,15 +1380,15 @@ config['format_item'].set('$foo') self.assertEqual(parser.parse_args([]), ({'format': None}, [])) - self.assertEqual(config['format_item'].as_str(), u'$foo') + self.assertEqual(config['format_item'].as_str(), '$foo') - self.assertEqual(parser.parse_args([u'-f', u'$bar']), - ({'format': u'$bar'}, [])) - self.assertEqual(parser.parse_args([u'--format', u'$baz']), - ({'format': u'$baz'}, [])) + self.assertEqual(parser.parse_args(['-f', '$bar']), + ({'format': '$bar'}, [])) + self.assertEqual(parser.parse_args(['--format', '$baz']), + ({'format': '$baz'}, [])) - self.assertEqual(config['format_item'].as_str(), u'$baz') - self.assertEqual(config['format_album'].as_str(), u'$baz') + self.assertEqual(config['format_item'].as_str(), '$baz') + self.assertEqual(config['format_album'].as_str(), '$baz') def test_format_option_with_target(self): with self.assertRaises(KeyError): @@ -1401,11 +1400,11 @@ config['format_item'].set('$item') config['format_album'].set('$album') - self.assertEqual(parser.parse_args([u'-f', u'$bar']), - ({'format': u'$bar'}, [])) + self.assertEqual(parser.parse_args(['-f', '$bar']), + ({'format': '$bar'}, [])) - self.assertEqual(config['format_item'].as_str(), u'$bar') - self.assertEqual(config['format_album'].as_str(), u'$album') + self.assertEqual(config['format_item'].as_str(), '$bar') + self.assertEqual(config['format_album'].as_str(), '$album') def test_format_option_with_album(self): parser = ui.CommonOptionsParser() @@ -1415,16 +1414,16 @@ config['format_item'].set('$item') config['format_album'].set('$album') - parser.parse_args([u'-f', u'$bar']) - self.assertEqual(config['format_item'].as_str(), u'$bar') - self.assertEqual(config['format_album'].as_str(), u'$album') - - parser.parse_args([u'-a', u'-f', u'$foo']) - self.assertEqual(config['format_item'].as_str(), u'$bar') - self.assertEqual(config['format_album'].as_str(), u'$foo') + parser.parse_args(['-f', '$bar']) + self.assertEqual(config['format_item'].as_str(), '$bar') + self.assertEqual(config['format_album'].as_str(), '$album') + + parser.parse_args(['-a', '-f', '$foo']) + self.assertEqual(config['format_item'].as_str(), '$bar') + self.assertEqual(config['format_album'].as_str(), '$foo') - parser.parse_args([u'-f', u'$foo2', u'-a']) - self.assertEqual(config['format_album'].as_str(), u'$foo2') + parser.parse_args(['-f', '$foo2', '-a']) + self.assertEqual(config['format_album'].as_str(), '$foo2') def test_add_all_common_options(self): parser = ui.CommonOptionsParser() diff -Nru beets-1.5.0/test/test_util.py beets-1.6.0/test/test_util.py --- beets-1.5.0/test/test_util.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_util.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,7 +13,6 @@ # included in all copies or substantial portions of the Software. """Tests for base utils from the beets.util package. """ -from __future__ import division, absolute_import, print_function import sys import re @@ -22,11 +20,10 @@ import subprocess import unittest -from mock import patch, Mock +from unittest.mock import patch, Mock from test import _common from beets import util -import six class UtilTest(unittest.TestCase): @@ -43,69 +40,67 @@ @patch('os.execlp') @patch('beets.util.open_anything') def test_interactive_open(self, mock_open, mock_execlp): - mock_open.return_value = u'tagada' + mock_open.return_value = 'tagada' util.interactive_open(['foo'], util.open_anything()) - mock_execlp.assert_called_once_with(u'tagada', u'tagada', u'foo') + mock_execlp.assert_called_once_with('tagada', 'tagada', 'foo') mock_execlp.reset_mock() - util.interactive_open(['foo'], u'bar') - mock_execlp.assert_called_once_with(u'bar', u'bar', u'foo') + util.interactive_open(['foo'], 'bar') + mock_execlp.assert_called_once_with('bar', 'bar', 'foo') def test_sanitize_unix_replaces_leading_dot(self): with _common.platform_posix(): - p = util.sanitize_path(u'one/.two/three') - self.assertFalse(u'.' in p) + p = util.sanitize_path('one/.two/three') + self.assertFalse('.' in p) def test_sanitize_windows_replaces_trailing_dot(self): with _common.platform_windows(): - p = util.sanitize_path(u'one/two./three') - self.assertFalse(u'.' in p) + p = util.sanitize_path('one/two./three') + self.assertFalse('.' in p) def test_sanitize_windows_replaces_illegal_chars(self): with _common.platform_windows(): - p = util.sanitize_path(u':*?"<>|') - self.assertFalse(u':' in p) - self.assertFalse(u'*' in p) - self.assertFalse(u'?' in p) - self.assertFalse(u'"' in p) - self.assertFalse(u'<' in p) - self.assertFalse(u'>' in p) - self.assertFalse(u'|' in p) + p = util.sanitize_path(':*?"<>|') + self.assertFalse(':' in p) + self.assertFalse('*' in p) + self.assertFalse('?' in p) + self.assertFalse('"' in p) + self.assertFalse('<' in p) + self.assertFalse('>' in p) + self.assertFalse('|' in p) def test_sanitize_windows_replaces_trailing_space(self): with _common.platform_windows(): - p = util.sanitize_path(u'one/two /three') - self.assertFalse(u' ' in p) + p = util.sanitize_path('one/two /three') + self.assertFalse(' ' in p) def test_sanitize_path_works_on_empty_string(self): with _common.platform_posix(): - p = util.sanitize_path(u'') - self.assertEqual(p, u'') + p = util.sanitize_path('') + self.assertEqual(p, '') def test_sanitize_with_custom_replace_overrides_built_in_sub(self): with _common.platform_posix(): - p = util.sanitize_path(u'a/.?/b', [ - (re.compile(r'foo'), u'bar'), + p = util.sanitize_path('a/.?/b', [ + (re.compile(r'foo'), 'bar'), ]) - self.assertEqual(p, u'a/.?/b') + self.assertEqual(p, 'a/.?/b') def test_sanitize_with_custom_replace_adds_replacements(self): with _common.platform_posix(): - p = util.sanitize_path(u'foo/bar', [ - (re.compile(r'foo'), u'bar'), + p = util.sanitize_path('foo/bar', [ + (re.compile(r'foo'), 'bar'), ]) - self.assertEqual(p, u'bar/bar') + self.assertEqual(p, 'bar/bar') - @unittest.skip(u'unimplemented: #359') + @unittest.skip('unimplemented: #359') def test_sanitize_empty_component(self): with _common.platform_posix(): - p = util.sanitize_path(u'foo//bar', [ - (re.compile(r'^$'), u'_'), + p = util.sanitize_path('foo//bar', [ + (re.compile(r'^$'), '_'), ]) - self.assertEqual(p, u'foo/_/bar') + self.assertEqual(p, 'foo/_/bar') - @unittest.skipIf(six.PY2, 'surrogateescape error handler not available' - 'on Python 2') def test_convert_command_args_keeps_undecodeable_bytes(self): arg = b'\x82' # non-ascii bytes cmd_args = util.convert_command_args([arg]) @@ -117,7 +112,7 @@ def test_command_output(self, mock_popen): def popen_fail(*args, **kwargs): m = Mock(returncode=1) - m.communicate.return_value = u'foo', u'bar' + m.communicate.return_value = 'foo', 'bar' return m mock_popen.side_effect = popen_fail @@ -130,10 +125,10 @@ class PathConversionTest(_common.TestCase): def test_syspath_windows_format(self): with _common.platform_windows(): - path = os.path.join(u'a', u'b', u'c') + path = os.path.join('a', 'b', 'c') outpath = util.syspath(path) - self.assertTrue(isinstance(outpath, six.text_type)) - self.assertTrue(outpath.startswith(u'\\\\?\\')) + self.assertTrue(isinstance(outpath, str)) + self.assertTrue(outpath.startswith('\\\\?\\')) def test_syspath_windows_format_unc_path(self): # The \\?\ prefix on Windows behaves differently with UNC @@ -141,12 +136,12 @@ path = '\\\\server\\share\\file.mp3' with _common.platform_windows(): outpath = util.syspath(path) - self.assertTrue(isinstance(outpath, six.text_type)) - self.assertEqual(outpath, u'\\\\?\\UNC\\server\\share\\file.mp3') + self.assertTrue(isinstance(outpath, str)) + self.assertEqual(outpath, '\\\\?\\UNC\\server\\share\\file.mp3') def test_syspath_posix_unchanged(self): with _common.platform_posix(): - path = os.path.join(u'a', u'b', u'c') + path = os.path.join('a', 'b', 'c') outpath = util.syspath(path) self.assertEqual(path, outpath) @@ -160,14 +155,14 @@ sys.getfilesystemencoding = old_gfse def test_bytestring_path_windows_encodes_utf8(self): - path = u'caf\xe9' + path = 'caf\xe9' outpath = self._windows_bytestring_path(path) self.assertEqual(path, outpath.decode('utf-8')) def test_bytesting_path_windows_removes_magic_prefix(self): - path = u'\\\\?\\C:\\caf\xe9' + path = '\\\\?\\C:\\caf\xe9' outpath = self._windows_bytestring_path(path) - self.assertEqual(outpath, u'C:\\caf\xe9'.encode('utf-8')) + self.assertEqual(outpath, 'C:\\caf\xe9'.encode()) class PathTruncationTest(_common.TestCase): @@ -178,13 +173,13 @@ def test_truncate_unicode(self): with _common.platform_posix(): - p = util.truncate_path(u'abcde/fgh', 4) - self.assertEqual(p, u'abcd/fgh') + p = util.truncate_path('abcde/fgh', 4) + self.assertEqual(p, 'abcd/fgh') def test_truncate_preserves_extension(self): with _common.platform_posix(): - p = util.truncate_path(u'abcde/fgh.ext', 5) - self.assertEqual(p, u'abcde/f.ext') + p = util.truncate_path('abcde/fgh.ext', 5) + self.assertEqual(p, 'abcde/f.ext') def suite(): diff -Nru beets-1.5.0/test/test_vfs.py beets-1.6.0/test/test_vfs.py --- beets-1.5.0/test/test_vfs.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_vfs.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,7 +13,6 @@ # included in all copies or substantial portions of the Software. """Tests for the virtual filesystem builder..""" -from __future__ import division, absolute_import, print_function import unittest from test import _common @@ -24,10 +22,10 @@ class VFSTest(_common.TestCase): def setUp(self): - super(VFSTest, self).setUp() + super().setUp() self.lib = library.Library(':memory:', path_formats=[ - (u'default', u'albums/$album/$title'), - (u'singleton:true', u'tracks/$artist/$title'), + ('default', 'albums/$album/$title'), + ('singleton:true', 'tracks/$artist/$title'), ]) self.lib.add(_common.item()) self.lib.add_album([_common.item()]) diff -Nru beets-1.5.0/test/test_web.py beets-1.6.0/test/test_web.py --- beets-1.5.0/test/test_web.py 2021-03-20 13:03:37.000000000 +0000 +++ beets-1.6.0/test/test_web.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,13 +1,9 @@ -# -*- coding: utf-8 -*- - """Tests for the 'web' plugin""" -from __future__ import division, absolute_import, print_function import json import unittest import os.path -from six import assertCountEqual import shutil from test import _common @@ -23,13 +19,13 @@ def setUp(self): - super(WebPluginTest, self).setUp() + super().setUp() self.log = logging.getLogger('beets.web') if platform.system() == 'Windows': - self.path_prefix = u'C:' + self.path_prefix = 'C:' else: - self.path_prefix = u'' + self.path_prefix = '' # Add fixtures for track in self.lib.items(): @@ -40,27 +36,27 @@ # The following adds will create items #1, #2 and #3 path1 = self.path_prefix + os.sep + \ os.path.join(b'path_1').decode('utf-8') - self.lib.add(Item(title=u'title', + self.lib.add(Item(title='title', path=path1, album_id=2, artist='AAA Singers')) path2 = self.path_prefix + os.sep + \ os.path.join(b'somewhere', b'a').decode('utf-8') - self.lib.add(Item(title=u'another title', + self.lib.add(Item(title='another title', path=path2, artist='AAA Singers')) path3 = self.path_prefix + os.sep + \ os.path.join(b'somewhere', b'abc').decode('utf-8') - self.lib.add(Item(title=u'and a third', + self.lib.add(Item(title='and a third', testattr='ABC', path=path3, album_id=2)) # The following adds will create albums #1 and #2 - self.lib.add(Album(album=u'album', + self.lib.add(Album(album='album', albumtest='xyz')) path4 = self.path_prefix + os.sep + \ os.path.join(b'somewhere2', b'art_path_2').decode('utf-8') - self.lib.add(Album(album=u'other album', + self.lib.add(Album(album='other album', artpath=path4)) web.app.config['TESTING'] = True @@ -122,7 +118,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(res_json['id'], 1) - self.assertEqual(res_json['title'], u'title') + self.assertEqual(res_json['title'], 'title') def test_get_multiple_items_by_id(self): response = self.client.get('/item/1,2') @@ -131,7 +127,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['items']), 2) response_titles = {item['title'] for item in res_json['items']} - self.assertEqual(response_titles, {u'title', u'another title'}) + self.assertEqual(response_titles, {'title', 'another title'}) def test_get_single_item_not_found(self): response = self.client.get('/item/4') @@ -144,7 +140,7 @@ res_json = json.loads(response.data.decode('utf-8')) self.assertEqual(response.status_code, 200) - self.assertEqual(res_json['title'], u'full') + self.assertEqual(res_json['title'], 'full') def test_get_single_item_by_path_not_found_if_not_in_library(self): data_path = os.path.join(_common.RSRC, b'full.mp3') @@ -170,7 +166,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['results']), 1) self.assertEqual(res_json['results'][0]['title'], - u'another title') + 'another title') def test_query_item_string(self): """ testing item query: testattr:ABC """ @@ -180,7 +176,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['results']), 1) self.assertEqual(res_json['results'][0]['title'], - u'and a third') + 'and a third') def test_query_item_regex(self): """ testing item query: testattr::[A-C]+ """ @@ -190,7 +186,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['results']), 1) self.assertEqual(res_json['results'][0]['title'], - u'and a third') + 'and a third') def test_query_item_regex_backslash(self): # """ testing item query: testattr::\w+ """ @@ -200,7 +196,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['results']), 1) self.assertEqual(res_json['results'][0]['title'], - u'and a third') + 'and a third') def test_query_item_path(self): # """ testing item query: path:\somewhere\a """ @@ -216,7 +212,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['results']), 1) self.assertEqual(res_json['results'][0]['title'], - u'another title') + 'another title') def test_get_all_albums(self): response = self.client.get('/album/') @@ -224,7 +220,7 @@ self.assertEqual(response.status_code, 200) response_albums = [album['album'] for album in res_json['albums']] - assertCountEqual(self, response_albums, [u'album', u'other album']) + self.assertCountEqual(response_albums, ['album', 'other album']) def test_get_single_album_by_id(self): response = self.client.get('/album/2') @@ -232,7 +228,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(res_json['id'], 2) - self.assertEqual(res_json['album'], u'other album') + self.assertEqual(res_json['album'], 'other album') def test_get_multiple_albums_by_id(self): response = self.client.get('/album/1,2') @@ -240,7 +236,7 @@ self.assertEqual(response.status_code, 200) response_albums = [album['album'] for album in res_json['albums']] - assertCountEqual(self, response_albums, [u'album', u'other album']) + self.assertCountEqual(response_albums, ['album', 'other album']) def test_get_album_empty_query(self): response = self.client.get('/album/query/') @@ -256,7 +252,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['results']), 1) self.assertEqual(res_json['results'][0]['album'], - u'other album') + 'other album') self.assertEqual(res_json['results'][0]['id'], 2) def test_get_album_details(self): @@ -266,11 +262,11 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['items']), 2) self.assertEqual(res_json['items'][0]['album'], - u'other album') + 'other album') self.assertEqual(res_json['items'][1]['album'], - u'other album') + 'other album') response_track_titles = {item['title'] for item in res_json['items']} - self.assertEqual(response_track_titles, {u'title', u'and a third'}) + self.assertEqual(response_track_titles, {'title', 'and a third'}) def test_query_album_string(self): """ testing query: albumtest:xy """ @@ -280,7 +276,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['results']), 1) self.assertEqual(res_json['results'][0]['album'], - u'album') + 'album') def test_query_album_artpath_regex(self): """ testing query: artpath::art_ """ @@ -290,7 +286,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['results']), 1) self.assertEqual(res_json['results'][0]['album'], - u'other album') + 'other album') def test_query_album_regex_backslash(self): # """ testing query: albumtest::\w+ """ @@ -300,7 +296,7 @@ self.assertEqual(response.status_code, 200) self.assertEqual(len(res_json['results']), 1) self.assertEqual(res_json['results'][0]['album'], - u'album') + 'album') def test_get_stats(self): response = self.client.get('/stats') @@ -315,7 +311,7 @@ web.app.config['READONLY'] = False # Create a temporary item - item_id = self.lib.add(Item(title=u'test_delete_item_id', + item_id = self.lib.add(Item(title='test_delete_item_id', test_delete_item_id=1)) # Check we can find the temporary item we just created @@ -397,7 +393,7 @@ web.app.config['READONLY'] = False # Create a temporary item - self.lib.add(Item(title=u'test_delete_item_query', + self.lib.add(Item(title='test_delete_item_query', test_delete_item_query=1)) # Check we can find the temporary item we just created @@ -434,7 +430,7 @@ web.app.config['READONLY'] = True # Create a temporary item - item_id = self.lib.add(Item(title=u'test_delete_item_id_ro', + item_id = self.lib.add(Item(title='test_delete_item_id_ro', test_delete_item_id_ro=1)) # Check we can find the temporary item we just created @@ -461,7 +457,7 @@ web.app.config['READONLY'] = True # Create a temporary item - item_id = self.lib.add(Item(title=u'test_delete_item_q_ro', + item_id = self.lib.add(Item(title='test_delete_item_q_ro', test_delete_item_q_ro=1)) # Check we can find the temporary item we just created @@ -488,7 +484,7 @@ web.app.config['READONLY'] = False # Create a temporary album - album_id = self.lib.add(Album(album=u'test_delete_album_id', + album_id = self.lib.add(Album(album='test_delete_album_id', test_delete_album_id=1)) # Check we can find the temporary album we just created @@ -513,7 +509,7 @@ web.app.config['READONLY'] = False # Create a temporary album - self.lib.add(Album(album=u'test_delete_album_query', + self.lib.add(Album(album='test_delete_album_query', test_delete_album_query=1)) # Check we can find the temporary album we just created @@ -550,7 +546,7 @@ web.app.config['READONLY'] = True # Create a temporary album - album_id = self.lib.add(Album(album=u'test_delete_album_id_ro', + album_id = self.lib.add(Album(album='test_delete_album_id_ro', test_delete_album_id_ro=1)) # Check we can find the temporary album we just created @@ -577,7 +573,7 @@ web.app.config['READONLY'] = True # Create a temporary album - album_id = self.lib.add(Album(album=u'test_delete_album_query_ro', + album_id = self.lib.add(Album(album='test_delete_album_query_ro', test_delete_album_query_ro=1)) # Check we can find the temporary album we just created @@ -607,7 +603,7 @@ web.app.config['READONLY'] = False # Create a temporary item - item_id = self.lib.add(Item(title=u'test_patch_item_id', + item_id = self.lib.add(Item(title='test_patch_item_id', test_patch_f1=1, test_patch_f2="Old")) @@ -649,7 +645,7 @@ web.app.config['READONLY'] = True # Create a temporary item - item_id = self.lib.add(Item(title=u'test_patch_item_id_ro', + item_id = self.lib.add(Item(title='test_patch_item_id_ro', test_patch_f1=2, test_patch_f2="Old")) @@ -675,5 +671,6 @@ def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == '__main__': unittest.main(defaultTest='suite') diff -Nru beets-1.5.0/test/test_zero.py beets-1.6.0/test/test_zero.py --- beets-1.5.0/test/test_zero.py 2020-05-17 18:44:34.000000000 +0000 +++ beets-1.6.0/test/test_zero.py 2021-09-28 19:51:02.000000000 +0000 @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- - """Tests for the 'zero' plugin""" -from __future__ import division, absolute_import, print_function import unittest from test.helper import TestHelper, control_stdin @@ -31,8 +28,8 @@ self.config['zero']['fields'] = ['comments', 'month'] item = self.add_item_fixture( - comments=u'test comment', - title=u'Title', + comments='test comment', + title='Title', month=1, year=2000, ) @@ -44,14 +41,14 @@ mf = MediaFile(syspath(item.path)) self.assertIsNone(mf.comments) self.assertIsNone(mf.month) - self.assertEqual(mf.title, u'Title') + self.assertEqual(mf.title, 'Title') self.assertEqual(mf.year, 2000) def test_pattern_match(self): self.config['zero']['fields'] = ['comments'] - self.config['zero']['comments'] = [u'encoded by'] + self.config['zero']['comments'] = ['encoded by'] - item = self.add_item_fixture(comments=u'encoded by encoder') + item = self.add_item_fixture(comments='encoded by encoder') item.write() self.load_plugins('zero') @@ -62,16 +59,16 @@ def test_pattern_nomatch(self): self.config['zero']['fields'] = ['comments'] - self.config['zero']['comments'] = [u'encoded by'] + self.config['zero']['comments'] = ['encoded by'] - item = self.add_item_fixture(comments=u'recorded at place') + item = self.add_item_fixture(comments='recorded at place') item.write() self.load_plugins('zero') item.write() mf = MediaFile(syspath(item.path)) - self.assertEqual(mf.comments, u'recorded at place') + self.assertEqual(mf.comments, 'recorded at place') def test_do_not_change_database(self): self.config['zero']['fields'] = ['year'] @@ -126,7 +123,7 @@ year=2016, day=13, month=3, - comments=u'test comment' + comments='test comment' ) item.write() item_id = item.id @@ -144,14 +141,14 @@ self.assertEqual(item['year'], 2016) self.assertEqual(mf.year, 2016) self.assertEqual(mf.comments, None) - self.assertEqual(item['comments'], u'') + self.assertEqual(item['comments'], '') def test_subcommand_update_database_false(self): item = self.add_item_fixture( year=2016, day=13, month=3, - comments=u'test comment' + comments='test comment' ) item.write() item_id = item.id @@ -169,7 +166,7 @@ self.assertEqual(item['year'], 2016) self.assertEqual(mf.year, 2016) - self.assertEqual(item['comments'], u'test comment') + self.assertEqual(item['comments'], 'test comment') self.assertEqual(mf.comments, None) def test_subcommand_query_include(self): @@ -177,7 +174,7 @@ year=2016, day=13, month=3, - comments=u'test comment' + comments='test comment' ) item.write() @@ -199,7 +196,7 @@ year=2016, day=13, month=3, - comments=u'test comment' + comments='test comment' ) item.write() @@ -214,7 +211,7 @@ mf = MediaFile(syspath(item.path)) self.assertEqual(mf.year, 2016) - self.assertEqual(mf.comments, u'test comment') + self.assertEqual(mf.comments, 'test comment') def test_no_fields(self): item = self.add_item_fixture(year=2016) @@ -240,8 +237,8 @@ self.assertEqual(mf.year, 2016) item_id = item.id - self.config['zero']['fields'] = [u'year'] - self.config['zero']['keep_fields'] = [u'comments'] + self.config['zero']['fields'] = ['year'] + self.config['zero']['keep_fields'] = ['comments'] self.load_plugins('zero') with control_stdin('y'): @@ -253,13 +250,13 @@ self.assertEqual(mf.year, 2016) def test_keep_fields(self): - item = self.add_item_fixture(year=2016, comments=u'test comment') - self.config['zero']['keep_fields'] = [u'year'] + item = self.add_item_fixture(year=2016, comments='test comment') + self.config['zero']['keep_fields'] = ['year'] self.config['zero']['fields'] = None self.config['zero']['update_database'] = True tags = { - 'comments': u'test comment', + 'comments': 'test comment', 'year': 2016, } self.load_plugins('zero') @@ -270,7 +267,7 @@ self.assertEqual(tags['year'], 2016) def test_keep_fields_removes_preserved_tags(self): - self.config['zero']['keep_fields'] = [u'year'] + self.config['zero']['keep_fields'] = ['year'] self.config['zero']['fields'] = None self.config['zero']['update_database'] = True @@ -279,7 +276,7 @@ self.assertNotIn('id', z.fields_to_progs) def test_fields_removes_preserved_tags(self): - self.config['zero']['fields'] = [u'year id'] + self.config['zero']['fields'] = ['year id'] self.config['zero']['update_database'] = True z = ZeroPlugin() @@ -291,7 +288,7 @@ year=2016, day=13, month=3, - comments=u'test comment' + comments='test comment' ) item.write() item_id = item.id @@ -308,8 +305,8 @@ self.assertEqual(item['year'], 2016) self.assertEqual(mf.year, 2016) - self.assertEqual(mf.comments, u'test comment') - self.assertEqual(item['comments'], u'test comment') + self.assertEqual(mf.comments, 'test comment') + self.assertEqual(item['comments'], 'test comment') def suite():