mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
Replace format calls with f-strings
This commit is contained in:
parent
6c21482b7a
commit
4a361bd501
50 changed files with 335 additions and 531 deletions
|
|
@ -238,11 +238,7 @@ There are a few coding conventions we use in beets:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
with g.lib.transaction() as tx:
|
with g.lib.transaction() as tx:
|
||||||
rows = tx.query(
|
rows = tx.query("SELECT DISTINCT {field} FROM {model._table} ORDER BY {sort_field}")
|
||||||
"SELECT DISTINCT '{0}' FROM '{1}' ORDER BY '{2}'".format(
|
|
||||||
field, model._table, sort_field
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
To fetch Item objects from the database, use lib.items(…) and supply a query
|
To fetch Item objects from the database, use lib.items(…) and supply a query
|
||||||
as an argument. Resist the urge to write raw SQL for your query. If you must
|
as an argument. Resist the urge to write raw SQL for your query. If you must
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class IncludeLazyConfig(confuse.LazyConfig):
|
||||||
except confuse.NotFoundError:
|
except confuse.NotFoundError:
|
||||||
pass
|
pass
|
||||||
except confuse.ConfigReadError as err:
|
except confuse.ConfigReadError as err:
|
||||||
stderr.write("configuration `import` failed: {}".format(err.reason))
|
stderr.write(f"configuration `import` failed: {err.reason}")
|
||||||
|
|
||||||
|
|
||||||
config = IncludeLazyConfig("beets", __name__)
|
config = IncludeLazyConfig("beets", __name__)
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,9 @@ def string_dist(str1: str | None, str2: str | None) -> float:
|
||||||
# "something, the".
|
# "something, the".
|
||||||
for word in SD_END_WORDS:
|
for word in SD_END_WORDS:
|
||||||
if str1.endswith(", %s" % word):
|
if str1.endswith(", %s" % word):
|
||||||
str1 = "{} {}".format(word, str1[: -len(word) - 2])
|
str1 = f"{word} {str1[: -len(word) - 2]}"
|
||||||
if str2.endswith(", %s" % word):
|
if str2.endswith(", %s" % word):
|
||||||
str2 = "{} {}".format(word, str2[: -len(word) - 2])
|
str2 = f"{word} {str2[: -len(word) - 2]}"
|
||||||
|
|
||||||
# Perform a couple of basic normalizing substitutions.
|
# Perform a couple of basic normalizing substitutions.
|
||||||
for pat, repl in SD_REPLACE:
|
for pat, repl in SD_REPLACE:
|
||||||
|
|
@ -230,7 +230,7 @@ class Distance:
|
||||||
"""Adds all the distance penalties from `dist`."""
|
"""Adds all the distance penalties from `dist`."""
|
||||||
if not isinstance(dist, Distance):
|
if not isinstance(dist, Distance):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"`dist` must be a Distance object, not {}".format(type(dist))
|
f"`dist` must be a Distance object, not {type(dist)}"
|
||||||
)
|
)
|
||||||
for key, penalties in dist._penalties.items():
|
for key, penalties in dist._penalties.items():
|
||||||
self._penalties.setdefault(key, []).extend(penalties)
|
self._penalties.setdefault(key, []).extend(penalties)
|
||||||
|
|
|
||||||
|
|
@ -390,9 +390,9 @@ class Model(ABC, Generic[D]):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "{}({})".format(
|
return (
|
||||||
type(self).__name__,
|
f"{type(self).__name__}"
|
||||||
", ".join(f"{k}={v!r}" for k, v in dict(self).items()),
|
f"({', '.join(f'{k}={v!r}' for k, v in dict(self).items())})"
|
||||||
)
|
)
|
||||||
|
|
||||||
def clear_dirty(self):
|
def clear_dirty(self):
|
||||||
|
|
@ -409,9 +409,9 @@ class Model(ABC, Generic[D]):
|
||||||
exception is raised otherwise.
|
exception is raised otherwise.
|
||||||
"""
|
"""
|
||||||
if not self._db:
|
if not self._db:
|
||||||
raise ValueError("{} has no database".format(type(self).__name__))
|
raise ValueError(f"{type(self).__name__} has no database")
|
||||||
if need_id and not self.id:
|
if need_id and not self.id:
|
||||||
raise ValueError("{} has no id".format(type(self).__name__))
|
raise ValueError(f"{type(self).__name__} has no id")
|
||||||
|
|
||||||
return self._db
|
return self._db
|
||||||
|
|
||||||
|
|
@ -595,9 +595,7 @@ class Model(ABC, Generic[D]):
|
||||||
with db.transaction() as tx:
|
with db.transaction() as tx:
|
||||||
# Main table update.
|
# Main table update.
|
||||||
if assignments:
|
if assignments:
|
||||||
query = "UPDATE {} SET {} WHERE id=?".format(
|
query = f"UPDATE {self._table} SET {','.join(assignments)} WHERE id=?"
|
||||||
self._table, ",".join(assignments)
|
|
||||||
)
|
|
||||||
subvars.append(self.id)
|
subvars.append(self.id)
|
||||||
tx.mutate(query, subvars)
|
tx.mutate(query, subvars)
|
||||||
|
|
||||||
|
|
@ -607,9 +605,9 @@ class Model(ABC, Generic[D]):
|
||||||
self._dirty.remove(key)
|
self._dirty.remove(key)
|
||||||
value = self._type(key).to_sql(value)
|
value = self._type(key).to_sql(value)
|
||||||
tx.mutate(
|
tx.mutate(
|
||||||
"INSERT INTO {} "
|
f"INSERT INTO {self._flex_table} "
|
||||||
"(entity_id, key, value) "
|
"(entity_id, key, value) "
|
||||||
"VALUES (?, ?, ?);".format(self._flex_table),
|
"VALUES (?, ?, ?);",
|
||||||
(self.id, key, value),
|
(self.id, key, value),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1173,9 +1171,7 @@ class Database:
|
||||||
columns = []
|
columns = []
|
||||||
for name, typ in fields.items():
|
for name, typ in fields.items():
|
||||||
columns.append(f"{name} {typ.sql}")
|
columns.append(f"{name} {typ.sql}")
|
||||||
setup_sql = "CREATE TABLE {} ({});\n".format(
|
setup_sql = f"CREATE TABLE {table} ({', '.join(columns)});\n"
|
||||||
table, ", ".join(columns)
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Table exists does not match the field set.
|
# Table exists does not match the field set.
|
||||||
|
|
@ -1183,8 +1179,8 @@ class Database:
|
||||||
for name, typ in fields.items():
|
for name, typ in fields.items():
|
||||||
if name in current_fields:
|
if name in current_fields:
|
||||||
continue
|
continue
|
||||||
setup_sql += "ALTER TABLE {} ADD COLUMN {} {};\n".format(
|
setup_sql += (
|
||||||
table, name, typ.sql
|
f"ALTER TABLE {table} ADD COLUMN {name} {typ.sql};\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.transaction() as tx:
|
with self.transaction() as tx:
|
||||||
|
|
@ -1195,18 +1191,16 @@ class Database:
|
||||||
for the given entity (if they don't exist).
|
for the given entity (if they don't exist).
|
||||||
"""
|
"""
|
||||||
with self.transaction() as tx:
|
with self.transaction() as tx:
|
||||||
tx.script(
|
tx.script(f"""
|
||||||
"""
|
CREATE TABLE IF NOT EXISTS {flex_table} (
|
||||||
CREATE TABLE IF NOT EXISTS {0} (
|
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
entity_id INTEGER,
|
entity_id INTEGER,
|
||||||
key TEXT,
|
key TEXT,
|
||||||
value TEXT,
|
value TEXT,
|
||||||
UNIQUE(entity_id, key) ON CONFLICT REPLACE);
|
UNIQUE(entity_id, key) ON CONFLICT REPLACE);
|
||||||
CREATE INDEX IF NOT EXISTS {0}_by_entity
|
CREATE INDEX IF NOT EXISTS {flex_table}_by_entity
|
||||||
ON {0} (entity_id);
|
ON {flex_table} (entity_id);
|
||||||
""".format(flex_table)
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
# Querying.
|
# Querying.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -475,7 +475,7 @@ class NumericQuery(FieldQuery[str]):
|
||||||
else:
|
else:
|
||||||
if self.rangemin is not None and self.rangemax is not None:
|
if self.rangemin is not None and self.rangemax is not None:
|
||||||
return (
|
return (
|
||||||
"{0} >= ? AND {0} <= ?".format(self.field),
|
f"{self.field} >= ? AND {self.field} <= ?",
|
||||||
(self.rangemin, self.rangemax),
|
(self.rangemin, self.rangemax),
|
||||||
)
|
)
|
||||||
elif self.rangemin is not None:
|
elif self.rangemin is not None:
|
||||||
|
|
@ -800,9 +800,7 @@ class DateInterval:
|
||||||
|
|
||||||
def __init__(self, start: datetime | None, end: datetime | None):
|
def __init__(self, start: datetime | None, end: datetime | None):
|
||||||
if start is not None and end is not None and not start < end:
|
if start is not None and end is not None and not start < end:
|
||||||
raise ValueError(
|
raise ValueError(f"start date {start} is not before end date {end}")
|
||||||
"start date {} is not before end date {}".format(start, end)
|
|
||||||
)
|
|
||||||
self.start = start
|
self.start = start
|
||||||
self.end = end
|
self.end = end
|
||||||
|
|
||||||
|
|
@ -1074,9 +1072,9 @@ class FixedFieldSort(FieldSort):
|
||||||
if self.case_insensitive:
|
if self.case_insensitive:
|
||||||
field = (
|
field = (
|
||||||
"(CASE "
|
"(CASE "
|
||||||
"WHEN TYPEOF({0})='text' THEN LOWER({0}) "
|
f"WHEN TYPEOF({self.field})='text' THEN LOWER({self.field}) "
|
||||||
"WHEN TYPEOF({0})='blob' THEN LOWER({0}) "
|
f"WHEN TYPEOF({self.field})='blob' THEN LOWER({self.field}) "
|
||||||
"ELSE {0} END)".format(self.field)
|
f"ELSE {self.field} END)"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
field = self.field
|
field = self.field
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ class BasePaddedInt(BaseInteger[N]):
|
||||||
self.digits = digits
|
self.digits = digits
|
||||||
|
|
||||||
def format(self, value: int | N) -> str:
|
def format(self, value: int | N) -> str:
|
||||||
return "{0:0{1}d}".format(value or 0, self.digits)
|
return f"{value or 0:0{self.digits}d}"
|
||||||
|
|
||||||
|
|
||||||
class PaddedInt(BasePaddedInt[int]):
|
class PaddedInt(BasePaddedInt[int]):
|
||||||
|
|
@ -219,7 +219,7 @@ class ScaledInt(Integer):
|
||||||
self.suffix = suffix
|
self.suffix = suffix
|
||||||
|
|
||||||
def format(self, value: int) -> str:
|
def format(self, value: int) -> str:
|
||||||
return "{}{}".format((value or 0) // self.unit, self.suffix)
|
return f"{(value or 0) // self.unit}{self.suffix}"
|
||||||
|
|
||||||
|
|
||||||
class Id(NullInteger):
|
class Id(NullInteger):
|
||||||
|
|
@ -249,7 +249,7 @@ class BaseFloat(Type[float, N]):
|
||||||
self.digits = digits
|
self.digits = digits
|
||||||
|
|
||||||
def format(self, value: float | N) -> str:
|
def format(self, value: float | N) -> str:
|
||||||
return "{0:.{1}f}".format(value or 0, self.digits)
|
return f"{value or 0:.{self.digits}f}"
|
||||||
|
|
||||||
|
|
||||||
class Float(BaseFloat[float]):
|
class Float(BaseFloat[float]):
|
||||||
|
|
|
||||||
|
|
@ -341,9 +341,7 @@ def _resolve_duplicates(session: ImportSession, task: ImportTask):
|
||||||
if task.choice_flag in (Action.ASIS, Action.APPLY, Action.RETAG):
|
if task.choice_flag in (Action.ASIS, Action.APPLY, Action.RETAG):
|
||||||
found_duplicates = task.find_duplicates(session.lib)
|
found_duplicates = task.find_duplicates(session.lib)
|
||||||
if found_duplicates:
|
if found_duplicates:
|
||||||
log.debug(
|
log.debug(f"found duplicates: {[o.id for o in found_duplicates]}")
|
||||||
"found duplicates: {}".format([o.id for o in found_duplicates])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the default action to follow from config.
|
# Get the default action to follow from config.
|
||||||
duplicate_action = config["import"]["duplicate_action"].as_choice(
|
duplicate_action = config["import"]["duplicate_action"].as_choice(
|
||||||
|
|
|
||||||
|
|
@ -844,12 +844,9 @@ class Item(LibModel):
|
||||||
# This must not use `with_album=True`, because that might access
|
# This must not use `with_album=True`, because that might access
|
||||||
# the database. When debugging, that is not guaranteed to succeed, and
|
# the database. When debugging, that is not guaranteed to succeed, and
|
||||||
# can even deadlock due to the database lock.
|
# can even deadlock due to the database lock.
|
||||||
return "{}({})".format(
|
return (
|
||||||
type(self).__name__,
|
f"{type(self).__name__}"
|
||||||
", ".join(
|
f"({', '.join(f'{k}={self[k]!r}' for k in self.keys(with_album=False))})"
|
||||||
"{}={!r}".format(k, self[k])
|
|
||||||
for k in self.keys(with_album=False)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def keys(self, computed=False, with_album=True):
|
def keys(self, computed=False, with_album=True):
|
||||||
|
|
|
||||||
|
|
@ -424,9 +424,9 @@ def types(model_cls: type[AnyModel]) -> dict[str, Type]:
|
||||||
for field in plugin_types:
|
for field in plugin_types:
|
||||||
if field in types and plugin_types[field] != types[field]:
|
if field in types and plugin_types[field] != types[field]:
|
||||||
raise PluginConflictError(
|
raise PluginConflictError(
|
||||||
"Plugin {} defines flexible field {} "
|
f"Plugin {plugin.name} defines flexible field {field} "
|
||||||
"which has already been defined with "
|
"which has already been defined with "
|
||||||
"another type.".format(plugin.name, field)
|
"another type."
|
||||||
)
|
)
|
||||||
types.update(plugin_types)
|
types.update(plugin_types)
|
||||||
return types
|
return types
|
||||||
|
|
@ -560,8 +560,8 @@ def feat_tokens(for_artist: bool = True) -> str:
|
||||||
feat_words = ["ft", "featuring", "feat", "feat.", "ft."]
|
feat_words = ["ft", "featuring", "feat", "feat.", "ft."]
|
||||||
if for_artist:
|
if for_artist:
|
||||||
feat_words += ["with", "vs", "and", "con", "&"]
|
feat_words += ["with", "vs", "and", "con", "&"]
|
||||||
return r"(?<=[\s(\[])(?:{})(?=\s)".format(
|
return (
|
||||||
"|".join(re.escape(x) for x in feat_words)
|
rf"(?<=[\s(\[])(?:{'|'.join(re.escape(x) for x in feat_words)})(?=\s)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -726,7 +726,7 @@ def get_replacements():
|
||||||
replacements.append((re.compile(pattern), repl))
|
replacements.append((re.compile(pattern), repl))
|
||||||
except re.error:
|
except re.error:
|
||||||
raise UserError(
|
raise UserError(
|
||||||
"malformed regular expression in replace: {}".format(pattern)
|
f"malformed regular expression in replace: {pattern}"
|
||||||
)
|
)
|
||||||
return replacements
|
return replacements
|
||||||
|
|
||||||
|
|
@ -1163,7 +1163,7 @@ def show_model_changes(new, old=None, fields=None, always=False):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
changes.append(
|
changes.append(
|
||||||
" {}: {}".format(field, colorize("text_highlight", new_fmt[field]))
|
f" {field}: {colorize('text_highlight', new_fmt[field])}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Print changes.
|
# Print changes.
|
||||||
|
|
@ -1204,22 +1204,16 @@ def show_path_changes(path_changes):
|
||||||
# Print every change over two lines
|
# Print every change over two lines
|
||||||
for source, dest in zip(sources, destinations):
|
for source, dest in zip(sources, destinations):
|
||||||
color_source, color_dest = colordiff(source, dest)
|
color_source, color_dest = colordiff(source, dest)
|
||||||
print_("{0} \n -> {1}".format(color_source, color_dest))
|
print_(f"{color_source} \n -> {color_dest}")
|
||||||
else:
|
else:
|
||||||
# Print every change on a single line, and add a header
|
# Print every change on a single line, and add a header
|
||||||
title_pad = max_width - len("Source ") + len(" -> ")
|
title_pad = max_width - len("Source ") + len(" -> ")
|
||||||
|
|
||||||
print_("Source {0} Destination".format(" " * title_pad))
|
print_(f"Source {' ' * title_pad} Destination")
|
||||||
for source, dest in zip(sources, destinations):
|
for source, dest in zip(sources, destinations):
|
||||||
pad = max_width - len(source)
|
pad = max_width - len(source)
|
||||||
color_source, color_dest = colordiff(source, dest)
|
color_source, color_dest = colordiff(source, dest)
|
||||||
print_(
|
print_(f"{color_source} {' ' * pad} -> {color_dest}")
|
||||||
"{0} {1} -> {2}".format(
|
|
||||||
color_source,
|
|
||||||
" " * pad,
|
|
||||||
color_dest,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Helper functions for option parsing.
|
# Helper functions for option parsing.
|
||||||
|
|
@ -1245,9 +1239,7 @@ def _store_dict(option, opt_str, value, parser):
|
||||||
raise ValueError
|
raise ValueError
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise UserError(
|
raise UserError(
|
||||||
"supplied argument `{}' is not of the form `key=value'".format(
|
f"supplied argument `{value}' is not of the form `key=value'"
|
||||||
value
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
option_values[key] = value
|
option_values[key] = value
|
||||||
|
|
@ -1426,8 +1418,8 @@ class Subcommand:
|
||||||
@root_parser.setter
|
@root_parser.setter
|
||||||
def root_parser(self, root_parser):
|
def root_parser(self, root_parser):
|
||||||
self._root_parser = root_parser
|
self._root_parser = root_parser
|
||||||
self.parser.prog = "{} {}".format(
|
self.parser.prog = (
|
||||||
as_string(root_parser.get_prog_name()), self.name
|
f"{as_string(root_parser.get_prog_name())} {self.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1637,10 +1629,8 @@ def _ensure_db_directory_exists(path):
|
||||||
newpath = os.path.dirname(path)
|
newpath = os.path.dirname(path)
|
||||||
if not os.path.isdir(newpath):
|
if not os.path.isdir(newpath):
|
||||||
if input_yn(
|
if input_yn(
|
||||||
"The database directory {} does not \
|
f"The database directory {util.displayable_path(newpath)} does not \
|
||||||
exist. Create it (Y/n)?".format(
|
exist. Create it (Y/n)?"
|
||||||
util.displayable_path(newpath)
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
os.makedirs(newpath)
|
os.makedirs(newpath)
|
||||||
|
|
||||||
|
|
@ -1660,9 +1650,8 @@ def _open_library(config: confuse.LazyConfig) -> library.Library:
|
||||||
except (sqlite3.OperationalError, sqlite3.DatabaseError) as db_error:
|
except (sqlite3.OperationalError, sqlite3.DatabaseError) as db_error:
|
||||||
log.debug("{}", traceback.format_exc())
|
log.debug("{}", traceback.format_exc())
|
||||||
raise UserError(
|
raise UserError(
|
||||||
"database file {} cannot not be opened: {}".format(
|
f"database file {util.displayable_path(dbpath)} cannot not be"
|
||||||
util.displayable_path(dbpath), db_error
|
f" opened: {db_error}"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
log.debug(
|
log.debug(
|
||||||
"library database: {0}\nlibrary directory: {1}",
|
"library database: {0}\nlibrary directory: {1}",
|
||||||
|
|
|
||||||
|
|
@ -112,15 +112,11 @@ def _parse_logfiles(logfiles):
|
||||||
yield from _paths_from_logfile(syspath(normpath(logfile)))
|
yield from _paths_from_logfile(syspath(normpath(logfile)))
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"malformed logfile {}: {}".format(
|
f"malformed logfile {util.displayable_path(logfile)}: {err}"
|
||||||
util.displayable_path(logfile), str(err)
|
|
||||||
)
|
|
||||||
) from err
|
) from err
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"unreadable logfile {}: {}".format(
|
f"unreadable logfile {util.displayable_path(logfile)}: {err}"
|
||||||
util.displayable_path(logfile), str(err)
|
|
||||||
)
|
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -213,10 +209,10 @@ def get_singleton_disambig_fields(info: hooks.TrackInfo) -> Sequence[str]:
|
||||||
out = []
|
out = []
|
||||||
chosen_fields = config["match"]["singleton_disambig_fields"].as_str_seq()
|
chosen_fields = config["match"]["singleton_disambig_fields"].as_str_seq()
|
||||||
calculated_values = {
|
calculated_values = {
|
||||||
"index": "Index {}".format(str(info.index)),
|
"index": f"Index {info.index}",
|
||||||
"track_alt": "Track {}".format(info.track_alt),
|
"track_alt": f"Track {info.track_alt}",
|
||||||
"album": (
|
"album": (
|
||||||
"[{}]".format(info.album)
|
f"[{info.album}]"
|
||||||
if (
|
if (
|
||||||
config["import"]["singleton_album_disambig"].get()
|
config["import"]["singleton_album_disambig"].get()
|
||||||
and info.get("album")
|
and info.get("album")
|
||||||
|
|
@ -242,7 +238,7 @@ def get_album_disambig_fields(info: hooks.AlbumInfo) -> Sequence[str]:
|
||||||
chosen_fields = config["match"]["album_disambig_fields"].as_str_seq()
|
chosen_fields = config["match"]["album_disambig_fields"].as_str_seq()
|
||||||
calculated_values = {
|
calculated_values = {
|
||||||
"media": (
|
"media": (
|
||||||
"{}x{}".format(info.mediums, info.media)
|
f"{info.mediums}x{info.media}"
|
||||||
if (info.mediums and info.mediums > 1)
|
if (info.mediums and info.mediums > 1)
|
||||||
else info.media
|
else info.media
|
||||||
),
|
),
|
||||||
|
|
@ -277,7 +273,7 @@ def dist_string(dist):
|
||||||
"""Formats a distance (a float) as a colorized similarity percentage
|
"""Formats a distance (a float) as a colorized similarity percentage
|
||||||
string.
|
string.
|
||||||
"""
|
"""
|
||||||
string = "{:.1f}%".format(((1 - dist) * 100))
|
string = f"{(1 - dist) * 100:.1f}%"
|
||||||
return dist_colorize(string, dist)
|
return dist_colorize(string, dist)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -295,7 +291,7 @@ def penalty_string(distance, limit=None):
|
||||||
if limit and len(penalties) > limit:
|
if limit and len(penalties) > limit:
|
||||||
penalties = penalties[:limit] + ["..."]
|
penalties = penalties[:limit] + ["..."]
|
||||||
# Prefix penalty string with U+2260: Not Equal To
|
# Prefix penalty string with U+2260: Not Equal To
|
||||||
penalty_string = "\u2260 {}".format(", ".join(penalties))
|
penalty_string = f"\u2260 {', '.join(penalties)}"
|
||||||
return ui.colorize("changed", penalty_string)
|
return ui.colorize("changed", penalty_string)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -697,11 +693,9 @@ class AlbumChange(ChangeRepresentation):
|
||||||
# Missing and unmatched tracks.
|
# Missing and unmatched tracks.
|
||||||
if self.match.extra_tracks:
|
if self.match.extra_tracks:
|
||||||
print_(
|
print_(
|
||||||
"Missing tracks ({0}/{1} - {2:.1%}):".format(
|
"Missing tracks"
|
||||||
len(self.match.extra_tracks),
|
f" ({len(self.match.extra_tracks)}/{len(self.match.info.tracks)} -"
|
||||||
len(self.match.info.tracks),
|
f" {len(self.match.extra_tracks) / len(self.match.info.tracks):.1%}):"
|
||||||
len(self.match.extra_tracks) / len(self.match.info.tracks),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for track_info in self.match.extra_tracks:
|
for track_info in self.match.extra_tracks:
|
||||||
line = f" ! {track_info.title} (#{self.format_index(track_info)})"
|
line = f" ! {track_info.title} (#{self.format_index(track_info)})"
|
||||||
|
|
@ -711,9 +705,9 @@ class AlbumChange(ChangeRepresentation):
|
||||||
if self.match.extra_items:
|
if self.match.extra_items:
|
||||||
print_(f"Unmatched tracks ({len(self.match.extra_items)}):")
|
print_(f"Unmatched tracks ({len(self.match.extra_items)}):")
|
||||||
for item in self.match.extra_items:
|
for item in self.match.extra_items:
|
||||||
line = " ! {} (#{})".format(item.title, self.format_index(item))
|
line = f" ! {item.title} (#{self.format_index(item)})"
|
||||||
if item.length:
|
if item.length:
|
||||||
line += " ({})".format(human_seconds_short(item.length))
|
line += f" ({human_seconds_short(item.length)})"
|
||||||
print_(ui.colorize("text_warning", line))
|
print_(ui.colorize("text_warning", line))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -769,7 +763,7 @@ def summarize_items(items, singleton):
|
||||||
"""
|
"""
|
||||||
summary_parts = []
|
summary_parts = []
|
||||||
if not singleton:
|
if not singleton:
|
||||||
summary_parts.append("{} items".format(len(items)))
|
summary_parts.append(f"{len(items)} items")
|
||||||
|
|
||||||
format_counts = {}
|
format_counts = {}
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
@ -789,10 +783,11 @@ def summarize_items(items, singleton):
|
||||||
average_bitrate = sum([item.bitrate for item in items]) / len(items)
|
average_bitrate = sum([item.bitrate for item in items]) / len(items)
|
||||||
total_duration = sum([item.length for item in items])
|
total_duration = sum([item.length for item in items])
|
||||||
total_filesize = sum([item.filesize for item in items])
|
total_filesize = sum([item.filesize for item in items])
|
||||||
summary_parts.append("{}kbps".format(int(average_bitrate / 1000)))
|
summary_parts.append(f"{int(average_bitrate / 1000)}kbps")
|
||||||
if items[0].format == "FLAC":
|
if items[0].format == "FLAC":
|
||||||
sample_bits = "{}kHz/{} bit".format(
|
sample_bits = (
|
||||||
round(int(items[0].samplerate) / 1000, 1), items[0].bitdepth
|
f"{round(int(items[0].samplerate) / 1000, 1)}kHz"
|
||||||
|
f"/{items[0].bitdepth} bit"
|
||||||
)
|
)
|
||||||
summary_parts.append(sample_bits)
|
summary_parts.append(sample_bits)
|
||||||
summary_parts.append(human_seconds_short(total_duration))
|
summary_parts.append(human_seconds_short(total_duration))
|
||||||
|
|
@ -885,7 +880,7 @@ def choose_candidate(
|
||||||
if singleton:
|
if singleton:
|
||||||
print_("No matching recordings found.")
|
print_("No matching recordings found.")
|
||||||
else:
|
else:
|
||||||
print_("No matching release found for {} tracks.".format(itemcount))
|
print_(f"No matching release found for {itemcount} tracks.")
|
||||||
print_(
|
print_(
|
||||||
"For help, see: "
|
"For help, see: "
|
||||||
"https://beets.readthedocs.org/en/latest/faq.html#nomatch"
|
"https://beets.readthedocs.org/en/latest/faq.html#nomatch"
|
||||||
|
|
@ -910,23 +905,21 @@ def choose_candidate(
|
||||||
# Display list of candidates.
|
# Display list of candidates.
|
||||||
print_("")
|
print_("")
|
||||||
print_(
|
print_(
|
||||||
'Finding tags for {} "{} - {}".'.format(
|
f"Finding tags for {'track' if singleton else 'album'}"
|
||||||
"track" if singleton else "album",
|
f'"{item.artist if singleton else cur_artist} -'
|
||||||
item.artist if singleton else cur_artist,
|
f' {item.title if singleton else cur_album}".'
|
||||||
item.title if singleton else cur_album,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
print_(ui.indent(2) + "Candidates:")
|
print_(ui.indent(2) + "Candidates:")
|
||||||
for i, match in enumerate(candidates):
|
for i, match in enumerate(candidates):
|
||||||
# Index, metadata, and distance.
|
# Index, metadata, and distance.
|
||||||
index0 = "{0}.".format(i + 1)
|
index0 = f"{i + 1}."
|
||||||
index = dist_colorize(index0, match.distance)
|
index = dist_colorize(index0, match.distance)
|
||||||
dist = "({:.1f}%)".format((1 - match.distance) * 100)
|
dist = f"({(1 - match.distance) * 100:.1f}%)"
|
||||||
distance = dist_colorize(dist, match.distance)
|
distance = dist_colorize(dist, match.distance)
|
||||||
metadata = "{0} - {1}".format(
|
metadata = (
|
||||||
match.info.artist,
|
f"{match.info.artist} -"
|
||||||
match.info.title if singleton else match.info.album,
|
f" {match.info.title if singleton else match.info.album}"
|
||||||
)
|
)
|
||||||
if i == 0:
|
if i == 0:
|
||||||
metadata = dist_colorize(metadata, match.distance)
|
metadata = dist_colorize(metadata, match.distance)
|
||||||
|
|
@ -1015,7 +1008,7 @@ def manual_id(session, task):
|
||||||
|
|
||||||
Input an ID, either for an album ("release") or a track ("recording").
|
Input an ID, either for an album ("release") or a track ("recording").
|
||||||
"""
|
"""
|
||||||
prompt = "Enter {} ID:".format("release" if task.is_album else "recording")
|
prompt = f"Enter {'release' if task.is_album else 'recording'} ID:"
|
||||||
search_id = input_(prompt).strip()
|
search_id = input_(prompt).strip()
|
||||||
|
|
||||||
if task.is_album:
|
if task.is_album:
|
||||||
|
|
@ -1043,7 +1036,7 @@ class TerminalImportSession(importer.ImportSession):
|
||||||
|
|
||||||
path_str0 = displayable_path(task.paths, "\n")
|
path_str0 = displayable_path(task.paths, "\n")
|
||||||
path_str = ui.colorize("import_path", path_str0)
|
path_str = ui.colorize("import_path", path_str0)
|
||||||
items_str0 = "({} items)".format(len(task.items))
|
items_str0 = f"({len(task.items)} items)"
|
||||||
items_str = ui.colorize("import_path_items", items_str0)
|
items_str = ui.colorize("import_path_items", items_str0)
|
||||||
print_(" ".join([path_str, items_str]))
|
print_(" ".join([path_str, items_str]))
|
||||||
|
|
||||||
|
|
@ -1217,8 +1210,8 @@ class TerminalImportSession(importer.ImportSession):
|
||||||
|
|
||||||
def should_resume(self, path):
|
def should_resume(self, path):
|
||||||
return ui.input_yn(
|
return ui.input_yn(
|
||||||
"Import of the directory:\n{}\n"
|
f"Import of the directory:\n{displayable_path(path)}\n"
|
||||||
"was interrupted. Resume (Y/n)?".format(displayable_path(path))
|
"was interrupted. Resume (Y/n)?"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_choices(self, task):
|
def _get_choices(self, task):
|
||||||
|
|
@ -1317,7 +1310,8 @@ def import_files(lib, paths: list[bytes], query):
|
||||||
loghandler = logging.FileHandler(logpath, encoding="utf-8")
|
loghandler = logging.FileHandler(logpath, encoding="utf-8")
|
||||||
except OSError:
|
except OSError:
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
f"Could not open log file for writing: {displayable_path(logpath)}"
|
"Could not open log file for writing:"
|
||||||
|
f" {displayable_path(logpath)}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
loghandler = None
|
loghandler = None
|
||||||
|
|
@ -1362,9 +1356,7 @@ def import_func(lib, opts, args: list[str]):
|
||||||
for path in byte_paths:
|
for path in byte_paths:
|
||||||
if not os.path.exists(syspath(normpath(path))):
|
if not os.path.exists(syspath(normpath(path))):
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"no such file or directory: {}".format(
|
f"no such file or directory: {displayable_path(path)}"
|
||||||
displayable_path(path)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check the directories from the logfiles, but don't throw an error in
|
# Check the directories from the logfiles, but don't throw an error in
|
||||||
|
|
@ -1374,9 +1366,7 @@ def import_func(lib, opts, args: list[str]):
|
||||||
for path in paths_from_logfiles:
|
for path in paths_from_logfiles:
|
||||||
if not os.path.exists(syspath(normpath(path))):
|
if not os.path.exists(syspath(normpath(path))):
|
||||||
log.warning(
|
log.warning(
|
||||||
"No such file or directory: {}".format(
|
f"No such file or directory: {displayable_path(path)}"
|
||||||
displayable_path(path)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -1808,7 +1798,7 @@ def remove_items(lib, query, album, delete, force):
|
||||||
if not force:
|
if not force:
|
||||||
# Prepare confirmation with user.
|
# Prepare confirmation with user.
|
||||||
album_str = (
|
album_str = (
|
||||||
" in {} album{}".format(len(albums), "s" if len(albums) > 1 else "")
|
f" in {len(albums)} album{'s' if len(albums) > 1 else ''}"
|
||||||
if album
|
if album
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
|
@ -1816,14 +1806,17 @@ def remove_items(lib, query, album, delete, force):
|
||||||
if delete:
|
if delete:
|
||||||
fmt = "$path - $title"
|
fmt = "$path - $title"
|
||||||
prompt = "Really DELETE"
|
prompt = "Really DELETE"
|
||||||
prompt_all = "Really DELETE {} file{}{}".format(
|
prompt_all = (
|
||||||
len(items), "s" if len(items) > 1 else "", album_str
|
"Really DELETE"
|
||||||
|
f" {len(items)} file{'s' if len(items) > 1 else ''}{album_str}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
fmt = ""
|
fmt = ""
|
||||||
prompt = "Really remove from the library?"
|
prompt = "Really remove from the library?"
|
||||||
prompt_all = "Really remove {} item{}{} from the library?".format(
|
prompt_all = (
|
||||||
len(items), "s" if len(items) > 1 else "", album_str
|
"Really remove"
|
||||||
|
f" {len(items)} item{'s' if len(items) > 1 else ''}{album_str}"
|
||||||
|
" from the library?"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Helpers for printing affected items
|
# Helpers for printing affected items
|
||||||
|
|
@ -1906,23 +1899,13 @@ def show_stats(lib, query, exact):
|
||||||
if exact:
|
if exact:
|
||||||
size_str += f" ({total_size} bytes)"
|
size_str += f" ({total_size} bytes)"
|
||||||
|
|
||||||
print_(
|
print_(f"""Tracks: {total_items}
|
||||||
"""Tracks: {}
|
Total time: {human_seconds(total_time)}
|
||||||
Total time: {}{}
|
{f" ({total_time:.2f} seconds)" if exact else ""}
|
||||||
{}: {}
|
{"Total size" if exact else "Approximate total size"}: {size_str}
|
||||||
Artists: {}
|
Artists: {len(artists)}
|
||||||
Albums: {}
|
Albums: {len(albums)}
|
||||||
Album artists: {}""".format(
|
Album artists: {len(album_artists)}""")
|
||||||
total_items,
|
|
||||||
human_seconds(total_time),
|
|
||||||
f" ({total_time:.2f} seconds)" if exact else "",
|
|
||||||
"Total size" if exact else "Approximate total size",
|
|
||||||
size_str,
|
|
||||||
len(artists),
|
|
||||||
len(albums),
|
|
||||||
len(album_artists),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def stats_func(lib, opts, args):
|
def stats_func(lib, opts, args):
|
||||||
|
|
@ -1977,7 +1960,7 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm, inherit):
|
||||||
|
|
||||||
# Apply changes *temporarily*, preview them, and collect modified
|
# Apply changes *temporarily*, preview them, and collect modified
|
||||||
# objects.
|
# objects.
|
||||||
print_("Modifying {} {}s.".format(len(objs), "album" if album else "item"))
|
print_(f"Modifying {len(objs)} {'album' if album else 'item'}s.")
|
||||||
changed = []
|
changed = []
|
||||||
templates = {
|
templates = {
|
||||||
key: functemplate.template(value) for key, value in mods.items()
|
key: functemplate.template(value) for key, value in mods.items()
|
||||||
|
|
@ -2213,9 +2196,7 @@ def move_func(lib, opts, args):
|
||||||
if dest is not None:
|
if dest is not None:
|
||||||
dest = normpath(dest)
|
dest = normpath(dest)
|
||||||
if not os.path.isdir(syspath(dest)):
|
if not os.path.isdir(syspath(dest)):
|
||||||
raise ui.UserError(
|
raise ui.UserError(f"no such directory: {displayable_path(dest)}")
|
||||||
"no such directory: {}".format(displayable_path(dest))
|
|
||||||
)
|
|
||||||
|
|
||||||
move_items(
|
move_items(
|
||||||
lib,
|
lib,
|
||||||
|
|
@ -2486,7 +2467,7 @@ def completion_script(commands):
|
||||||
# Command aliases
|
# Command aliases
|
||||||
yield " local aliases='%s'\n" % " ".join(aliases.keys())
|
yield " local aliases='%s'\n" % " ".join(aliases.keys())
|
||||||
for alias, cmd in aliases.items():
|
for alias, cmd in aliases.items():
|
||||||
yield " local alias__{}={}\n".format(alias.replace("-", "_"), cmd)
|
yield f" local alias__{alias.replace('-', '_')}={cmd}\n"
|
||||||
yield "\n"
|
yield "\n"
|
||||||
|
|
||||||
# Fields
|
# Fields
|
||||||
|
|
@ -2502,8 +2483,9 @@ def completion_script(commands):
|
||||||
for option_type, option_list in opts.items():
|
for option_type, option_list in opts.items():
|
||||||
if option_list:
|
if option_list:
|
||||||
option_list = " ".join(option_list)
|
option_list = " ".join(option_list)
|
||||||
yield " local {}__{}='{}'\n".format(
|
yield (
|
||||||
option_type, cmd.replace("-", "_"), option_list
|
" local"
|
||||||
|
f" {option_type}__{cmd.replace('-', '_')}='{option_list}'\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
yield " _beet_dispatch\n"
|
yield " _beet_dispatch\n"
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ class HumanReadableError(Exception):
|
||||||
elif hasattr(self.reason, "strerror"): # i.e., EnvironmentError
|
elif hasattr(self.reason, "strerror"): # i.e., EnvironmentError
|
||||||
return self.reason.strerror
|
return self.reason.strerror
|
||||||
else:
|
else:
|
||||||
return '"{}"'.format(str(self.reason))
|
return f'"{self.reason}"'
|
||||||
|
|
||||||
def get_message(self):
|
def get_message(self):
|
||||||
"""Create the human-readable description of the error, sans
|
"""Create the human-readable description of the error, sans
|
||||||
|
|
@ -142,18 +142,16 @@ class FilesystemError(HumanReadableError):
|
||||||
def get_message(self):
|
def get_message(self):
|
||||||
# Use a nicer English phrasing for some specific verbs.
|
# Use a nicer English phrasing for some specific verbs.
|
||||||
if self.verb in ("move", "copy", "rename"):
|
if self.verb in ("move", "copy", "rename"):
|
||||||
clause = "while {} {} to {}".format(
|
clause = (
|
||||||
self._gerund(),
|
f"while {self._gerund()} {displayable_path(self.paths[0])} to"
|
||||||
displayable_path(self.paths[0]),
|
f" {displayable_path(self.paths[1])}"
|
||||||
displayable_path(self.paths[1]),
|
|
||||||
)
|
)
|
||||||
elif self.verb in ("delete", "write", "create", "read"):
|
elif self.verb in ("delete", "write", "create", "read"):
|
||||||
clause = "while {} {}".format(
|
clause = f"while {self._gerund()} {displayable_path(self.paths[0])}"
|
||||||
self._gerund(), displayable_path(self.paths[0])
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
clause = "during {} of paths {}".format(
|
clause = (
|
||||||
self.verb, ", ".join(displayable_path(p) for p in self.paths)
|
f"during {self.verb} of paths"
|
||||||
|
f" {', '.join(displayable_path(p) for p in self.paths)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return f"{self._reasonstr()} {clause}"
|
return f"{self._reasonstr()} {clause}"
|
||||||
|
|
@ -226,9 +224,8 @@ def sorted_walk(
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
if logger:
|
if logger:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"could not list directory {}: {}".format(
|
f"could not list directory {displayable_path(bytes_path)}:"
|
||||||
displayable_path(bytes_path), exc.strerror
|
f" {exc.strerror}"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
dirs = []
|
dirs = []
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ def resize_url(url: str, maxwidth: int, quality: int = 0) -> str:
|
||||||
if quality > 0:
|
if quality > 0:
|
||||||
params["q"] = quality
|
params["q"] = quality
|
||||||
|
|
||||||
return "{}?{}".format(PROXY_URL, urlencode(params))
|
return f"{PROXY_URL}?{urlencode(params)}"
|
||||||
|
|
||||||
|
|
||||||
class LocalBackendNotAvailableError(Exception):
|
class LocalBackendNotAvailableError(Exception):
|
||||||
|
|
|
||||||
|
|
@ -165,9 +165,7 @@ class Call:
|
||||||
self.original = original
|
self.original = original
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "Call({}, {}, {})".format(
|
return f"Call({self.ident!r}, {self.args!r}, {self.original!r})"
|
||||||
repr(self.ident), repr(self.args), repr(self.original)
|
|
||||||
)
|
|
||||||
|
|
||||||
def evaluate(self, env):
|
def evaluate(self, env):
|
||||||
"""Evaluate the function call in the environment, returning a
|
"""Evaluate the function call in the environment, returning a
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,7 @@ def call(args):
|
||||||
try:
|
try:
|
||||||
return util.command_output(args).stdout
|
return util.command_output(args).stdout
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
raise ABSubmitError(
|
raise ABSubmitError(f"{args[0]} exited with status {e.returncode}")
|
||||||
"{} exited with status {}".format(args[0], e.returncode)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):
|
class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):
|
||||||
|
|
@ -63,9 +61,7 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):
|
||||||
# Explicit path to extractor
|
# Explicit path to extractor
|
||||||
if not os.path.isfile(self.extractor):
|
if not os.path.isfile(self.extractor):
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"Extractor command does not exist: {0}.".format(
|
f"Extractor command does not exist: {self.extractor}."
|
||||||
self.extractor
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Implicit path to extractor, search for it in path
|
# Implicit path to extractor, search for it in path
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ class AURADocument:
|
||||||
else:
|
else:
|
||||||
# Increment page token by 1
|
# Increment page token by 1
|
||||||
next_url = request.url.replace(
|
next_url = request.url.replace(
|
||||||
f"page={page}", "page={}".format(page + 1)
|
f"page={page}", f"page={page + 1}"
|
||||||
)
|
)
|
||||||
# Get only the items in the page range
|
# Get only the items in the page range
|
||||||
data = [
|
data = [
|
||||||
|
|
@ -427,9 +427,7 @@ class TrackDocument(AURADocument):
|
||||||
return self.error(
|
return self.error(
|
||||||
"404 Not Found",
|
"404 Not Found",
|
||||||
"No track with the requested id.",
|
"No track with the requested id.",
|
||||||
"There is no track with an id of {} in the library.".format(
|
f"There is no track with an id of {track_id} in the library.",
|
||||||
track_id
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return self.single_resource_document(
|
return self.single_resource_document(
|
||||||
self.get_resource_object(self.lib, track)
|
self.get_resource_object(self.lib, track)
|
||||||
|
|
@ -513,9 +511,7 @@ class AlbumDocument(AURADocument):
|
||||||
return self.error(
|
return self.error(
|
||||||
"404 Not Found",
|
"404 Not Found",
|
||||||
"No album with the requested id.",
|
"No album with the requested id.",
|
||||||
"There is no album with an id of {} in the library.".format(
|
f"There is no album with an id of {album_id} in the library.",
|
||||||
album_id
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return self.single_resource_document(
|
return self.single_resource_document(
|
||||||
self.get_resource_object(self.lib, album)
|
self.get_resource_object(self.lib, album)
|
||||||
|
|
@ -600,9 +596,7 @@ class ArtistDocument(AURADocument):
|
||||||
return self.error(
|
return self.error(
|
||||||
"404 Not Found",
|
"404 Not Found",
|
||||||
"No artist with the requested id.",
|
"No artist with the requested id.",
|
||||||
"There is no artist with an id of {} in the library.".format(
|
f"There is no artist with an id of {artist_id} in the library.",
|
||||||
artist_id
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return self.single_resource_document(artist_resource)
|
return self.single_resource_document(artist_resource)
|
||||||
|
|
||||||
|
|
@ -727,9 +721,7 @@ class ImageDocument(AURADocument):
|
||||||
return self.error(
|
return self.error(
|
||||||
"404 Not Found",
|
"404 Not Found",
|
||||||
"No image with the requested id.",
|
"No image with the requested id.",
|
||||||
"There is no image with an id of {} in the library.".format(
|
f"There is no image with an id of {image_id} in the library.",
|
||||||
image_id
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return self.single_resource_document(image_resource)
|
return self.single_resource_document(image_resource)
|
||||||
|
|
||||||
|
|
@ -775,9 +767,7 @@ def audio_file(track_id):
|
||||||
return AURADocument.error(
|
return AURADocument.error(
|
||||||
"404 Not Found",
|
"404 Not Found",
|
||||||
"No track with the requested id.",
|
"No track with the requested id.",
|
||||||
"There is no track with an id of {} in the library.".format(
|
f"There is no track with an id of {track_id} in the library.",
|
||||||
track_id
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
path = os.fsdecode(track.path)
|
path = os.fsdecode(track.path)
|
||||||
|
|
@ -785,9 +775,8 @@ def audio_file(track_id):
|
||||||
return AURADocument.error(
|
return AURADocument.error(
|
||||||
"404 Not Found",
|
"404 Not Found",
|
||||||
"No audio file for the requested track.",
|
"No audio file for the requested track.",
|
||||||
(
|
f"There is no audio file for track {track_id} at the expected"
|
||||||
"There is no audio file for track {} at the expected location"
|
" location",
|
||||||
).format(track_id),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
file_mimetype = guess_type(path)[0]
|
file_mimetype = guess_type(path)[0]
|
||||||
|
|
@ -795,10 +784,8 @@ def audio_file(track_id):
|
||||||
return AURADocument.error(
|
return AURADocument.error(
|
||||||
"500 Internal Server Error",
|
"500 Internal Server Error",
|
||||||
"Requested audio file has an unknown mimetype.",
|
"Requested audio file has an unknown mimetype.",
|
||||||
(
|
f"The audio file for track {track_id} has an unknown mimetype. "
|
||||||
"The audio file for track {} has an unknown mimetype. "
|
f"Its file extension is {path.split('.')[-1]}.",
|
||||||
"Its file extension is {}."
|
|
||||||
).format(track_id, path.split(".")[-1]),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that the Accept header contains the file's mimetype
|
# Check that the Accept header contains the file's mimetype
|
||||||
|
|
@ -810,10 +797,8 @@ def audio_file(track_id):
|
||||||
return AURADocument.error(
|
return AURADocument.error(
|
||||||
"406 Not Acceptable",
|
"406 Not Acceptable",
|
||||||
"Unsupported MIME type or bitrate parameter in Accept header.",
|
"Unsupported MIME type or bitrate parameter in Accept header.",
|
||||||
(
|
f"The audio file for track {track_id} is only available as"
|
||||||
"The audio file for track {} is only available as {} and "
|
f" {file_mimetype} and bitrate parameters are not supported.",
|
||||||
"bitrate parameters are not supported."
|
|
||||||
).format(track_id, file_mimetype),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return send_file(
|
return send_file(
|
||||||
|
|
@ -896,9 +881,7 @@ def image_file(image_id):
|
||||||
return AURADocument.error(
|
return AURADocument.error(
|
||||||
"404 Not Found",
|
"404 Not Found",
|
||||||
"No image with the requested id.",
|
"No image with the requested id.",
|
||||||
"There is no image with an id of {} in the library".format(
|
f"There is no image with an id of {image_id} in the library",
|
||||||
image_id
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return send_file(img_path)
|
return send_file(img_path)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,9 +110,7 @@ class BadFiles(BeetsPlugin):
|
||||||
self._log.debug("checking path: {}", dpath)
|
self._log.debug("checking path: {}", dpath)
|
||||||
if not os.path.exists(item.path):
|
if not os.path.exists(item.path):
|
||||||
ui.print_(
|
ui.print_(
|
||||||
"{}: file does not exist".format(
|
f"{ui.colorize('text_error', dpath)}: file does not exist"
|
||||||
ui.colorize("text_error", dpath)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run the checker against the file if one is found
|
# Run the checker against the file if one is found
|
||||||
|
|
@ -141,25 +139,21 @@ class BadFiles(BeetsPlugin):
|
||||||
|
|
||||||
if status > 0:
|
if status > 0:
|
||||||
error_lines.append(
|
error_lines.append(
|
||||||
"{}: checker exited with status {}".format(
|
f"{ui.colorize('text_error', dpath)}: checker exited with"
|
||||||
ui.colorize("text_error", dpath), status
|
f" status {status}"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for line in output:
|
for line in output:
|
||||||
error_lines.append(f" {line}")
|
error_lines.append(f" {line}")
|
||||||
|
|
||||||
elif errors > 0:
|
elif errors > 0:
|
||||||
error_lines.append(
|
error_lines.append(
|
||||||
"{}: checker found {} errors or warnings".format(
|
f"{ui.colorize('text_warning', dpath)}: checker found"
|
||||||
ui.colorize("text_warning", dpath), errors
|
f" {status} errors or warnings"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for line in output:
|
for line in output:
|
||||||
error_lines.append(f" {line}")
|
error_lines.append(f" {line}")
|
||||||
elif self.verbose:
|
elif self.verbose:
|
||||||
error_lines.append(
|
error_lines.append(f"{ui.colorize('text_success', dpath)}: ok")
|
||||||
"{}: ok".format(ui.colorize("text_success", dpath))
|
|
||||||
)
|
|
||||||
|
|
||||||
return error_lines
|
return error_lines
|
||||||
|
|
||||||
|
|
@ -180,9 +174,8 @@ class BadFiles(BeetsPlugin):
|
||||||
def on_import_task_before_choice(self, task, session):
|
def on_import_task_before_choice(self, task, session):
|
||||||
if hasattr(task, "_badfiles_checks_failed"):
|
if hasattr(task, "_badfiles_checks_failed"):
|
||||||
ui.print_(
|
ui.print_(
|
||||||
"{} one or more files failed checks:".format(
|
f"{ui.colorize('text_warning', 'BAD')} one or more files failed"
|
||||||
ui.colorize("text_warning", "BAD")
|
" checks:"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for error in task._badfiles_checks_failed:
|
for error in task._badfiles_checks_failed:
|
||||||
for error_line in error:
|
for error_line in error:
|
||||||
|
|
|
||||||
|
|
@ -212,14 +212,10 @@ class BeatportClient:
|
||||||
try:
|
try:
|
||||||
response = self.api.get(self._make_url(endpoint), params=kwargs)
|
response = self.api.get(self._make_url(endpoint), params=kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BeatportAPIError(
|
raise BeatportAPIError(f"Error connecting to Beatport API: {e}")
|
||||||
"Error connecting to Beatport API: {}".format(e)
|
|
||||||
)
|
|
||||||
if not response:
|
if not response:
|
||||||
raise BeatportAPIError(
|
raise BeatportAPIError(
|
||||||
"Error {0.status_code} for '{0.request.path_url}".format(
|
f"Error {response.status_code} for '{response.request.path_url}"
|
||||||
response
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return response.json()["results"]
|
return response.json()["results"]
|
||||||
|
|
||||||
|
|
@ -275,15 +271,14 @@ class BeatportRelease(BeatportObject):
|
||||||
self.genre = data.get("genre")
|
self.genre = data.get("genre")
|
||||||
|
|
||||||
if "slug" in data:
|
if "slug" in data:
|
||||||
self.url = "https://beatport.com/release/{}/{}".format(
|
self.url = (
|
||||||
data["slug"], data["id"]
|
f"https://beatport.com/release/{data['slug']}/{data['id']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "<BeatportRelease: {} - {} ({})>".format(
|
return (
|
||||||
self.artists_str(),
|
"<BeatportRelease: "
|
||||||
self.name,
|
f"{self.artists_str()} - {self.name} ({self.catalog_number})>"
|
||||||
self.catalog_number,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -311,9 +306,7 @@ class BeatportTrack(BeatportObject):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
if "slug" in data:
|
if "slug" in data:
|
||||||
self.url = "https://beatport.com/track/{}/{}".format(
|
self.url = f"https://beatport.com/track/{data['slug']}/{data['id']}"
|
||||||
data["slug"], data["id"]
|
|
||||||
)
|
|
||||||
self.track_number = data.get("trackNumber")
|
self.track_number = data.get("trackNumber")
|
||||||
self.bpm = data.get("bpm")
|
self.bpm = data.get("bpm")
|
||||||
self.initial_key = str((data.get("key") or {}).get("shortName"))
|
self.initial_key = str((data.get("key") or {}).get("shortName"))
|
||||||
|
|
|
||||||
|
|
@ -759,7 +759,7 @@ class Connection:
|
||||||
"""Create a new connection for the accepted socket `client`."""
|
"""Create a new connection for the accepted socket `client`."""
|
||||||
self.server = server
|
self.server = server
|
||||||
self.sock = sock
|
self.sock = sock
|
||||||
self.address = "{}:{}".format(*sock.sock.getpeername())
|
self.address = ":".join(map(str, sock.sock.getpeername()))
|
||||||
|
|
||||||
def debug(self, message, kind=" "):
|
def debug(self, message, kind=" "):
|
||||||
"""Log a debug message about this connection."""
|
"""Log a debug message about this connection."""
|
||||||
|
|
@ -899,9 +899,7 @@ class MPDConnection(Connection):
|
||||||
return
|
return
|
||||||
except BPDIdleError as e:
|
except BPDIdleError as e:
|
||||||
self.idle_subscriptions = e.subsystems
|
self.idle_subscriptions = e.subsystems
|
||||||
self.debug(
|
self.debug(f"awaiting: {' '.join(e.subsystems)}", kind="z")
|
||||||
"awaiting: {}".format(" ".join(e.subsystems)), kind="z"
|
|
||||||
)
|
|
||||||
yield bluelet.call(self.server.dispatch_events())
|
yield bluelet.call(self.server.dispatch_events())
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -933,7 +931,7 @@ class ControlConnection(Connection):
|
||||||
func = command.delegate("ctrl_", self)
|
func = command.delegate("ctrl_", self)
|
||||||
yield bluelet.call(func(*command.args))
|
yield bluelet.call(func(*command.args))
|
||||||
except (AttributeError, TypeError) as e:
|
except (AttributeError, TypeError) as e:
|
||||||
yield self.send("ERROR: {}".format(e.args[0]))
|
yield self.send(f"ERROR: {e.args[0]}")
|
||||||
except Exception:
|
except Exception:
|
||||||
yield self.send(
|
yield self.send(
|
||||||
["ERROR: server error", traceback.format_exc().rstrip()]
|
["ERROR: server error", traceback.format_exc().rstrip()]
|
||||||
|
|
@ -1011,7 +1009,7 @@ class Command:
|
||||||
# If the command accepts a variable number of arguments skip the check.
|
# If the command accepts a variable number of arguments skip the check.
|
||||||
if wrong_num and not argspec.varargs:
|
if wrong_num and not argspec.varargs:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'wrong number of arguments for "{}"'.format(self.name),
|
f'wrong number of arguments for "{self.name}"',
|
||||||
self.name,
|
self.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1110,10 +1108,8 @@ class Server(BaseServer):
|
||||||
self.lib = library
|
self.lib = library
|
||||||
self.player = gstplayer.GstPlayer(self.play_finished)
|
self.player = gstplayer.GstPlayer(self.play_finished)
|
||||||
self.cmd_update(None)
|
self.cmd_update(None)
|
||||||
log.info("Server ready and listening on {}:{}".format(host, port))
|
log.info(f"Server ready and listening on {host}:{port}")
|
||||||
log.debug(
|
log.debug(f"Listening for control signals on {host}:{ctrl_port}")
|
||||||
"Listening for control signals on {}:{}".format(host, ctrl_port)
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.player.run()
|
self.player.run()
|
||||||
|
|
@ -1142,9 +1138,7 @@ class Server(BaseServer):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for tagtype, field in self.tagtype_map.items():
|
for tagtype, field in self.tagtype_map.items():
|
||||||
info_lines.append(
|
info_lines.append(f"{tagtype}: {getattr(item, field)}")
|
||||||
"{}: {}".format(tagtype, str(getattr(item, field)))
|
|
||||||
)
|
|
||||||
|
|
||||||
return info_lines
|
return info_lines
|
||||||
|
|
||||||
|
|
@ -1303,19 +1297,12 @@ class Server(BaseServer):
|
||||||
|
|
||||||
yield (
|
yield (
|
||||||
"bitrate: " + str(item.bitrate / 1000),
|
"bitrate: " + str(item.bitrate / 1000),
|
||||||
"audio: {}:{}:{}".format(
|
f"audio: {item.samplerate}:{item.bitdepth}:{item.channels}",
|
||||||
str(item.samplerate),
|
|
||||||
str(item.bitdepth),
|
|
||||||
str(item.channels),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
(pos, total) = self.player.time()
|
(pos, total) = self.player.time()
|
||||||
yield (
|
yield (
|
||||||
"time: {}:{}".format(
|
f"time: {int(pos)}:{int(total)}",
|
||||||
str(int(pos)),
|
|
||||||
str(int(total)),
|
|
||||||
),
|
|
||||||
"elapsed: " + f"{pos:.3f}",
|
"elapsed: " + f"{pos:.3f}",
|
||||||
"duration: " + f"{total:.3f}",
|
"duration: " + f"{total:.3f}",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ def span_from_str(span_str):
|
||||||
years = [int(x) for x in re.findall(r"\d+", span_str)]
|
years = [int(x) for x in re.findall(r"\d+", span_str)]
|
||||||
if not years:
|
if not years:
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"invalid range defined for year bucket '%s': no "
|
"invalid range defined for year bucket '%s': no year found"
|
||||||
"year found" % span_str
|
% span_str
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
years = [normalize_year(x, years[0]) for x in years]
|
years = [normalize_year(x, years[0]) for x in years]
|
||||||
|
|
@ -125,11 +125,8 @@ def str2fmt(s):
|
||||||
"fromnchars": len(m.group("fromyear")),
|
"fromnchars": len(m.group("fromyear")),
|
||||||
"tonchars": len(m.group("toyear")),
|
"tonchars": len(m.group("toyear")),
|
||||||
}
|
}
|
||||||
res["fmt"] = "{}%s{}{}{}".format(
|
res["fmt"] = (
|
||||||
m.group("bef"),
|
f"{m['bef']}%s{m['sep']}{'%s' if res['tonchars'] else ''}{m['after']}"
|
||||||
m.group("sep"),
|
|
||||||
"%s" if res["tonchars"] else "",
|
|
||||||
m.group("after"),
|
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,7 @@ def get_format(fmt=None):
|
||||||
command = format_info["command"]
|
command = format_info["command"]
|
||||||
extension = format_info.get("extension", fmt)
|
extension = format_info.get("extension", fmt)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ui.UserError(
|
raise ui.UserError(f'convert: format {fmt} needs the "command" field')
|
||||||
'convert: format {} needs the "command" field'.format(fmt)
|
|
||||||
)
|
|
||||||
except ConfigTypeError:
|
except ConfigTypeError:
|
||||||
command = config["convert"]["formats"][fmt].get(str)
|
command = config["convert"]["formats"][fmt].get(str)
|
||||||
extension = fmt
|
extension = fmt
|
||||||
|
|
@ -77,8 +75,8 @@ def get_format(fmt=None):
|
||||||
command = config["convert"]["command"].as_str()
|
command = config["convert"]["command"].as_str()
|
||||||
elif "opts" in keys:
|
elif "opts" in keys:
|
||||||
# Undocumented option for backwards compatibility with < 1.3.1.
|
# Undocumented option for backwards compatibility with < 1.3.1.
|
||||||
command = "ffmpeg -i $source -y {} $dest".format(
|
command = (
|
||||||
config["convert"]["opts"].as_str()
|
f"ffmpeg -i $source -y {config['convert']['opts'].as_str()} $dest"
|
||||||
)
|
)
|
||||||
if "extension" in keys:
|
if "extension" in keys:
|
||||||
extension = config["convert"]["extension"].as_str()
|
extension = config["convert"]["extension"].as_str()
|
||||||
|
|
@ -125,18 +123,25 @@ class ConvertPlugin(BeetsPlugin):
|
||||||
"id3v23": "inherit",
|
"id3v23": "inherit",
|
||||||
"formats": {
|
"formats": {
|
||||||
"aac": {
|
"aac": {
|
||||||
"command": "ffmpeg -i $source -y -vn -acodec aac "
|
"command": (
|
||||||
"-aq 1 $dest",
|
"ffmpeg -i $source -y -vn -acodec aac -aq 1 $dest"
|
||||||
|
),
|
||||||
"extension": "m4a",
|
"extension": "m4a",
|
||||||
},
|
},
|
||||||
"alac": {
|
"alac": {
|
||||||
"command": "ffmpeg -i $source -y -vn -acodec alac $dest",
|
"command": (
|
||||||
|
"ffmpeg -i $source -y -vn -acodec alac $dest"
|
||||||
|
),
|
||||||
"extension": "m4a",
|
"extension": "m4a",
|
||||||
},
|
},
|
||||||
"flac": "ffmpeg -i $source -y -vn -acodec flac $dest",
|
"flac": "ffmpeg -i $source -y -vn -acodec flac $dest",
|
||||||
"mp3": "ffmpeg -i $source -y -vn -aq 2 $dest",
|
"mp3": "ffmpeg -i $source -y -vn -aq 2 $dest",
|
||||||
"opus": "ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest",
|
"opus": (
|
||||||
"ogg": "ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest",
|
"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",
|
"wma": "ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest",
|
||||||
},
|
},
|
||||||
"max_bitrate": None,
|
"max_bitrate": None,
|
||||||
|
|
@ -323,7 +328,7 @@ class ConvertPlugin(BeetsPlugin):
|
||||||
raise
|
raise
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"convert: couldn't invoke '{}': {}".format(" ".join(args), exc)
|
f"convert: couldn't invoke {' '.join(args)!r}: {exc}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not quiet and not pretend:
|
if not quiet and not pretend:
|
||||||
|
|
|
||||||
|
|
@ -552,7 +552,7 @@ class DiscogsPlugin(MetadataSourcePlugin):
|
||||||
idx, medium_idx, sub_idx = self.get_track_index(
|
idx, medium_idx, sub_idx = self.get_track_index(
|
||||||
subtracks[0]["position"]
|
subtracks[0]["position"]
|
||||||
)
|
)
|
||||||
position = "{}{}".format(idx or "", medium_idx or "")
|
position = f"{idx or ''}{medium_idx or ''}"
|
||||||
|
|
||||||
if tracklist and not tracklist[-1]["position"]:
|
if tracklist and not tracklist[-1]["position"]:
|
||||||
# Assume the previous index track contains the track title.
|
# Assume the previous index track contains the track title.
|
||||||
|
|
@ -574,8 +574,8 @@ class DiscogsPlugin(MetadataSourcePlugin):
|
||||||
# option is set
|
# option is set
|
||||||
if self.config["index_tracks"]:
|
if self.config["index_tracks"]:
|
||||||
for subtrack in subtracks:
|
for subtrack in subtracks:
|
||||||
subtrack["title"] = "{}: {}".format(
|
subtrack["title"] = (
|
||||||
index_track["title"], subtrack["title"]
|
f"{index_track['title']}: {subtrack['title']}"
|
||||||
)
|
)
|
||||||
tracklist.extend(subtracks)
|
tracklist.extend(subtracks)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,7 @@ def edit(filename, log):
|
||||||
try:
|
try:
|
||||||
subprocess.call(cmd)
|
subprocess.call(cmd)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise ui.UserError(
|
raise ui.UserError(f"could not run editor command {cmd[0]!r}: {exc}")
|
||||||
"could not run editor command {!r}: {}".format(cmd[0], exc)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def dump(arg):
|
def dump(arg):
|
||||||
|
|
@ -71,9 +69,7 @@ def load(s):
|
||||||
for d in yaml.safe_load_all(s):
|
for d in yaml.safe_load_all(s):
|
||||||
if not isinstance(d, dict):
|
if not isinstance(d, dict):
|
||||||
raise ParseError(
|
raise ParseError(
|
||||||
"each entry must be a dictionary; found {}".format(
|
f"each entry must be a dictionary; found {type(d).__name__}"
|
||||||
type(d).__name__
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert all keys to strings. They started out as strings,
|
# Convert all keys to strings. They started out as strings,
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,9 @@ def _confirm(objs, album):
|
||||||
to items).
|
to items).
|
||||||
"""
|
"""
|
||||||
noun = "album" if album else "file"
|
noun = "album" if album else "file"
|
||||||
prompt = "Modify artwork for {} {}{} (Y/n)?".format(
|
prompt = (
|
||||||
len(objs), noun, "s" if len(objs) > 1 else ""
|
"Modify artwork for"
|
||||||
|
f" {len(objs)} {noun}{'s' if len(objs) > 1 else ''} (Y/n)?"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Show all the items or albums.
|
# Show all the items or albums.
|
||||||
|
|
@ -110,9 +111,7 @@ class EmbedCoverArtPlugin(BeetsPlugin):
|
||||||
imagepath = normpath(opts.file)
|
imagepath = normpath(opts.file)
|
||||||
if not os.path.isfile(syspath(imagepath)):
|
if not os.path.isfile(syspath(imagepath)):
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"image file {} not found".format(
|
f"image file {displayable_path(imagepath)} not found"
|
||||||
displayable_path(imagepath)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
items = lib.items(args)
|
items = lib.items(args)
|
||||||
|
|
@ -137,7 +136,7 @@ class EmbedCoverArtPlugin(BeetsPlugin):
|
||||||
response = requests.get(opts.url, timeout=5)
|
response = requests.get(opts.url, timeout=5)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
self._log.error("{}".format(e))
|
self._log.error(f"{e}")
|
||||||
return
|
return
|
||||||
extension = guess_extension(response.headers["Content-Type"])
|
extension = guess_extension(response.headers["Content-Type"])
|
||||||
if extension is None:
|
if extension is None:
|
||||||
|
|
@ -149,7 +148,7 @@ class EmbedCoverArtPlugin(BeetsPlugin):
|
||||||
with open(tempimg, "wb") as f:
|
with open(tempimg, "wb") as f:
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log.error("Unable to save image: {}".format(e))
|
self._log.error(f"Unable to save image: {e}")
|
||||||
return
|
return
|
||||||
items = lib.items(args)
|
items = lib.items(args)
|
||||||
# Confirm with user.
|
# Confirm with user.
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,7 @@ def api_url(host, port, endpoint):
|
||||||
hostname_list.insert(0, "http://")
|
hostname_list.insert(0, "http://")
|
||||||
hostname = "".join(hostname_list)
|
hostname = "".join(hostname_list)
|
||||||
|
|
||||||
joined = urljoin(
|
joined = urljoin(f"{hostname}:{port}", endpoint)
|
||||||
"{hostname}:{port}".format(hostname=hostname, port=port), endpoint
|
|
||||||
)
|
|
||||||
|
|
||||||
scheme, netloc, path, query_string, fragment = urlsplit(joined)
|
scheme, netloc, path, query_string, fragment = urlsplit(joined)
|
||||||
query_params = parse_qs(query_string)
|
query_params = parse_qs(query_string)
|
||||||
|
|
@ -81,12 +79,12 @@ def create_headers(user_id, token=None):
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
authorization = (
|
authorization = (
|
||||||
'MediaBrowser UserId="{user_id}", '
|
f'MediaBrowser UserId="{user_id}", '
|
||||||
'Client="other", '
|
'Client="other", '
|
||||||
'Device="beets", '
|
'Device="beets", '
|
||||||
'DeviceId="beets", '
|
'DeviceId="beets", '
|
||||||
'Version="0.0.0"'
|
'Version="0.0.0"'
|
||||||
).format(user_id=user_id)
|
)
|
||||||
|
|
||||||
headers["x-emby-authorization"] = authorization
|
headers["x-emby-authorization"] = authorization
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,9 @@ class FishPlugin(BeetsPlugin):
|
||||||
"-o",
|
"-o",
|
||||||
"--output",
|
"--output",
|
||||||
default="~/.config/fish/completions/beet.fish",
|
default="~/.config/fish/completions/beet.fish",
|
||||||
help="where to save the script. default: "
|
help=(
|
||||||
"~/.config/fish/completions",
|
"where to save the script. default: ~/.config/fish/completions"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return [cmd]
|
return [cmd]
|
||||||
|
|
||||||
|
|
@ -127,16 +128,12 @@ class FishPlugin(BeetsPlugin):
|
||||||
totstring += "" if nobasicfields else get_standard_fields(fields)
|
totstring += "" if nobasicfields else get_standard_fields(fields)
|
||||||
totstring += get_extravalues(lib, extravalues) if extravalues else ""
|
totstring += get_extravalues(lib, extravalues) if extravalues else ""
|
||||||
totstring += (
|
totstring += (
|
||||||
"\n"
|
"\n" + "# ====== setup basic beet completion =====" + "\n" * 2
|
||||||
+ "# ====== {} =====".format("setup basic beet completion")
|
|
||||||
+ "\n" * 2
|
|
||||||
)
|
)
|
||||||
totstring += get_basic_beet_options()
|
totstring += get_basic_beet_options()
|
||||||
totstring += (
|
totstring += (
|
||||||
"\n"
|
"\n"
|
||||||
+ "# ====== {} =====".format(
|
+ "# ====== setup field completion for subcommands ====="
|
||||||
"setup field completion for subcommands"
|
|
||||||
)
|
|
||||||
+ "\n"
|
+ "\n"
|
||||||
)
|
)
|
||||||
totstring += get_subcommands(cmd_names_help, nobasicfields, extravalues)
|
totstring += get_subcommands(cmd_names_help, nobasicfields, extravalues)
|
||||||
|
|
@ -226,11 +223,7 @@ def get_subcommands(cmd_name_and_help, nobasicfields, extravalues):
|
||||||
for cmdname, cmdhelp in cmd_name_and_help:
|
for cmdname, cmdhelp in cmd_name_and_help:
|
||||||
cmdname = _escape(cmdname)
|
cmdname = _escape(cmdname)
|
||||||
|
|
||||||
word += (
|
word += "\n" + f"# ------ fieldsetups for {cmdname} -------" + "\n"
|
||||||
"\n"
|
|
||||||
+ "# ------ {} -------".format("fieldsetups for " + cmdname)
|
|
||||||
+ "\n"
|
|
||||||
)
|
|
||||||
word += BL_NEED2.format(
|
word += BL_NEED2.format(
|
||||||
("-a " + cmdname), ("-f " + "-d " + wrap(clean_whitespace(cmdhelp)))
|
("-a " + cmdname), ("-f " + "-d " + wrap(clean_whitespace(cmdhelp)))
|
||||||
)
|
)
|
||||||
|
|
@ -268,11 +261,7 @@ def get_all_commands(beetcmds):
|
||||||
name = _escape(name)
|
name = _escape(name)
|
||||||
|
|
||||||
word += "\n"
|
word += "\n"
|
||||||
word += (
|
word += ("\n" * 2) + f"# ====== completions for {name} =====" + "\n"
|
||||||
("\n" * 2)
|
|
||||||
+ "# ====== {} =====".format("completions for " + name)
|
|
||||||
+ "\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
for option in cmd.parser._get_all_options()[1:]:
|
for option in cmd.parser._get_all_options()[1:]:
|
||||||
cmd_l = (
|
cmd_l = (
|
||||||
|
|
@ -332,7 +321,7 @@ def clean_whitespace(word):
|
||||||
def wrap(word):
|
def wrap(word):
|
||||||
# Need " or ' around strings but watch out if they're in the string
|
# Need " or ' around strings but watch out if they're in the string
|
||||||
sptoken = '"'
|
sptoken = '"'
|
||||||
if ('"') in word and ("'") in word:
|
if '"' in word and ("'") in word:
|
||||||
word.replace('"', sptoken)
|
word.replace('"', sptoken)
|
||||||
return '"' + word + '"'
|
return '"' + word + '"'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ def apply_matches(d, log):
|
||||||
for item in d:
|
for item in d:
|
||||||
if not item.artist:
|
if not item.artist:
|
||||||
item.artist = artist
|
item.artist = artist
|
||||||
log.info("Artist replaced with: {}".format(item.artist))
|
log.info(f"Artist replaced with: {item.artist}")
|
||||||
|
|
||||||
# No artist field: remaining field is the title.
|
# No artist field: remaining field is the title.
|
||||||
else:
|
else:
|
||||||
|
|
@ -122,11 +122,11 @@ def apply_matches(d, log):
|
||||||
for item in d:
|
for item in d:
|
||||||
if bad_title(item.title):
|
if bad_title(item.title):
|
||||||
item.title = str(d[item][title_field])
|
item.title = str(d[item][title_field])
|
||||||
log.info("Title replaced with: {}".format(item.title))
|
log.info(f"Title replaced with: {item.title}")
|
||||||
|
|
||||||
if "track" in d[item] and item.track == 0:
|
if "track" in d[item] and item.track == 0:
|
||||||
item.track = int(d[item]["track"])
|
item.track = int(d[item]["track"])
|
||||||
log.info("Track replaced with: {}".format(item.track))
|
log.info(f"Track replaced with: {item.track}")
|
||||||
|
|
||||||
|
|
||||||
# Plugin structure and hook into import process.
|
# Plugin structure and hook into import process.
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ def search_pairs(item):
|
||||||
# examples include (live), (remix), and (acoustic).
|
# examples include (live), (remix), and (acoustic).
|
||||||
r"(.+?)\s+[(].*[)]$",
|
r"(.+?)\s+[(].*[)]$",
|
||||||
# Remove any featuring artists from the title
|
# Remove any featuring artists from the title
|
||||||
r"(.*?) {}".format(plugins.feat_tokens(for_artist=False)),
|
rf"(.*?) {plugins.feat_tokens(for_artist=False)}",
|
||||||
# Remove part of title after colon ':' for songs with subtitles
|
# Remove part of title after colon ':' for songs with subtitles
|
||||||
r"(.+?)\s*:.*",
|
r"(.+?)\s*:.*",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,7 @@ class MusicBrainzCollectionPlugin(BeetsPlugin):
|
||||||
collection = self.config["collection"].as_str()
|
collection = self.config["collection"].as_str()
|
||||||
if collection:
|
if collection:
|
||||||
if collection not in collection_ids:
|
if collection not in collection_ids:
|
||||||
raise ui.UserError(
|
raise ui.UserError(f"invalid collection ID: {collection}")
|
||||||
"invalid collection ID: {}".format(collection)
|
|
||||||
)
|
|
||||||
return collection
|
return collection
|
||||||
|
|
||||||
# No specified collection. Just return the first collection ID
|
# No specified collection. Just return the first collection ID
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ class MetaSyncPlugin(BeetsPlugin):
|
||||||
try:
|
try:
|
||||||
cls = META_SOURCES[player]
|
cls = META_SOURCES[player]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self._log.error("Unknown metadata source '{}'".format(player))
|
self._log.error(f"Unknown metadata source '{player}'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
meta_source_instances[player] = cls(self.config, self._log)
|
meta_source_instances[player] = cls(self.config, self._log)
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,7 @@ class MusicBrainzAPIError(util.HumanReadableError):
|
||||||
super().__init__(reason, verb, tb)
|
super().__init__(reason, verb, tb)
|
||||||
|
|
||||||
def get_message(self):
|
def get_message(self):
|
||||||
return "{} in {} with query {}".format(
|
return f"{self._reasonstr()} in {self.verb} with query {self.query!r}"
|
||||||
self._reasonstr(), self.verb, repr(self.query)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
RELEASE_INCLUDES = list(
|
RELEASE_INCLUDES = list(
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ def play(
|
||||||
"""
|
"""
|
||||||
# Print number of tracks or albums to be played, log command to be run.
|
# Print number of tracks or albums to be played, log command to be run.
|
||||||
item_type += "s" if len(selection) > 1 else ""
|
item_type += "s" if len(selection) > 1 else ""
|
||||||
ui.print_("Playing {} {}.".format(len(selection), item_type))
|
ui.print_(f"Playing {len(selection)} {item_type}.")
|
||||||
log.debug("executing command: {} {!r}", command_str, open_args)
|
log.debug("executing command: {} {!r}", command_str, open_args)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -179,9 +179,7 @@ class PlayPlugin(BeetsPlugin):
|
||||||
ui.print_(
|
ui.print_(
|
||||||
ui.colorize(
|
ui.colorize(
|
||||||
"text_warning",
|
"text_warning",
|
||||||
"You are about to queue {} {}.".format(
|
f"You are about to queue {len(selection)} {item_type}.",
|
||||||
len(selection), item_type
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,21 +133,16 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin):
|
||||||
try:
|
try:
|
||||||
self.update_playlist(playlist, base_dir)
|
self.update_playlist(playlist, base_dir)
|
||||||
except beets.util.FilesystemError:
|
except beets.util.FilesystemError:
|
||||||
self._log.error(
|
self._log.error("Failed to update playlist: {}", playlist)
|
||||||
"Failed to update playlist: {}".format(
|
|
||||||
beets.util.displayable_path(playlist)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def find_playlists(self):
|
def find_playlists(self):
|
||||||
"""Find M3U playlists in the playlist directory."""
|
"""Find M3U playlists in the playlist directory."""
|
||||||
|
playlist_dir = beets.util.syspath(self.playlist_dir)
|
||||||
try:
|
try:
|
||||||
dir_contents = os.listdir(beets.util.syspath(self.playlist_dir))
|
dir_contents = os.listdir(playlist_dir)
|
||||||
except OSError:
|
except OSError:
|
||||||
self._log.warning(
|
self._log.warning(
|
||||||
"Unable to open playlist directory {}".format(
|
"Unable to open playlist directory {}", self.playlist_dir
|
||||||
beets.util.displayable_path(self.playlist_dir)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -195,9 +190,10 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin):
|
||||||
|
|
||||||
if changes or deletions:
|
if changes or deletions:
|
||||||
self._log.info(
|
self._log.info(
|
||||||
"Updated playlist {} ({} changes, {} deletions)".format(
|
"Updated playlist {} ({} changes, {} deletions)",
|
||||||
filename, changes, deletions
|
filename,
|
||||||
)
|
changes,
|
||||||
|
deletions,
|
||||||
)
|
)
|
||||||
beets.util.copy(new_playlist, filename, replace=True)
|
beets.util.copy(new_playlist, filename, replace=True)
|
||||||
beets.util.remove(new_playlist)
|
beets.util.remove(new_playlist)
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,7 @@ def get_music_section(
|
||||||
):
|
):
|
||||||
"""Getting the section key for the music library in Plex."""
|
"""Getting the section key for the music library in Plex."""
|
||||||
api_endpoint = append_token("library/sections", token)
|
api_endpoint = append_token("library/sections", token)
|
||||||
url = urljoin(
|
url = urljoin(f"{get_protocol(secure)}://{host}:{port}", api_endpoint)
|
||||||
"{}://{}:{}".format(get_protocol(secure), host, port), api_endpoint
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sends request.
|
# Sends request.
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
|
|
@ -54,9 +52,7 @@ def update_plex(host, port, token, library_name, secure, ignore_cert_errors):
|
||||||
)
|
)
|
||||||
api_endpoint = f"library/sections/{section_key}/refresh"
|
api_endpoint = f"library/sections/{section_key}/refresh"
|
||||||
api_endpoint = append_token(api_endpoint, token)
|
api_endpoint = append_token(api_endpoint, token)
|
||||||
url = urljoin(
|
url = urljoin(f"{get_protocol(secure)}://{host}:{port}", api_endpoint)
|
||||||
"{}://{}:{}".format(get_protocol(secure), host, port), api_endpoint
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sends request and returns requests object.
|
# Sends request and returns requests object.
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,7 @@ def call(args: list[str], log: Logger, **kwargs: Any):
|
||||||
return command_output(args, **kwargs)
|
return command_output(args, **kwargs)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
log.debug(e.output.decode("utf8", "ignore"))
|
log.debug(e.output.decode("utf8", "ignore"))
|
||||||
raise ReplayGainError(
|
raise ReplayGainError(f"{args[0]} exited with status {e.returncode}")
|
||||||
"{} exited with status {}".format(args[0], e.returncode)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def db_to_lufs(db: float) -> float:
|
def db_to_lufs(db: float) -> float:
|
||||||
|
|
@ -170,9 +168,8 @@ class RgTask:
|
||||||
# `track_gains` without throwing FatalReplayGainError
|
# `track_gains` without throwing FatalReplayGainError
|
||||||
# => raise non-fatal exception & continue
|
# => raise non-fatal exception & continue
|
||||||
raise ReplayGainError(
|
raise ReplayGainError(
|
||||||
"ReplayGain backend `{}` failed for track {}".format(
|
f"ReplayGain backend `{self.backend_name}` failed for track"
|
||||||
self.backend_name, item
|
f" {item}"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._store_track_gain(item, self.track_gains[0])
|
self._store_track_gain(item, self.track_gains[0])
|
||||||
|
|
@ -191,10 +188,8 @@ class RgTask:
|
||||||
# `album_gain` without throwing FatalReplayGainError
|
# `album_gain` without throwing FatalReplayGainError
|
||||||
# => raise non-fatal exception & continue
|
# => raise non-fatal exception & continue
|
||||||
raise ReplayGainError(
|
raise ReplayGainError(
|
||||||
"ReplayGain backend `{}` failed "
|
f"ReplayGain backend `{self.backend_name}` failed "
|
||||||
"for some tracks in album {}".format(
|
f"for some tracks in album {self.album}"
|
||||||
self.backend_name, self.album
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for item, track_gain in zip(self.items, self.track_gains):
|
for item, track_gain in zip(self.items, self.track_gains):
|
||||||
self._store_track_gain(item, track_gain)
|
self._store_track_gain(item, track_gain)
|
||||||
|
|
@ -501,12 +496,10 @@ class FfmpegBackend(Backend):
|
||||||
if self._parse_float(b"M: " + line[1]) >= gating_threshold:
|
if self._parse_float(b"M: " + line[1]) >= gating_threshold:
|
||||||
n_blocks += 1
|
n_blocks += 1
|
||||||
self._log.debug(
|
self._log.debug(
|
||||||
"{}: {} blocks over {} LUFS".format(
|
f"{item}: {n_blocks} blocks over {gating_threshold} LUFS"
|
||||||
item, n_blocks, gating_threshold
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._log.debug("{}: gain {} LU, peak {}".format(item, gain, peak))
|
self._log.debug(f"{item}: gain {gain} LU, peak {peak}")
|
||||||
|
|
||||||
return Gain(gain, peak), n_blocks
|
return Gain(gain, peak), n_blocks
|
||||||
|
|
||||||
|
|
@ -526,9 +519,7 @@ class FfmpegBackend(Backend):
|
||||||
if output[i].startswith(search):
|
if output[i].startswith(search):
|
||||||
return i
|
return i
|
||||||
raise ReplayGainError(
|
raise ReplayGainError(
|
||||||
"ffmpeg output: missing {} after line {}".format(
|
f"ffmpeg output: missing {search!r} after line {start_line}"
|
||||||
repr(search), start_line
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _parse_float(self, line: bytes) -> float:
|
def _parse_float(self, line: bytes) -> float:
|
||||||
|
|
@ -575,7 +566,7 @@ class CommandBackend(Backend):
|
||||||
# Explicit executable path.
|
# Explicit executable path.
|
||||||
if not os.path.isfile(self.command):
|
if not os.path.isfile(self.command):
|
||||||
raise FatalReplayGainError(
|
raise FatalReplayGainError(
|
||||||
"replaygain command does not exist: {}".format(self.command)
|
f"replaygain command does not exist: {self.command}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Check whether the program is in $PATH.
|
# Check whether the program is in $PATH.
|
||||||
|
|
@ -1229,10 +1220,8 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
|
|
||||||
if self.backend_name not in BACKENDS:
|
if self.backend_name not in BACKENDS:
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"Selected ReplayGain backend {} is not supported. "
|
f"Selected ReplayGain backend {self.backend_name} is not"
|
||||||
"Please select one of: {}".format(
|
f" supported. Please select one of: {', '.join(BACKENDS)}"
|
||||||
self.backend_name, ", ".join(BACKENDS.keys())
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# FIXME: Consider renaming the configuration option to 'peak_method'
|
# FIXME: Consider renaming the configuration option to 'peak_method'
|
||||||
|
|
@ -1240,10 +1229,9 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
peak_method = self.config["peak"].as_str()
|
peak_method = self.config["peak"].as_str()
|
||||||
if peak_method not in PeakMethod.__members__:
|
if peak_method not in PeakMethod.__members__:
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"Selected ReplayGain peak method {} is not supported. "
|
f"Selected ReplayGain peak method {peak_method} is not"
|
||||||
"Please select one of: {}".format(
|
" supported. Please select one of:"
|
||||||
peak_method, ", ".join(PeakMethod.__members__)
|
f" {', '.join(PeakMethod.__members__)}"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
# This only applies to plain old rg tags, r128 doesn't store peak
|
# This only applies to plain old rg tags, r128 doesn't store peak
|
||||||
# values.
|
# values.
|
||||||
|
|
@ -1526,18 +1514,16 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
if opts.album:
|
if opts.album:
|
||||||
albums = lib.albums(args)
|
albums = lib.albums(args)
|
||||||
self._log.info(
|
self._log.info(
|
||||||
"Analyzing {} albums ~ {} backend...".format(
|
f"Analyzing {len(albums)} albums ~"
|
||||||
len(albums), self.backend_name
|
f" {self.backend_name} backend..."
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for album in albums:
|
for album in albums:
|
||||||
self.handle_album(album, write, force)
|
self.handle_album(album, write, force)
|
||||||
else:
|
else:
|
||||||
items = lib.items(args)
|
items = lib.items(args)
|
||||||
self._log.info(
|
self._log.info(
|
||||||
"Analyzing {} tracks ~ {} backend...".format(
|
f"Analyzing {len(items)} tracks ~"
|
||||||
len(items), self.backend_name
|
f" {self.backend_name} backend..."
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for item in items:
|
for item in items:
|
||||||
self.handle_track(item, write, force)
|
self.handle_track(item, write, force)
|
||||||
|
|
@ -1565,8 +1551,10 @@ class ReplayGainPlugin(BeetsPlugin):
|
||||||
dest="force",
|
dest="force",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="analyze all files, including those that "
|
help=(
|
||||||
"already have ReplayGain metadata",
|
"analyze all files, including those that already have"
|
||||||
|
" ReplayGain metadata"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
cmd.parser.add_option(
|
cmd.parser.add_option(
|
||||||
"-w",
|
"-w",
|
||||||
|
|
|
||||||
|
|
@ -138,10 +138,9 @@ class SmartPlaylistPlugin(BeetsPlugin):
|
||||||
if name in args
|
if name in args
|
||||||
}
|
}
|
||||||
if not playlists:
|
if not playlists:
|
||||||
|
unmatched = [name for name, _, _ in self._unmatched_playlists]
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"No playlist matching any of {} found".format(
|
f"No playlist matching any of {unmatched} found"
|
||||||
[name for name, _, _ in self._unmatched_playlists]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._matched_playlists = playlists
|
self._matched_playlists = playlists
|
||||||
|
|
@ -331,8 +330,9 @@ class SmartPlaylistPlugin(BeetsPlugin):
|
||||||
for key, value in attr
|
for key, value in attr
|
||||||
]
|
]
|
||||||
attrs = "".join(al)
|
attrs = "".join(al)
|
||||||
comment = "#EXTINF:{}{},{} - {}\n".format(
|
comment = (
|
||||||
int(item.length), attrs, item.artist, item.title
|
f"#EXTINF:{int(item.length)}{attrs},"
|
||||||
|
f"{item.artist} - {item.title}\n"
|
||||||
)
|
)
|
||||||
f.write(comment.encode("utf-8") + entry.uri + b"\n")
|
f.write(comment.encode("utf-8") + entry.uri + b"\n")
|
||||||
# Send an event when playlists were updated.
|
# Send an event when playlists were updated.
|
||||||
|
|
|
||||||
|
|
@ -168,8 +168,9 @@ class SpotifyPlugin(
|
||||||
c_secret: str = self.config["client_secret"].as_str()
|
c_secret: str = self.config["client_secret"].as_str()
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Basic {}".format(
|
"Authorization": (
|
||||||
base64.b64encode(f"{c_id}:{c_secret}".encode()).decode()
|
"Basic"
|
||||||
|
f" {base64.b64encode(f'{c_id}:{c_secret}'.encode()).decode()}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
|
|
@ -182,7 +183,7 @@ class SpotifyPlugin(
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"Spotify authorization failed: {}\n{}".format(e, response.text)
|
f"Spotify authorization failed: {e}\n{response.text}"
|
||||||
)
|
)
|
||||||
self.access_token = response.json()["access_token"]
|
self.access_token = response.json()["access_token"]
|
||||||
|
|
||||||
|
|
@ -236,7 +237,7 @@ class SpotifyPlugin(
|
||||||
if e.response.status_code == 401:
|
if e.response.status_code == 401:
|
||||||
self._log.debug(
|
self._log.debug(
|
||||||
f"{self.data_source} access token has expired. "
|
f"{self.data_source} access token has expired. "
|
||||||
f"Reauthenticating."
|
"Reauthenticating."
|
||||||
)
|
)
|
||||||
self._authenticate()
|
self._authenticate()
|
||||||
return self._handle_response(
|
return self._handle_response(
|
||||||
|
|
@ -314,9 +315,7 @@ class SpotifyPlugin(
|
||||||
else:
|
else:
|
||||||
raise ui.UserError(
|
raise ui.UserError(
|
||||||
"Invalid `release_date_precision` returned "
|
"Invalid `release_date_precision` returned "
|
||||||
"by {} API: '{}'".format(
|
f"by {self.data_source} API: '{release_date_precision}'"
|
||||||
self.data_source, release_date_precision
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tracks_data = album_data["tracks"]
|
tracks_data = album_data["tracks"]
|
||||||
|
|
@ -472,17 +471,17 @@ class SpotifyPlugin(
|
||||||
"-m",
|
"-m",
|
||||||
"--mode",
|
"--mode",
|
||||||
action="store",
|
action="store",
|
||||||
help='"open" to open {} with playlist, '
|
help=(
|
||||||
'"list" to print (default)'.format(self.data_source),
|
f'"open" to open {self.data_source} with playlist, '
|
||||||
|
'"list" to print (default)'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
spotify_cmd.parser.add_option(
|
spotify_cmd.parser.add_option(
|
||||||
"-f",
|
"-f",
|
||||||
"--show-failures",
|
"--show-failures",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
dest="show_failures",
|
dest="show_failures",
|
||||||
help="list tracks that did not match a {} ID".format(
|
help=f"list tracks that did not match a {self.data_source} ID",
|
||||||
self.data_source
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
spotify_cmd.func = queries
|
spotify_cmd.func = queries
|
||||||
|
|
||||||
|
|
@ -647,9 +646,7 @@ class SpotifyPlugin(
|
||||||
spotify_ids = [track_data["id"] for track_data in results]
|
spotify_ids = [track_data["id"] for track_data in results]
|
||||||
if self.config["mode"].get() == "open":
|
if self.config["mode"].get() == "open":
|
||||||
self._log.info(
|
self._log.info(
|
||||||
"Attempting to open {} with playlist".format(
|
f"Attempting to open {self.data_source} with playlist"
|
||||||
self.data_source
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
spotify_url = "spotify:trackset:Playlist:" + ",".join(
|
spotify_url = "spotify:trackset:Playlist:" + ",".join(
|
||||||
spotify_ids
|
spotify_ids
|
||||||
|
|
|
||||||
|
|
@ -168,9 +168,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin):
|
||||||
params["v"] = "1.12.0"
|
params["v"] = "1.12.0"
|
||||||
params["c"] = "beets"
|
params["c"] = "beets"
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
"{}/rest/{}?{}".format(
|
f"{self.config['base_url'].get()}/rest/{endpoint}?{urlencode(params)}",
|
||||||
self.config["base_url"].get(), endpoint, urlencode(params)
|
|
||||||
),
|
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
return resp
|
return resp
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ class ThumbnailsPlugin(BeetsPlugin):
|
||||||
artfile = os.path.split(album.artpath)[1]
|
artfile = os.path.split(album.artpath)[1]
|
||||||
with open(syspath(outfilename), "w") as f:
|
with open(syspath(outfilename), "w") as f:
|
||||||
f.write("[Desktop Entry]\n")
|
f.write("[Desktop Entry]\n")
|
||||||
f.write("Icon=./{}".format(artfile.decode("utf-8")))
|
f.write(f"Icon=./{artfile.decode('utf-8')}")
|
||||||
f.close()
|
f.close()
|
||||||
self._log.debug("Wrote file {0}", displayable_path(outfilename))
|
self._log.debug("Wrote file {0}", displayable_path(outfilename))
|
||||||
|
|
||||||
|
|
@ -266,9 +266,7 @@ class GioURI(URIGetter):
|
||||||
g_file_ptr = self.libgio.g_file_new_for_path(path)
|
g_file_ptr = self.libgio.g_file_new_for_path(path)
|
||||||
if not g_file_ptr:
|
if not g_file_ptr:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"No gfile pointer received for {}".format(
|
f"No gfile pointer received for {displayable_path(path)}"
|
||||||
displayable_path(path)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,6 @@ class TypesPlugin(BeetsPlugin):
|
||||||
mytypes[key] = types.DATE
|
mytypes[key] = types.DATE
|
||||||
else:
|
else:
|
||||||
raise ConfigValueError(
|
raise ConfigValueError(
|
||||||
"unknown type '{}' for the '{}' field".format(value, key)
|
f"unknown type '{value}' for the '{key}' field"
|
||||||
)
|
)
|
||||||
return mytypes
|
return mytypes
|
||||||
|
|
|
||||||
|
|
@ -232,9 +232,7 @@ def _get_unique_table_field_values(model, field, sort_field):
|
||||||
raise KeyError
|
raise KeyError
|
||||||
with g.lib.transaction() as tx:
|
with g.lib.transaction() as tx:
|
||||||
rows = tx.query(
|
rows = tx.query(
|
||||||
"SELECT DISTINCT '{}' FROM '{}' ORDER BY '{}'".format(
|
f"SELECT DISTINCT '{field}' FROM '{model._table}' ORDER BY '{sort_field}'"
|
||||||
field, model._table, sort_field
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return [row[0] for row in rows]
|
return [row[0] for row in rows]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,7 @@ select = [
|
||||||
"PT", # flake8-pytest-style
|
"PT", # flake8-pytest-style
|
||||||
# "RUF", # ruff
|
# "RUF", # ruff
|
||||||
# "UP", # pyupgrade
|
# "UP", # pyupgrade
|
||||||
|
"UP032", # use f-string instead of format call
|
||||||
"TCH", # flake8-type-checking
|
"TCH", # flake8-type-checking
|
||||||
"W", # pycodestyle
|
"W", # pycodestyle
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,8 @@ class CAAHelper:
|
||||||
MBID_RELASE = "rid"
|
MBID_RELASE = "rid"
|
||||||
MBID_GROUP = "rgid"
|
MBID_GROUP = "rgid"
|
||||||
|
|
||||||
RELEASE_URL = "coverartarchive.org/release/{}".format(MBID_RELASE)
|
RELEASE_URL = f"coverartarchive.org/release/{MBID_RELASE}"
|
||||||
GROUP_URL = "coverartarchive.org/release-group/{}".format(MBID_GROUP)
|
GROUP_URL = f"coverartarchive.org/release-group/{MBID_GROUP}"
|
||||||
|
|
||||||
RELEASE_URL = "https://" + RELEASE_URL
|
RELEASE_URL = "https://" + RELEASE_URL
|
||||||
GROUP_URL = "https://" + GROUP_URL
|
GROUP_URL = "https://" + GROUP_URL
|
||||||
|
|
@ -305,10 +305,8 @@ class FSArtTest(UseThePlugin):
|
||||||
class CombinedTest(FetchImageTestCase, CAAHelper):
|
class CombinedTest(FetchImageTestCase, CAAHelper):
|
||||||
ASIN = "xxxx"
|
ASIN = "xxxx"
|
||||||
MBID = "releaseid"
|
MBID = "releaseid"
|
||||||
AMAZON_URL = "https://images.amazon.com/images/P/{}.01.LZZZZZZZ.jpg".format(
|
AMAZON_URL = f"https://images.amazon.com/images/P/{ASIN}.01.LZZZZZZZ.jpg"
|
||||||
ASIN
|
AAO_URL = f"https://www.albumart.org/index_detail.php?asin={ASIN}"
|
||||||
)
|
|
||||||
AAO_URL = "https://www.albumart.org/index_detail.php?asin={}".format(ASIN)
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,12 @@ class ConvertMixin:
|
||||||
"""
|
"""
|
||||||
if re.search("[^a-zA-Z0-9]", tag):
|
if re.search("[^a-zA-Z0-9]", tag):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"tag '{}' must only contain letters and digits".format(tag)
|
f"tag '{tag}' must only contain letters and digits"
|
||||||
)
|
)
|
||||||
|
|
||||||
# A Python script that copies the file and appends a tag.
|
# A Python script that copies the file and appends a tag.
|
||||||
stub = os.path.join(_common.RSRC, b"convert_stub.py").decode("utf-8")
|
stub = os.path.join(_common.RSRC, b"convert_stub.py").decode("utf-8")
|
||||||
return "{} {} $source $dest {}".format(
|
return f"{shell_quote(sys.executable)} {shell_quote(stub)} $source $dest {tag}"
|
||||||
shell_quote(sys.executable), shell_quote(stub), tag
|
|
||||||
)
|
|
||||||
|
|
||||||
def file_endswith(self, path: Path, tag: str):
|
def file_endswith(self, path: Path, tag: str):
|
||||||
"""Check the path is a file and if its content ends with `tag`."""
|
"""Check the path is a file and if its content ends with `tag`."""
|
||||||
|
|
|
||||||
|
|
@ -144,9 +144,7 @@ class EmbedartCliTest(IOMixin, PluginMixin, FetchImageHelper, BeetsTestCase):
|
||||||
if os.path.isfile(syspath(tmp_path)):
|
if os.path.isfile(syspath(tmp_path)):
|
||||||
os.remove(syspath(tmp_path))
|
os.remove(syspath(tmp_path))
|
||||||
self.fail(
|
self.fail(
|
||||||
"Artwork file {} was not deleted".format(
|
f"Artwork file {displayable_path(tmp_path)} was not deleted"
|
||||||
displayable_path(tmp_path)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_art_file_missing(self):
|
def test_art_file_missing(self):
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ class IPFSPluginTest(PluginTestCase):
|
||||||
try:
|
try:
|
||||||
if check_item.get("ipfs", with_album=False):
|
if check_item.get("ipfs", with_album=False):
|
||||||
ipfs_item = os.fsdecode(os.path.basename(want_item.path))
|
ipfs_item = os.fsdecode(os.path.basename(want_item.path))
|
||||||
want_path = "/ipfs/{}/{}".format(test_album.ipfs, ipfs_item)
|
want_path = f"/ipfs/{test_album.ipfs}/{ipfs_item}"
|
||||||
want_path = bytestring_path(want_path)
|
want_path = bytestring_path(want_path)
|
||||||
assert check_item.path == want_path
|
assert check_item.path == want_path
|
||||||
assert (
|
assert (
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,7 @@ class PlayPluginTest(CleanupModulesMixin, PluginTestCase):
|
||||||
open_mock.assert_called_once_with(ANY, open_anything())
|
open_mock.assert_called_once_with(ANY, open_anything())
|
||||||
with open(open_mock.call_args[0][0][0], "rb") as f:
|
with open(open_mock.call_args[0][0][0], "rb") as f:
|
||||||
playlist = f.read().decode("utf-8")
|
playlist = f.read().decode("utf-8")
|
||||||
assert (
|
assert f"{self.item.filepath.parent}\n" == playlist
|
||||||
f"{os.path.dirname(self.item.path.decode('utf-8'))}\n" == playlist
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_raw(self, open_mock):
|
def test_raw(self, open_mock):
|
||||||
self.config["play"]["raw"] = True
|
self.config["play"]["raw"] = True
|
||||||
|
|
@ -125,9 +123,7 @@ class PlayPluginTest(CleanupModulesMixin, PluginTestCase):
|
||||||
self.config["play"]["warning_threshold"] = 1
|
self.config["play"]["warning_threshold"] = 1
|
||||||
self.other_item = self.add_item(title="another NiceTitle")
|
self.other_item = self.add_item(title="another NiceTitle")
|
||||||
|
|
||||||
expected_playlist = "{}\n{}".format(
|
expected_playlist = f"{self.item.filepath}\n{self.other_item.filepath}"
|
||||||
self.item.path.decode("utf-8"), self.other_item.path.decode("utf-8")
|
|
||||||
)
|
|
||||||
|
|
||||||
with control_stdin("a"):
|
with control_stdin("a"):
|
||||||
self.run_and_assert(
|
self.run_and_assert(
|
||||||
|
|
|
||||||
|
|
@ -91,14 +91,7 @@ class PlaylistQueryTest:
|
||||||
assert {i.title for i in results} == {"some item", "another item"}
|
assert {i.title for i in results} == {"some item", "another item"}
|
||||||
|
|
||||||
def test_path_query_with_absolute_paths_in_playlist(self):
|
def test_path_query_with_absolute_paths_in_playlist(self):
|
||||||
q = "playlist:{}".format(
|
q = f"playlist:{quote(os.path.join(self.playlist_dir, 'absolute.m3u'))}"
|
||||||
quote(
|
|
||||||
os.path.join(
|
|
||||||
self.playlist_dir,
|
|
||||||
"absolute.m3u",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
results = self.lib.items(q)
|
results = self.lib.items(q)
|
||||||
assert {i.title for i in results} == {"some item", "another item"}
|
assert {i.title for i in results} == {"some item", "another item"}
|
||||||
|
|
||||||
|
|
@ -108,14 +101,7 @@ class PlaylistQueryTest:
|
||||||
assert {i.title for i in results} == {"some item", "another item"}
|
assert {i.title for i in results} == {"some item", "another item"}
|
||||||
|
|
||||||
def test_path_query_with_relative_paths_in_playlist(self):
|
def test_path_query_with_relative_paths_in_playlist(self):
|
||||||
q = "playlist:{}".format(
|
q = f"playlist:{quote(os.path.join(self.playlist_dir, 'relative.m3u'))}"
|
||||||
quote(
|
|
||||||
os.path.join(
|
|
||||||
self.playlist_dir,
|
|
||||||
"relative.m3u",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
results = self.lib.items(q)
|
results = self.lib.items(q)
|
||||||
assert {i.title for i in results} == {"some item", "another item"}
|
assert {i.title for i in results} == {"some item", "another item"}
|
||||||
|
|
||||||
|
|
@ -125,15 +111,7 @@ class PlaylistQueryTest:
|
||||||
assert set(results) == set()
|
assert set(results) == set()
|
||||||
|
|
||||||
def test_path_query_with_nonexisting_playlist(self):
|
def test_path_query_with_nonexisting_playlist(self):
|
||||||
q = "playlist:{}".format(
|
q = f"playlist:{os.path.join(self.playlist_dir, 'nonexisting.m3u')!r}"
|
||||||
quote(
|
|
||||||
os.path.join(
|
|
||||||
self.playlist_dir,
|
|
||||||
self.playlist_dir,
|
|
||||||
"nonexisting.m3u",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
results = self.lib.items(q)
|
results = self.lib.items(q)
|
||||||
assert set(results) == set()
|
assert set(results) == set()
|
||||||
|
|
||||||
|
|
@ -141,20 +119,22 @@ class PlaylistQueryTest:
|
||||||
class PlaylistTestRelativeToLib(PlaylistQueryTest, PlaylistTestCase):
|
class PlaylistTestRelativeToLib(PlaylistQueryTest, PlaylistTestCase):
|
||||||
def setup_test(self):
|
def setup_test(self):
|
||||||
with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f:
|
with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f:
|
||||||
f.write(
|
f.writelines(
|
||||||
"{}\n".format(os.path.join(self.music_dir, "a", "b", "c.mp3"))
|
[
|
||||||
)
|
os.path.join(self.music_dir, "a", "b", "c.mp3") + "\n",
|
||||||
f.write(
|
os.path.join(self.music_dir, "d", "e", "f.mp3") + "\n",
|
||||||
"{}\n".format(os.path.join(self.music_dir, "d", "e", "f.mp3"))
|
os.path.join(self.music_dir, "nonexisting.mp3") + "\n",
|
||||||
)
|
]
|
||||||
f.write(
|
|
||||||
"{}\n".format(os.path.join(self.music_dir, "nonexisting.mp3"))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f:
|
with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f:
|
||||||
f.write("{}\n".format(os.path.join("a", "b", "c.mp3")))
|
f.writelines(
|
||||||
f.write("{}\n".format(os.path.join("d", "e", "f.mp3")))
|
[
|
||||||
f.write("{}\n".format("nonexisting.mp3"))
|
os.path.join("a", "b", "c.mp3") + "\n",
|
||||||
|
os.path.join("d", "e", "f.mp3") + "\n",
|
||||||
|
"nonexisting.mp3" + "\n",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
self.config["playlist"]["relative_to"] = "library"
|
self.config["playlist"]["relative_to"] = "library"
|
||||||
|
|
||||||
|
|
@ -162,20 +142,22 @@ class PlaylistTestRelativeToLib(PlaylistQueryTest, PlaylistTestCase):
|
||||||
class PlaylistTestRelativeToDir(PlaylistQueryTest, PlaylistTestCase):
|
class PlaylistTestRelativeToDir(PlaylistQueryTest, PlaylistTestCase):
|
||||||
def setup_test(self):
|
def setup_test(self):
|
||||||
with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f:
|
with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f:
|
||||||
f.write(
|
f.writelines(
|
||||||
"{}\n".format(os.path.join(self.music_dir, "a", "b", "c.mp3"))
|
[
|
||||||
)
|
os.path.join(self.music_dir, "a", "b", "c.mp3") + "\n",
|
||||||
f.write(
|
os.path.join(self.music_dir, "d", "e", "f.mp3") + "\n",
|
||||||
"{}\n".format(os.path.join(self.music_dir, "d", "e", "f.mp3"))
|
os.path.join(self.music_dir, "nonexisting.mp3") + "\n",
|
||||||
)
|
]
|
||||||
f.write(
|
|
||||||
"{}\n".format(os.path.join(self.music_dir, "nonexisting.mp3"))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f:
|
with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f:
|
||||||
f.write("{}\n".format(os.path.join("a", "b", "c.mp3")))
|
f.writelines(
|
||||||
f.write("{}\n".format(os.path.join("d", "e", "f.mp3")))
|
[
|
||||||
f.write("{}\n".format("nonexisting.mp3"))
|
os.path.join("a", "b", "c.mp3") + "\n",
|
||||||
|
os.path.join("d", "e", "f.mp3") + "\n",
|
||||||
|
"nonexisting.mp3" + "\n",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
self.config["playlist"]["relative_to"] = self.music_dir
|
self.config["playlist"]["relative_to"] = self.music_dir
|
||||||
|
|
||||||
|
|
@ -183,40 +165,33 @@ class PlaylistTestRelativeToDir(PlaylistQueryTest, PlaylistTestCase):
|
||||||
class PlaylistTestRelativeToPls(PlaylistQueryTest, PlaylistTestCase):
|
class PlaylistTestRelativeToPls(PlaylistQueryTest, PlaylistTestCase):
|
||||||
def setup_test(self):
|
def setup_test(self):
|
||||||
with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f:
|
with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f:
|
||||||
f.write(
|
f.writelines(
|
||||||
"{}\n".format(os.path.join(self.music_dir, "a", "b", "c.mp3"))
|
[
|
||||||
)
|
os.path.join(self.music_dir, "a", "b", "c.mp3") + "\n",
|
||||||
f.write(
|
os.path.join(self.music_dir, "d", "e", "f.mp3") + "\n",
|
||||||
"{}\n".format(os.path.join(self.music_dir, "d", "e", "f.mp3"))
|
os.path.join(self.music_dir, "nonexisting.mp3") + "\n",
|
||||||
)
|
]
|
||||||
f.write(
|
|
||||||
"{}\n".format(os.path.join(self.music_dir, "nonexisting.mp3"))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f:
|
with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f:
|
||||||
f.write(
|
f.writelines(
|
||||||
"{}\n".format(
|
[
|
||||||
os.path.relpath(
|
os.path.relpath(
|
||||||
os.path.join(self.music_dir, "a", "b", "c.mp3"),
|
os.path.join(self.music_dir, "a", "b", "c.mp3"),
|
||||||
start=self.playlist_dir,
|
start=self.playlist_dir,
|
||||||
)
|
)
|
||||||
)
|
+ "\n",
|
||||||
)
|
|
||||||
f.write(
|
|
||||||
"{}\n".format(
|
|
||||||
os.path.relpath(
|
os.path.relpath(
|
||||||
os.path.join(self.music_dir, "d", "e", "f.mp3"),
|
os.path.join(self.music_dir, "d", "e", "f.mp3"),
|
||||||
start=self.playlist_dir,
|
start=self.playlist_dir,
|
||||||
)
|
)
|
||||||
)
|
+ "\n",
|
||||||
)
|
|
||||||
f.write(
|
|
||||||
"{}\n".format(
|
|
||||||
os.path.relpath(
|
os.path.relpath(
|
||||||
os.path.join(self.music_dir, "nonexisting.mp3"),
|
os.path.join(self.music_dir, "nonexisting.mp3"),
|
||||||
start=self.playlist_dir,
|
start=self.playlist_dir,
|
||||||
)
|
)
|
||||||
)
|
+ "\n",
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.config["playlist"]["relative_to"] = "playlist"
|
self.config["playlist"]["relative_to"] = "playlist"
|
||||||
|
|
@ -226,20 +201,22 @@ class PlaylistTestRelativeToPls(PlaylistQueryTest, PlaylistTestCase):
|
||||||
class PlaylistUpdateTest:
|
class PlaylistUpdateTest:
|
||||||
def setup_test(self):
|
def setup_test(self):
|
||||||
with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f:
|
with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f:
|
||||||
f.write(
|
f.writelines(
|
||||||
"{}\n".format(os.path.join(self.music_dir, "a", "b", "c.mp3"))
|
[
|
||||||
)
|
os.path.join(self.music_dir, "a", "b", "c.mp3") + "\n",
|
||||||
f.write(
|
os.path.join(self.music_dir, "d", "e", "f.mp3") + "\n",
|
||||||
"{}\n".format(os.path.join(self.music_dir, "d", "e", "f.mp3"))
|
os.path.join(self.music_dir, "nonexisting.mp3") + "\n",
|
||||||
)
|
]
|
||||||
f.write(
|
|
||||||
"{}\n".format(os.path.join(self.music_dir, "nonexisting.mp3"))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f:
|
with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f:
|
||||||
f.write("{}\n".format(os.path.join("a", "b", "c.mp3")))
|
f.writelines(
|
||||||
f.write("{}\n".format(os.path.join("d", "e", "f.mp3")))
|
[
|
||||||
f.write("{}\n".format("nonexisting.mp3"))
|
os.path.join("a", "b", "c.mp3") + "\n",
|
||||||
|
os.path.join("d", "e", "f.mp3") + "\n",
|
||||||
|
"nonexisting.mp3" + "\n",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
self.config["playlist"]["auto"] = True
|
self.config["playlist"]["auto"] = True
|
||||||
self.config["playlist"]["relative_to"] = "library"
|
self.config["playlist"]["relative_to"] = "library"
|
||||||
|
|
@ -249,9 +226,7 @@ class PlaylistTestItemMoved(PlaylistUpdateTest, PlaylistTestCase):
|
||||||
def test_item_moved(self):
|
def test_item_moved(self):
|
||||||
# Emit item_moved event for an item that is in a playlist
|
# Emit item_moved event for an item that is in a playlist
|
||||||
results = self.lib.items(
|
results = self.lib.items(
|
||||||
"path:{}".format(
|
f"path:{quote(os.path.join(self.music_dir, 'd', 'e', 'f.mp3'))}"
|
||||||
quote(os.path.join(self.music_dir, "d", "e", "f.mp3"))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
item = results[0]
|
item = results[0]
|
||||||
beets.plugins.send(
|
beets.plugins.send(
|
||||||
|
|
@ -265,9 +240,7 @@ class PlaylistTestItemMoved(PlaylistUpdateTest, PlaylistTestCase):
|
||||||
|
|
||||||
# Emit item_moved event for an item that is not in a playlist
|
# Emit item_moved event for an item that is not in a playlist
|
||||||
results = self.lib.items(
|
results = self.lib.items(
|
||||||
"path:{}".format(
|
f"path:{quote(os.path.join(self.music_dir, 'x', 'y', 'z.mp3'))}"
|
||||||
quote(os.path.join(self.music_dir, "x", "y", "z.mp3"))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
item = results[0]
|
item = results[0]
|
||||||
beets.plugins.send(
|
beets.plugins.send(
|
||||||
|
|
@ -309,18 +282,14 @@ class PlaylistTestItemRemoved(PlaylistUpdateTest, PlaylistTestCase):
|
||||||
def test_item_removed(self):
|
def test_item_removed(self):
|
||||||
# Emit item_removed event for an item that is in a playlist
|
# Emit item_removed event for an item that is in a playlist
|
||||||
results = self.lib.items(
|
results = self.lib.items(
|
||||||
"path:{}".format(
|
f"path:{quote(os.path.join(self.music_dir, 'd', 'e', 'f.mp3'))}"
|
||||||
quote(os.path.join(self.music_dir, "d", "e", "f.mp3"))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
item = results[0]
|
item = results[0]
|
||||||
beets.plugins.send("item_removed", item=item)
|
beets.plugins.send("item_removed", item=item)
|
||||||
|
|
||||||
# Emit item_removed event for an item that is not in a playlist
|
# Emit item_removed event for an item that is not in a playlist
|
||||||
results = self.lib.items(
|
results = self.lib.items(
|
||||||
"path:{}".format(
|
f"path:{quote(os.path.join(self.music_dir, 'x', 'y', 'z.mp3'))}"
|
||||||
quote(os.path.join(self.music_dir, "x", "y", "z.mp3"))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
item = results[0]
|
item = results[0]
|
||||||
beets.plugins.send("item_removed", item=item)
|
beets.plugins.send("item_removed", item=item)
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ class RandomTest(TestHelper, unittest.TestCase):
|
||||||
# Print a histogram (useful for debugging).
|
# Print a histogram (useful for debugging).
|
||||||
if histogram:
|
if histogram:
|
||||||
for i in range(len(self.items)):
|
for i in range(len(self.items)):
|
||||||
print("{:2d} {}".format(i, "*" * positions.count(i)))
|
print(f"{i:2d} {'*' * positions.count(i)}")
|
||||||
return self._stats(positions)
|
return self._stats(positions)
|
||||||
|
|
||||||
mean1, stdev1, median1 = experiment("artist")
|
mean1, stdev1, median1 = experiment("artist")
|
||||||
|
|
|
||||||
|
|
@ -204,9 +204,7 @@ class ReplayGainCliTest:
|
||||||
# This test is a lot less interesting if the backend cannot write
|
# This test is a lot less interesting if the backend cannot write
|
||||||
# both tag types.
|
# both tag types.
|
||||||
self.skipTest(
|
self.skipTest(
|
||||||
"r128 tags for opus not supported on backend {}".format(
|
f"r128 tags for opus not supported on backend {self.backend}"
|
||||||
self.backend
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
album_rg = self._add_album(1)
|
album_rg = self._add_album(1)
|
||||||
|
|
@ -263,9 +261,7 @@ class ReplayGainCliTest:
|
||||||
def test_cli_writes_only_r128_tags(self):
|
def test_cli_writes_only_r128_tags(self):
|
||||||
if not self.has_r128_support:
|
if not self.has_r128_support:
|
||||||
self.skipTest(
|
self.skipTest(
|
||||||
"r128 tags for opus not supported on backend {}".format(
|
f"r128 tags for opus not supported on backend {self.backend}"
|
||||||
self.backend
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
album = self._add_album(2, ext="opus")
|
album = self._add_album(2, ext="opus")
|
||||||
|
|
@ -299,9 +295,7 @@ class ReplayGainCliTest:
|
||||||
def test_r128_targetlevel_has_effect(self):
|
def test_r128_targetlevel_has_effect(self):
|
||||||
if not self.has_r128_support:
|
if not self.has_r128_support:
|
||||||
self.skipTest(
|
self.skipTest(
|
||||||
"r128 tags for opus not supported on backend {}".format(
|
f"r128 tags for opus not supported on backend {self.backend}"
|
||||||
self.backend
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
album = self._add_album(1, ext="opus")
|
album = self._add_album(1, ext="opus")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue