Fix movie watchlist completion sync
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 0.45.10 - 2026-05-26
|
||||
|
||||
- Fixed watchlist-synced movie downloads so successful movie jobs move existing watchlist entries into `Finished` instead of leaving them stuck in `Watching` or `Planned`.
|
||||
- Preserved queued job media type when creating missing watchlist entries from completed downloads, which prevents synced movies from being recreated as TV entries and keeps Jellyfin routing correct.
|
||||
|
||||
## 0.45.9 - 2026-05-26
|
||||
|
||||
- Clarified queue cleanup wording so the UI and README now match the real `Clear finished` behavior, which removes both finished and canceled jobs.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Small local web UI for a system-wide `ani-cli` install.
|
||||
|
||||
Current version: `0.45.8`
|
||||
Current version: `0.45.10`
|
||||
|
||||
## Project Layout
|
||||
|
||||
@@ -216,6 +216,7 @@ Watchlist queueing:
|
||||
- Manual `Download all` and automatic watchlist downloads now queue directly from the stored watchlist title and `show_id`, so they no longer fail just because a fresh provider search returns a shifted or incomplete result list.
|
||||
- Removing a watchlist entry now detaches its existing queue jobs from later watchlist sync, so finishing an old queued download cannot silently recreate that deleted show.
|
||||
- Manual watchlist `Download all` now uses the anime's own configured auto-download quality when one is set.
|
||||
- Completed movie downloads now keep their `movie` media type during watchlist sync and move movie-tagged watchlist entries into `Finished`, which keeps later Jellyfin handoff eligibility consistent.
|
||||
|
||||
Discord notification events:
|
||||
|
||||
|
||||
@@ -2001,13 +2001,14 @@ class WatchlistStore:
|
||||
)
|
||||
return self.schedule_refresh(existing["show_id"], source=source)
|
||||
|
||||
def mark_downloaded(self, show_id, title="", mode="", episodes=""):
|
||||
def mark_downloaded(self, show_id, title="", mode="", episodes="", media_type=""):
|
||||
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()
|
||||
normalized_media_type = normalize_media_type(media_type or "tv")
|
||||
episode_spec = str(episodes or "").strip()
|
||||
now = now_iso()
|
||||
downloaded_episode_values = []
|
||||
@@ -2030,7 +2031,10 @@ class WatchlistStore:
|
||||
completed_item = dict(existing_item)
|
||||
completed_item["downloaded_sub_episodes"] = decode_episode_values(sub_payload)
|
||||
completed_item["downloaded_dub_episodes"] = decode_episode_values(dub_payload)
|
||||
is_complete, _reason = watchlist_item_download_complete(completed_item, mode=normalized_mode)
|
||||
if normalize_media_type(existing_item.get("media_type")) == "movie":
|
||||
is_complete = True
|
||||
else:
|
||||
is_complete, _reason = watchlist_item_download_complete(completed_item, mode=normalized_mode)
|
||||
target_category = "finished" if is_complete else existing_item["category"]
|
||||
conn.execute(
|
||||
"""
|
||||
@@ -2058,7 +2062,10 @@ class WatchlistStore:
|
||||
completed_item = dict(matched_item)
|
||||
completed_item["downloaded_sub_episodes"] = decode_episode_values(sub_payload)
|
||||
completed_item["downloaded_dub_episodes"] = decode_episode_values(dub_payload)
|
||||
is_complete, _reason = watchlist_item_download_complete(completed_item, mode=normalized_mode)
|
||||
if normalize_media_type(matched_item.get("media_type")) == "movie":
|
||||
is_complete = True
|
||||
else:
|
||||
is_complete, _reason = watchlist_item_download_complete(completed_item, mode=normalized_mode)
|
||||
target_category = "finished" if is_complete else matched_item["category"]
|
||||
conn.execute(
|
||||
"""
|
||||
@@ -2117,7 +2124,7 @@ class WatchlistStore:
|
||||
"alternative_titles_json": None,
|
||||
"anidb_aid": None,
|
||||
"anidb_title": None,
|
||||
"media_type": "tv",
|
||||
"media_type": normalized_media_type,
|
||||
"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,
|
||||
@@ -2599,7 +2606,13 @@ 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, mode=job.get("mode"), episodes=job.get("episodes"))
|
||||
return WATCHLIST.mark_downloaded(
|
||||
show_id,
|
||||
title=title,
|
||||
mode=job.get("mode"),
|
||||
episodes=job.get("episodes"),
|
||||
media_type=job.get("media_type"),
|
||||
)
|
||||
|
||||
def prepare_download_job(job):
|
||||
config = ensure_runtime()["config"]
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ from uuid import uuid4
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
|
||||
APP_NAME = "ani-cli-web"
|
||||
VERSION = "0.45.9"
|
||||
VERSION = "0.45.10"
|
||||
ALLANIME_BASE = "allanime.day"
|
||||
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
|
||||
ALLANIME_REFERER = "https://allmanga.to"
|
||||
|
||||
+29
-1
@@ -1251,6 +1251,24 @@ class WatchlistCompletionTests(unittest.TestCase):
|
||||
self.assertEqual(item["finished_badge"], "downloaded")
|
||||
self.assertEqual(item["downloaded_dub_episodes"], ["1", "2", "3"])
|
||||
|
||||
def test_mark_downloaded_moves_existing_movie_to_finished(self):
|
||||
self.seed_watchlist_item(
|
||||
show_id="movie-1",
|
||||
title="Movie Show",
|
||||
category="watching",
|
||||
downloaded=False,
|
||||
media_type="movie",
|
||||
)
|
||||
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id, source="manual": APP.WATCHLIST.get(show_id)), mock.patch.object(
|
||||
APP.WATCHLIST, "refresh", side_effect=AssertionError("mark_downloaded should not refresh synchronously")
|
||||
):
|
||||
item = APP.WATCHLIST.mark_downloaded("movie-1", title="Movie Show", media_type="movie")
|
||||
|
||||
self.assertEqual(item["category"], "finished")
|
||||
self.assertEqual(item["media_type"], "movie")
|
||||
self.assertTrue(item["downloaded"])
|
||||
|
||||
def test_mark_downloaded_creates_missing_show_in_finished_category(self):
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id, source="manual": APP.WATCHLIST.get(show_id)), mock.patch.object(
|
||||
APP.WATCHLIST, "refresh", side_effect=AssertionError("mark_downloaded should not refresh synchronously")
|
||||
@@ -1262,6 +1280,16 @@ class WatchlistCompletionTests(unittest.TestCase):
|
||||
self.assertEqual(item["finished_badge"], "downloaded")
|
||||
self.assertEqual(item["status"], "queued")
|
||||
|
||||
def test_mark_downloaded_creates_missing_movie_in_finished_category(self):
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id, source="manual": APP.WATCHLIST.get(show_id)), mock.patch.object(
|
||||
APP.WATCHLIST, "refresh", side_effect=AssertionError("mark_downloaded should not refresh synchronously")
|
||||
):
|
||||
item = APP.WATCHLIST.mark_downloaded("movie-2", title="Downloaded Movie", media_type="movie")
|
||||
|
||||
self.assertEqual(item["category"], "finished")
|
||||
self.assertEqual(item["media_type"], "movie")
|
||||
self.assertTrue(item["downloaded"])
|
||||
|
||||
def test_mark_downloaded_does_not_recreate_removed_show(self):
|
||||
self.seed_watchlist_item(show_id="show-removed", title="Removed Show", category="watching")
|
||||
|
||||
@@ -1369,7 +1397,7 @@ class WatchlistCompletionTests(unittest.TestCase):
|
||||
) as mark_downloaded:
|
||||
APP.sync_downloaded_job_to_watchlist(job)
|
||||
|
||||
mark_downloaded.assert_called_once_with("show-42", title="Queue Show", mode="sub", episodes=None)
|
||||
mark_downloaded.assert_called_once_with("show-42", title="Queue Show", mode="sub", episodes=None, media_type=None)
|
||||
|
||||
def test_prepare_download_job_persists_resolved_show_id(self):
|
||||
job = {"query": "Queue Show", "mode": "sub", "result_index": 1, "title": "Queue Show", "show_id": ""}
|
||||
|
||||
Reference in New Issue
Block a user