diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 4c874425f..0843f6ee0 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -95,18 +95,19 @@ def in_no_convert(item: Item) -> bool: return False -def should_transcode(item, fmt, override_never_lossy: bool = False): +def should_transcode(item, fmt, force: bool = False): """Determine whether the item should be transcoded as part of conversion (i.e., its bitrate is high or it has the wrong format). - If ``override_never_lossy`` is True, the ``never_convert_lossy_files`` - check is ignored (used when the user explicitly requests a format - on the CLI). + If ``force`` is True, safety checks like ``no_convert`` and + ``never_convert_lossy_files`` are ignored and the item is always + transcoded. """ + if force: + return True if in_no_convert(item) or ( config["convert"]["never_convert_lossy_files"].get(bool) and item.format.lower() not in LOSSLESS_FORMATS - and not override_never_lossy ): return False maxbr = config["convert"]["max_bitrate"].get(Optional(int)) @@ -241,6 +242,16 @@ class ConvertPlugin(BeetsPlugin): drive, relative paths pointing to media files will be used.""", ) + cmd.parser.add_option( + "-F", + "--force", + action="store_true", + dest="force", + help=( + "force transcoding, overriding safety checks such as " + "never_convert_lossy_files and no_convert" + ), + ) cmd.parser.add_album_option() cmd.func = self.convert_func return [cmd] @@ -264,7 +275,7 @@ class ConvertPlugin(BeetsPlugin): hardlink, link, playlist, - fmt_from_cli, + force, ) = self._get_opts_and_config(empty_opts) items = task.imported_items() @@ -278,7 +289,7 @@ class ConvertPlugin(BeetsPlugin): hardlink, threads, items, - fmt_from_cli, + force, ) # Utilities converted from functions to methods on logging overhaul @@ -354,7 +365,7 @@ class ConvertPlugin(BeetsPlugin): pretend=False, link=False, hardlink=False, - fmt_from_cli=False, + force=False, ): """A pipeline thread that converts `Item` objects from a library. @@ -380,15 +391,11 @@ class ConvertPlugin(BeetsPlugin): if keep_new: original = dest converted = item.path - if should_transcode( - item, fmt, override_never_lossy=fmt_from_cli - ): + if should_transcode(item, fmt, force): converted = replace_ext(converted, ext) else: original = item.path - if should_transcode( - item, fmt, override_never_lossy=fmt_from_cli - ): + if should_transcode(item, fmt, force): dest = replace_ext(dest, ext) converted = dest @@ -418,7 +425,7 @@ class ConvertPlugin(BeetsPlugin): ) util.move(item.path, original) - if should_transcode(item, fmt, override_never_lossy=fmt_from_cli): + if should_transcode(item, fmt, force): linked = False try: self.encode(command, original, converted, pretend) @@ -589,7 +596,7 @@ class ConvertPlugin(BeetsPlugin): hardlink, link, playlist, - fmt_from_cli, + force, ) = self._get_opts_and_config(opts) if opts.album: @@ -626,7 +633,7 @@ class ConvertPlugin(BeetsPlugin): hardlink, threads, items, - fmt_from_cli, + force, ) if playlist: @@ -729,7 +736,6 @@ class ConvertPlugin(BeetsPlugin): path_formats = ui.get_path_formats(self.config["paths"] or None) - fmt_from_cli = opts.format is not None fmt = opts.format or self.config["format"].as_str().lower() playlist = opts.playlist or self.config["playlist"].get() @@ -750,7 +756,7 @@ class ConvertPlugin(BeetsPlugin): else: hardlink = self.config["hardlink"].get(bool) link = self.config["link"].get(bool) - + force = getattr(opts, "force", False) return ( dest, threads, @@ -760,7 +766,7 @@ class ConvertPlugin(BeetsPlugin): hardlink, link, playlist, - fmt_from_cli, + force, ) def _parallel_convert( @@ -774,7 +780,7 @@ class ConvertPlugin(BeetsPlugin): hardlink, threads, items, - fmt_from_cli, + force, ): """Run the convert_item function for every items on as many thread as defined in threads @@ -788,7 +794,7 @@ class ConvertPlugin(BeetsPlugin): pretend, link, hardlink, - fmt_from_cli, + force, ) for _ in range(threads) ] diff --git a/docs/changelog.rst b/docs/changelog.rst index 3a322769a..24e2a99b3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -48,8 +48,9 @@ Bug fixes: query, the query eg. `GET /item/values/albumartist` would return the literal "albumartist" instead of a list of unique album artists. - :doc:`/plugins/convert`: beet convert ignored an explicit --format option when - never_convert_lossy_files was enabled. Lossy files selected by a query and - given a target format on the CLI are now transcoded instead of being copied + never_convert_lossy_files was enabled. Now, --force wil override this config + setting. Lossy files selected by a query and given a target format on the CLI + are now transcoded when given the --force flag instead of being copied unchanged. :bug:`5625` For plugin developers: diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index c1ec9e23d..abf31529d 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -51,6 +51,11 @@ instead, passing ``-H`` (``--hardlink``) creates hard links. Note that album art embedding is disabled for files that are linked. Refer to the ``link`` and ``hardlink`` options below. +The ``-F`` (or ``--force``) option forces transcoding even when safety options +such as ``no_convert`` or ``never_convert_lossy_files`` would normally cause a +file to be copied or skipped instead. This can be combined with ``--format`` to +explicitly transcode lossy inputs to a chosen target format. + The ``-m`` (or ``--playlist``) option enables the plugin to create an m3u8 playlist file in the destination folder given by the ``-d`` (``--dest``) option or the ``dest`` configuration. The path to the playlist file can either be @@ -114,10 +119,9 @@ The available options are: even further. If set to ``yes``, lossy files are always copied. Default: ``no``. When ``never_convert_lossy_files`` is enabled, lossy source files (for example MP3 or Ogg Vorbis) are normally not transcoded and are instead copied - or linked as-is. If you explicitly pass a target format on the command line - with ``--format``, this override is respected: lossy files matching the query - will be transcoded to the requested format even when - ``never_convert_lossy_files`` is enabled. + or linked as-is. To explicitly transcode lossy files in spite of this, use the + ``--force`` option with the ``convert`` command (optionally together with + ``--format`` to choose a target format) - **paths**: The directory structure and naming scheme for the converted files. Uses the same format as the top-level ``paths`` section (see :ref:`path-format-config`). Default: Reuse your top-level path format diff --git a/test/plugins/test_convert.py b/test/plugins/test_convert.py index d0e162b98..f45f50379 100644 --- a/test/plugins/test_convert.py +++ b/test/plugins/test_convert.py @@ -301,16 +301,15 @@ class NeverConvertLossyFilesTest(ConvertTestCase, ConvertCommand): converted = self.convert_dest / "converted.ogg" assert not self.file_endswith(converted, "mp3") - def test_cli_format_overrides_never_convert_lossy_files(self): + def test_force_overrides_never_convert_lossy_files(self): self.config["convert"]["formats"]["opus"] = { "command": self.tagged_copy_cmd("opus"), "extension": "ops", } - [item] = self.add_item_fixtures(ext="ogg") with control_stdin("y"): - self.run_convert_path(item, "--format", "opus") + self.run_convert_path(item, "--format", "opus", "--force") converted = self.convert_dest / "converted.ops" assert self.file_endswith(converted, "opus")