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:
+98
-72
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user