Add watchlist filesystem reconciliation

This commit is contained in:
Codex
2026-07-21 09:41:24 +02:00
parent ef2df14aab
commit 0e40c9bda9
7 changed files with 332 additions and 4 deletions
+121
View File
@@ -2288,6 +2288,103 @@ class JellyfinSyncTests(unittest.TestCase):
self.assertIn("title mismatch", str(ctx.exception).lower())
class WatchlistFilesystemReconcileTests(unittest.TestCase):
def setUp(self):
with APP.WATCHLIST._connect() as conn:
conn.execute("DELETE FROM watchlist")
def seed_watchlist_item(self, **overrides):
now = APP.now_iso()
item = {
"show_id": "show-reconcile-1",
"title": "Reconcile Show",
"category": "watching",
"downloaded": True,
"status": "updated",
"status_message": "Tracked.",
"created_at": now,
"updated_at": now,
"last_checked": now,
"sub_count": 12,
"dub_count": 12,
"expected_count": 12,
"airing_status": "Finished",
"sub_latest_episode": "12",
"dub_latest_episode": "12",
"animeschedule_route": None,
"animeschedule_title": None,
"anidb_aid": None,
"anidb_title": None,
"media_type": "tv",
"auto_download_mode": "dub",
"auto_download_quality": "best",
"auto_download_name": "Reconcile Show",
"auto_download_source_name": "Reconcile Show",
"auto_download_series": "2",
"auto_download_offset": "13",
"downloaded_sub_episodes_json": None,
"downloaded_dub_episodes_json": APP.encode_episode_values(["1", "3"]),
"thumbnail_path": None,
"thumbnail_checked_at": None,
}
item.update(overrides)
with APP.WATCHLIST.lock, APP.WATCHLIST._connect() as conn:
APP.WATCHLIST._upsert_conn(conn, item)
def test_reconcile_watchlist_downloaded_files_updates_episode_sets_from_filesystem(self):
self.seed_watchlist_item()
with tempfile.TemporaryDirectory() as temp_root:
season_dir = Path(temp_root) / "tv" / "Reconcile Show" / "Season 02"
season_dir.mkdir(parents=True, exist_ok=True)
(season_dir / "Reconcile Show - S02E13.mp4").write_bytes(b"one")
(season_dir / "Reconcile Show - S02E14.mkv").write_bytes(b"two")
result = APP.reconcile_watchlist_downloaded_files({"download_dir": temp_root, "mode": "sub", "quality": "best"})
self.assertEqual(result["changed"], 1)
item = APP.WATCHLIST.get("show-reconcile-1")
self.assertTrue(item["downloaded"])
self.assertEqual(item["downloaded_dub_episodes"], ["1", "2"])
self.assertEqual(item["downloaded_sub_episodes"], [])
def test_reconcile_watchlist_downloaded_files_clears_missing_files(self):
self.seed_watchlist_item(
downloaded_sub_episodes_json=APP.encode_episode_values(["1"]),
downloaded_dub_episodes_json=APP.encode_episode_values(["1", "2"]),
)
result = APP.reconcile_watchlist_downloaded_files({"download_dir": "/tmp/missing-download-root", "mode": "sub", "quality": "best"})
self.assertEqual(result["changed"], 1)
item = APP.WATCHLIST.get("show-reconcile-1")
self.assertFalse(item["downloaded"])
self.assertEqual(item["downloaded_sub_episodes"], [])
self.assertEqual(item["downloaded_dub_episodes"], [])
def test_reconcile_watchlist_downloaded_files_marks_movie_present_when_file_exists(self):
self.seed_watchlist_item(
show_id="show-movie-1",
title="Movie Reconcile",
media_type="movie",
downloaded=False,
auto_download_name="Movie Reconcile",
auto_download_series="1",
auto_download_offset=None,
downloaded_sub_episodes_json=None,
downloaded_dub_episodes_json=None,
)
with tempfile.TemporaryDirectory() as temp_root:
movie_dir = Path(temp_root) / "movie" / "Movie Reconcile"
movie_dir.mkdir(parents=True, exist_ok=True)
(movie_dir / "Movie Reconcile.mp4").write_bytes(b"movie")
result = APP.reconcile_watchlist_downloaded_files({"download_dir": temp_root, "mode": "sub", "quality": "best"})
self.assertEqual(result["changed"], 1)
item = APP.WATCHLIST.get("show-movie-1")
self.assertTrue(item["downloaded"])
class WatchlistQueuedRefreshRecoveryTests(unittest.TestCase):
def setUp(self):
with APP.WATCHLIST._connect() as conn:
@@ -3641,6 +3738,30 @@ class HandlerRouteTests(unittest.TestCase):
}
)
def test_watchlist_reconcile_post_merges_partial_payload_with_saved_config(self):
body = b'{"download_dir":"/tmp/override"}'
handler = DummyHandler("/api/config/watchlist/reconcile-downloads", body=body, content_length=len(body))
payload = {"message": "ok", "total": 2, "changed": 1, "items": []}
handler.handler_context = mock.Mock(
get_config_snapshot=mock.Mock(
return_value={
"download_dir": "/tmp/example",
"mode": "sub",
"quality": "best",
}
),
reconcile_watchlist_downloaded_files=mock.Mock(return_value=payload),
)
APP.Handler.do_POST(handler)
self.assertEqual(handler.json_status, HTTPStatus.OK)
handler.handler_context.reconcile_watchlist_downloaded_files.assert_called_once_with(
{
"download_dir": "/tmp/override",
"mode": "sub",
"quality": "best",
}
)
def test_generic_handler_exception_skips_traceback_when_debug_disabled(self):
handler = DummyHandler("/api/config")
with mock.patch.object(http_handler, "debug_enabled", return_value=False), mock.patch.object(