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:
@@ -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,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
|
||||||
|
|||||||
+27
-1
@@ -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,11 +1124,17 @@ 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 = 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:
|
if ep is None:
|
||||||
ep = str(fallback_episode)
|
ep = str(fallback_episode)
|
||||||
fallback_episode += 1
|
fallback_episode += 1
|
||||||
|
|||||||
+30
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user