[FEATURE] cli-to-sub argument to convert yt-dlp args to ytdl-sub (#1376)

Example usage:
```
$ ytdl-sub cli-to-sub -S vcodec:h264,res:480,acodec:m4a
ytdl_options:
  format_sort:
  - vcodec:h264
  - res:480
  - acodec:m4a
```
This commit is contained in:
Jesse Bannon 2025-11-06 21:05:18 -08:00 committed by GitHub
parent c3ca3c6379
commit ed55f3a3f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 100 additions and 2 deletions

View file

@ -105,3 +105,11 @@ Preview the source variables for a given URL. Helpful to create new subscription
-sc, --split-chapters
View source variables after splitting by chapters
CLI to SUB Options
------------------
Convert yt-dlp cli arguments to ytdl-sub `ytdl_options` arguments.
.. code-block::
ytdl-sub cli-to-sub [YT-DLP ARGS]

View file

@ -12,6 +12,7 @@ from yt_dlp.utils import sanitize_filename
from ytdl_sub.cli.output_summary import output_summary
from ytdl_sub.cli.output_transaction_log import _maybe_validate_transaction_log_file
from ytdl_sub.cli.output_transaction_log import output_transaction_log
from ytdl_sub.cli.parsers.cli_to_sub import print_cli_to_sub
from ytdl_sub.cli.parsers.dl import DownloadArgsParser
from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME
from ytdl_sub.cli.parsers.main import parser
@ -206,6 +207,10 @@ def main() -> List[Subscription]:
args, extra_args = parser.parse_known_args()
if args.subparser == "cli-to-sub":
print_cli_to_sub(args=extra_args)
return []
# Load the config
if args.config:
config = ConfigFile.from_file_path(args.config)
@ -263,7 +268,7 @@ def main() -> List[Subscription]:
_view_url_from_cli(config=config, url=args.url, split_chapters=args.split_chapters)
)
else:
raise ValidationException("Must provide one of the commands: sub, dl, view")
raise ValidationException("Must provide one of the commands: sub, dl, view, cli-to-sub")
if not args.suppress_transaction_log:
output_transaction_log(

View file

@ -0,0 +1,64 @@
from typing import List
import yt_dlp
import yt_dlp.options
from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.yaml import dump_yaml
logger = Logger.get()
# pylint: disable=missing-function-docstring
##############################################################
# --- BEGIN ----
# Copy of https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py
create_parser = yt_dlp.options.create_parser
def parse_patched_options(opts):
patched_parser = create_parser()
patched_parser.defaults.update(
{
"ignoreerrors": False,
"retries": 0,
"fragment_retries": 0,
"extract_flat": False,
"concat_playlist": "never",
"update_self": False,
}
)
yt_dlp.options.create_parser = lambda: patched_parser
try:
return yt_dlp.parse_options(opts)
finally:
yt_dlp.options.create_parser = create_parser
default_opts = parse_patched_options([]).ydl_opts
def cli_to_api(opts, cli_defaults=False):
opts = (yt_dlp.parse_options if cli_defaults else parse_patched_options)(opts).ydl_opts
diff = {k: v for k, v in opts.items() if default_opts[k] != v}
if "postprocessors" in diff:
diff["postprocessors"] = [
pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]
]
return diff
# --- END ----
##############################################################
def print_cli_to_sub(args: List[str]) -> None:
api_args = cli_to_api(args)
if not api_args:
logger.info("Does not resolve to any yt-dlp args")
return
print(dump_yaml({"ytdl_options": api_args}))

View file

@ -221,3 +221,7 @@ view_parser.add_argument(
help="View source variables after splitting by chapters",
)
view_parser.add_argument("url", help="URL to view source variables for")
###################################################################################################
# CLI-TO-SUB PARSER
cli_to_sub_parser = subparsers.add_parser("cli-to-sub")

View file

@ -244,7 +244,9 @@ def test_no_positional_arg_command(mock_sys_exit, tv_show_config_path):
main()
assert mock_error.call_count == 1
assert mock_error.call_args.args[0] == "Must provide one of the commands: sub, dl, view"
assert mock_error.call_args.args[0] == (
"Must provide one of the commands: sub, dl, view, cli-to-sub"
)
def test_bad_config_path(mock_sys_exit):
@ -264,3 +266,18 @@ def test_bad_config_path(mock_sys_exit):
"The config file 'does_not_exist.yaml' could not be found. "
"Did you set --config correctly?"
)
def test_cli_to_sub(mock_sys_exit, capsys):
with (
mock_sys_exit(expected_exit_code=0),
patch.object(
sys,
"argv",
["ytdl-sub", "cli-to-sub", "--mark-watched", "--mtime"],
),
):
main()
captured = capsys.readouterr()
assert captured.out == ("ytdl_options:\n mark_watched: true\n updatetime: true\n\n")