fix(api): sync REST API download endpoint with updated dl command (#98)

The REST API download endpoint was broken after recent dl command changes.

- Add missing vbitrate_range, abitrate_range, and worst parameters to the API call and DEFAULT_DOWNLOAD_PARAMS
- Convert wanted episode strings (S01E01) to internal SxE format via SeasonRange so episode filtering works correctly
- Track completed output files via dl.completed_files instead of returning an empty list

Closes #98
This commit is contained in:
imSp4rky
2026-04-08 23:04:58 -06:00
parent 50d2b127ec
commit 8f4f947d0d
5 changed files with 62 additions and 2 deletions

View File

@@ -66,6 +66,7 @@ dependencies = [
"wasmtime>=41.0.0", "wasmtime>=41.0.0",
"animeapi-py>=0.6.0", "animeapi-py>=0.6.0",
"rnet>=2.4.2", "rnet>=2.4.2",
"bandit>=1.9.4",
] ]
[project.urls] [project.urls]

View File

@@ -573,6 +573,7 @@ class dl:
raise ValueError("A subcommand to invoke was not specified, the main code cannot continue.") raise ValueError("A subcommand to invoke was not specified, the main code cannot continue.")
self.log = logging.getLogger("download") self.log = logging.getLogger("download")
self.completed_files: list[Path] = []
if not config.output_template: if not config.output_template:
raise click.ClickException( raise click.ClickException(
@@ -2559,6 +2560,7 @@ class dl:
final_path = final_dir / f"{base_filename}{track_path.suffix}" final_path = final_dir / f"{base_filename}{track_path.suffix}"
shutil.move(track_path, final_path) shutil.move(track_path, final_path)
self.completed_files.append(final_path)
self.log.debug(f"Saved: {final_path.name}") self.log.debug(f"Saved: {final_path.name}")
else: else:
# Handle muxed files # Handle muxed files
@@ -2596,6 +2598,7 @@ class dl:
final_path.unlink() final_path.unlink()
shutil.move(muxed_path, final_path) shutil.move(muxed_path, final_path)
used_final_paths.add(final_path) used_final_paths.add(final_path)
self.completed_files.append(final_path)
tags.tag_file(final_path, title, self.tmdb_id, self.imdb_id) tags.tag_file(final_path, title, self.tmdb_id, self.imdb_id)
title_dl_time = time_elapsed_since(dl_start_time) title_dl_time = time_elapsed_since(dl_start_time)

View File

@@ -2,6 +2,7 @@ import asyncio
import json import json
import logging import logging
import os import os
import re
import sys import sys
import tempfile import tempfile
import threading import threading
@@ -149,6 +150,20 @@ def _perform_download(
if params.get("export"): if params.get("export"):
params["export"] = bool(params["export"]) params["export"] = bool(params["export"])
# Convert wanted episode strings to internal "SxE" format
# Accepts: "S01E01", "S01-S03", "s1e1", "1x1", or already-parsed format
wanted_raw = params.get("wanted")
if wanted_raw:
from unshackle.core.utils.click_types import SeasonRange
if isinstance(wanted_raw, str):
wanted_raw = [wanted_raw]
# Only convert if not already in internal "SxE" format
needs_conversion = any(not re.match(r"^\d+x\d+$", w) for w in wanted_raw)
if needs_conversion:
season_range = SeasonRange()
params["wanted"] = season_range.parse_tokens(*wanted_raw)
# Load service configuration # Load service configuration
service_config_path = Services.get_path(service) / config.filenames.config service_config_path = Services.get_path(service) / config.filenames.config
if service_config_path.exists(): if service_config_path.exists():
@@ -271,6 +286,8 @@ def _perform_download(
acodec=params.get("acodec"), acodec=params.get("acodec"),
vbitrate=params.get("vbitrate"), vbitrate=params.get("vbitrate"),
abitrate=params.get("abitrate"), abitrate=params.get("abitrate"),
vbitrate_range=params.get("vbitrate_range"),
abitrate_range=params.get("abitrate_range"),
range_=params.get("range", ["SDR"]), range_=params.get("range", ["SDR"]),
channels=params.get("channels"), channels=params.get("channels"),
no_atmos=params.get("no_atmos", False), no_atmos=params.get("no_atmos", False),
@@ -306,6 +323,7 @@ def _perform_download(
no_mux=params.get("no_mux", False), no_mux=params.get("no_mux", False),
workers=params.get("workers"), workers=params.get("workers"),
downloads=params.get("downloads", 1), downloads=params.get("downloads", 1),
worst=params.get("worst", False),
best_available=params.get("best_available", False), best_available=params.get("best_available", False),
split_audio=params.get("split_audio"), split_audio=params.get("split_audio"),
) )
@@ -327,9 +345,10 @@ def _perform_download(
log.error(f"Stderr: {stderr_str}") log.error(f"Stderr: {stderr_str}")
raise raise
log.info(f"Download completed for job {job_id}, files in {original_download_dir}") output_files = [str(p) for p in dl_instance.completed_files]
log.info(f"Download completed for job {job_id}, {len(output_files)} file(s) in {original_download_dir}")
return [] return output_files
class DownloadQueueManager: class DownloadQueueManager:

View File

@@ -29,6 +29,8 @@ DEFAULT_DOWNLOAD_PARAMS = {
"acodec": None, "acodec": None,
"vbitrate": None, "vbitrate": None,
"abitrate": None, "abitrate": None,
"vbitrate_range": None,
"abitrate_range": None,
"range": ["SDR"], "range": ["SDR"],
"channels": None, "channels": None,
"no_atmos": False, "no_atmos": False,
@@ -62,6 +64,7 @@ DEFAULT_DOWNLOAD_PARAMS = {
"no_mux": False, "no_mux": False,
"workers": None, "workers": None,
"downloads": 1, "downloads": 1,
"worst": False,
"best_available": False, "best_available": False,
"repack": False, "repack": False,
"imdb_id": None, "imdb_id": None,
@@ -981,6 +984,14 @@ def validate_download_parameters(data: Dict[str, Any]) -> Optional[str]:
if not isinstance(data["abitrate"], int) or data["abitrate"] <= 0: if not isinstance(data["abitrate"], int) or data["abitrate"] <= 0:
return "abitrate must be a positive integer" return "abitrate must be a positive integer"
if "vbitrate_range" in data and data["vbitrate_range"] is not None:
if not isinstance(data["vbitrate_range"], str) or "-" not in data["vbitrate_range"]:
return "vbitrate_range must be a string in 'MIN-MAX' format (e.g., '6000-7000')"
if "abitrate_range" in data and data["abitrate_range"] is not None:
if not isinstance(data["abitrate_range"], str) or "-" not in data["abitrate_range"]:
return "abitrate_range must be a string in 'MIN-MAX' format (e.g., '128-256')"
if "channels" in data and data["channels"] is not None: if "channels" in data and data["channels"] is not None:
if not isinstance(data["channels"], (int, float)) or data["channels"] <= 0: if not isinstance(data["channels"], (int, float)) or data["channels"] <= 0:
return "channels must be a positive number" return "channels must be a positive number"

26
uv.lock generated
View File

@@ -150,6 +150,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
] ]
[[package]]
name = "bandit"
version = "1.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "pyyaml" },
{ name = "rich" },
{ name = "stevedore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" },
]
[[package]] [[package]]
name = "beautifulsoup4" name = "beautifulsoup4"
version = "4.14.3" version = "4.14.3"
@@ -1520,6 +1535,15 @@ version = "3.5.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/b7/4a1bc231e0681ebf339337b0cd05b91dc6a0d701fa852bb812e244b7a030/srt-3.5.3.tar.gz", hash = "sha256:4884315043a4f0740fd1f878ed6caa376ac06d70e135f306a6dc44632eed0cc0", size = 28296, upload-time = "2023-03-28T02:35:44.007Z" } sdist = { url = "https://files.pythonhosted.org/packages/66/b7/4a1bc231e0681ebf339337b0cd05b91dc6a0d701fa852bb812e244b7a030/srt-3.5.3.tar.gz", hash = "sha256:4884315043a4f0740fd1f878ed6caa376ac06d70e135f306a6dc44632eed0cc0", size = 28296, upload-time = "2023-03-28T02:35:44.007Z" }
[[package]]
name = "stevedore"
version = "5.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" },
]
[[package]] [[package]]
name = "subby" name = "subby"
version = "0.3.27" version = "0.3.27"
@@ -1634,6 +1658,7 @@ dependencies = [
{ name = "aiohttp-swagger3" }, { name = "aiohttp-swagger3" },
{ name = "animeapi-py" }, { name = "animeapi-py" },
{ name = "appdirs" }, { name = "appdirs" },
{ name = "bandit" },
{ name = "brotli" }, { name = "brotli" },
{ name = "chardet" }, { name = "chardet" },
{ name = "click" }, { name = "click" },
@@ -1691,6 +1716,7 @@ requires-dist = [
{ name = "aiohttp-swagger3", specifier = ">=0.9.0,<1" }, { name = "aiohttp-swagger3", specifier = ">=0.9.0,<1" },
{ name = "animeapi-py", specifier = ">=0.6.0" }, { name = "animeapi-py", specifier = ">=0.6.0" },
{ name = "appdirs", specifier = ">=1.4.4,<2" }, { name = "appdirs", specifier = ">=1.4.4,<2" },
{ name = "bandit", specifier = ">=1.9.4" },
{ name = "brotli", specifier = ">=1.1.0,<2" }, { name = "brotli", specifier = ">=1.1.0,<2" },
{ name = "chardet", specifier = ">=5.2.0,<6" }, { name = "chardet", specifier = ">=5.2.0,<6" },
{ name = "click", specifier = ">=8.1.8,<9" }, { name = "click", specifier = ">=8.1.8,<9" },