Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf9e15d40 | ||
|
|
0c17499efa |
@@ -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`.
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ RUN curl -fL "${YT_DLP_RELEASE_URL}" -o /usr/local/bin/yt-dlp \
|
|||||||
&& chmod 0755 /usr/local/bin/yt-dlp \
|
&& chmod 0755 /usr/local/bin/yt-dlp \
|
||||||
&& yt-dlp --version
|
&& yt-dlp --version
|
||||||
|
|
||||||
|
RUN python3 -m pip install --no-cache-dir anipy-cli \
|
||||||
|
&& anipy-cli --version
|
||||||
|
|
||||||
RUN git clone --depth 1 --branch "${ANI_CLI_WEB_BRANCH}" "${ANI_CLI_WEB}" /app \
|
RUN git clone --depth 1 --branch "${ANI_CLI_WEB_BRANCH}" "${ANI_CLI_WEB}" /app \
|
||||||
&& rm -rf /app/.git \
|
&& rm -rf /app/.git \
|
||||||
&& rm -f \
|
&& rm -f \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ Current version: `0.47.0`
|
|||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
- `ani-cli` installed and available in `PATH`
|
- `ani-cli` installed and available in `PATH`
|
||||||
- Optional: `anipy-cli` installed and available in `PATH` if you want automatic fallback retries
|
- Optional: `anipy-cli` installed and available in `PATH` if you want automatic fallback retries outside Docker
|
||||||
- Common runtime tools used by `ani-cli`, especially `curl`, `ffmpeg`, `fzf`, `grep`, `openssl`, `sed`, `aria2c`, and `yt-dlp`
|
- Common runtime tools used by `ani-cli`, especially `curl`, `ffmpeg`, `fzf`, `grep`, `openssl`, `sed`, `aria2c`, and `yt-dlp`
|
||||||
|
|
||||||
Run locally:
|
Run locally:
|
||||||
@@ -93,6 +93,7 @@ Docker notes:
|
|||||||
- `USER_UID` and `USER_GID` help keep mounted files owned by your host user
|
- `USER_UID` and `USER_GID` help keep mounted files owned by your host user
|
||||||
- `UPDATE_ON_START=true` runs `ani-cli --update` before startup
|
- `UPDATE_ON_START=true` runs `ani-cli --update` before startup
|
||||||
- `ANI_CLI_WEB_BRANCH` lets the image clone a specific `ani-cli-web` branch at build time
|
- `ANI_CLI_WEB_BRANCH` lets the image clone a specific `ani-cli-web` branch at build time
|
||||||
|
- `anipy-cli` is installed systemwide inside the container, so fallback retries are available without extra container setup
|
||||||
|
|
||||||
## Key behavior
|
## Key behavior
|
||||||
|
|
||||||
@@ -115,7 +116,8 @@ 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`
|
||||||
- `anipy-cli` should be installed separately on the host or inside your container if you want the retry path to be available
|
- 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
|
### Jellyfin handoff
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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