Archived
Add watchlist filesystem reconciliation
This commit is contained in:
+121
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user