Track downloaded episodes for auto-download

This commit is contained in:
Dymas
2026-05-25 10:11:43 +02:00
parent baea85e24c
commit e48bc21944
6 changed files with 167 additions and 22 deletions
+113 -14
View File
@@ -938,6 +938,61 @@ def episode_specs_for_values(values, available_episodes):
return specs
def encode_episode_values(values):
normalized = []
seen = set()
for value in values or []:
text = str(value or "").strip()
if text and text not in seen:
seen.add(text)
normalized.append(text)
return json.dumps(normalized, ensure_ascii=True)
def decode_episode_values(payload):
if not payload:
return []
try:
raw = json.loads(payload)
except (TypeError, ValueError, json.JSONDecodeError):
return []
normalized = []
seen = set()
for value in raw if isinstance(raw, list) else []:
text = str(value or "").strip()
if text and text not in seen:
seen.add(text)
normalized.append(text)
return normalized
def expand_episode_spec_against_available(episode_spec, available_episodes):
specs = [part for part in re.split(r"\s+", str(episode_spec or "").strip()) if part]
values = [str(value).strip() for value in available_episodes or [] if str(value).strip()]
if not specs or not values:
return []
index_map = {value: index for index, value in enumerate(values)}
expanded = []
seen = set()
for spec in specs:
if "-" in spec:
start, end = [piece.strip() for piece in spec.split("-", 1)]
if start in index_map and end in index_map:
start_index = index_map[start]
end_index = index_map[end]
if start_index > end_index:
start_index, end_index = end_index, start_index
for value in values[start_index : end_index + 1]:
if value not in seen:
seen.add(value)
expanded.append(value)
continue
if spec in index_map and spec not in seen:
seen.add(spec)
expanded.append(spec)
return expanded
class WatchlistStore:
def __init__(self, config_getter=None, start_worker=True):
self.lock = threading.RLock()
@@ -1139,6 +1194,8 @@ class WatchlistStore:
auto_download_quality TEXT,
auto_download_name TEXT,
auto_download_series TEXT,
downloaded_sub_episodes_json TEXT,
downloaded_dub_episodes_json TEXT,
thumbnail_path TEXT,
thumbnail_checked_at TEXT
)
@@ -1181,6 +1238,10 @@ class WatchlistStore:
conn.execute("ALTER TABLE watchlist ADD COLUMN auto_download_name TEXT")
if "auto_download_series" not in columns:
conn.execute("ALTER TABLE watchlist ADD COLUMN auto_download_series TEXT")
if "downloaded_sub_episodes_json" not in columns:
conn.execute("ALTER TABLE watchlist ADD COLUMN downloaded_sub_episodes_json TEXT")
if "downloaded_dub_episodes_json" not in columns:
conn.execute("ALTER TABLE watchlist ADD COLUMN downloaded_dub_episodes_json TEXT")
conn.execute(
"UPDATE watchlist SET category = 'watching' WHERE category IS NULL OR TRIM(category) = ''"
)
@@ -1218,6 +1279,8 @@ class WatchlistStore:
item["auto_download_quality"] = quality if quality in QUALITY_CHOICES else defaults["auto_download_quality"]
item["auto_download_name"] = sanitize_path_component(item.get("auto_download_name") or defaults["auto_download_name"], defaults["auto_download_name"])
item["auto_download_series"] = normalize_season(item.get("auto_download_series") or defaults["auto_download_series"])
item["downloaded_sub_episodes"] = decode_episode_values(item.get("downloaded_sub_episodes_json"))
item["downloaded_dub_episodes"] = decode_episode_values(item.get("downloaded_dub_episodes_json"))
if item.get("animeschedule_route"):
title = str(item.get("animeschedule_title") or item.get("title") or "").strip() or "unknown title"
item["thumbnail_debug"] = f"AnimeSchedule: {item['animeschedule_route']} - {title}"
@@ -1241,8 +1304,9 @@ class WatchlistStore:
anilist_id, anilist_title, anilist_site_url, alternative_titles_json,
anidb_aid, anidb_title,
auto_download_mode, auto_download_quality, auto_download_name, auto_download_series,
downloaded_sub_episodes_json, downloaded_dub_episodes_json,
thumbnail_path, thumbnail_checked_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(show_id) DO UPDATE SET
title = excluded.title,
category = excluded.category,
@@ -1269,6 +1333,8 @@ class WatchlistStore:
auto_download_quality = excluded.auto_download_quality,
auto_download_name = excluded.auto_download_name,
auto_download_series = excluded.auto_download_series,
downloaded_sub_episodes_json = excluded.downloaded_sub_episodes_json,
downloaded_dub_episodes_json = excluded.downloaded_dub_episodes_json,
thumbnail_path = excluded.thumbnail_path,
thumbnail_checked_at = excluded.thumbnail_checked_at
""",
@@ -1300,6 +1366,8 @@ class WatchlistStore:
item.get("auto_download_quality"),
item.get("auto_download_name"),
item.get("auto_download_series"),
item.get("downloaded_sub_episodes_json"),
item.get("downloaded_dub_episodes_json"),
item.get("thumbnail_path"),
item.get("thumbnail_checked_at"),
),
@@ -1353,6 +1421,8 @@ class WatchlistStore:
"auto_download_quality": None,
"auto_download_name": None,
"auto_download_series": None,
"downloaded_sub_episodes_json": None,
"downloaded_dub_episodes_json": None,
"thumbnail_path": None,
"thumbnail_checked_at": None,
},
@@ -1485,6 +1555,8 @@ class WatchlistStore:
"anidb_title": None,
"thumbnail_path": None,
"thumbnail_checked_at": None,
"downloaded_sub_episodes_json": None,
"downloaded_dub_episodes_json": None,
}
self._upsert_conn(conn, item)
@@ -1675,29 +1747,53 @@ class WatchlistStore:
)
return self.schedule_refresh(existing["show_id"], source=source)
def mark_downloaded(self, show_id, title=""):
def mark_downloaded(self, show_id, title="", mode="", episodes=""):
normalized_show_id = str(show_id or "").strip()
if not normalized_show_id:
raise ValueError("Missing show_id for watchlist download sync.")
normalized_title = str(title or "").strip()
normalized_mode = str(mode or "").strip().lower()
episode_spec = str(episodes or "").strip()
now = now_iso()
downloaded_episode_values = []
if normalized_mode in MODE_CHOICES and episode_spec:
try:
available_episodes = episode_list(normalized_show_id, normalized_mode)
except Exception:
available_episodes = []
downloaded_episode_values = expand_episode_spec_against_available(episode_spec, available_episodes)
with self.lock, self._connect() as conn:
row = conn.execute("SELECT * FROM watchlist WHERE show_id = ?", (normalized_show_id,)).fetchone()
if row:
existing_item = self._row_to_item(row)
sub_payload = existing_item.get("downloaded_sub_episodes_json")
dub_payload = existing_item.get("downloaded_dub_episodes_json")
if normalized_mode == "sub" and downloaded_episode_values:
sub_payload = encode_episode_values(existing_item.get("downloaded_sub_episodes", []) + downloaded_episode_values)
elif normalized_mode == "dub" and downloaded_episode_values:
dub_payload = encode_episode_values(existing_item.get("downloaded_dub_episodes", []) + downloaded_episode_values)
conn.execute(
"""
UPDATE watchlist
SET category = 'finished', downloaded = 1, status = 'queued', status_message = ?, updated_at = ?
SET category = 'finished', downloaded = 1, status = 'queued', status_message = ?, updated_at = ?,
downloaded_sub_episodes_json = ?, downloaded_dub_episodes_json = ?
WHERE show_id = ?
""",
("Queued refresh after successful download.", now, normalized_show_id),
("Queued refresh after successful download.", now, sub_payload, dub_payload, normalized_show_id),
)
else:
matched_row = self._find_download_title_match_conn(conn, normalized_title, exclude_show_id=normalized_show_id)
if matched_row is not None:
matched_item = self._row_to_item(matched_row)
previous_show_id = str(matched_row["show_id"]).strip()
resolved_title = normalized_title or str(matched_row["title"] or "").strip() or "Unknown Anime"
sub_payload = matched_item.get("downloaded_sub_episodes_json")
dub_payload = matched_item.get("downloaded_dub_episodes_json")
if normalized_mode == "sub" and downloaded_episode_values:
sub_payload = encode_episode_values(matched_item.get("downloaded_sub_episodes", []) + downloaded_episode_values)
elif normalized_mode == "dub" and downloaded_episode_values:
dub_payload = encode_episode_values(matched_item.get("downloaded_dub_episodes", []) + downloaded_episode_values)
conn.execute(
"""
UPDATE watchlist
@@ -1708,7 +1804,9 @@ class WatchlistStore:
downloaded = 1,
status = 'queued',
status_message = ?,
updated_at = ?
updated_at = ?,
downloaded_sub_episodes_json = ?,
downloaded_dub_episodes_json = ?
WHERE show_id = ?
""",
(
@@ -1716,6 +1814,8 @@ class WatchlistStore:
resolved_title,
"Queued refresh after successful download.",
now,
sub_payload,
dub_payload,
previous_show_id,
),
)
@@ -1751,6 +1851,8 @@ class WatchlistStore:
"anidb_aid": None,
"anidb_title": None,
"auto_download_series": None,
"downloaded_sub_episodes_json": encode_episode_values(downloaded_episode_values) if normalized_mode == "sub" else None,
"downloaded_dub_episodes_json": encode_episode_values(downloaded_episode_values) if normalized_mode == "dub" else None,
"thumbnail_path": None,
"thumbnail_checked_at": None,
}
@@ -2029,8 +2131,6 @@ def queue_watchlist_auto_download(item, refresh_source="manual"):
return {"queued": False, "reason": "category"}
if not item.get("previous_last_checked"):
return {"queued": False, "reason": "initial_refresh"}
if not item.get("had_new_episodes"):
return {"queued": False, "reason": "no_new_episodes"}
mode = str(item.get("auto_download_mode") or config.get("auto_download_mode") or "dub").strip().lower()
quality = str(item.get("auto_download_quality") or config.get("auto_download_quality") or "best").strip().lower()
@@ -2039,16 +2139,15 @@ def queue_watchlist_auto_download(item, refresh_source="manual"):
if quality not in QUALITY_CHOICES:
quality = "best"
available_episodes = list(item.get(f"{mode}_episode_values") or [])
previous_count = int(item.get("previous_dub_count") or 0) if mode == "dub" else int(item.get("previous_sub_count") or 0)
new_episodes = available_episodes[previous_count:] if previous_count < len(available_episodes) else []
if not new_episodes:
return {"queued": False, "reason": "mode_has_no_new_episodes"}
downloaded_episodes = set(item.get("downloaded_dub_episodes", []) if mode == "dub" else item.get("downloaded_sub_episodes", []))
if not available_episodes:
return {"queued": False, "reason": "mode_has_no_available_episodes"}
covered = runtime["download_queue"].covered_episodes(item.get("show_id"), mode, available_episodes)
missing = [episode for episode in new_episodes if episode not in covered]
missing = [episode for episode in available_episodes if episode not in downloaded_episodes and episode not in covered]
specs = episode_specs_for_values(missing, available_episodes)
if not specs:
return {"queued": False, "reason": "already_covered"}
return {"queued": False, "reason": "nothing_missing"}
query = str(item.get("title") or item.get("show_id") or "").strip()
if not query:
@@ -2139,7 +2238,7 @@ def sync_downloaded_job_to_watchlist(job):
if not show_id:
debug_log("watchlist.sync.resolve_failed", job=job, reason=reason)
raise ValueError(f"Could not resolve a watchlist show_id for the completed download: {reason}.")
return WATCHLIST.mark_downloaded(show_id, title=title)
return WATCHLIST.mark_downloaded(show_id, title=title, mode=job.get("mode"), episodes=job.get("episodes"))
def prepare_download_job(job):
config = ensure_runtime()["config"]