diff --git a/CHANGELOG.md b/CHANGELOG.md index 9115ef5..12237bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 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 - Added an optional `anipy-cli` fallback toggle on the Config page so failed `ani-cli` queue jobs can automatically retry once with `anipy-cli`. diff --git a/README.md b/README.md index 8948017..b52ab37 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Local web UI for a system-wide `ani-cli` install. -Current version: `0.47.0` +Current version: `0.47.1` ## What it does @@ -116,6 +116,7 @@ Docker notes: - Optional and configured from `/config` - 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 ### Jellyfin handoff diff --git a/VERSION b/VERSION index 421ab54..650298f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.47.0 +0.47.1 diff --git a/app_support.py b/app_support.py index 43cb2c3..93174c8 100644 --- a/app_support.py +++ b/app_support.py @@ -1076,6 +1076,26 @@ def extract_episode_from_filename(path): 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): if not path.exists(): return path @@ -1104,11 +1124,17 @@ def finalize_library_files(job): moved = [] 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: if media_type == "movie": final_name = f"{library_name}{source.suffix}" else: - ep = extract_episode_from_filename(source) + 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) if ep is None: ep = str(fallback_episode) fallback_episode += 1 diff --git a/test_app.py b/test_app.py index d314e91..526cd08 100644 --- a/test_app.py +++ b/test_app.py @@ -589,6 +589,36 @@ class QueueApiTests(unittest.TestCase): self.assertTrue(Path(moved[0]).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): queue = object.__new__(APP.DownloadQueue) queue.lock = threading.RLock()