mirror of
https://github.com/beetbox/beets.git
synced 2026-02-12 18:31:48 +01:00
Merge pull request #4030 from arogl/pyupgrade
pyupgrade of beets to Python 3.6
This commit is contained in:
commit
018396ccf7
182 changed files with 4877 additions and 5392 deletions
2
beet
2
beet
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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']:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
42
beets/art.py
42
beets/art.py
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
358
beets/library.py
358
beets/library.py
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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('; ')
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' ', u' ')
|
||||
out = text.replace(' ', ' ')
|
||||
|
||||
def replchar(m):
|
||||
num = m.group(1)
|
||||
return unichar(int(num))
|
||||
out = re.sub(u"&#(\\d+);", replchar, out)
|
||||
out = re.sub("&#(\\d+);", replchar, out)
|
||||
return out
|
||||
|
||||
|
||||
|
|
@ -141,7 +138,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
|
|
|||
|
|
@ -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 ''
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue