Improve anikoto backup search fallback
- prefer the canonical queued title for anikoto-cli backup searches instead of relying only on the ani-cli source query - retry backup searches with alternate title variants when AniList misses the first lookup - bump the project version to 0.46.4 and document the fix in the changelog and README
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.46.4 - 2026-07-09
|
||||||
|
|
||||||
|
- Improved backup `anikoto-cli` search handling by preferring the canonical queued title over provider-specific source overrides and retrying with alternate title variants when AniList search misses the first query.
|
||||||
|
|
||||||
## 0.46.3 - 2026-07-09
|
## 0.46.3 - 2026-07-09
|
||||||
|
|
||||||
- Fixed backup `anikoto-cli` queue runs to force `fzf` into exact-match auto-select mode, which keeps the fallback downloader non-interactive inside the background worker instead of failing with `nothing selected` and `inappropriate ioctl for device`.
|
- Fixed backup `anikoto-cli` queue runs to force `fzf` into exact-match auto-select mode, which keeps the fallback downloader non-interactive inside the background worker instead of failing with `nothing selected` and `inappropriate ioctl for device`.
|
||||||
|
|||||||
@@ -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.46.3`
|
Current version: `0.46.4`
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
@@ -126,6 +126,7 @@ Docker notes:
|
|||||||
- When enabled, any failed `ani-cli` queue job automatically retries with `anikoto-cli`
|
- When enabled, any failed `ani-cli` queue job automatically retries with `anikoto-cli`
|
||||||
- `anikoto-cli` ignores the saved quality setting, so backup attempts use the source's default stream quality
|
- `anikoto-cli` ignores the saved quality setting, so backup attempts use the source's default stream quality
|
||||||
- Backup retries now auto-select the queued anime title non-interactively so the fallback works inside the background download worker
|
- Backup retries now auto-select the queued anime title non-interactively so the fallback works inside the background download worker
|
||||||
|
- Backup retries also prefer the canonical queued anime title and can try alternate title variants if a source-specific search name does not exist on AniList
|
||||||
|
|
||||||
### Jellyfin handoff
|
### Jellyfin handoff
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -1161,7 +1161,8 @@ def build_job(payload, config):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def command_for_job(job, downloader="ani-cli"):
|
def command_for_job(job, downloader="ani-cli", query_override=None):
|
||||||
|
search_query = str(query_override or job.get("query") or job.get("title") or "").strip()
|
||||||
if str(downloader or "ani-cli").strip().lower() == "anikoto-cli":
|
if str(downloader or "ani-cli").strip().lower() == "anikoto-cli":
|
||||||
command = [
|
command = [
|
||||||
ANIKOTO_CLI,
|
ANIKOTO_CLI,
|
||||||
@@ -1173,7 +1174,7 @@ def command_for_job(job, downloader="ani-cli"):
|
|||||||
command.append("--dub")
|
command.append("--dub")
|
||||||
else:
|
else:
|
||||||
command.append("--sub")
|
command.append("--sub")
|
||||||
command.append(job["query"])
|
command.append(search_query)
|
||||||
return command
|
return command
|
||||||
|
|
||||||
command = [
|
command = [
|
||||||
@@ -1186,5 +1187,5 @@ def command_for_job(job, downloader="ani-cli"):
|
|||||||
]
|
]
|
||||||
if job["mode"] == "dub":
|
if job["mode"] == "dub":
|
||||||
command.append("--dub")
|
command.append("--dub")
|
||||||
command.append(job["query"])
|
command.append(search_query)
|
||||||
return command
|
return command
|
||||||
|
|||||||
+30
-4
@@ -33,6 +33,7 @@ from app_support import (
|
|||||||
sh_quote,
|
sh_quote,
|
||||||
strip_control,
|
strip_control,
|
||||||
)
|
)
|
||||||
|
from title_matching import title_lookup_queries
|
||||||
|
|
||||||
|
|
||||||
LOG_PERSIST_INTERVAL_SECONDS = 0.75
|
LOG_PERSIST_INTERVAL_SECONDS = 0.75
|
||||||
@@ -1347,10 +1348,19 @@ class DownloadQueue:
|
|||||||
attempts.append(("anikoto-cli", lambda job: command_for_job(job, downloader="anikoto-cli")))
|
attempts.append(("anikoto-cli", lambda job: command_for_job(job, downloader="anikoto-cli")))
|
||||||
return attempts
|
return attempts
|
||||||
|
|
||||||
def _attempt_env(self, base_env, job, downloader_name):
|
def _backup_search_queries(self, job):
|
||||||
|
queries = []
|
||||||
|
for value in (job.get("title"), job.get("query"), job.get("anime_name")):
|
||||||
|
for candidate in title_lookup_queries(value):
|
||||||
|
text = str(candidate or "").strip()
|
||||||
|
if text and text not in queries:
|
||||||
|
queries.append(text)
|
||||||
|
return queries or [str(job.get("query") or job.get("title") or "").strip()]
|
||||||
|
|
||||||
|
def _attempt_env(self, base_env, downloader_name, search_query):
|
||||||
attempt_env = dict(base_env)
|
attempt_env = dict(base_env)
|
||||||
if downloader_name == "anikoto-cli":
|
if downloader_name == "anikoto-cli":
|
||||||
query = str(job.get("query") or job.get("title") or "").strip()
|
query = str(search_query or "").strip()
|
||||||
if query:
|
if query:
|
||||||
# Force fzf into non-interactive exact-match mode so backup downloads
|
# Force fzf into non-interactive exact-match mode so backup downloads
|
||||||
# can auto-select the intended anime inside the queue worker.
|
# can auto-select the intended anime inside the queue worker.
|
||||||
@@ -1451,7 +1461,6 @@ class DownloadQueue:
|
|||||||
download_succeeded = False
|
download_succeeded = False
|
||||||
|
|
||||||
for attempt_index, (downloader_name, command_builder) in enumerate(attempts):
|
for attempt_index, (downloader_name, command_builder) in enumerate(attempts):
|
||||||
command = command_builder(job)
|
|
||||||
if attempt_index > 0:
|
if attempt_index > 0:
|
||||||
self._cleanup_staging_dir(job)
|
self._cleanup_staging_dir(job)
|
||||||
staging_dir.mkdir(parents=True, exist_ok=True)
|
staging_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -1461,6 +1470,21 @@ class DownloadQueue:
|
|||||||
self._append_log(job, "Backup downloader anikoto-cli is not installed or not executable.")
|
self._append_log(job, "Backup downloader anikoto-cli is not installed or not executable.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
search_queries = [str(job.get("query") or "").strip()]
|
||||||
|
if downloader_name == "anikoto-cli":
|
||||||
|
search_queries = self._backup_search_queries(job)
|
||||||
|
|
||||||
|
for query_index, search_query in enumerate(search_queries):
|
||||||
|
if downloader_name == "anikoto-cli" and query_index > 0:
|
||||||
|
self._cleanup_staging_dir(job)
|
||||||
|
staging_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._append_log(job, f"Trying alternate anikoto-cli search query: {search_query}")
|
||||||
|
|
||||||
|
command = command_builder(job) if downloader_name != "anikoto-cli" else command_for_job(
|
||||||
|
job,
|
||||||
|
downloader="anikoto-cli",
|
||||||
|
query_override=search_query,
|
||||||
|
)
|
||||||
with self.lock:
|
with self.lock:
|
||||||
job["command"] = printable_command(command)
|
job["command"] = printable_command(command)
|
||||||
job["updated_at"] = now_iso()
|
job["updated_at"] = now_iso()
|
||||||
@@ -1468,7 +1492,7 @@ class DownloadQueue:
|
|||||||
self._append_log(job, f"Launching {downloader_name}: {job['command']}")
|
self._append_log(job, f"Launching {downloader_name}: {job['command']}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
attempt_env = self._attempt_env(env, job, downloader_name)
|
attempt_env = self._attempt_env(env, downloader_name, search_query)
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
command,
|
command,
|
||||||
cwd=str(PROJECT_ROOT),
|
cwd=str(PROJECT_ROOT),
|
||||||
@@ -1535,6 +1559,8 @@ class DownloadQueue:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
watchlist_sync_error = exc
|
watchlist_sync_error = exc
|
||||||
break
|
break
|
||||||
|
if canceled or download_succeeded:
|
||||||
|
break
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
job["exit_code"] = last_exit_code
|
job["exit_code"] = last_exit_code
|
||||||
|
|||||||
+2
-1
@@ -679,7 +679,7 @@ class DownloadQueueWorkerFailureTests(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
start_worker=False,
|
start_worker=False,
|
||||||
)
|
)
|
||||||
job = queue.add({"query": "Queue Show", "title": "Queue Show", "season": "1", "episodes": "1"})
|
job = queue.add({"query": "Queue Source", "title": "Queue Show", "season": "1", "episodes": "1"})
|
||||||
|
|
||||||
class FailedProc:
|
class FailedProc:
|
||||||
pid = 1111
|
pid = 1111
|
||||||
@@ -721,6 +721,7 @@ class DownloadQueueWorkerFailureTests(unittest.TestCase):
|
|||||||
self.assertIn("--exact", popen_calls[1]["env"]["FZF_DEFAULT_OPTS"])
|
self.assertIn("--exact", popen_calls[1]["env"]["FZF_DEFAULT_OPTS"])
|
||||||
self.assertIn("--select-1", popen_calls[1]["env"]["FZF_DEFAULT_OPTS"])
|
self.assertIn("--select-1", popen_calls[1]["env"]["FZF_DEFAULT_OPTS"])
|
||||||
self.assertIn("Queue Show", popen_calls[1]["env"]["FZF_DEFAULT_OPTS"])
|
self.assertIn("Queue Show", popen_calls[1]["env"]["FZF_DEFAULT_OPTS"])
|
||||||
|
self.assertEqual(popen_calls[1]["command"][-1], "Queue Show")
|
||||||
|
|
||||||
def test_run_job_marks_post_start_exception_failed_without_killing_worker_state(self):
|
def test_run_job_marks_post_start_exception_failed_without_killing_worker_state(self):
|
||||||
queue = APP.DownloadQueue(lambda: {"mode": "sub", "quality": "best", "download_dir": "/tmp/example"}, start_worker=False)
|
queue = APP.DownloadQueue(lambda: {"mode": "sub", "quality": "best", "download_dir": "/tmp/example"}, start_worker=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user