diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 7d9f0cee7..ae8e0ddf6 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -411,39 +411,6 @@ class BooleanQuery(MatchQuery[int]): super().__init__(field_name, pattern_int, fast) -class BytesQuery(FieldQuery[bytes]): - """Match a raw bytes field (i.e., a path). This is a necessary hack - to work around the `sqlite3` module's desire to treat `bytes` and - `unicode` equivalently in Python 2. Always use this query instead of - `MatchQuery` when matching on BLOB values. - """ - - def __init__(self, field_name: str, pattern: bytes | str | memoryview): - # Use a buffer/memoryview representation of the pattern for SQLite - # matching. This instructs SQLite to treat the blob as binary - # rather than encoded Unicode. - if isinstance(pattern, (str, bytes)): - if isinstance(pattern, str): - bytes_pattern = pattern.encode("utf-8") - else: - bytes_pattern = pattern - self.buf_pattern = memoryview(bytes_pattern) - elif isinstance(pattern, memoryview): - self.buf_pattern = pattern - bytes_pattern = bytes(pattern) - else: - raise ValueError("pattern must be bytes, str, or memoryview") - - super().__init__(field_name, bytes_pattern) - - def col_clause(self) -> tuple[str, Sequence[SQLiteType]]: - return self.field + " = ?", [self.buf_pattern] - - @classmethod - def value_match(cls, pattern: bytes, value: Any) -> bool: - return pattern == value - - class NumericQuery(FieldQuery[str]): """Matches numeric fields. A syntax using Ruby-style range ellipses (``..``) lets users specify one- or two-sided ranges. For example, diff --git a/beets/importer/tasks.py b/beets/importer/tasks.py index 75f04cf5a..441224b6b 100644 --- a/beets/importer/tasks.py +++ b/beets/importer/tasks.py @@ -26,7 +26,8 @@ from typing import TYPE_CHECKING, Callable, Iterable, Sequence import mediafile -from beets import autotag, config, dbcore, library, plugins, util +from beets import autotag, config, library, plugins, util +from beets.dbcore.query import PathQuery from .state import ImportState @@ -520,9 +521,7 @@ class ImportTask(BaseImportTask): ) replaced_album_ids = set() for item in self.imported_items(): - dup_items = list( - lib.items(query=dbcore.query.BytesQuery("path", item.path)) - ) + dup_items = list(lib.items(query=PathQuery("path", item.path))) self.replaced_items[item] = dup_items for dup_item in dup_items: if ( diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index b7033e41b..74dee550c 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -104,30 +104,15 @@ def _stream_encoding(stream, default="utf-8"): return stream.encoding or default -def decargs(arglist): - """Given a list of command-line argument bytestrings, attempts to - decode them to Unicode strings when running under Python 2. - """ - return arglist - - -def print_(*strings, **kwargs): +def print_(*strings: str, end: str = "\n") -> None: """Like print, but rather than raising an error when a character is not in the terminal's encoding's character set, just silently replaces it. - The arguments must be Unicode strings: `unicode` on Python 2; `str` on - Python 3. - The `end` keyword argument behaves similarly to the built-in `print` (it defaults to a newline). """ - if not strings: - strings = [""] - assert isinstance(strings[0], str) - - txt = " ".join(strings) - txt += kwargs.get("end", "\n") + txt = " ".join(strings or ("",)) + end # Encode the string and write it to stdout. # On Python 3, sys.stdout expects text strings and uses the @@ -1308,14 +1293,9 @@ class CommonOptionsParser(optparse.OptionParser): setattr(parser.values, option.dest, True) # Use the explicitly specified format, or the string from the option. - if fmt: - value = fmt - elif value: - (value,) = decargs([value]) - else: - value = "" - + value = fmt or value or "" parser.values.format = value + if target: config[target._format_config_key].set(value) else: diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 3117262f1..25af95646 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -28,7 +28,6 @@ import beets from beets import autotag, config, importer, library, logging, plugins, ui, util from beets.autotag import Recommendation, hooks from beets.ui import ( - decargs, input_, print_, print_column_layout, @@ -1303,7 +1302,7 @@ class TerminalImportSession(importer.ImportSession): # The import command. -def import_files(lib, paths, query): +def import_files(lib, paths: list[bytes], query): """Import the files in the given list of paths or matching the query. """ @@ -1334,7 +1333,7 @@ def import_files(lib, paths, query): plugins.send("import", lib=lib, paths=paths) -def import_func(lib, opts, args): +def import_func(lib, opts, args: list[str]): config["import"].set_args(opts) # Special case: --copy flag suppresses import_move (which would @@ -1343,7 +1342,7 @@ def import_func(lib, opts, args): config["import"]["move"] = False if opts.library: - query = decargs(args) + query = args paths = [] else: query = None @@ -1356,15 +1355,11 @@ def import_func(lib, opts, args): if not paths and not paths_from_logfiles: raise ui.UserError("no path specified") - # On Python 2, we used to get filenames as raw bytes, which is - # what we need. On Python 3, we need to undo the "helpful" - # conversion to Unicode strings to get the real bytestring - # filename. - paths = [os.fsencode(p) for p in paths] + byte_paths = [os.fsencode(p) for p in paths] paths_from_logfiles = [os.fsencode(p) for p in paths_from_logfiles] # Check the user-specified directories. - for path in paths: + for path in byte_paths: if not os.path.exists(syspath(normpath(path))): raise ui.UserError( "no such file or directory: {}".format( @@ -1385,14 +1380,14 @@ def import_func(lib, opts, args): ) continue - paths.append(path) + byte_paths.append(path) # If all paths were read from a logfile, and none of them exist, throw # an error if not paths: raise ui.UserError("none of the paths are importable") - import_files(lib, paths, query) + import_files(lib, byte_paths, query) import_cmd = ui.Subcommand( @@ -1596,7 +1591,7 @@ def list_items(lib, query, album, fmt=""): def list_func(lib, opts, args): - list_items(lib, decargs(args), opts.album) + list_items(lib, args, opts.album) list_cmd = ui.Subcommand("list", help="query the library", aliases=("ls",)) @@ -1739,7 +1734,7 @@ def update_func(lib, opts, args): return update_items( lib, - decargs(args), + args, opts.album, ui.should_move(opts.move), opts.pretend, @@ -1861,7 +1856,7 @@ def remove_items(lib, query, album, delete, force): def remove_func(lib, opts, args): - remove_items(lib, decargs(args), opts.album, opts.delete, opts.force) + remove_items(lib, args, opts.album, opts.delete, opts.force) remove_cmd = ui.Subcommand( @@ -1931,7 +1926,7 @@ Album artists: {}""".format( def stats_func(lib, opts, args): - show_stats(lib, decargs(args), opts.exact) + show_stats(lib, args, opts.exact) stats_cmd = ui.Subcommand( @@ -2059,7 +2054,7 @@ def modify_parse_args(args): def modify_func(lib, opts, args): - query, mods, dels = modify_parse_args(decargs(args)) + query, mods, dels = modify_parse_args(args) if not mods and not dels: raise ui.UserError("no modifications specified") modify_items( @@ -2217,7 +2212,7 @@ def move_func(lib, opts, args): move_items( lib, dest, - decargs(args), + args, opts.copy, opts.album, opts.pretend, @@ -2298,7 +2293,7 @@ def write_items(lib, query, pretend, force): def write_func(lib, opts, args): - write_items(lib, decargs(args), opts.pretend, opts.force) + write_items(lib, args, opts.pretend, opts.force) write_cmd = ui.Subcommand("write", help="write tag information to files") diff --git a/beets/util/__init__.py b/beets/util/__init__.py index c1c76c860..00c9ce05d 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -28,6 +28,7 @@ import sys import tempfile import traceback from collections import Counter +from collections.abc import Sequence from contextlib import suppress from enum import Enum from functools import cache @@ -41,7 +42,6 @@ from typing import ( AnyStr, Callable, Generic, - Iterable, NamedTuple, TypeVar, Union, @@ -53,23 +53,17 @@ import beets from beets.util import hidden if TYPE_CHECKING: - from collections.abc import Iterator, Sequence + from collections.abc import Iterable, Iterator from logging import Logger from beets.library import Item -if sys.version_info >= (3, 10): - from typing import TypeAlias -else: - from typing_extensions import TypeAlias - MAX_FILENAME_LENGTH = 200 WINDOWS_MAGIC_PREFIX = "\\\\?\\" T = TypeVar("T") -BytesOrStr = Union[str, bytes] -PathLike = Union[BytesOrStr, Path] -Replacements: TypeAlias = "Sequence[tuple[Pattern[str], str]]" +PathLike = Union[str, bytes, Path] +Replacements = Sequence[tuple[Pattern[str], str]] # Here for now to allow for a easy replace later on # once we can move to a PathLike (mainly used in importer) @@ -860,7 +854,9 @@ class CommandOutput(NamedTuple): stderr: bytes -def command_output(cmd: list[BytesOrStr], shell: bool = False) -> CommandOutput: +def command_output( + cmd: list[str] | list[bytes], shell: bool = False +) -> CommandOutput: """Runs the command and returns its output after it has exited. Returns a CommandOutput. The attributes ``stdout`` and ``stderr`` contain @@ -878,8 +874,6 @@ def command_output(cmd: list[BytesOrStr], shell: bool = False) -> CommandOutput: This replaces `subprocess.check_output` which can have problems if lots of output is sent to stderr. """ - converted_cmd = [os.fsdecode(a) for a in cmd] - devnull = subprocess.DEVNULL proc = subprocess.Popen( @@ -894,7 +888,7 @@ def command_output(cmd: list[BytesOrStr], shell: bool = False) -> CommandOutput: if proc.returncode: raise subprocess.CalledProcessError( returncode=proc.returncode, - cmd=" ".join(converted_cmd), + cmd=" ".join(map(os.fsdecode, cmd)), output=stdout + stderr, ) return CommandOutput(stdout, stderr) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 33b98c413..fe67c506e 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -214,9 +214,9 @@ class IMBackend(LocalBackend): else: return cls._version - convert_cmd: list[str | bytes] - identify_cmd: list[str | bytes] - compare_cmd: list[str | bytes] + convert_cmd: list[str] + identify_cmd: list[str] + compare_cmd: list[str] def __init__(self) -> None: """Initialize a wrapper around ImageMagick for local image operations. @@ -265,7 +265,7 @@ class IMBackend(LocalBackend): # with regards to the height. # ImageMagick already seems to default to no interlace, but we include # it here for the sake of explicitness. - cmd: list[str | bytes] = self.convert_cmd + [ + cmd: list[str] = self.convert_cmd + [ syspath(path_in, prefix=False), "-resize", f"{maxwidth}x>", @@ -295,7 +295,7 @@ class IMBackend(LocalBackend): return path_out def get_size(self, path_in: bytes) -> tuple[int, int] | None: - cmd: list[str | bytes] = self.identify_cmd + [ + cmd: list[str] = self.identify_cmd + [ "-format", "%w %h", syspath(path_in, prefix=False), @@ -480,10 +480,11 @@ class IMBackend(LocalBackend): return True def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None: - assignments = list( - chain.from_iterable(("-set", k, v) for k, v in metadata.items()) + assignments = chain.from_iterable( + ("-set", k, v) for k, v in metadata.items() ) - command = self.convert_cmd + [file, *assignments, file] + str_file = os.fsdecode(file) + command = self.convert_cmd + [str_file, *assignments, str_file] util.command_output(command) diff --git a/beetsplug/absubmit.py b/beetsplug/absubmit.py index 3c48f8897..3d3227ed2 100644 --- a/beetsplug/absubmit.py +++ b/beetsplug/absubmit.py @@ -137,7 +137,7 @@ only files which would be processed", ) else: # Get items from arguments - items = lib.items(ui.decargs(args)) + items = lib.items(args) self.opts = opts util.par_map(self.analyze_submit, items) diff --git a/beetsplug/acousticbrainz.py b/beetsplug/acousticbrainz.py index 714751ac9..56ac0f6c5 100644 --- a/beetsplug/acousticbrainz.py +++ b/beetsplug/acousticbrainz.py @@ -116,7 +116,7 @@ class AcousticPlugin(plugins.BeetsPlugin): ) def func(lib, opts, args): - items = lib.items(ui.decargs(args)) + items = lib.items(args) self._fetch_info( items, ui.should_write(), diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index 0903ebabf..0511d960d 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -204,7 +204,7 @@ class BadFiles(BeetsPlugin): def command(self, lib, opts, args): # Get items from arguments - items = lib.items(ui.decargs(args)) + items = lib.items(args) self.verbose = opts.verbose def check_and_print(item): diff --git a/beetsplug/bareasc.py b/beetsplug/bareasc.py index 3a52c41dd..d2852bb1d 100644 --- a/beetsplug/bareasc.py +++ b/beetsplug/bareasc.py @@ -23,7 +23,7 @@ from unidecode import unidecode from beets import ui from beets.dbcore.query import StringFieldQuery from beets.plugins import BeetsPlugin -from beets.ui import decargs, print_ +from beets.ui import print_ class BareascQuery(StringFieldQuery[str]): @@ -83,14 +83,13 @@ class BareascPlugin(BeetsPlugin): def unidecode_list(self, lib, opts, args): """Emulate normal 'list' command but with unidecode output.""" - query = decargs(args) album = opts.album # Copied from commands.py - list_items if album: - for album in lib.albums(query): + for album in lib.albums(args): bare = unidecode(str(album)) print_(bare) else: - for item in lib.items(query): + for item in lib.items(args): bare = unidecode(str(item)) print_(bare) diff --git a/beetsplug/bench.py b/beetsplug/bench.py index 62d512ce7..cf72527e8 100644 --- a/beetsplug/bench.py +++ b/beetsplug/bench.py @@ -125,7 +125,7 @@ class BenchmarkPlugin(BeetsPlugin): "-i", "--id", default=None, help="album ID to match against" ) match_bench_cmd.func = lambda lib, opts, args: match_benchmark( - lib, opts.profile, ui.decargs(args), opts.id + lib, opts.profile, args, opts.id ) return [aunique_bench_cmd, match_bench_cmd] diff --git a/beetsplug/bpm.py b/beetsplug/bpm.py index 946769cdc..145986a95 100644 --- a/beetsplug/bpm.py +++ b/beetsplug/bpm.py @@ -63,9 +63,8 @@ class BPMPlugin(BeetsPlugin): return [cmd] def command(self, lib, opts, args): - items = lib.items(ui.decargs(args)) write = ui.should_write() - self.get_bpm(items, write) + self.get_bpm(lib.items(args), write) def get_bpm(self, items, write=False): overwrite = self.config["overwrite"].get(bool) diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py index 05be94c99..ccd781b28 100644 --- a/beetsplug/bpsync.py +++ b/beetsplug/bpsync.py @@ -65,10 +65,9 @@ class BPSyncPlugin(BeetsPlugin): move = ui.should_move(opts.move) pretend = opts.pretend write = ui.should_write(opts.write) - query = ui.decargs(args) - self.singletons(lib, query, move, pretend, write) - self.albums(lib, query, move, pretend, write) + self.singletons(lib, args, move, pretend, write) + self.albums(lib, args, move, pretend, write) def singletons(self, lib, query, move, pretend, write): """Retrieve and apply info from the autotagger for items matched by diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 5c718154b..de3ac525a 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -233,7 +233,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): apikey = config["acoustid"]["apikey"].as_str() except confuse.NotFoundError: raise ui.UserError("no Acoustid user API key provided") - submit_items(self._log, apikey, lib.items(ui.decargs(args))) + submit_items(self._log, apikey, lib.items(args)) submit_cmd.func = submit_cmd_func @@ -242,7 +242,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): ) def fingerprint_cmd_func(lib, opts, args): - for item in lib.items(ui.decargs(args)): + for item in lib.items(args): fingerprint_item(self._log, item, write=ui.should_write()) fingerprint_cmd.func = fingerprint_cmd_func diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 7586c2a1b..c4df9ab57 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -301,7 +301,7 @@ class ConvertPlugin(BeetsPlugin): encode_cmd.append(os.fsdecode(args[i])) if pretend: - self._log.info("{0}", " ".join(ui.decargs(args))) + self._log.info("{0}", " ".join(args)) return try: @@ -323,9 +323,7 @@ class ConvertPlugin(BeetsPlugin): raise except OSError as exc: raise ui.UserError( - "convert: couldn't invoke '{}': {}".format( - " ".join(ui.decargs(args)), exc - ) + "convert: couldn't invoke '{}': {}".format(" ".join(args), exc) ) if not quiet and not pretend: @@ -579,13 +577,13 @@ class ConvertPlugin(BeetsPlugin): ) = self._get_opts_and_config(opts) if opts.album: - albums = lib.albums(ui.decargs(args)) + albums = lib.albums(args) items = [i for a in albums for i in a.items()] if not pretend: for a in albums: ui.print_(format(a, "")) else: - items = list(lib.items(ui.decargs(args))) + items = list(lib.items(args)) if not pretend: for i in items: ui.print_(format(i, "")) diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index c9703c4dd..7e4896437 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -54,7 +54,7 @@ class DeezerPlugin(MetadataSourcePlugin[Response], BeetsPlugin): ) def func(lib: Library, opts, args): - items = lib.items(ui.decargs(args)) + items = lib.items(args) self.deezerupdate(list(items), ui.should_write()) deezer_update_cmd.func = func diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 5a2be0cd2..ea7abaaff 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -19,7 +19,7 @@ import shlex from beets.library import Album, Item from beets.plugins import BeetsPlugin -from beets.ui import Subcommand, UserError, decargs, print_ +from beets.ui import Subcommand, UserError, print_ from beets.util import ( MoveOperation, bytestring_path, @@ -163,11 +163,11 @@ class DuplicatesPlugin(BeetsPlugin): if album: if not keys: keys = ["mb_albumid"] - items = lib.albums(decargs(args)) + items = lib.albums(args) else: if not keys: keys = ["mb_trackid", "mb_albumid"] - items = lib.items(decargs(args)) + items = lib.items(args) # If there's nothing to do, return early. The code below assumes # `items` to be non-empty. diff --git a/beetsplug/edit.py b/beetsplug/edit.py index b92c48839..52387c314 100644 --- a/beetsplug/edit.py +++ b/beetsplug/edit.py @@ -180,8 +180,7 @@ class EditPlugin(plugins.BeetsPlugin): def _edit_command(self, lib, opts, args): """The CLI command function for the `beet edit` command.""" # Get the objects to edit. - query = ui.decargs(args) - items, albums = _do_query(lib, query, opts.album, False) + items, albums = _do_query(lib, args, opts.album, False) objs = albums if opts.album else items if not objs: ui.print_("Nothing to edit.") diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 2a4e06a93..8df3c3c05 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -22,7 +22,7 @@ import requests from beets import art, config, ui from beets.plugins import BeetsPlugin -from beets.ui import decargs, print_ +from beets.ui import print_ from beets.util import bytestring_path, displayable_path, normpath, syspath from beets.util.artresizer import ArtResizer @@ -115,7 +115,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): ) ) - items = lib.items(decargs(args)) + items = lib.items(args) # Confirm with user. if not opts.yes and not _confirm(items, not opts.file): @@ -151,7 +151,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): except Exception as e: self._log.error("Unable to save image: {}".format(e)) return - items = lib.items(decargs(args)) + items = lib.items(args) # Confirm with user. if not opts.yes and not _confirm(items, not opts.url): os.remove(tempimg) @@ -169,7 +169,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): ) os.remove(tempimg) else: - albums = lib.albums(decargs(args)) + albums = lib.albums(args) # Confirm with user. if not opts.yes and not _confirm(albums, not opts.file): return @@ -212,7 +212,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): def extract_func(lib, opts, args): if opts.outpath: art.extract_first( - self._log, normpath(opts.outpath), lib.items(decargs(args)) + self._log, normpath(opts.outpath), lib.items(args) ) else: filename = bytestring_path( @@ -223,7 +223,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): "Only specify a name rather than a path for -n" ) return - for album in lib.albums(decargs(args)): + for album in lib.albums(args): artpath = normpath(os.path.join(album.path, filename)) artpath = art.extract_first( self._log, artpath, album.items() @@ -244,11 +244,11 @@ class EmbedCoverArtPlugin(BeetsPlugin): ) def clear_func(lib, opts, args): - items = lib.items(decargs(args)) + items = lib.items(args) # Confirm with user. if not opts.yes and not _confirm(items, False): return - art.clear(self._log, lib, decargs(args)) + art.clear(self._log, lib, args) clear_cmd.func = clear_func diff --git a/beetsplug/export.py b/beetsplug/export.py index 9b8ad3580..05ca3f24a 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -144,7 +144,7 @@ class ExportPlugin(BeetsPlugin): items = [] for data_emitter in data_collector( lib, - ui.decargs(args), + args, album=opts.album, ): try: diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index b442633da..e1ec5aa09 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -1503,9 +1503,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): ) def func(lib: Library, opts, args) -> None: - self.batch_fetch_art( - lib, lib.albums(ui.decargs(args)), opts.force, opts.quiet - ) + self.batch_fetch_art(lib, lib.albums(args), opts.force, opts.quiet) cmd.func = func return [cmd] diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index a85aa9719..150f230aa 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -118,7 +118,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin): keep_in_artist_field = self.config["keep_in_artist"].get(bool) write = ui.should_write() - for item in lib.items(ui.decargs(args)): + for item in lib.items(args): if self.ft_in_title(item, drop_feat, keep_in_artist_field): item.store() if write: diff --git a/beetsplug/hook.py b/beetsplug/hook.py index 5ce5ef828..90d66553a 100644 --- a/beetsplug/hook.py +++ b/beetsplug/hook.py @@ -14,27 +14,21 @@ """Allows custom commands to be run when an event is emitted by beets""" +from __future__ import annotations + +import os import shlex import string import subprocess -import sys +from typing import Any from beets.plugins import BeetsPlugin -class CodingFormatter(string.Formatter): - """A variant of `string.Formatter` that converts everything to `unicode` - strings. +class BytesToStrFormatter(string.Formatter): + """A variant of `string.Formatter` that converts `bytes` to `str`.""" - This was necessary on Python 2, in needs to be kept for backwards - compatibility. - """ - - def __init__(self, coding): - """Creates a new coding formatter with the provided coding.""" - self._coding = coding - - def convert_field(self, value, conversion): + def convert_field(self, value: Any, conversion: str | None) -> Any: """Converts the provided value given a conversion type. This method decodes the converted value using the formatter's coding. @@ -42,7 +36,7 @@ class CodingFormatter(string.Formatter): converted = super().convert_field(value, conversion) if isinstance(converted, bytes): - return converted.decode(self._coding) + return os.fsdecode(converted) return converted @@ -72,8 +66,8 @@ class HookPlugin(BeetsPlugin): return # For backwards compatibility, use a string formatter that decodes - # bytes (in particular, paths) to unicode strings. - formatter = CodingFormatter(sys.getfilesystemencoding()) + # bytes (in particular, paths) to strings. + formatter = BytesToStrFormatter() command_pieces = [ formatter.format(piece, event=event, **kwargs) for piece in shlex.split(command) diff --git a/beetsplug/info.py b/beetsplug/info.py index d759d6066..c4d5aacbf 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -215,7 +215,7 @@ class InfoPlugin(BeetsPlugin): summary = {} for data_emitter in data_collector( lib, - ui.decargs(args), + args, album=opts.album, ): try: @@ -232,7 +232,7 @@ class InfoPlugin(BeetsPlugin): if opts.keys_only: print_data_keys(data, item) else: - fmt = ui.decargs([opts.format])[0] if opts.format else None + fmt = [opts.format][0] if opts.format else None print_data(data, item, fmt) first = False diff --git a/beetsplug/ipfs.py b/beetsplug/ipfs.py index 1c8c89aa9..3c6425c06 100644 --- a/beetsplug/ipfs.py +++ b/beetsplug/ipfs.py @@ -74,7 +74,7 @@ class IPFSPlugin(BeetsPlugin): def func(lib, opts, args): if opts.add: - for album in lib.albums(ui.decargs(args)): + for album in lib.albums(args): if len(album.items()) == 0: self._log.info( "{0} does not contain items, aborting", album @@ -84,19 +84,19 @@ class IPFSPlugin(BeetsPlugin): album.store() if opts.get: - self.ipfs_get(lib, ui.decargs(args)) + self.ipfs_get(lib, args) if opts.publish: self.ipfs_publish(lib) if opts._import: - self.ipfs_import(lib, ui.decargs(args)) + self.ipfs_import(lib, args) if opts._list: - self.ipfs_list(lib, ui.decargs(args)) + self.ipfs_list(lib, args) if opts.play: - self.ipfs_play(lib, opts, ui.decargs(args)) + self.ipfs_play(lib, opts, args) cmd.func = func return [cmd] diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index 87f0cc427..00b688d4f 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -43,7 +43,7 @@ class KeyFinderPlugin(BeetsPlugin): return [cmd] def command(self, lib, opts, args): - self.find_key(lib.items(ui.decargs(args)), write=ui.should_write()) + self.find_key(lib.items(args), write=ui.should_write()) def imported(self, session, task): self.find_key(task.imported_items()) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 30b44e187..b67f1fae2 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -521,7 +521,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): if opts.album: # Fetch genres for whole albums - for album in lib.albums(ui.decargs(args)): + for album in lib.albums(args): album.genre, src = self._get_genre(album) self._log.info( 'genre for album "{0.album}" ({1}): {0.genre}', @@ -550,7 +550,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): else: # Just query singletons, i.e. items that are not part of # an album - for item in lib.items(ui.decargs(args)): + for item in lib.items(args): item.genre, src = self._get_genre(item) item.store() self._log.info( diff --git a/beetsplug/limit.py b/beetsplug/limit.py index 0a13a78aa..aae99a717 100644 --- a/beetsplug/limit.py +++ b/beetsplug/limit.py @@ -25,7 +25,7 @@ from itertools import islice from beets.dbcore import FieldQuery from beets.plugins import BeetsPlugin -from beets.ui import Subcommand, decargs, print_ +from beets.ui import Subcommand, print_ def lslimit(lib, opts, args): @@ -36,11 +36,10 @@ def lslimit(lib, opts, args): if (opts.head or opts.tail or 0) < 0: raise ValueError("Limit value must be non-negative") - query = decargs(args) if opts.album: - objs = lib.albums(query) + objs = lib.albums(args) else: - objs = lib.items(query) + objs = lib.items(args) if opts.head is not None: objs = islice(objs, opts.head) diff --git a/beetsplug/mbsubmit.py b/beetsplug/mbsubmit.py index d215e616c..e23c0d610 100644 --- a/beetsplug/mbsubmit.py +++ b/beetsplug/mbsubmit.py @@ -86,7 +86,7 @@ class MBSubmitPlugin(BeetsPlugin): ) def func(lib, opts, args): - items = lib.items(ui.decargs(args)) + items = lib.items(args) self._mbsubmit(items) mbsubmit_cmd.func = func diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 94870232c..d38b25e9f 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -63,10 +63,9 @@ class MBSyncPlugin(BeetsPlugin): move = ui.should_move(opts.move) pretend = opts.pretend write = ui.should_write(opts.write) - query = ui.decargs(args) - self.singletons(lib, query, move, pretend, write) - self.albums(lib, query, move, pretend, write) + self.singletons(lib, args, move, pretend, write) + self.albums(lib, args, move, pretend, write) def singletons(self, lib, query, move, pretend, write): """Retrieve and apply info from the autotagger for items matched by diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index 2466efe54..f99e820b5 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -97,7 +97,6 @@ class MetaSyncPlugin(BeetsPlugin): def func(self, lib, opts, args): """Command handler for the metasync function.""" pretend = opts.pretend - query = ui.decargs(args) sources = [] for source in opts.sources: @@ -106,7 +105,7 @@ class MetaSyncPlugin(BeetsPlugin): sources = sources or self.config["source"].as_str_seq() meta_source_instances = {} - items = lib.items(query) + items = lib.items(args) # Avoid needlessly instantiating meta sources (can be expensive) if not items: diff --git a/beetsplug/missing.py b/beetsplug/missing.py index c4bbb83fd..8c328e647 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -25,7 +25,7 @@ from beets import config, plugins from beets.dbcore import types from beets.library import Album, Item, Library from beets.plugins import BeetsPlugin -from beets.ui import Subcommand, decargs, print_ +from beets.ui import Subcommand, print_ MB_ARTIST_QUERY = r"mb_albumartistid::^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$" @@ -135,7 +135,7 @@ class MissingPlugin(BeetsPlugin): albms = self.config["album"].get() helper = self._missing_albums if albms else self._missing_tracks - helper(lib, decargs(args)) + helper(lib, args) self._command.func = _miss return [self._command] diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 463a455f5..ab2d39b2b 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -88,7 +88,7 @@ class ParentWorkPlugin(BeetsPlugin): force_parent = self.config["force"].get(bool) write = ui.should_write() - for item in lib.items(ui.decargs(args)): + for item in lib.items(args): changed = self.find_work(item, force_parent, verbose=True) if changed: item.store() diff --git a/beetsplug/play.py b/beetsplug/play.py index ddebd7d41..3e7ba0a9e 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -107,7 +107,7 @@ class PlayPlugin(BeetsPlugin): # Perform search by album and add folders rather than tracks to # playlist. if opts.album: - selection = lib.albums(ui.decargs(args)) + selection = lib.albums(args) paths = [] sort = lib.get_default_album_sort() @@ -120,7 +120,7 @@ class PlayPlugin(BeetsPlugin): # Perform item query and add tracks to playlist. else: - selection = lib.items(ui.decargs(args)) + selection = lib.items(args) paths = [item.path for item in selection] item_type = "track" diff --git a/beetsplug/random.py b/beetsplug/random.py index 05f2cdf77..c791af414 100644 --- a/beetsplug/random.py +++ b/beetsplug/random.py @@ -16,17 +16,16 @@ from beets.plugins import BeetsPlugin from beets.random import random_objs -from beets.ui import Subcommand, decargs, print_ +from beets.ui import Subcommand, print_ def random_func(lib, opts, args): """Select some random items or albums and print the results.""" # Fetch all the objects matching the query into a list. - query = decargs(args) if opts.album: - objs = list(lib.albums(query)) + objs = list(lib.albums(args)) else: - objs = list(lib.items(query)) + objs = list(lib.items(args)) # Print a random subset. objs = random_objs( diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 3aad8cd89..00b651d99 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -62,7 +62,7 @@ class FatalGstreamerPluginReplayGainError(FatalReplayGainError): loading the required plugins.""" -def call(args: list[Any], log: Logger, **kwargs: Any): +def call(args: list[str], log: Logger, **kwargs: Any): """Execute the command and return its output or raise a ReplayGainError on failure. """ @@ -73,11 +73,6 @@ def call(args: list[Any], log: Logger, **kwargs: Any): raise ReplayGainError( "{} exited with status {}".format(args[0], e.returncode) ) - except UnicodeEncodeError: - # Due to a bug in Python 2's subprocess on Windows, Unicode - # filenames can fail to encode on that platform. See: - # https://github.com/google-code-export/beets/issues/499 - raise ReplayGainError("argument encoding failed") def db_to_lufs(db: float) -> float: @@ -403,20 +398,18 @@ class FfmpegBackend(Backend): def _construct_cmd( self, item: Item, peak_method: PeakMethod | None - ) -> list[str | bytes]: + ) -> list[str]: """Construct the shell command to analyse items.""" return [ self._ffmpeg_path, "-nostats", "-hide_banner", "-i", - item.path, + str(item.filepath), "-map", "a:0", "-filter", - "ebur128=peak={}".format( - "none" if peak_method is None else peak_method.name - ), + f"ebur128=peak={'none' if peak_method is None else peak_method.name}", "-f", "null", "-", @@ -660,7 +653,7 @@ class CommandBackend(Backend): # tag-writing; this turns the mp3gain/aacgain tool into a gain # calculator rather than a tag manipulator because we take care # of changing tags ourselves. - cmd: list[bytes | str] = [self.command, "-o", "-s", "s"] + cmd: list[str] = [self.command, "-o", "-s", "s"] if self.noclip: # Adjust to avoid clipping. cmd = cmd + ["-k"] @@ -1039,7 +1032,7 @@ class AudioToolsBackend(Backend): os.fsdecode(syspath(item.path)) ) except OSError: - raise ReplayGainError(f"File {item.path} was not found") + raise ReplayGainError(f"File {item.filepath} was not found") except self._mod_audiotools.UnsupportedFile: raise ReplayGainError(f"Unsupported file type {item.format}") @@ -1530,7 +1523,7 @@ class ReplayGainPlugin(BeetsPlugin): self.open_pool(threads) if opts.album: - albums = lib.albums(ui.decargs(args)) + albums = lib.albums(args) self._log.info( "Analyzing {} albums ~ {} backend...".format( len(albums), self.backend_name @@ -1539,7 +1532,7 @@ class ReplayGainPlugin(BeetsPlugin): for album in albums: self.handle_album(album, write, force) else: - items = lib.items(ui.decargs(args)) + items = lib.items(args) self._log.info( "Analyzing {} tracks ~ {} backend...".format( len(items), self.backend_name diff --git a/beetsplug/scrub.py b/beetsplug/scrub.py index 630a4e6e6..813effb5f 100644 --- a/beetsplug/scrub.py +++ b/beetsplug/scrub.py @@ -58,7 +58,7 @@ class ScrubPlugin(BeetsPlugin): def commands(self): def scrub_func(lib, opts, args): # Walk through matching files and remove tags. - for item in lib.items(ui.decargs(args)): + for item in lib.items(args): self._log.info( "scrubbing: {0}", util.displayable_path(item.path) ) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 5ea3c6bff..e65d59649 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -127,7 +127,7 @@ class SmartPlaylistPlugin(BeetsPlugin): def update_cmd(self, lib, opts, args): self.build_queries() if args: - args = set(ui.decargs(args)) + args = set(args) for a in list(args): if not a.endswith(".m3u"): args.add(f"{a}.m3u") diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 595da4892..36790b56b 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -453,7 +453,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): def queries(lib, opts, args): success = self._parse_opts(opts) if success: - results = self._match_library_tracks(lib, ui.decargs(args)) + results = self._match_library_tracks(lib, args) self._output_match_results(results) spotify_cmd = ui.Subcommand( @@ -491,7 +491,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): ) def func(lib, opts, args): - items = lib.items(ui.decargs(args)) + items = lib.items(args) self._fetch_info(items, ui.should_write(), opts.force_refetch) sync_cmd.func = func diff --git a/beetsplug/thumbnails.py b/beetsplug/thumbnails.py index e11b75390..5460d3fec 100644 --- a/beetsplug/thumbnails.py +++ b/beetsplug/thumbnails.py @@ -28,7 +28,7 @@ from pathlib import PurePosixPath from xdg import BaseDirectory from beets.plugins import BeetsPlugin -from beets.ui import Subcommand, decargs +from beets.ui import Subcommand from beets.util import bytestring_path, displayable_path, syspath from beets.util.artresizer import ArtResizer @@ -78,7 +78,7 @@ class ThumbnailsPlugin(BeetsPlugin): def process_query(self, lib, opts, args): self.config.set_args(opts) if self._check_local_ok(): - for album in lib.albums(decargs(args)): + for album in lib.albums(args): self.process_album(album) def _check_local_ok(self): diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index c1b0b5029..559f0622c 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -308,18 +308,8 @@ def all_items(): def item_file(item_id): item = g.lib.get_item(item_id) - # On Windows under Python 2, Flask wants a Unicode path. On Python 3, it - # *always* wants a Unicode path. - if os.name == "nt": - item_path = util.syspath(item.path) - else: - item_path = os.fsdecode(item.path) - + item_path = util.syspath(item.path) base_filename = os.path.basename(item_path) - if isinstance(base_filename, bytes): - unicode_base_filename = util.displayable_path(base_filename) - else: - unicode_base_filename = base_filename try: # Imitate http.server behaviour @@ -327,7 +317,7 @@ def item_file(item_id): except UnicodeError: safe_filename = unidecode(base_filename) else: - safe_filename = unicode_base_filename + safe_filename = base_filename response = flask.send_file( item_path, as_attachment=True, download_name=safe_filename @@ -470,7 +460,7 @@ class WebPlugin(BeetsPlugin): ) def func(lib, opts, args): - args = ui.decargs(args) + args = args if args: self.config["host"] = args.pop(0) if args: diff --git a/beetsplug/zero.py b/beetsplug/zero.py index 7ee624ce7..05e55bfcd 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -21,7 +21,7 @@ from mediafile import MediaFile from beets.importer import Action from beets.plugins import BeetsPlugin -from beets.ui import Subcommand, decargs, input_yn +from beets.ui import Subcommand, input_yn __author__ = "baobab@heresiarch.info" @@ -75,11 +75,11 @@ class ZeroPlugin(BeetsPlugin): zero_command = Subcommand("zero", help="set fields to null") def zero_fields(lib, opts, args): - if not decargs(args) and not input_yn( + if not args and not input_yn( "Remove fields for all items? (Y/n)", True ): return - for item in lib.items(decargs(args)): + for item in lib.items(args): self.process_item(item) zero_command.func = zero_fields diff --git a/test/plugins/test_thumbnails.py b/test/plugins/test_thumbnails.py index bd3e22714..fadac34c2 100644 --- a/test/plugins/test_thumbnails.py +++ b/test/plugins/test_thumbnails.py @@ -232,8 +232,7 @@ class ThumbnailsTest(BeetsTestCase): ) @patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock()) - @patch("beetsplug.thumbnails.decargs") - def test_invokations(self, mock_decargs): + def test_invokations(self): plugin = ThumbnailsPlugin() plugin.process_album = Mock() album = Mock() @@ -243,7 +242,6 @@ class ThumbnailsTest(BeetsTestCase): album2 = Mock() lib.albums.return_value = [album, album2] plugin.process_query(lib, Mock(), None) - lib.albums.assert_called_once_with(mock_decargs.return_value) plugin.process_album.assert_has_calls( [call(album), call(album2)], any_order=True )