Fix anipy fallback episode numbering

Use the queued episode spec when finalizing anipy-cli fallback TV downloads so retries keep the requested episode numbers instead of being renumbered from 1, and update tests plus release docs for the fix.
This commit is contained in:
Dymas
2026-07-09 15:16:53 +02:00
parent 0c17499efa
commit bcf9e15d40
5 changed files with 64 additions and 3 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog # Changelog
## 0.47.1 - 2026-07-09
- Fixed `anipy-cli` fallback renaming for queued TV downloads so retried jobs now keep the requested episode numbers from the queue, avoiding cases like a retried `S02E11` being renamed as `S02E01`.
## 0.47.0 - 2026-07-09 ## 0.47.0 - 2026-07-09
- Added an optional `anipy-cli` fallback toggle on the Config page so failed `ani-cli` queue jobs can automatically retry once with `anipy-cli`. - Added an optional `anipy-cli` fallback toggle on the Config page so failed `ani-cli` queue jobs can automatically retry once with `anipy-cli`.
+2 -1
View File
@@ -2,7 +2,7 @@
Local web UI for a system-wide `ani-cli` install. Local web UI for a system-wide `ani-cli` install.
Current version: `0.47.0` Current version: `0.47.1`
## What it does ## What it does
@@ -116,6 +116,7 @@ Docker notes:
- Optional and configured from `/config` - Optional and configured from `/config`
- When enabled, a failed `ani-cli` queue job retries once with `anipy-cli` - When enabled, a failed `ani-cli` queue job retries once with `anipy-cli`
- Fallback retries keep the queued episode numbers when the downloaded `anipy-cli` filenames are renumbered from `1`
- Docker images install `anipy-cli` automatically; non-Docker installs should provide it in `PATH` themselves - Docker images install `anipy-cli` automatically; non-Docker installs should provide it in `PATH` themselves
### Jellyfin handoff ### Jellyfin handoff
+1 -1
View File
@@ -1 +1 @@
0.47.0 0.47.1
+26
View File
@@ -1076,6 +1076,26 @@ def extract_episode_from_filename(path):
return match.group(1) if match else None return match.group(1) if match else None
def episode_values_from_spec(value):
spec = str(value or "").strip()
if not spec:
return []
values = []
for part in re.split(r"\s+", spec):
if not part:
continue
if "-" in part:
start_text, end_text = [piece.strip() for piece in part.split("-", 1)]
if start_text.isdigit() and end_text.isdigit():
start = int(start_text, 10)
end = int(end_text, 10)
if start <= end:
values.extend(str(number) for number in range(start, end + 1))
continue
values.append(part)
return values
def unique_destination(path): def unique_destination(path):
if not path.exists(): if not path.exists():
return path return path
@@ -1104,10 +1124,16 @@ def finalize_library_files(job):
moved = [] moved = []
fallback_episode = 1 fallback_episode = 1
requested_episode_values = episode_values_from_spec(job.get("episodes"))
backend = str(job.get("download_backend") or "").strip().lower()
for source in files: for source in files:
if media_type == "movie": if media_type == "movie":
final_name = f"{library_name}{source.suffix}" final_name = f"{library_name}{source.suffix}"
else: else:
ep = None
if backend == "anipy-cli" and requested_episode_values:
ep = requested_episode_values.pop(0)
if ep is None:
ep = extract_episode_from_filename(source) ep = extract_episode_from_filename(source)
if ep is None: if ep is None:
ep = str(fallback_episode) ep = str(fallback_episode)
+30
View File
@@ -589,6 +589,36 @@ class QueueApiTests(unittest.TestCase):
self.assertTrue(Path(moved[0]).exists()) self.assertTrue(Path(moved[0]).exists())
self.assertTrue(Path(moved[1]).exists()) self.assertTrue(Path(moved[1]).exists())
def test_tv_finalizer_uses_requested_episode_numbers_for_anipy_fallback(self):
with tempfile.TemporaryDirectory() as temp_root:
job = APP.app_support.build_job(
{
"query": "Fallback Show",
"title": "Fallback Show",
"anime_name": "Fallback Show",
"media_type": "tv",
"mode": "sub",
"quality": "best",
"episodes": "11",
"download_dir": temp_root,
"season": "2",
},
{"mode": "sub", "quality": "best", "download_dir": temp_root},
)
job["download_backend"] = "anipy-cli"
staging_dir = APP.app_support.job_staging_dir(job)
staging_dir.mkdir(parents=True, exist_ok=True)
source = staging_dir / "Fallback Show - S02E01.mp4"
source.write_bytes(b"fallback")
moved = APP.app_support.finalize_library_files(job)
self.assertEqual(
moved,
[f"{temp_root}/tv/Fallback Show/Season 02/Fallback Show - S02E11.mp4"],
)
self.assertTrue(Path(moved[0]).exists())
def test_shutdown_wait_cancels_active_process_for_deterministic_teardown(self): def test_shutdown_wait_cancels_active_process_for_deterministic_teardown(self):
queue = object.__new__(APP.DownloadQueue) queue = object.__new__(APP.DownloadQueue)
queue.lock = threading.RLock() queue.lock = threading.RLock()