[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "yt-dlp"
maintainers = [
    {name = "pukkandan", email = "pukkandan.ytdlp@gmail.com"},
    {name = "Grub4K", email = "contact@grub4k.xyz"},
    {name = "bashonly", email = "bashonly@protonmail.com"},
    {name = "coletdjnz", email = "coletdjnz@protonmail.com"},
    {name = "sepro", email = "sepro@sepr0.com"},
]
description = "A feature-rich command-line audio/video downloader"
readme = "README.md"
requires-python = ">=3.9"
keywords = [
    "youtube-dl",
    "video-downloader",
    "youtube-downloader",
    "sponsorblock",
    "youtube-dlc",
    "yt-dlp",
]
license = {file = "LICENSE"}
classifiers = [
    "Topic :: Multimedia :: Video",
    "Development Status :: 5 - Production/Stable",
    "Environment :: Console",
    "Programming Language :: Python",
    "Programming Language :: Python :: 3 :: Only",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Programming Language :: Python :: Implementation",
    "Programming Language :: Python :: Implementation :: CPython",
    "Programming Language :: Python :: Implementation :: PyPy",
    "License :: OSI Approved :: The Unlicense (Unlicense)",
    "Operating System :: OS Independent",
]
dynamic = ["version"]
dependencies = []

[project.optional-dependencies]
default = [
    "brotli; implementation_name=='cpython'",
    "brotlicffi; implementation_name!='cpython'",
    "certifi",
    "mutagen",
    "pycryptodomex",
    "requests>=2.32.2,<3",
    "urllib3>=1.26.17,<3",
    "websockets>=13.0",
]
curl-cffi = [
    "curl-cffi==0.5.10; os_name=='nt' and implementation_name=='cpython'",
    "curl-cffi>=0.5.10,!=0.6.*,<0.7.2; os_name!='nt' and implementation_name=='cpython'",
]
secretstorage = [
    "cffi",
    "secretstorage",
]
build = [
    "build",
    "hatchling",
    "pip",
    "setuptools>=71.0.2",  # 71.0.0 broke pyinstaller
    "wheel",
]
dev = [
    "pre-commit",
    "yt-dlp[static-analysis]",
    "yt-dlp[test]",
]
static-analysis = [
    "autopep8~=2.0",
    "ruff~=0.8.0",
]
test = [
    "pytest~=8.1",
    "pytest-rerunfailures~=14.0",
]
pyinstaller = [
    "pyinstaller>=6.11.1",  # Windows temp cleanup fixed in 6.11.1
]

[project.urls]
Documentation = "https://github.com/yt-dlp/yt-dlp#readme"
Repository = "https://github.com/yt-dlp/yt-dlp"
Tracker = "https://github.com/yt-dlp/yt-dlp/issues"
Funding = "https://github.com/yt-dlp/yt-dlp/blob/master/Collaborators.md#collaborators"

[project.scripts]
yt-dlp = "yt_dlp:main"

[project.entry-points.pyinstaller40]
hook-dirs = "yt_dlp.__pyinstaller:get_hook_dirs"

[tool.hatch.build.targets.sdist]
include = [
    "/yt_dlp",
    "/devscripts",
    "/test",
    "/.gitignore",  # included by default, needed for auto-excludes
    "/Changelog.md",
    "/LICENSE",  # included as license
    "/pyproject.toml",  # included by default
    "/README.md",  # included as readme
    "/setup.cfg",
    "/supportedsites.md",
]
artifacts = [
    "/yt_dlp/extractor/lazy_extractors.py",
    "/completions",
    "/AUTHORS",  # included by default
    "/README.txt",
    "/yt-dlp.1",
]

[tool.hatch.build.targets.wheel]
packages = ["yt_dlp"]
artifacts = ["/yt_dlp/extractor/lazy_extractors.py"]

[tool.hatch.build.targets.wheel.shared-data]
"completions/bash/yt-dlp" = "share/bash-completion/completions/yt-dlp"
"completions/zsh/_yt-dlp" = "share/zsh/site-functions/_yt-dlp"
"completions/fish/yt-dlp.fish" = "share/fish/vendor_completions.d/yt-dlp.fish"
"README.txt" = "share/doc/yt_dlp/README.txt"
"yt-dlp.1" = "share/man/man1/yt-dlp.1"

[tool.hatch.version]
path = "yt_dlp/version.py"
pattern = "_pkg_version = '(?P<version>[^']+)'"

[tool.hatch.envs.default]
features = ["curl-cffi", "default"]
dependencies = ["pre-commit"]
path = ".venv"
installer = "uv"

[tool.hatch.envs.default.scripts]
setup = "pre-commit install --config .pre-commit-hatch.yaml"
yt-dlp = "python -Werror -Xdev -m yt_dlp {args}"

[tool.hatch.envs.hatch-static-analysis]
detached = true
features = ["static-analysis"]
dependencies = []  # override hatch ruff version
config-path = "pyproject.toml"

[tool.hatch.envs.hatch-static-analysis.scripts]
format-check = "autopep8 --diff {args:.}"
format-fix = "autopep8 --in-place {args:.}"
lint-check = "ruff check {args:.}"
lint-fix = "ruff check --fix {args:.}"

[tool.hatch.envs.hatch-test]
features = ["test"]
dependencies = [
    "pytest-randomly~=3.15",
    "pytest-xdist[psutil]~=3.5",
]

[tool.hatch.envs.hatch-test.scripts]
run = "python -m devscripts.run_tests {args}"
run-cov = "echo Code coverage not implemented && exit 1"

[[tool.hatch.envs.hatch-test.matrix]]
python = [
    "3.9",
    "3.10",
    "3.11",
    "3.12",
    "3.13",
    "pypy3.10",
]

[tool.ruff]
line-length = 120

[tool.ruff.lint]
ignore = [
    "E402",    # module-import-not-at-top-of-file
    "E501",    # line-too-long
    "E731",    # lambda-assignment
    "E741",    # ambiguous-variable-name
    "UP031",   # printf-string-formatting
    "UP036",   # outdated-version-block
    "B006",    # mutable-argument-default
    "B008",    # function-call-in-default-argument
    "B011",    # assert-false
    "B017",    # assert-raises-exception
    "B023",    # function-uses-loop-variable (false positives)
    "B028",    # no-explicit-stacklevel
    "B904",    # raise-without-from-inside-except
    "C401",    # unnecessary-generator-set
    "C402",    # unnecessary-generator-dict
    "PIE790",  # unnecessary-placeholder
    "SIM102",  # collapsible-if
    "SIM108",  # if-else-block-instead-of-if-exp
    "SIM112",  # uncapitalized-environment-variables
    "SIM113",  # enumerate-for-loop
    "SIM114",  # if-with-same-arms
    "SIM115",  # open-file-with-context-handler
    "SIM117",  # multiple-with-statements
    "SIM223",  # expr-and-false
    "SIM300",  # yoda-conditions
    "TD001",   # invalid-todo-tag
    "TD002",   # missing-todo-author
    "TD003",   # missing-todo-link
    "PLE0604", # invalid-all-object (false positives)
    "PLE0643", # potential-index-error (false positives)
    "PLW0603", # global-statement
    "PLW1510", # subprocess-run-without-check
    "PLW2901", # redefined-loop-name
    "RUF001",  # ambiguous-unicode-character-string
    "RUF012",  # mutable-class-default
    "RUF100",  # unused-noqa (flake8 has slightly different behavior)
]
select = [
    "E",      # pycodestyle Error
    "W",      # pycodestyle Warning
    "F",      # Pyflakes
    "I",      # isort
    "Q",      # flake8-quotes
    "N803",   # invalid-argument-name
    "N804",   # invalid-first-argument-name-for-class-method
    "UP",     # pyupgrade
    "B",      # flake8-bugbear
    "A",      # flake8-builtins
    "COM",    # flake8-commas
    "C4",     # flake8-comprehensions
    "FA",     # flake8-future-annotations
    "ISC",    # flake8-implicit-str-concat
    "ICN003", # banned-import-from
    "PIE",    # flake8-pie
    "T20",    # flake8-print
    "RSE",    # flake8-raise
    "RET504", # unnecessary-assign
    "SIM",    # flake8-simplify
    "TID251", # banned-api
    "TD",     # flake8-todos
    "PLC",    # Pylint Convention
    "PLE",    # Pylint Error
    "PLW",    # Pylint Warning
    "RUF",    # Ruff-specific rules
]

[tool.ruff.lint.per-file-ignores]
"devscripts/lazy_load_template.py" = [
    "F401",   # unused-import
]
"!yt_dlp/extractor/**.py" = [
    "I",      # isort
    "ICN003", # banned-import-from
    "T20",    # flake8-print
    "A002",   # builtin-argument-shadowing
    "C408",   # unnecessary-collection-call
]

[tool.ruff.lint.isort]
known-first-party = [
    "bundle",
    "devscripts",
    "test",
]
relative-imports-order = "closest-to-furthest"

[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
multiline-quotes = "single"
inline-quotes = "single"
avoid-escape = false

[tool.ruff.lint.pep8-naming]
classmethod-decorators = [
    "yt_dlp.utils.classproperty",
]

[tool.ruff.lint.flake8-import-conventions]
banned-from = [
    "base64",
    "datetime",
    "functools",
    "glob",
    "hashlib",
    "itertools",
    "json",
    "math",
    "os",
    "pathlib",
    "random",
    "re",
    "string",
    "sys",
    "time",
    "urllib.parse",
    "uuid",
    "xml",
]

[tool.ruff.lint.flake8-tidy-imports.banned-api]
"yt_dlp.compat.compat_str".msg = "Use `str` instead."
"yt_dlp.compat.compat_b64decode".msg = "Use `base64.b64decode` instead."
"yt_dlp.compat.compat_urlparse".msg = "Use `urllib.parse` instead."
"yt_dlp.compat.compat_parse_qs".msg = "Use `urllib.parse.parse_qs` instead."
"yt_dlp.compat.compat_urllib_parse_unquote".msg = "Use `urllib.parse.unquote` instead."
"yt_dlp.compat.compat_urllib_parse_urlencode".msg = "Use `urllib.parse.urlencode` instead."
"yt_dlp.compat.compat_urllib_parse_urlparse".msg = "Use `urllib.parse.urlparse` instead."
"yt_dlp.compat.compat_shlex_quote".msg = "Use `yt_dlp.utils.shell_quote` instead."
"yt_dlp.utils.error_to_compat_str".msg = "Use `str` instead."
"yt_dlp.utils.bytes_to_intlist".msg = "Use `list` instead."
"yt_dlp.utils.intlist_to_bytes".msg = "Use `bytes` instead."
"yt_dlp.utils.decodeArgument".msg = "Do not use"
"yt_dlp.utils.decodeFilename".msg = "Do not use"
"yt_dlp.utils.encodeFilename".msg = "Do not use"
"yt_dlp.compat.compat_os_name".msg = "Use `os.name` instead."
"yt_dlp.compat.compat_realpath".msg = "Use `os.path.realpath` instead."
"yt_dlp.compat.functools".msg = "Use `functools` instead."
"yt_dlp.utils.decodeOption".msg = "Do not use"
"yt_dlp.utils.compiled_regex_type".msg = "Use `re.Pattern` instead."

[tool.autopep8]
max_line_length = 120
recursive = true
exit-code = true
jobs = 0
select = [
    "E101",
    "E112",
    "E113",
    "E115",
    "E116",
    "E117",
    "E121",
    "E122",
    "E123",
    "E124",
    "E125",
    "E126",
    "E127",
    "E128",
    "E129",
    "E131",
    "E201",
    "E202",
    "E203",
    "E211",
    "E221",
    "E222",
    "E223",
    "E224",
    "E225",
    "E226",
    "E227",
    "E228",
    "E231",
    "E241",
    "E242",
    "E251",
    "E252",
    "E261",
    "E262",
    "E265",
    "E266",
    "E271",
    "E272",
    "E273",
    "E274",
    "E275",
    "E301",
    "E302",
    "E303",
    "E304",
    "E305",
    "E306",
    "E502",
    "E701",
    "E702",
    "E704",
    "W391",
    "W504",
]

[tool.pytest.ini_options]
addopts = "-ra -v --strict-markers"
markers = [
    "download",
]
