Merge pull request #4030 from arogl/pyupgrade

pyupgrade of beets to Python 3.6
This commit is contained in:
Adrian Sampson 2021-09-28 15:50:49 -04:00 committed by GitHub
commit 018396ccf7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
182 changed files with 4877 additions and 5392 deletions

2
beet
View file

@ -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 beets.ui

View file

@ -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.1'
__author__ = u'Adrian Sampson <adrian@radbox.org>'
__version__ = '1.5.1'
__author__ = 'Adrian Sampson <adrian@radbox.org>'
class IncludeLazyConfig(confuse.LazyConfig):
@ -27,7 +25,7 @@ class IncludeLazyConfig(confuse.LazyConfig):
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']:

View file

@ -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

View file

@ -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 @@ def get_art(log, item):
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 @@ def embed_item(log, item, imagepath, maxwidth=None, itempath=None,
# 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 @@ def embed_album(log, album, maxwidth=None, quiet=False, compare_threshold=0,
"""
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 @@ def resize_image(log, imagepath, maxwidth, quality):
"""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 @@ def check_art_similarity(log, item, imagepath, compare_threshold):
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 @@ def check_art_similarity(log, item, imagepath, compare_threshold):
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 @@ def check_art_similarity(log, item, imagepath, compare_threshold):
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 @@ def check_art_similarity(log, item, imagepath, compare_threshold):
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 @@ def extract(log, outpath, item):
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 extract_first(log, outpath, items):
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})

View file

@ -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

View file

@ -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.util import as_string
from beets.autotag import mb
from jellyfish import levenshtein_distance
from unidecode import unidecode
import six
log = logging.getLogger('beets')
@ -70,6 +67,7 @@ class AlbumInfo(AttrDict):
``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 @@ class TrackInfo(AttrDict):
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 @@ def _string_dist_basic(str1, str2):
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 @@ def string_dist(str1, str2):
# "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 @@ def string_dist(str1, str2):
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 @@ class LazyClassProperty(object):
@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 @@ class Distance(object):
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 @@ class Distance(object):
"""
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 @@ class Distance(object):
"""
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 @@ def album_for_mbid(release_id):
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 @@ def track_for_mbid(recording_id):
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 @@ def albums_for_id(album_id):
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 @@ def tracks_for_id(track_id):
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 @@ def album_candidates(items, artist, album, va_likely, extra_tags):
# 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 @@ def item_candidates(item, artist, title):
# 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)

View file

@ -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 @@ from beets.util.enumeration import OrderedEnum
# 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 @@ def assign_items(items, tracks):
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 @@ def match_by_id(items):
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 @@ def _add_candidate(items, results, info):
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 @@ def _add_candidate(items, results, info):
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 @@ def tag_album(items, search_artist=None, search_album=None,
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 @@ def tag_album(items, search_artist=None, search_album=None,
# 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 @@ def tag_album(items, search_artist=None, search_album=None,
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 @@ def tag_album(items, search_artist=None, search_album=None,
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 @@ def tag_album(items, search_artist=None, search_album=None,
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 @@ def tag_item(item, search_artist=None, search_title=None,
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 @@ def tag_item(item, search_artist=None, search_title=None,
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 @@ def tag_item(item, search_artist=None, search_title=None,
# 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 @@ def tag_item(item, search_artist=None, search_title=None,
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)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -15,13 +14,10 @@
"""Searches for albums in the MusicBrainz database.
"""
from __future__ import division, absolute_import, print_function
import musicbrainzngs
import re
import traceback
from collections import Counter
from six.moves.urllib.parse import urljoin
from beets import logging
from beets import plugins
@ -29,14 +25,12 @@ import beets.autotag.hooks
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]']
@ -56,17 +50,19 @@ class MusicBrainzAPIError(util.HumanReadableException):
"""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',
@ -160,7 +156,7 @@ def _flatten_artist_credit(credit):
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)
@ -213,7 +209,7 @@ def track_info(recording, index=None, medium=None, medium_index=None,
medium=medium,
medium_index=medium_index,
medium_total=medium_total,
data_source=u'MusicBrainz',
data_source='MusicBrainz',
data_url=track_url(recording['id']),
)
@ -256,10 +252,10 @@ def track_info(recording, index=None, medium=None, medium_index=None,
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', ()):
@ -268,7 +264,7 @@ def track_info(recording, index=None, medium=None, medium_index=None,
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)
@ -313,14 +309,14 @@ def album_info(release):
# 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']:
@ -393,7 +389,7 @@ def album_info(release):
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
@ -460,9 +456,9 @@ def album_info(release):
if config['musicbrainz']['genres']:
sources = [
release['release-group'].get('genre-list', []),
release.get('genre-list', []),
]
release['release-group'].get('genre-list', []),
release.get('genre-list', []),
]
genres = Counter()
for source in sources:
for genreitem in source:
@ -494,15 +490,15 @@ def match_album(artist, album, tracks=None, extra_tags=None):
# 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
@ -511,7 +507,7 @@ def match_album(artist, album, tracks=None, extra_tags=None):
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:
@ -552,7 +548,7 @@ def _parse_id(s):
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()
@ -562,19 +558,19 @@ def album_for_id(releaseid):
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'])
@ -585,14 +581,14 @@ def track_for_id(releaseid):
"""
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'])

View file

@ -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

View file

@ -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,7 +28,6 @@ from beets.util import functemplate
from beets.util import py3_path
from beets.dbcore import types
from .query import MatchQuery, NullSort, TrueQuery
import six
from collections.abc import Mapping
@ -83,7 +80,7 @@ class FormattedMapping(Mapping):
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))
@ -104,7 +101,7 @@ class FormattedMapping(Mapping):
return value
class LazyConvertDict(object):
class LazyConvertDict:
"""Lazily convert types for attributes fetched from the database
"""
@ -200,7 +197,7 @@ class LazyConvertDict(object):
# 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
@ -314,9 +311,9 @@ class Model(object):
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):
@ -334,10 +331,10 @@ class Model(object):
"""
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.
@ -428,9 +425,9 @@ class Model(object):
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
@ -480,22 +477,22 @@ class Model(object):
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]
@ -524,7 +521,7 @@ class Model(object):
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)
@ -535,7 +532,7 @@ class Model(object):
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),
@ -544,7 +541,7 @@ class Model(object):
# 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)
)
@ -562,7 +559,7 @@ class Model(object):
# 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))
@ -574,11 +571,11 @@ class Model(object):
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,)
)
@ -596,7 +593,7 @@ class Model(object):
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()
@ -623,7 +620,7 @@ class Model(object):
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())
@ -634,8 +631,8 @@ class Model(object):
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)
@ -647,10 +644,11 @@ class Model(object):
# 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
@ -748,8 +746,8 @@ class Results(object):
""" 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)
@ -798,7 +796,7 @@ class Results(object):
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
@ -811,7 +809,7 @@ class Results(object):
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.
"""
@ -886,7 +884,7 @@ class Transaction(object):
self.db._connection().executescript(statements)
class Database(object):
class Database:
"""A container for Model objects that wraps an SQLite database as
the backend.
"""
@ -998,7 +996,7 @@ class Database(object):
"""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)
@ -1015,7 +1013,7 @@ class Database(object):
# 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):
@ -1026,9 +1024,9 @@ class Database(object):
# 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.
@ -1036,7 +1034,7 @@ class Database(object):
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
)
@ -1072,23 +1070,23 @@ class Database(object):
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:

View file

@ -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,7 +21,6 @@ from beets import util
from datetime import datetime, timedelta
import unicodedata
from functools import reduce
import six
class ParsingError(ValueError):
@ -41,8 +38,8 @@ class InvalidQueryError(ParsingError):
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):
@ -53,13 +50,13 @@ class InvalidQueryArgumentValueError(ParsingError):
"""
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.
"""
@ -79,7 +76,7 @@ class Query(object):
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)
@ -126,7 +123,7 @@ class FieldQuery(Query):
"{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):
@ -148,7 +145,7 @@ class NoneQuery(FieldQuery):
"""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", ()
@ -207,14 +204,14 @@ class RegexpQuery(StringFieldQuery):
"""
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
@ -235,8 +232,8 @@ class BooleanQuery(MatchQuery):
"""
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)
@ -249,13 +246,13 @@ class BytesQuery(MatchQuery):
"""
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 = memoryview(self.pattern)
elif isinstance(self.pattern, memoryview):
@ -290,10 +287,10 @@ class NumericQuery(FieldQuery):
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:
@ -311,7 +308,7 @@ class NumericQuery(FieldQuery):
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:
@ -328,14 +325,14 @@ class NumericQuery(FieldQuery):
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):
@ -380,7 +377,7 @@ class CollectionQuery(Query):
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):
@ -404,7 +401,7 @@ class AnyFieldQuery(CollectionQuery):
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')
@ -420,7 +417,7 @@ class AnyFieldQuery(CollectionQuery):
"{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):
@ -470,7 +467,7 @@ class NotQuery(Query):
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.
@ -483,7 +480,7 @@ class NotQuery(Query):
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):
@ -539,7 +536,7 @@ def _parse_periods(pattern):
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
@ -565,7 +562,7 @@ class Period(object):
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
@ -646,10 +643,10 @@ class Period(object):
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.
@ -658,7 +655,7 @@ class DateInterval(object):
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
@ -679,7 +676,7 @@ class DateInterval(object):
return True
def __str__(self):
return '[{0}, {1})'.format(self.start, self.end)
return f'[{self.start}, {self.end})'
class DateQuery(FieldQuery):
@ -693,7 +690,7 @@ class DateQuery(FieldQuery):
"""
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)
@ -752,12 +749,12 @@ class DurationQuery(NumericQuery):
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.
"""
@ -844,13 +841,13 @@ class MultipleSort(Sort):
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
@ -871,14 +868,14 @@ class FieldSort(Sort):
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 '-',
@ -888,7 +885,7 @@ class FieldSort(Sort):
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
@ -906,7 +903,7 @@ class FixedFieldSort(FieldSort):
'ELSE {0} END)'.format(self.field)
else:
field = self.field
return "{0} {1}".format(field, order)
return f"{field} {order}"
class SlowFieldSort(FieldSort):

View file

@ -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 @@ def parse_sorted_query(model_cls, parts, prefixes={},
# 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 @@ def parse_sorted_query(model_cls, parts, prefixes={},
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:

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -15,22 +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
# 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.
"""
@ -38,7 +35,7 @@ class Type(object):
"""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
@ -60,11 +57,11 @@ class Type(object):
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
@ -103,7 +100,7 @@ class Type(object):
"""
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)
@ -124,7 +121,7 @@ class Default(Type):
class Integer(Type):
"""A basic integer type.
"""
sql = u'INTEGER'
sql = 'INTEGER'
query = query.NumericQuery
model_type = int
@ -145,7 +142,7 @@ class PaddedInt(Integer):
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):
@ -158,12 +155,12 @@ class ScaledInt(Integer):
"""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):
@ -174,14 +171,14 @@ class Id(Integer):
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
@ -189,7 +186,7 @@ class Float(Type):
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):
@ -201,7 +198,7 @@ class NullFloat(Float):
class String(Type):
"""A Unicode string type.
"""
sql = u'TEXT'
sql = 'TEXT'
query = query.SubstringQuery
def normalize(self, value):
@ -214,12 +211,12 @@ class String(Type):
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)

View file

@ -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 @@ def _open_state():
# 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 @@ def _save_state(state):
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 @@ def history_get():
# 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 @@ class ImportSession(object):
"""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 @@ class ImportSession(object):
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 @@ class ImportSession(object):
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 @@ class ImportSession(object):
"""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 @@ class ImportSession(object):
# 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 @@ class ImportSession(object):
# 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 @@ class ImportTask(BaseImportTask):
* `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 @@ class ImportTask(BaseImportTask):
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 @@ class ImportTask(BaseImportTask):
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 @@ class ImportTask(BaseImportTask):
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 @@ class ImportTask(BaseImportTask):
# 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 @@ class ImportTask(BaseImportTask):
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 @@ class ImportTask(BaseImportTask):
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 @@ class ImportTask(BaseImportTask):
"""
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 @@ class SingletonImportTask(ImportTask):
"""
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 @@ class SingletonImportTask(ImportTask):
"""
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 @@ class SentinelImportTask(ImportTask):
"""
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 @@ class ArchiveImportTask(SentinelImportTask):
"""
def __init__(self, toppath):
super(ArchiveImportTask, self).__init__(toppath, ())
super().__init__(toppath, ())
self.extracted = False
@classmethod
@ -1073,7 +1074,7 @@ class ArchiveImportTask(SentinelImportTask):
"""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 @@ class ArchiveImportTask(SentinelImportTask):
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 @@ class ImportTaskFactory(object):
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 @@ class ImportTaskFactory(object):
"""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 @@ class ImportTaskFactory(object):
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 @@ class ImportTaskFactory(object):
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 @@ class ImportTaskFactory(object):
# 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 @@ def read_tasks(session):
# 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 @@ def query_tasks(session):
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 @@ def lookup_candidates(session, task):
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 user_query(session, task):
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 @@ def resolve_duplicates(session, task):
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 @@ def import_asis(session, task):
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 @@ def log_files(session, task):
"""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):

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -15,14 +14,12 @@
"""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
@ -62,7 +59,7 @@ class PathQuery(dbcore.FieldQuery):
`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.
@ -147,7 +144,7 @@ class PathType(types.Type):
`bytes` objects, in keeping with the Unix filesystem abstraction.
"""
sql = u'BLOB'
sql = 'BLOB'
query = PathQuery
model_type = bytes
@ -171,7 +168,7 @@ class PathType(types.Type):
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)
@ -249,6 +246,7 @@ class SmartArtistSort(dbcore.query.Sort):
"""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
@ -264,12 +262,15 @@ class SmartArtistSort(dbcore.query.Sort):
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)
@ -280,17 +281,17 @@ PF_KEY_DEFAULT = 'default'
# 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
@ -298,9 +299,9 @@ class FileOperationError(Exception):
"""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
@ -308,25 +309,24 @@ class FileOperationError(Exception):
__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.
"""
@ -341,21 +341,21 @@ class LibModel(dbcore.Model):
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):
@ -376,8 +376,8 @@ class FormattedItemMapping(dbcore.db.FormattedMapping):
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.
@ -451,85 +451,85 @@ class Item(LibModel):
_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,
'albumtypes': 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(),
'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(),
'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',
@ -607,14 +607,14 @@ class Item(LibModel):
"""
# 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.
@ -624,7 +624,7 @@ class Item(LibModel):
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]
@ -634,9 +634,9 @@ class Item(LibModel):
# 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)),
)
@ -644,7 +644,7 @@ class Item(LibModel):
"""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))
@ -666,7 +666,7 @@ class Item(LibModel):
"""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']
@ -706,7 +706,7 @@ class Item(LibModel):
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
@ -778,7 +778,7 @@ class Item(LibModel):
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):
@ -798,7 +798,7 @@ class Item(LibModel):
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()
@ -861,7 +861,7 @@ class Item(LibModel):
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.
@ -871,7 +871,7 @@ class Item(LibModel):
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:
@ -967,7 +967,7 @@ class Item(LibModel):
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:
@ -1001,9 +1001,9 @@ class Item(LibModel):
# 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
)
@ -1022,50 +1022,50 @@ class Album(LibModel):
_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,
'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,
'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,
}
@ -1138,7 +1138,7 @@ class Album(LibModel):
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()
# Delete art file.
if delete:
@ -1163,7 +1163,7 @@ class Album(LibModel):
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 +1173,7 @@ class Album(LibModel):
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 +1230,7 @@ class Album(LibModel):
"""
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 +1327,7 @@ class Album(LibModel):
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,8 +1392,8 @@ def parse_query_string(s, model_cls):
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 = shlex.split(s)
except ValueError as exc:
@ -1424,7 +1424,7 @@ class Library(dbcore.Database):
'$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
@ -1433,7 +1433,7 @@ class Library(dbcore.Database):
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
@ -1455,10 +1455,10 @@ class Library(dbcore.Database):
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.
@ -1483,7 +1483,7 @@ class Library(dbcore.Database):
# 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)
@ -1495,7 +1495,7 @@ class Library(dbcore.Database):
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
)
@ -1554,7 +1554,7 @@ def _int_arg(s):
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
@ -1606,7 +1606,7 @@ class DefaultTemplateFunctions(object):
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).
"""
@ -1648,7 +1648,7 @@ class DefaultTemplateFunctions(object):
"""
# 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
@ -1656,7 +1656,7 @@ class DefaultTemplateFunctions(object):
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)
@ -1675,14 +1675,14 @@ class DefaultTemplateFunctions(object):
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 = []
@ -1694,13 +1694,13 @@ class DefaultTemplateFunctions(object):
# 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
@ -1710,7 +1710,7 @@ class DefaultTemplateFunctions(object):
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
@ -1719,15 +1719,15 @@ class DefaultTemplateFunctions(object):
# 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
@ -1741,7 +1741,7 @@ class DefaultTemplateFunctions(object):
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).

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -21,13 +20,11 @@ that when getLogger(name) instantiates a logger that logger uses
{}-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 @@ def logsafe(val):
example.
"""
# Already Unicode.
if isinstance(val, six.text_type):
if isinstance(val, str):
return val
# Bytestring: needs decoding.
@ -57,7 +54,7 @@ def logsafe(val):
# 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 @@ class StrFormatLogger(Logger):
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 @@ class StrFormatLogger(Logger):
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):

View file

@ -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

View file

@ -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 @@ from functools import wraps
import beets
from beets import logging
import mediafile
import six
PLUGIN_NAMESPACE = 'beetsplug'
@ -52,26 +49,28 @@ class PluginLogFilter(logging.Filter):
"""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.
"""
@ -139,8 +138,8 @@ class BeetsPlugin(object):
log_level = max(logging.DEBUG, base_log_level - 10 * verbosity)
self._log.setLevel(log_level)
if argspec.varkw is None:
kwargs = dict((k, v) for k, v in kwargs.items()
if k in argspec.args)
kwargs = {k: v for k, v in kwargs.items()
if k in argspec.args}
try:
return func(*args, **kwargs)
@ -263,14 +262,14 @@ def load_plugins(names=()):
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:
@ -281,7 +280,7 @@ def load_plugins(names=()):
except Exception:
log.warning(
u'** error loading plugin {}:\n{}',
'** error loading plugin {}:\n{}',
name,
traceback.format_exc(),
)
@ -333,16 +332,16 @@ def queries():
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
@ -350,7 +349,7 @@ def types(model_cls):
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, {})
@ -382,17 +381,15 @@ def candidates(items, artist, album, va_likely, extra_tags=None):
"""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):
@ -485,7 +482,7 @@ def send(event, **arguments):
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)
@ -503,7 +500,7 @@ def feat_tokens(for_artist=True):
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)
)
@ -620,10 +617,9 @@ def apply_item_changes(lib, item, move, pretend, write):
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
@ -705,7 +701,7 @@ class MetadataSourcePlugin(object):
: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:

View file

@ -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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -18,7 +17,6 @@ interface. To invoke the CLI, just call beets.ui.main(). The actual
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 re
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.autotag import mb
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 @@ log.propagate = False # Don't propagate to root handler.
PF_KEY_QUERIES = {
'comp': u'comp:true',
'singleton': u'singleton:true',
'comp': 'comp:true',
'singleton': 'singleton:true',
}
@ -128,11 +124,11 @@ def print_(*strings, **kwargs):
(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.
# On Python 3, sys.stdout expects text strings and uses the
@ -198,12 +194,12 @@ def input_(prompt=None):
# 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')
return resp
@ -249,7 +245,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
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)
@ -257,7 +253,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
# 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()
@ -293,11 +289,11 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
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]))
@ -332,9 +328,9 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
# 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)
@ -373,9 +369,9 @@ def input_yn(prompt, require=False):
"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):
@ -389,24 +385,24 @@ def input_select_objects(prompt, objs, rep, prompt_all=None):
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
@ -418,14 +414,14 @@ def input_select_objects(prompt, objs, rep, prompt_all=None):
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):
@ -433,13 +429,13 @@ def human_seconds(interval):
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]
@ -452,7 +448,7 @@ def human_seconds(interval):
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):
@ -460,7 +456,7 @@ def human_seconds_short(interval):
string.
"""
interval = int(interval)
return u'%i:%02i' % (interval // 60, interval % 60)
return '%i:%02i' % (interval // 60, interval % 60)
# Colorization.
@ -513,7 +509,7 @@ def _colorize(color, text):
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
@ -526,14 +522,14 @@ def colorize(color_name, text):
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)
@ -545,11 +541,11 @@ def _colordiff(a, b, highlight='text_highlight',
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:
@ -587,7 +583,7 @@ def _colordiff(a, b, highlight='text_highlight',
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'):
@ -597,7 +593,7 @@ def colordiff(a, b, highlight='text_highlight'):
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):
@ -622,7 +618,7 @@ def get_replacements():
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
)
)
@ -643,7 +639,7 @@ def term_width():
try:
buf = fcntl.ioctl(0, termios.TIOCGWINSZ, ' ' * 4)
except IOError:
except OSError:
return fallback
try:
height, width = struct.unpack('hh', buf)
@ -671,18 +667,18 @@ def _field_diff(field, old, old_fmt, new, new_fmt):
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):
@ -712,14 +708,14 @@ def show_model_changes(new, old=None, fields=None, always=False):
# 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])
))
@ -728,7 +724,7 @@ def show_model_changes(new, old=None, fields=None, always=False):
if changes or always:
print_(format(old))
if changes:
print_(u'\n'.join(changes))
print_('\n'.join(changes))
return bool(changes)
@ -761,15 +757,15 @@ def show_path_changes(path_changes):
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)
log.info('{0} \n -> {1}', source, 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)
log.info('Source {0} Destination', ' ' * title_pad)
for source, dest in zip(sources, destinations):
pad = max_width - len(source)
log.info(u'{0} {1} -> {2}', source, ' ' * pad, dest)
log.info('{0} {1} -> {2}', source, ' ' * pad, dest)
# Helper functions for option parsing.
@ -797,13 +793,13 @@ def _store_dict(option, opt_str, value, parser):
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:
@ -818,8 +814,9 @@ class CommonOptionsParser(optparse.OptionParser, object):
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
@ -833,7 +830,7 @@ class CommonOptionsParser(optparse.OptionParser, object):
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)
@ -851,7 +848,7 @@ class CommonOptionsParser(optparse.OptionParser, object):
elif value:
value, = decargs([value])
else:
value = u''
value = ''
parser.values.format = value
if target:
@ -878,14 +875,14 @@ class CommonOptionsParser(optparse.OptionParser, object):
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):
@ -905,7 +902,7 @@ class CommonOptionsParser(optparse.OptionParser, object):
"""
kwargs = {}
if target:
if isinstance(target, six.string_types):
if isinstance(target, str):
target = {'item': library.Item,
'album': library.Album}[target]
kwargs['target'] = target
@ -913,7 +910,7 @@ class CommonOptionsParser(optparse.OptionParser, object):
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):
@ -932,10 +929,11 @@ class CommonOptionsParser(optparse.OptionParser, object):
# 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
@ -963,7 +961,7 @@ class Subcommand(object):
@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)
@ -979,13 +977,13 @@ class SubcommandsOptionParser(CommonOptionsParser):
"""
# 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()
@ -1002,7 +1000,7 @@ class SubcommandsOptionParser(CommonOptionsParser):
# 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
@ -1088,7 +1086,7 @@ class SubcommandsOptionParser(CommonOptionsParser):
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
@ -1104,7 +1102,7 @@ def _load_plugins(options, config):
"""
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]
@ -1186,18 +1184,18 @@ def _configure(options):
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
@ -1215,13 +1213,13 @@ def _open_library(config):
)
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
@ -1235,17 +1233,17 @@ def _raw_main(args, lib=None):
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)
@ -1280,7 +1278,7 @@ def main(args=None):
_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)
@ -1292,12 +1290,12 @@ def main(args=None):
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()
@ -1305,11 +1303,11 @@ def main(args=None):
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)

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -15,7 +14,6 @@
"""Miscellaneous utility functions."""
from __future__ import division, absolute_import, print_function
import os
import sys
import errno
@ -31,14 +29,12 @@ import subprocess
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 +56,27 @@ class HumanReadableException(Exception):
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 +90,7 @@ class HumanReadableException(Exception):
"""
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 +98,30 @@ class FilesystemError(HumanReadableException):
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 +183,7 @@ def sorted_walk(path, ignore=(), ignore_hidden=False, logger=None):
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 +197,7 @@ def sorted_walk(path, ignore=(), ignore_hidden=False, logger=None):
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 +222,7 @@ def sorted_walk(path, ignore=(), ignore_hidden=False, logger=None):
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 +240,7 @@ def mkdirall(path):
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 +378,18 @@ def bytestring_path(path):
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 +408,7 @@ def syspath(path, prefix=True):
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 +423,9 @@ def syspath(path, prefix=True):
# 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 +447,7 @@ def remove(path, soft=True):
return
try:
os.remove(path)
except (OSError, IOError) as exc:
except OSError as exc:
raise FilesystemError(exc, 'delete', (path,), traceback.format_exc())
@ -466,10 +462,10 @@ def copy(path, dest, replace=False):
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())
@ -487,7 +483,7 @@ def move(path, dest, replace=False):
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:
@ -497,7 +493,7 @@ def move(path, dest, replace=False):
try:
shutil.copyfile(path, dest)
os.remove(path)
except (OSError, IOError) as exc:
except OSError as exc:
raise FilesystemError(exc, 'move', (path, dest),
traceback.format_exc())
@ -511,18 +507,18 @@ def link(path, dest, replace=False):
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 +532,15 @@ def hardlink(path, dest, replace=False):
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 +564,7 @@ def reflink(path, dest, replace=False, fallback=False):
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 +572,7 @@ def reflink(path, dest, replace=False, fallback=False):
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 +593,23 @@ def unique_path(path):
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,17 +733,15 @@ def py3_path(path):
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):
@ -754,13 +749,13 @@ def as_string(value):
None becomes the empty string. Bytestrings are silently decoded.
"""
if value is None:
return u''
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'):
@ -783,7 +778,7 @@ def plurality(objs):
"""
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]
@ -804,7 +799,7 @@ def cpu_count():
'/usr/sbin/sysctl',
'-n',
'hw.ncpu',
]).stdout)
]).stdout)
except (ValueError, OSError, subprocess.CalledProcessError):
num = 0
else:
@ -948,7 +943,7 @@ def _windows_long_path_name(short_path):
"""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
@ -1009,7 +1004,7 @@ def raw_seconds_short(string):
"""
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)
@ -1047,16 +1042,10 @@ def par_map(transform, items):
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):

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Fabrice Laporte
#
@ -16,26 +15,21 @@
"""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 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 +46,7 @@ def resize_url(url, maxwidth, quality=0):
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 +65,7 @@ def pil_resize(maxwidth, path_in, path_out=None, quality=0, max_filesize=0):
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:
@ -95,7 +89,7 @@ def pil_resize(maxwidth, path_in, path_out=None, quality=0, max_filesize=0):
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
@ -108,14 +102,14 @@ def pil_resize(maxwidth, path_in, path_out=None, quality=0, max_filesize=0):
im.save(
util.py3_path(path_out), quality=lower_qual, optimize=True
)
log.warning(u"PIL Failed to resize file to below {0}B",
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,7 +121,7 @@ def im_resize(maxwidth, path_in, path_out=None, quality=0, max_filesize=0):
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
@ -135,23 +129,23 @@ def im_resize(maxwidth, path_in, path_out=None, quality=0, max_filesize=0):
# with regards to the height.
cmd = ArtResizer.shared.im_convert_cmd + [
util.syspath(path_in, prefix=False),
'-resize', '{0}x>'.format(maxwidth),
'-resize', f'{maxwidth}x>',
]
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 +164,8 @@ def pil_getsize(path_in):
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 +176,17 @@ def im_getsize(path_in):
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 = {
@ -209,7 +203,7 @@ class Shareable(type):
"""
def __init__(cls, name, bases, dict):
super(Shareable, cls).__init__(name, bases, dict)
super().__init__(name, bases, dict)
cls._instance = None
@property
@ -219,7 +213,7 @@ class Shareable(type):
return cls._instance
class ArtResizer(six.with_metaclass(Shareable, object)):
class ArtResizer(metaclass=Shareable):
"""A singleton class that performs image resizes.
"""
@ -227,7 +221,7 @@ class ArtResizer(six.with_metaclass(Shareable, object)):
"""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
@ -323,7 +317,7 @@ def get_im_version():
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 +335,7 @@ def get_pil_version():
"""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

View file

@ -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 @@ asyncore.
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 @@ import collections
# 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 @@ class ThreadException(Exception):
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 @@ def run(root_coro):
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 @@ class SocketClosedError(Exception):
pass
class Listener(object):
class Listener:
"""A socket wrapper object for listening sockets.
"""
def __init__(self, host, port):
@ -420,7 +416,7 @@ class Listener(object):
self.sock.close()
class Connection(object):
class Connection:
"""A socket wrapper object for connected sockets.
"""
def __init__(self, sock, addr):
@ -545,7 +541,7 @@ def spawn(coro):
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 @@ def call(coro):
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)

View file

@ -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

View file

@ -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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -27,31 +26,30 @@ This is sort of like a tiny, horrible degeneration of a real templating
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 @@ def ex_literal(val):
"""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 @@ def ex_call(func, args):
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 @@ def ex_call(func, args):
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):
@ -170,14 +146,15 @@ def compile_func(arg_names, statements, name='_the_func', debug=False):
# 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
@ -194,19 +171,20 @@ class Symbol(object):
"""Compile the variable lookup."""
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
@ -219,15 +197,15 @@ class Call(object):
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()
funcnames = set([self.ident])
funcnames = {self.ident}
arg_exprs = []
for arg in self.args:
@ -238,11 +216,11 @@ class Call(object):
# 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()),
]
)],
@ -255,15 +233,16 @@ class Call(object):
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
@ -271,11 +250,11 @@ class Expression(object):
"""
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
@ -285,7 +264,7 @@ class Expression(object):
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()
@ -301,7 +280,7 @@ class ParseError(Exception):
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
@ -314,6 +293,7 @@ class Parser(object):
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
@ -329,7 +309,7 @@ class Parser(object):
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,)
@ -346,7 +326,7 @@ class Parser(object):
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
)
@ -390,7 +370,7 @@ class Parser(object):
# 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:
@ -412,7 +392,7 @@ class Parser(object):
# 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}``)
@ -567,9 +547,10 @@ def template(fmt):
# 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
@ -618,7 +599,7 @@ class Template(object):
for funcname in funcnames:
args[FUNCTION_PREFIX + funcname] = functions[funcname]
parts = func(**args)
return u''.join(parts)
return ''.join(parts)
return wrapper_func
@ -627,9 +608,9 @@ class Template(object):
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)
@ -638,4 +619,4 @@ if __name__ == '__main__':
'from __main__ import _tmpl, _vars, _funcs',
number=10000)
print(comp_time)
print(u'Speedup:', interp_time / comp_time)
print('Speedup:', interp_time / comp_time)

View file

@ -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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -32,12 +31,10 @@ To do so, pass an iterable of coroutines to the Pipeline constructor
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 @@ class CountedQueue(queue.Queue):
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 @@ class CountedQueue(queue.Queue):
_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 @@ def _allmsgs(obj):
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 @@ class FirstPipelineThread(PipelineThread):
"""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 @@ class MiddlePipelineThread(PipelineThread):
"""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 @@ class LastPipelineThread(PipelineThread):
"""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 @@ class LastPipelineThread(PipelineThread):
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 @@ class Pipeline(object):
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 @@ class Pipeline(object):
for msg in msgs:
yield msg
# Smoke test.
if __name__ == '__main__':
import time
@ -474,14 +479,14 @@ if __name__ == '__main__':
# 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 @@ if __name__ == '__main__':
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 @@ if __name__ == '__main__':
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 @@ if __name__ == '__main__':
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)

View file

@ -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

View file

@ -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

View file

@ -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 @@ def call(args):
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 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):
# 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 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):
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 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):
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 @@ only files which would be processed'
# 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 @@ only files which would be processed'
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 @@ only files which would be processed'
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)

View file

@ -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 @@ class AcousticPlugin(plugins.BeetsPlugin):
}
def __init__(self):
super(AcousticPlugin, self).__init__()
super().__init__()
self.config.add({
'auto': True,
@ -152,11 +150,11 @@ class AcousticPlugin(plugins.BeetsPlugin):
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 @@ class AcousticPlugin(plugins.BeetsPlugin):
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 @@ class AcousticPlugin(plugins.BeetsPlugin):
# 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 @@ class AcousticPlugin(plugins.BeetsPlugin):
# 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 @@ class AcousticPlugin(plugins.BeetsPlugin):
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 @@ class AcousticPlugin(plugins.BeetsPlugin):
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):

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2021, Edgars Supe.
#
@ -16,7 +14,6 @@
"""Adds an album template field for formatted album types."""
from __future__ import division, absolute_import, print_function
from beets.autotag.mb import VARIOUS_ARTISTS_ID
from beets.library import Album
@ -28,7 +25,7 @@ class AlbumTypesPlugin(BeetsPlugin):
def __init__(self):
"""Init AlbumTypesPlugin."""
super(AlbumTypesPlugin, self).__init__()
super().__init__()
self.album_template_fields['atypes'] = self._atypes
self.config.add({
'types': [
@ -54,8 +51,8 @@ class AlbumTypesPlugin(BeetsPlugin):
bracket_l = bracket[0]
bracket_r = bracket[1]
else:
bracket_l = u''
bracket_r = u''
bracket_l = ''
bracket_r = ''
res = ''
albumtypes = item.albumtypes.split('; ')

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2020, Callum Brown.
#
@ -16,7 +14,6 @@
"""An AURA server using Flask."""
from __future__ import division, absolute_import, print_function
from mimetypes import guess_type
import re
@ -215,7 +212,7 @@ class AURADocument:
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 +262,7 @@ class AURADocument:
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 +459,7 @@ class AlbumDocument(AURADocument):
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}]
}
@ -886,7 +883,7 @@ def create_app():
"""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 +929,7 @@ class AURAPlugin(BeetsPlugin):
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 +951,13 @@ class AURAPlugin(BeetsPlugin):
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]

View file

@ -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 shlex
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 CheckerCommandException(Exception):
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 @@ class BadFiles(BeetsPlugin):
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 @@ class BadFiles(BeetsPlugin):
# 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 @@ class BadFiles(BeetsPlugin):
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 @@ class BadFiles(BeetsPlugin):
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]

View file

@ -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 @@ class BareascPlugin(BeetsPlugin):
"""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 @@ class BareascPlugin(BeetsPlugin):
"""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 @@ class BareascPlugin(BeetsPlugin):
# 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)

View file

@ -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 @@ import confuse
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 @@ class BeatportClient(object):
"""
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 @@ class BeatportClient(object):
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"<BeatportRelease: {0} - {1} ({2})>".format(
return "<BeatportRelease: {} - {} ({})>".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 @@ class BeatportRelease(BeatportObject):
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"<BeatportTrack: {0} - {1} ({2})>"
return ("<BeatportTrack: {} - {} ({})>"
.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 @@ class BeatportTrack(BeatportObject):
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 @@ class BeatportPlugin(BeetsPlugin):
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 @@ class BeatportPlugin(BeetsPlugin):
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 @@ class BeatportPlugin(BeetsPlugin):
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 @@ class BeatportPlugin(BeetsPlugin):
"""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 @@ class BeatportPlugin(BeetsPlugin):
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 @@ class BeatportPlugin(BeetsPlugin):
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 @@ class BeatportPlugin(BeetsPlugin):
"""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,

View file

@ -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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -18,7 +17,6 @@ Beets library. Attempts to implement a compatible protocol to allow
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.util import bluelet
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 @@ VOLUME_MAX = 100
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 @@ class BPDError(Exception):
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 @@ def make_bpd_error(s_code, s_message):
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 @@ class BPDIdle(Exception):
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 @@ class BaseServer(object):
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 @@ class BaseServer(object):
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 @@ class BaseServer(object):
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 @@ class BaseServer(object):
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 @@ class BaseServer(object):
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 @@ class BaseServer(object):
"""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 @@ class BaseServer(object):
"""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 @@ class BaseServer(object):
"""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 @@ class BaseServer(object):
"""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 @@ class BaseServer(object):
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 @@ class BaseServer(object):
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 @@ class BaseServer(object):
'a' + 2
class Connection(object):
class Connection:
"""A connection between a client and the server.
"""
def __init__(self, server, sock):
@ -739,12 +736,12 @@ class Connection(object):
"""
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 @@ class Connection(object):
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 @@ class MPDConnection(Connection):
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 @@ class MPDConnection(Connection):
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 @@ class MPDConnection(Connection):
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 @@ class MPDConnection(Connection):
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 @@ class ControlConnection(Connection):
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 @@ class ControlConnection(Connection):
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 @@ class Command(object):
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,7 +978,7 @@ class Command(object):
# 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)
argspec = inspect.getfullargspec(func)
@ -997,7 +994,7 @@ class Command(object):
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
@ -1018,7 +1015,7 @@ class Command(object):
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
@ -1044,7 +1041,7 @@ class Command(object):
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):
@ -1096,46 +1093,46 @@ class Server(BaseServer):
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
@ -1149,7 +1146,7 @@ class Server(BaseServer):
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)
@ -1159,14 +1156,14 @@ class Server(BaseServer):
# 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')
@ -1177,7 +1174,7 @@ class Server(BaseServer):
"""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:
@ -1199,25 +1196,25 @@ class Server(BaseServer):
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
@ -1229,25 +1226,23 @@ class Server(BaseServer):
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)
@ -1264,11 +1259,9 @@ class Server(BaseServer):
# 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
@ -1277,7 +1270,7 @@ class Server(BaseServer):
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')
@ -1294,28 +1287,27 @@ class Server(BaseServer):
# 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'.
@ -1331,47 +1323,47 @@ class Server(BaseServer):
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):
@ -1379,7 +1371,7 @@ class Server(BaseServer):
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
@ -1391,7 +1383,7 @@ class Server(BaseServer):
# 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
@ -1404,13 +1396,13 @@ class Server(BaseServer):
# 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))
@ -1447,9 +1439,9 @@ class Server(BaseServer):
# 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()
@ -1464,7 +1456,7 @@ class Server(BaseServer):
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
@ -1476,44 +1468,44 @@ class Server(BaseServer):
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.
@ -1521,9 +1513,9 @@ class Server(BaseServer):
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):
@ -1534,7 +1526,7 @@ class Server(BaseServer):
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()
@ -1545,7 +1537,7 @@ class Server(BaseServer):
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:
@ -1555,28 +1547,28 @@ class Server(BaseServer):
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
@ -1587,12 +1579,12 @@ class BPDPlugin(BeetsPlugin):
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
@ -1604,13 +1596,13 @@ class BPDPlugin(BeetsPlugin):
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):
@ -1622,7 +1614,7 @@ class BPDPlugin(BeetsPlugin):
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,

View file

@ -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 @@ class QueryError(Exception):
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 @@ class GstPlayer(object):
# 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 @@ class GstPlayer(object):
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)

View file

@ -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 @@ def bpm(max_strokes):
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 @@ class BPMPlugin(BeetsPlugin):
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'])

View file

@ -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 @@ from .beatport import BeatportPlugin
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 @@ class BPSyncPlugin(BeetsPlugin):
"""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 @@ class BPSyncPlugin(BeetsPlugin):
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 @@ class BPSyncPlugin(BeetsPlugin):
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 @@ class BPSyncPlugin(BeetsPlugin):
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 @@ class BPSyncPlugin(BeetsPlugin):
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 @@ class BPSyncPlugin(BeetsPlugin):
# 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()

View file

@ -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 @@ def span_from_str(span_str):
"""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 @@ def span_from_str(span_str):
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 @@ def str2fmt(s):
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 @@ def build_alpha_spans(alpha_spans_str, alpha_regexs):
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 @@ def build_alpha_spans(alpha_spans_str, alpha_regexs):
class BucketPlugin(plugins.BeetsPlugin):
def __init__(self):
super(BucketPlugin, self).__init__()
super().__init__()
self.template_funcs['bucket'] = self._tmpl_bucket
self.config.add({

View file

@ -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 @@ def acoustid_match(log, path):
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 @@ def acoustid_match(log, path):
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 @@ def acoustid_match(log, path):
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 @@ def _all_releases(items):
class AcoustidPlugin(plugins.BeetsPlugin):
def __init__(self):
super(AcoustidPlugin, self).__init__()
super().__init__()
self.config.add({
'auto': True,
@ -198,7 +196,7 @@ class AcoustidPlugin(plugins.BeetsPlugin):
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 @@ class AcoustidPlugin(plugins.BeetsPlugin):
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_items(log, userkey, items, chunksize=64):
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 @@ def submit_items(log, userkey, items, chunksize=64):
}
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 @@ def submit_items(log, userkey, items, chunksize=64):
'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 @@ def fingerprint_item(log, item, write=False):
"""
# 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)

View file

@ -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
@ -38,8 +36,8 @@ _temp_files = [] # Keep track of temporary transcoded files for deletion.
# 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']
@ -67,7 +65,7 @@ def get_format(fmt=None):
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:
@ -80,7 +78,7 @@ def get_format(fmt=None):
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:
@ -109,72 +107,72 @@ def should_transcode(item, fmt):
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
@ -201,7 +199,7 @@ class ConvertPlugin(BeetsPlugin):
quiet = self.config['quiet'].get(bool)
if not quiet and not pretend:
self._log.info(u'Encoding {0}', util.displayable_path(source))
self._log.info('Encoding {0}', util.displayable_path(source))
command = command.decode(arg_encoding(), 'surrogateescape')
source = decode_commandline_path(source)
@ -218,16 +216,16 @@ class ConvertPlugin(BeetsPlugin):
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)
@ -236,13 +234,13 @@ class ConvertPlugin(BeetsPlugin):
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,
@ -279,17 +277,17 @@ class ConvertPlugin(BeetsPlugin):
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)
@ -304,7 +302,7 @@ class ConvertPlugin(BeetsPlugin):
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)
@ -313,7 +311,7 @@ class ConvertPlugin(BeetsPlugin):
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)
@ -344,7 +342,7 @@ class ConvertPlugin(BeetsPlugin):
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)
@ -385,7 +383,7 @@ class ConvertPlugin(BeetsPlugin):
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
@ -399,12 +397,12 @@ class ConvertPlugin(BeetsPlugin):
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:
@ -413,7 +411,7 @@ class ConvertPlugin(BeetsPlugin):
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)
@ -421,7 +419,7 @@ class ConvertPlugin(BeetsPlugin):
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)
@ -435,7 +433,7 @@ class ConvertPlugin(BeetsPlugin):
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)
@ -464,17 +462,17 @@ class ConvertPlugin(BeetsPlugin):
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']:
@ -525,7 +523,7 @@ class ConvertPlugin(BeetsPlugin):
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):

View file

@ -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 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin):
}
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 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin):
"""
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 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin):
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,

View file

@ -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 @@ import confuse
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 @@ import traceback
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 @@ CONNECTION_ERRORS = (ConnectionError, socket.error, http_client.HTTPException,
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
@ -87,7 +85,7 @@ class DiscogsPlugin(BeetsPlugin):
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:
@ -134,24 +132,24 @@ class DiscogsPlugin(BeetsPlugin):
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,18 +183,18 @@ class DiscogsPlugin(BeetsPlugin):
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 []
def album_for_id(self, album_id):
@ -206,7 +204,7 @@ class DiscogsPlugin(BeetsPlugin):
if not self.discogs_client:
return
self._log.debug(u'Searching for release {0}', album_id)
self._log.debug('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
@ -221,14 +219,14 @@ class DiscogsPlugin(BeetsPlugin):
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)
@ -251,7 +249,7 @@ class DiscogsPlugin(BeetsPlugin):
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,7 +259,7 @@ class DiscogsPlugin(BeetsPlugin):
"""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()
@ -271,14 +269,14 @@ class DiscogsPlugin(BeetsPlugin):
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 +295,7 @@ class DiscogsPlugin(BeetsPlugin):
# 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 +384,8 @@ class DiscogsPlugin(BeetsPlugin):
# 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 +423,7 @@ class DiscogsPlugin(BeetsPlugin):
# 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 +482,7 @@ class DiscogsPlugin(BeetsPlugin):
# 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 +505,7 @@ class DiscogsPlugin(BeetsPlugin):
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 +559,7 @@ class DiscogsPlugin(BeetsPlugin):
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 +595,7 @@ class DiscogsPlugin(BeetsPlugin):
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

View file

@ -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 @@ class DuplicatesPlugin(BeetsPlugin):
"""List duplicate tracks or albums
"""
def __init__(self):
super(DuplicatesPlugin, self).__init__()
super().__init__()
self.config.add({
'album': False,
@ -57,54 +54,54 @@ class DuplicatesPlugin(BeetsPlugin):
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 @@ class DuplicatesPlugin(BeetsPlugin):
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 @@ class DuplicatesPlugin(BeetsPlugin):
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 @@ class DuplicatesPlugin(BeetsPlugin):
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 @@ class DuplicatesPlugin(BeetsPlugin):
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 @@ class DuplicatesPlugin(BeetsPlugin):
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 @@ class DuplicatesPlugin(BeetsPlugin):
# 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 @@ class DuplicatesPlugin(BeetsPlugin):
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 @@ class DuplicatesPlugin(BeetsPlugin):
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),

View file

@ -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,6 @@ import subprocess
import yaml
from tempfile import NamedTemporaryFile
import os
import six
import shlex
@ -48,11 +45,11 @@ def edit(filename, log):
"""
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
))
@ -78,17 +75,17 @@ def load(s):
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
@ -144,13 +141,13 @@ def apply_(obj, data):
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.
@ -167,18 +164,18 @@ class EditPlugin(plugins.BeetsPlugin):
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
@ -192,7 +189,7 @@ class EditPlugin(plugins.BeetsPlugin):
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.
@ -262,15 +259,15 @@ class EditPlugin(plugins.BeetsPlugin):
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
@ -285,18 +282,18 @@ class EditPlugin(plugins.BeetsPlugin):
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)
@ -318,7 +315,7 @@ class EditPlugin(plugins.BeetsPlugin):
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}
@ -329,7 +326,7 @@ class EditPlugin(plugins.BeetsPlugin):
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:
@ -344,7 +341,7 @@ class EditPlugin(plugins.BeetsPlugin):
# 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.

View file

@ -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 @@ def _confirm(objs, album):
`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 @@ class EmbedCoverArtPlugin(BeetsPlugin):
"""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 @@ class EmbedCoverArtPlugin(BeetsPlugin):
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 @@ class EmbedCoverArtPlugin(BeetsPlugin):
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 @@ class EmbedCoverArtPlugin(BeetsPlugin):
# 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 @@ class EmbedCoverArtPlugin(BeetsPlugin):
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 @@ class EmbedCoverArtPlugin(BeetsPlugin):
# 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 @@ class EmbedCoverArtPlugin(BeetsPlugin):
"""
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()

View file

@ -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 @@ def get_user(host, port, username):
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 @@ class EmbyUpdate(BeetsPlugin):
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 @@ class EmbyUpdate(BeetsPlugin):
# 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 @@ class EmbyUpdate(BeetsPlugin):
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 @@ class EmbyUpdate(BeetsPlugin):
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.')

View file

@ -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 ExportEncoder(json.JSONEncoder):
class ExportPlugin(BeetsPlugin):
def __init__(self):
super(ExportPlugin, self).__init__()
super().__init__()
self.config.add({
'default_format': 'json',
@ -83,28 +81,28 @@ class ExportPlugin(BeetsPlugin):
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(
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]
@ -133,8 +131,8 @@ class ExportPlugin(BeetsPlugin):
for data_emitter in data_collector(lib, ui.decargs(args)):
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():
@ -152,9 +150,9 @@ class ExportPlugin(BeetsPlugin):
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 +177,8 @@ class ExportFormat(object):
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 +187,8 @@ class JsonFormat(ExportFormat):
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 +199,16 @@ class CSVFormat(ExportFormat):
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

View file

@ -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.artresizer import ArtResizer
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 @@ CONTENT_TYPES = {
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.
"""
@ -56,7 +53,7 @@ class Candidate(object):
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
@ -86,14 +83,14 @@ class Candidate(object):
# 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 +98,7 @@ class Candidate(object):
# 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 +107,27 @@ class Candidate(object):
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,7 +136,7 @@ class Candidate(object):
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
@ -206,7 +203,7 @@ def _logged_get(log, *args, **kwargs):
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 +241,7 @@ class ArtSource(RequestMixin):
class LocalArtSource(ArtSource):
IS_LOCAL = True
LOC_STR = u'local'
LOC_STR = 'local'
def fetch_image(self, candidate, plugin):
pass
@ -252,7 +249,7 @@ class LocalArtSource(ArtSource):
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 +261,7 @@ class RemoteArtSource(ArtSource):
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 +289,16 @@ class RemoteArtSource(ArtSource):
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 +308,15 @@ class RemoteArtSource(ArtSource):
# 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 +324,16 @@ class RemoteArtSource(ArtSource):
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 +344,14 @@ class CoverArtArchive(RemoteArtSource):
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 +388,8 @@ class CoverArtArchive(RemoteArtSource):
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 +402,8 @@ class Amazon(RemoteArtSource):
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 +414,9 @@ class AlbumArtOrg(RemoteArtSource):
# 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 +425,15 @@ class AlbumArtOrg(RemoteArtSource):
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 +453,20 @@ class GoogleImages(RemoteArtSource):
'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 +477,13 @@ class GoogleImages(RemoteArtSource):
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 +496,51 @@ class FanartTV(RemoteArtSource):
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 +549,31 @@ class ITunesStore(RemoteArtSource):
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 +592,7 @@ class ITunesStore(RemoteArtSource):
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 +603,16 @@ class ITunesStore(RemoteArtSource):
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: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SPARQL_QUERY = '''PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpprop: <http://dbpedia.org/property/>
PREFIX owl: <http://dbpedia.org/ontology/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
@ -666,7 +653,7 @@ class Wikipedia(RemoteArtSource):
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 +663,9 @@ class Wikipedia(RemoteArtSource):
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 +680,7 @@ class Wikipedia(RemoteArtSource):
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 +698,7 @@ class Wikipedia(RemoteArtSource):
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 +713,7 @@ class Wikipedia(RemoteArtSource):
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 +732,7 @@ class Wikipedia(RemoteArtSource):
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 +743,12 @@ class Wikipedia(RemoteArtSource):
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 +793,7 @@ class FileSystem(LocalArtSource):
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 +802,14 @@ class FileSystem(LocalArtSource):
# 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 +820,10 @@ class LastFM(RemoteArtSource):
('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 +838,7 @@ class LastFM(RemoteArtSource):
'format': 'json',
})
except requests.RequestException:
self._log.debug(u'lastfm: error receiving response')
self._log.debug('lastfm: error receiving response')
return
try:
@ -878,26 +862,26 @@ class LastFM(RemoteArtSource):
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 +893,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
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,7 +911,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
'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,
@ -949,10 +933,10 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
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'%':
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
@ -974,11 +958,11 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
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 +972,9 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
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 +1016,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
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 +1036,14 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
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 +1067,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
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 +1078,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
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 +1099,8 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
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 +1110,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
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)

View file

@ -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 @@ from beets.importer import SingletonImportTask
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({

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2015, winters jean-marie.
# Copyright 2020, Justin Mayer <https://justinmayer.com>
@ -23,7 +22,6 @@ by default but can be added via the `-e` / `--extravalues` flag. For example:
`beet fish -e genre -e albumartist`
"""
from __future__ import division, absolute_import, print_function
from beets.plugins import BeetsPlugin
from beets import library, ui

View file

@ -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 @@ class FreedesktopPlugin(BeetsPlugin):
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")

View file

@ -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 @@ def apply_matches(d):
# 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 @@ def apply_matches(d):
class FromFilenamePlugin(plugins.BeetsPlugin):
def __init__(self):
super(FromFilenamePlugin, self).__init__()
super().__init__()
self.register_listener('import_task_start', filename_task)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Verrus, <github.com/Verrus/beets-plugin-featInTitle>
#
@ -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 @@ def find_feat_part(artist, albumartist):
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 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
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 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
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 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
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')

View file

@ -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 FuzzyQuery(StringFieldQuery):
class FuzzyPlugin(BeetsPlugin):
def __init__(self):
super(FuzzyPlugin, self).__init__()
super().__init__()
self.config.add({
'prefix': '~',
'threshold': 0.7,

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2017, Tigran Kostandyan.
#
@ -15,7 +14,6 @@
"""Upload files to Google Play Music and list songs in its library."""
from __future__ import absolute_import, division, print_function
import os.path
from beets.plugins import BeetsPlugin
@ -29,7 +27,7 @@ import gmusicapi.clients
class Gmusic(BeetsPlugin):
def __init__(self):
super(Gmusic, self).__init__()
super().__init__()
self.m = Musicmanager()
# OAUTH_FILEPATH was moved in gmusicapi 12.0.0.
@ -39,22 +37,22 @@ class Gmusic(BeetsPlugin):
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,
'auto': False,
'uploader_id': '',
'uploader_name': '',
'device_id': '',
'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')
help='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')
help='list of songs in Google Play Music library')
search.parser.add_option('-t', '--track', dest='track',
action='store_true',
help='Search by track name')
@ -83,17 +81,17 @@ class Gmusic(BeetsPlugin):
items = lib.items(ui.decargs(args))
files = self.getpaths(items)
self.authenticate()
ui.print_(u'Uploading your files...')
ui.print_('Uploading your files...')
self.m.upload(filepaths=files)
ui.print_(u'Your files were successfully added to library')
ui.print_('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._log.info('Uploading files to Google Play Music...', files)
self.m.upload(filepaths=files)
self._log.info(u'Your files were successfully added to your '
self._log.info('Your files were successfully added to your '
+ 'Google Play Music library')
def getpaths(self, items):
@ -117,7 +115,7 @@ class Gmusic(BeetsPlugin):
files = mobile.get_all_songs()
except NotLoggedIn:
ui.print_(
u'Authentication error. Please check your email and password.'
'Authentication error. Please check your email and password.'
)
return
if not args:

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2015, Adrian Sampson.
#
@ -14,7 +13,6 @@
# 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
@ -49,8 +47,8 @@ class CodingFormatter(string.Formatter):
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.
@ -59,8 +57,8 @@ class CodingFormatter(string.Formatter):
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)
@ -70,8 +68,9 @@ class CodingFormatter(string.Formatter):
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': []
@ -102,15 +101,15 @@ class HookPlugin(BeetsPlugin):
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)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Blemjhoo Tezoulbr <baobab@heresiarch.info>.
#
@ -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 @@ def summary(task):
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 @@ class IHatePlugin(BeetsPlugin):
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')

View file

@ -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 @@ from beets.plugins import BeetsPlugin
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 @@ class ImportAddedPlugin(BeetsPlugin):
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 @@ class ImportAddedPlugin(BeetsPlugin):
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 @@ class ImportAddedPlugin(BeetsPlugin):
"""
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 @@ class ImportAddedPlugin(BeetsPlugin):
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 @@ class ImportAddedPlugin(BeetsPlugin):
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)

View file

@ -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 @@ def _write_m3u(m3u_path, items_paths):
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 @@ class ImportFeedsPlugin(BeetsPlugin):
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())

View file

@ -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
@ -110,7 +108,7 @@ def print_data(data, item=None, fmt=None):
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 @@ def print_data(data, item=None, fmt=None):
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 @@ def print_data(data, item=None, fmt=None):
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 @@ def print_data_keys(data, item=None):
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,24 @@ def print_data_keys(data, item=None):
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',
'-s', '--summarize', action='store_true',
help='summarize the tags of all files',
)
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'-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]
@ -204,8 +202,8 @@ class InfoPlugin(BeetsPlugin):
for data_emitter in data_collector(lib, ui.decargs(args)):
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:

View file

@ -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 @@ def _compile_func(body):
"""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 @@ def _compile_func(body):
class InlinePlugin(BeetsPlugin):
def __init__(self):
super(InlinePlugin, self).__init__()
super().__init__()
config.add({
'pathfields': {}, # Legacy name.
@ -64,14 +61,14 @@ class InlinePlugin(BeetsPlugin):
# 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 @@ class InlinePlugin(BeetsPlugin):
"""
# 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

View file

@ -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 @@ import tempfile
class IPFSPlugin(BeetsPlugin):
def __init__(self):
super(IPFSPlugin, self).__init__()
super().__init__()
self.config.add({
'auto': True,
'nocopy': False,
@ -125,7 +123,7 @@ class IPFSPlugin(BeetsPlugin):
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 @@ class IPFSPlugin(BeetsPlugin):
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 @@ class IPFSPlugin(BeetsPlugin):
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 @@ class IPFSPlugin(BeetsPlugin):
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 @@ class IPFSPlugin(BeetsPlugin):
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 @@ class IPFSPlugin(BeetsPlugin):
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)

View file

@ -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 @@ from beets.plugins import BeetsPlugin
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 @@ class KeyFinderPlugin(BeetsPlugin):
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 @@ class KeyFinderPlugin(BeetsPlugin):
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 @@ class KeyFinderPlugin(BeetsPlugin):
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:

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2017, Pauli Kettunen.
#
@ -23,18 +22,16 @@ Put something like the following in your config.yaml to configure:
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 @@ def update_kodi(host, port, user, password):
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 @@ class KodiUpdate(BeetsPlugin):
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 @@ class KodiUpdate(BeetsPlugin):
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')

View file

@ -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 @@ PYLAST_EXCEPTIONS = (
)
REPLACE = {
u'\u2010': '-',
'\u2010': '-',
}
@ -74,7 +70,7 @@ def flatten_tree(elem, path, branches):
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 @@ C14N_TREE = os.path.join(os.path.dirname(__file__), 'genres-tree.yaml')
class LastGenrePlugin(plugins.BeetsPlugin):
def __init__(self):
super(LastGenrePlugin, self).__init__()
super().__init__()
self.config.add({
'whitelist': True,
@ -109,7 +105,7 @@ class LastGenrePlugin(plugins.BeetsPlugin):
'source': 'album',
'force': True,
'auto': True,
'separator': u', ',
'separator': ', ',
'prefer_specific': False,
'title_case': True,
})
@ -134,7 +130,7 @@ class LastGenrePlugin(plugins.BeetsPlugin):
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 @@ class LastGenrePlugin(plugins.BeetsPlugin):
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 @@ class LastGenrePlugin(plugins.BeetsPlugin):
"""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 @@ class LastGenrePlugin(plugins.BeetsPlugin):
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 @@ class LastGenrePlugin(plugins.BeetsPlugin):
# 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 @@ class LastGenrePlugin(plugins.BeetsPlugin):
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 @@ class LastGenrePlugin(plugins.BeetsPlugin):
# 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 @@ class LastGenrePlugin(plugins.BeetsPlugin):
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 @@ class LastGenrePlugin(plugins.BeetsPlugin):
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).

View file

@ -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 @@ API_URL = 'https://ws.audioscrobbler.com/2.0/'
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 @@ class LastImportPlugin(plugins.BeetsPlugin):
}
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 @@ class CustomUser(pylast.User):
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 @@ def import_lastfm(lib, log):
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 @@ def import_lastfm(lib, log):
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 @@ def import_lastfm(lib, log):
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 @@ def process_tracks(lib, tracks, log):
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 @@ def process_tracks(lib, tracks, log):
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 @@ def process_tracks(lib, tracks, log):
# 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 @@ def process_tracks(lib, tracks, log):
# 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 @@ def process_tracks(lib, tracks, log):
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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2019, Jack Wilsdon <jack.wilsdon@gmail.com>
#
@ -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 @@ import sqlite3
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 @@ class LoadExtPlugin(BeetsPlugin):
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)

View file

@ -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 requests
import unicodedata
from unidecode import unidecode
import warnings
import six
from six.moves import urllib
import urllib
try:
import bs4
@ -49,7 +46,7 @@ try:
# 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 @@ COMMENT_RE = re.compile(r'<!--.*-->', re.S)
TAG_RE = re.compile(r'<[^>]*>')
BREAK_RE = re.compile(r'\n?\s*<br([\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 <genindex>`
@ -95,11 +92,11 @@ Artist index:
'''
# 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 @@ epub_tocdup = False
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 @@ def unescape(text):
"""Resolve &#xxx; HTML entities (and some others)."""
if isinstance(text, bytes):
text = text.decode('utf-8', 'ignore')
out = text.replace(u'&nbsp;', u' ')
out = text.replace('&nbsp;', ' ')
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 @@ def extract_text_between(html, start_marker, end_marker):
_, html = html.split(start_marker, 1)
html, _ = html.split(end_marker, 1)
except ValueError:
return u''
return ''
return html
@ -174,7 +171,7 @@ def search_pairs(item):
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 @@ def search_pairs(item):
# 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 @@ else:
return None
class Backend(object):
class Backend:
REQUIRES_BS = False
def __init__(self, config, log):
@ -240,7 +237,7 @@ class Backend(object):
@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 @@ class Backend(object):
'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 @@ class MusiXmatch(Backend):
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 @@ class MusiXmatch(Backend):
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('<p class="mxm-lyrics__content')
@ -336,7 +333,7 @@ class Genius(Backend):
base_url = "https://api.genius.com"
def __init__(self, config, log):
super(Genius, self).__init__(config, log)
super().__init__(config, log)
self.api_key = config['genius_api_key'].as_str()
self.headers = {
'Authorization': "Bearer %s" % self.api_key,
@ -352,7 +349,7 @@ class Genius(Backend):
"""
json = self._search(artist, title)
if not json:
self._log.debug(u'Genius API request returned invalid JSON')
self._log.debug('Genius API request returned invalid JSON')
return None
# find a matching artist in the json
@ -365,7 +362,7 @@ class Genius(Backend):
return None
return self._scrape_lyrics_from_html(html)
self._log.debug(u'Genius failed to find a matching artist for \'{0}\'',
self._log.debug('Genius failed to find a matching artist for \'{0}\'',
artist)
return None
@ -382,7 +379,7 @@ class Genius(Backend):
response = requests.get(
search_url, data=data, headers=self.headers)
except requests.RequestException as exc:
self._log.debug(u'Genius API request failed: {0}', exc)
self._log.debug('Genius API request failed: {0}', exc)
return None
try:
@ -406,7 +403,7 @@ class Genius(Backend):
# likely for easier ad placement
lyrics_div = soup.find("div", class_="lyrics")
if not lyrics_div:
self._log.debug(u'Received unusual song page html')
self._log.debug('Received unusual song page html')
verse_div = soup.find("div",
class_=re.compile("Lyrics__Container"))
if not verse_div:
@ -521,7 +518,7 @@ def _scrape_strip_cruft(html, plain_text_out=False):
html = re.sub(r' +', ' ', html) # Whitespaces collapse.
html = BREAK_RE.sub('\n', html) # <br> eats up surrounding '\n'.
html = re.sub(r'(?s)<(script).*?</\1>', '', 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 @@ class Google(Backend):
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 @@ class Google(Backend):
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 @@ class Google(Backend):
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 @@ class Google(Backend):
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 @@ class Google(Backend):
"""
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 @@ class Google(Backend):
[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 @@ class Google(Backend):
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 @@ class Google(Backend):
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 @@ class LyricsPlugin(plugins.BeetsPlugin):
}
def __init__(self):
super(LyricsPlugin, self).__init__()
super().__init__()
self.import_stages = [self.imported]
self.config.add({
'auto': True,
@ -705,7 +702,7 @@ class LyricsPlugin(plugins.BeetsPlugin):
'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 @@ class LyricsPlugin(plugins.BeetsPlugin):
# 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 @@ class LyricsPlugin(plugins.BeetsPlugin):
# 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 @@ class LyricsPlugin(plugins.BeetsPlugin):
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]
@ -761,9 +758,9 @@ class LyricsPlugin(plugins.BeetsPlugin):
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.'
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 @@ class LyricsPlugin(plugins.BeetsPlugin):
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 @@ class LyricsPlugin(plugins.BeetsPlugin):
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 @@ class LyricsPlugin(plugins.BeetsPlugin):
# 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 @@ class LyricsPlugin(plugins.BeetsPlugin):
"""
# 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 @@ class LyricsPlugin(plugins.BeetsPlugin):
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 @@ class LyricsPlugin(plugins.BeetsPlugin):
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 @@ class LyricsPlugin(plugins.BeetsPlugin):
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 @@ class LyricsPlugin(plugins.BeetsPlugin):
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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright (c) 2011, Jeffrey Aylesworth <mail@jeffrey.red>
#
@ -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 @@ def mb_call(func, *args, **kwargs):
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 @@ def submit_albums(collection_id, release_ids):
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 @@ class MusicBrainzCollectionPlugin(BeetsPlugin):
)
self.config.add({
'auto': False,
'collection': u'',
'collection': '',
'remove': False,
})
if self.config['auto']:
@ -72,18 +70,18 @@ class MusicBrainzCollectionPlugin(BeetsPlugin):
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 @@ class MusicBrainzCollectionPlugin(BeetsPlugin):
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 @@ class MusicBrainzCollectionPlugin(BeetsPlugin):
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 @@ class MusicBrainzCollectionPlugin(BeetsPlugin):
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.')

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson and Diego Moreda.
#
@ -22,8 +21,6 @@ implemented by MusicBrainz yet.
[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 @@ from beetsplug.info import print_data
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 @@ class MBSubmitPlugin(BeetsPlugin):
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):

View file

@ -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 @@ MBID_REGEX = r"(\d|\w){8}-(\d|\w){4}-(\d|\w){4}-(\d|\w){4}-(\d|\w){12}"
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 @@ class MBSyncPlugin(BeetsPlugin):
"""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 @@ class MBSyncPlugin(BeetsPlugin):
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 @@ class MBSyncPlugin(BeetsPlugin):
# 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 @@ class MBSyncPlugin(BeetsPlugin):
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 @@ class MBSyncPlugin(BeetsPlugin):
# 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()

View file

@ -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 importlib import import_module
from confuse import ConfigValueError
from beets import ui
from beets.plugins import BeetsPlugin
import six
METASYNC_MODULE = 'beetsplug.metasync'
@ -36,7 +33,7 @@ SOURCES = {
}
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 @@ class MetaSyncPlugin(BeetsPlugin):
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 @@ class MetaSyncPlugin(BeetsPlugin):
# 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 @@ class MetaSyncPlugin(BeetsPlugin):
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

View file

@ -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 @@ class Amarok(MetaSource):
'amarok_lastplayed': DateType(),
}
query_xml = u'<query version="1.0"> \
query_xml = '<query version="1.0"> \
<filters> \
<and><include field="filename" value=%s /></and> \
</filters> \
</query>'
def __init__(self, config, log):
super(Amarok, self).__init__(config, log)
super().__init__(config, log)
if not dbus:
raise ImportError('failed to import dbus')

View file

@ -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,7 +22,7 @@ import shutil
import tempfile
import plistlib
from six.moves.urllib.parse import urlparse, unquote
from urllib.parse import urlparse, unquote
from time import mktime
from beets import util
@ -63,16 +61,16 @@ def _norm_itunes_path(path):
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'
@ -83,20 +81,20 @@ class Itunes(MetaSource):
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:
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)
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
@ -107,7 +105,7 @@ class Itunes(MetaSource):
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')

View file

@ -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 @@ class MissingPlugin(BeetsPlugin):
}
def __init__(self):
super(MissingPlugin, self).__init__()
super().__init__()
self.config.add({
'count': False,
@ -107,14 +105,14 @@ class MissingPlugin(BeetsPlugin):
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 @@ class MissingPlugin(BeetsPlugin):
# 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 @@ class MissingPlugin(BeetsPlugin):
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 @@ class MissingPlugin(BeetsPlugin):
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 @@ class MissingPlugin(BeetsPlugin):
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

View file

@ -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 @@ def is_url(path):
return path.split('://', 1)[0] in ['http', 'https']
class MPDClientWrapper(object):
class MPDClientWrapper:
def __init__(self, log):
self._log = log
@ -60,13 +55,7 @@ class MPDClientWrapper(object):
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 @@ class MPDClientWrapper(object):
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 @@ class MPDClientWrapper(object):
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 @@ class MPDClientWrapper(object):
"""
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 @@ class MPDClientWrapper(object):
return self.get('idle')
class MPDStats(object):
class MPDStats:
def __init__(self, lib, log):
self.lib = lib
self._log = log
@ -186,7 +175,7 @@ class MPDStats(object):
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 @@ class MPDStats(object):
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 @@ class MPDStats(object):
"""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 @@ class MPDStats(object):
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 @@ class MPDStats(object):
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 @@ class MPDStats(object):
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 MPDStats(object):
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)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -21,19 +20,17 @@ Put something like the following in your config.yaml to configure:
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 BufferedSocket(object):
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 @@ class MPDUpdatePlugin(BeetsPlugin):
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 @@ class MPDUpdatePlugin(BeetsPlugin):
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.')

View file

@ -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 @@ def find_parentwork_info(mb_workid):
class ParentWorkPlugin(BeetsPlugin):
def __init__(self):
super(ParentWorkPlugin, self).__init__()
super().__init__()
self.config.add({
'auto': False,
@ -96,12 +94,12 @@ class ParentWorkPlugin(BeetsPlugin):
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 @@ class ParentWorkPlugin(BeetsPlugin):
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:

View file

@ -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 @@ import os
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 @@ def convert_perm(perm):
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 @@ def assert_permissions(path, permission, log):
"""
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,12 +55,12 @@ def dirs_in_library(library, item):
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)
@ -105,7 +100,7 @@ class Permissions(BeetsPlugin):
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)
@ -115,9 +110,9 @@ class Permissions(BeetsPlugin):
# Change permissions for the directories.
for path in dirs:
# Chaning permissions on the destination directory.
# 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)

View file

@ -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
@ -40,8 +38,8 @@ def play(command_str, selection, paths, open_args, log, item_type='track',
"""
# 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:
@ -52,13 +50,13 @@ def play(command_str, selection, paths, open_args, log, item_type='track',
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,
@ -75,18 +73,18 @@ class PlayPlugin(BeetsPlugin):
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]
@ -125,7 +123,7 @@ class PlayPlugin(BeetsPlugin):
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)
@ -149,7 +147,7 @@ class PlayPlugin(BeetsPlugin):
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, "")
@ -176,10 +174,10 @@ class PlayPlugin(BeetsPlugin):
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

View file

@ -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 @@ class PlaylistQuery(beets.dbcore.Query):
pattern,
os.path.abspath(os.path.join(
config['playlist_dir'].as_filename(),
'{0}.m3u'.format(pattern),
f'{pattern}.m3u',
)),
)
@ -45,7 +43,7 @@ class PlaylistQuery(beets.dbcore.Query):
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 @@ class PlaylistQuery(beets.dbcore.Query):
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 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin):
item_queries = {'playlist': PlaylistQuery}
def __init__(self):
super(PlaylistPlugin, self).__init__()
super().__init__()
self.config.add({
'auto': False,
'playlist_dir': '.',
@ -116,7 +114,7 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin):
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 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin):
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 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin):
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 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin):
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)

View file

@ -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 @@ Put something like the following in your config.yaml to configure:
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 @@ def get_music_section(host, port, token, library_name, secure,
"""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 @@ def update_plex(host, port, token, library_name, secure,
# 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 @@ def get_protocol(secure):
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 @@ class PlexUpdate(BeetsPlugin):
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 @@ class PlexUpdate(BeetsPlugin):
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.')

View file

@ -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 @@ def random_func(lib, opts, args):
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

View file

@ -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 @@ def call(args, **kwargs):
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 @@ class Peak(enum.Enum):
sample = 2
class Backend(object):
class Backend:
"""An abstract class representing engine for calculating RG values.
"""
@ -141,7 +138,7 @@ class FfmpegBackend(Backend):
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 @@ class FfmpegBackend(Backend):
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 @@ class FfmpegBackend(Backend):
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 @@ class FfmpegBackend(Backend):
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 @@ class FfmpegBackend(Backend):
"-map",
"a:0",
"-filter",
"ebur128=peak={0}".format(peak_method),
f"ebur128=peak={peak_method}",
"-f",
"null",
"-",
@ -270,10 +267,10 @@ class FfmpegBackend(Backend):
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 @@ class FfmpegBackend(Backend):
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 @@ class FfmpegBackend(Backend):
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 @@ class FfmpegBackend(Backend):
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 @@ class FfmpegBackend(Backend):
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 @@ class FfmpegBackend(Backend):
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 @@ class CommandBackend(Backend):
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 @@ class CommandBackend(Backend):
# 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 @@ class CommandBackend(Backend):
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 @@ class CommandBackend(Backend):
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 @@ class CommandBackend(Backend):
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 @@ class CommandBackend(Backend):
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 @@ class CommandBackend(Backend):
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 CommandBackend(Backend):
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 @@ class GStreamerBackend(Backend):
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 @@ class GStreamerBackend(Backend):
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 @@ class GStreamerBackend(Backend):
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 @@ class GStreamerBackend(Backend):
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 @@ class GStreamerBackend(Backend):
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 @@ class GStreamerBackend(Backend):
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 @@ class GStreamerBackend(Backend):
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 @@ class AudioToolsBackend(Backend):
"""
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 @@ class AudioToolsBackend(Backend):
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 @@ class AudioToolsBackend(Backend):
"""
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 @@ class AudioToolsBackend(Backend):
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 @@ class AudioToolsBackend(Backend):
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 @@ class AudioToolsBackend(Backend):
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 @@ class AudioToolsBackend(Backend):
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 @@ class ExceptionWatcher(Thread):
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 @@ class ReplayGainPlugin(BeetsPlugin):
}
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 @@ class ReplayGainPlugin(BeetsPlugin):
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 @@ class ReplayGainPlugin(BeetsPlugin):
)
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 @@ class ReplayGainPlugin(BeetsPlugin):
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 @@ class ReplayGainPlugin(BeetsPlugin):
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 @@ class ReplayGainPlugin(BeetsPlugin):
# `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 @@ class ReplayGainPlugin(BeetsPlugin):
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 @@ class ReplayGainPlugin(BeetsPlugin):
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 @@ class ReplayGainPlugin(BeetsPlugin):
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 @@ class ReplayGainPlugin(BeetsPlugin):
# `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 @@ class ReplayGainPlugin(BeetsPlugin):
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 @@ class ReplayGainPlugin(BeetsPlugin):
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]

View file

@ -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 @@ def rewriter(field, rules):
class RewritePlugin(BeetsPlugin):
def __init__(self):
super(RewritePlugin, self).__init__()
super().__init__()
self.config.add({})
@ -55,11 +53,11 @@ class RewritePlugin(BeetsPlugin):
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':

View file

@ -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 @@ _MUTAGEN_FORMATS = {
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 @@ class ScrubPlugin(BeetsPlugin):
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 @@ class ScrubPlugin(BeetsPlugin):
"""
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 @@ class ScrubPlugin(BeetsPlugin):
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 @@ class ScrubPlugin(BeetsPlugin):
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 @@ class ScrubPlugin(BeetsPlugin):
# 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)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Dang Mai <contact@dangmai.net>.
#
@ -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.library import Item, Album, parse_query_string
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 @@ except ImportError:
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 @@ class SmartPlaylistPlugin(BeetsPlugin):
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 @@ class SmartPlaylistPlugin(BeetsPlugin):
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 @@ class SmartPlaylistPlugin(BeetsPlugin):
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 @@ class SmartPlaylistPlugin(BeetsPlugin):
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 @@ class SmartPlaylistPlugin(BeetsPlugin):
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 @@ class SmartPlaylistPlugin(BeetsPlugin):
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 @@ class SmartPlaylistPlugin(BeetsPlugin):
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 @@ class SmartPlaylistPlugin(BeetsPlugin):
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))

View file

@ -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 @@ import soco
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 @@ class SonosUpdate(BeetsPlugin):
"""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')

View file

@ -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 base64
import webbrowser
import collections
import six
import unidecode
import requests
import confuse
@ -53,7 +50,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
}
def __init__(self):
super(SpotifyPlugin, self).__init__()
super().__init__()
self.config.add(
{
'mode': 'list',
@ -81,7 +78,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
# 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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
"""
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
' '.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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
.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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
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'
)

View file

@ -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 filter_to_be_removed(items, keys):
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 @@ def filter_to_be_removed(items, keys):
class SubsonicPlaylistPlugin(BeetsPlugin):
def __init__(self):
super(SubsonicPlaylistPlugin, self).__init__()
super().__init__()
self.config.add(
{
'delete': False,
@ -76,7 +74,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin):
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 @@ class SubsonicPlaylistPlugin(BeetsPlugin):
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]

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
@ -29,7 +28,6 @@ is not supported, use password instead:
pass: password
auth: pass
"""
from __future__ import division, absolute_import, print_function
import hashlib
import random
@ -46,7 +44,7 @@ __author__ = 'https://github.com/maffo999'
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 @@ class SubsonicUpdate(BeetsPlugin):
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 @@ class SubsonicUpdate(BeetsPlugin):
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 @@ class SubsonicUpdate(BeetsPlugin):
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}')

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Blemjhoo Tezoulbr <baobab@heresiarch.info>.
#
@ -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 @@ from beets.plugins import BeetsPlugin
__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 @@ class ThePlugin(BeetsPlugin):
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 @@ class ThePlugin(BeetsPlugin):
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 @@ class ThePlugin(BeetsPlugin):
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 @@ class ThePlugin(BeetsPlugin):
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 ''

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Bruno Cauet
#
@ -19,7 +18,6 @@ This plugin is POSIX-only.
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.plugins import BeetsPlugin
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 @@ LARGE_DIR = util.bytestring_path(os.path.join(BASE_DIR, "large"))
class ThumbnailsPlugin(BeetsPlugin):
def __init__(self):
super(ThumbnailsPlugin, self).__init__()
super().__init__()
self.config.add({
'auto': True,
'force': False,
@ -58,15 +55,15 @@ class ThumbnailsPlugin(BeetsPlugin):
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 @@ class ThumbnailsPlugin(BeetsPlugin):
- 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 @@ class ThumbnailsPlugin(BeetsPlugin):
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 @@ class ThumbnailsPlugin(BeetsPlugin):
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 @@ class ThumbnailsPlugin(BeetsPlugin):
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 @@ class ThumbnailsPlugin(BeetsPlugin):
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 @@ class ThumbnailsPlugin(BeetsPlugin):
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 @@ class ThumbnailsPlugin(BeetsPlugin):
"""
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 @@ class ThumbnailsPlugin(BeetsPlugin):
"""
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 @@ class ThumbnailsPlugin(BeetsPlugin):
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 @@ def write_metadata_pil(file, metadata):
return True
class URIGetter(object):
class URIGetter:
available = False
name = "Abstract base"
@ -269,7 +266,7 @@ class GioURI(URIGetter):
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 @@ class GioURI(URIGetter):
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 @@ class GioURI(URIGetter):
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}"
)

View file

@ -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 @@ class TypesPlugin(BeetsPlugin):
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

Some files were not shown because too many files have changed in this diff Show more