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:
Dymas
2026-07-09 13:39:44 +02:00
parent 897e12e019
commit 34da6b26c5
6 changed files with 111 additions and 78 deletions
+98 -72
View File
@@ -33,6 +33,7 @@ from app_support import (
sh_quote,
strip_control,
)
from title_matching import title_lookup_queries
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")))
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)
if downloader_name == "anikoto-cli":
query = str(job.get("query") or job.get("title") or "").strip()
query = str(search_query or "").strip()
if query:
# Force fzf into non-interactive exact-match mode so backup downloads
# can auto-select the intended anime inside the queue worker.
@@ -1451,7 +1461,6 @@ class DownloadQueue:
download_succeeded = False
for attempt_index, (downloader_name, command_builder) in enumerate(attempts):
command = command_builder(job)
if attempt_index > 0:
self._cleanup_staging_dir(job)
staging_dir.mkdir(parents=True, exist_ok=True)
@@ -1461,80 +1470,97 @@ class DownloadQueue:
self._append_log(job, "Backup downloader anikoto-cli is not installed or not executable.")
continue
with self.lock:
job["command"] = printable_command(command)
job["updated_at"] = now_iso()
self._save_job_locked(job)
self._append_log(job, f"Launching {downloader_name}: {job['command']}")
search_queries = [str(job.get("query") or "").strip()]
if downloader_name == "anikoto-cli":
search_queries = self._backup_search_queries(job)
try:
attempt_env = self._attempt_env(env, job, downloader_name)
process = subprocess.Popen(
command,
cwd=str(PROJECT_ROOT),
env=attempt_env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
start_new_session=True,
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,
)
except Exception as exc:
last_exit_code = 1
last_finalize_error = None
self._append_log(job, f"Could not start {downloader_name}: {exc}")
continue
with self.lock:
self.current_process = process
self.current_job_id = job["id"]
self.current_job = job
job["pid"] = process.pid
self._save_job_locked(job)
with self.lock:
job["command"] = printable_command(command)
job["updated_at"] = now_iso()
self._save_job_locked(job)
self._append_log(job, f"Launching {downloader_name}: {job['command']}")
assert process.stdout is not None
for line in process.stdout:
self._append_log(job, line)
exit_code = process.wait()
with self.lock:
job["pid"] = None
self.current_process = None
self.current_job_id = None
self.current_job = None
self._save_job_locked(job)
canceled = bool(job.get("cancel_requested"))
if canceled:
last_exit_code = exit_code
break
if exit_code != 0:
last_exit_code = exit_code
last_finalize_error = None
self._append_log(job, f"{downloader_name} failed with exit code {exit_code}.")
continue
try:
moved_files = finalize_library_files(job)
except Exception as exc:
last_exit_code = 1
last_finalize_error = exc
message = str(exc).strip()
if "No downloaded files were found in the staging folder" in message:
self._append_log(
job,
f"{downloader_name} exited without downloading any files. The selected result or episodes may not be downloadable.",
)
else:
self._append_log(job, f"Library rename failed after {downloader_name}: {exc}")
continue
download_succeeded = True
last_exit_code = 0
last_finalize_error = None
if self.watchlist_sync_fn is not None:
try:
synced_watchlist_item = self.watchlist_sync_fn(job)
attempt_env = self._attempt_env(env, downloader_name, search_query)
process = subprocess.Popen(
command,
cwd=str(PROJECT_ROOT),
env=attempt_env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
start_new_session=True,
)
except Exception as exc:
watchlist_sync_error = exc
break
last_exit_code = 1
last_finalize_error = None
self._append_log(job, f"Could not start {downloader_name}: {exc}")
continue
with self.lock:
self.current_process = process
self.current_job_id = job["id"]
self.current_job = job
job["pid"] = process.pid
self._save_job_locked(job)
assert process.stdout is not None
for line in process.stdout:
self._append_log(job, line)
exit_code = process.wait()
with self.lock:
job["pid"] = None
self.current_process = None
self.current_job_id = None
self.current_job = None
self._save_job_locked(job)
canceled = bool(job.get("cancel_requested"))
if canceled:
last_exit_code = exit_code
break
if exit_code != 0:
last_exit_code = exit_code
last_finalize_error = None
self._append_log(job, f"{downloader_name} failed with exit code {exit_code}.")
continue
try:
moved_files = finalize_library_files(job)
except Exception as exc:
last_exit_code = 1
last_finalize_error = exc
message = str(exc).strip()
if "No downloaded files were found in the staging folder" in message:
self._append_log(
job,
f"{downloader_name} exited without downloading any files. The selected result or episodes may not be downloadable.",
)
else:
self._append_log(job, f"Library rename failed after {downloader_name}: {exc}")
continue
download_succeeded = True
last_exit_code = 0
last_finalize_error = None
if self.watchlist_sync_fn is not None:
try:
synced_watchlist_item = self.watchlist_sync_fn(job)
except Exception as exc:
watchlist_sync_error = exc
break
if canceled or download_succeeded:
break
with self.lock:
job["exit_code"] = last_exit_code