', APP.CONFIG_HTML)
self.assertLess(APP.CONFIG_HTML.index('
'), APP.CONFIG_HTML.index('
'))
self.assertIn("@media (max-width: 1080px)", APP.CONFIG_HTML)
self.assertIn("justify-items: stretch;", APP.CONFIG_HTML)
self.assertIn('id="versionLine"', APP.CONFIG_HTML)
self.assertIn('id="providerLine"', APP.CONFIG_HTML)
self.assertIn('id="openChangelogBtn"', APP.CONFIG_HTML)
self.assertIn('id="changelogOverlay"', APP.CONFIG_HTML)
self.assertIn('const data = await api("/api/changelog");', APP.CONFIG_HTML)
self.assertIn('changelogContent.textContent = data.content || "Changelog unavailable.";', APP.CONFIG_HTML)
def test_config_page_includes_host_path_browser_controls(self):
self.assertIn('class="ghost browse-path-btn"', APP.CONFIG_HTML)
self.assertIn('id="pathBrowserOverlay"', APP.CONFIG_HTML)
self.assertIn('/api/fs/browse?path=${encodeURIComponent(path || state.pathBrowser.path || "")}', APP.CONFIG_HTML)
self.assertIn('rootGroup.className = "browser-roots";', APP.CONFIG_HTML)
self.assertIn('const active = data.path === rootPath || String(data.path || "").startsWith(`${rootPath}/`);', APP.CONFIG_HTML)
def test_search_page_sends_watchlist_auto_download_series(self):
self.assertIn('auto_download_series: $("seasonInput").value || "1"', APP.INDEX_HTML)
def test_search_page_closes_selection_modal_after_successful_actions(self):
self.assertIn('setNotice(data.message || "Added to watchlist.");\n closeSelectionModal();', APP.INDEX_HTML)
self.assertIn('setNotice("Added to queue. Open Queue to monitor progress.");\n closeSelectionModal();', APP.INDEX_HTML)
def test_queue_page_includes_clear_failed_bulk_action(self):
self.assertIn('id="clearFailedBtn"', APP.QUEUE_HTML)
self.assertIn('/api/queue/clear-failed', APP.QUEUE_HTML)
self.assertIn('Removed failed jobs', APP.QUEUE_HTML)
def test_queue_page_formats_jellyfin_handoff_jobs(self):
self.assertIn('job.job_type === "jellyfin_handoff"', APP.QUEUE_HTML)
self.assertIn('const progress = `${job.completed || 0}/${job.total || 0} entries`;', APP.QUEUE_HTML)
self.assertIn('const moved = `${job.moved || 0} moved`;', APP.QUEUE_HTML)
def test_queue_page_renders_download_progress_bar(self):
self.assertIn('class="progress-wrap"', APP.QUEUE_HTML)
self.assertIn('class="progress-fill"', APP.QUEUE_HTML)
self.assertIn("function progressPercent(job)", APP.QUEUE_HTML)
self.assertIn('item.querySelector(".progress-fill").style.width = `${percent}%`;', APP.QUEUE_HTML)
self.assertIn("segment ${progress.segment}/${progress.segment_total}", APP.QUEUE_HTML)
def test_search_page_queues_selected_title_with_result_index(self):
self.assertIn('query: state.selected.title,', APP.INDEX_HTML)
self.assertIn('result_index: state.selected.index,', APP.INDEX_HTML)
def test_search_page_renders_poster_results_under_selection_panel(self):
self.assertIn('class="results-grid" id="results"', APP.INDEX_HTML)
self.assertIn("Select results", APP.INDEX_HTML)
self.assertIn("resultThumbnailUrl(item)", APP.INDEX_HTML)
self.assertIn('/api/search/thumb?title=', APP.INDEX_HTML)
self.assertIn("result-subtitle", APP.INDEX_HTML)
self.assertIn("Also known as:", APP.INDEX_HTML)
def test_search_anime_includes_alternative_titles(self):
payload = {"results": [{"id": "show-1", "title": "Example Show", "provider": "anikoto"}]}
with mock.patch.object(provider_bridge, "search", return_value=payload), mock.patch.object(
provider_bridge, "episodes", return_value=[{"number": "1", "langs": ["sub"]}],
), mock.patch.object(
APP, "extract_anidb_alternative_titles_for_title",
return_value=[{"label": "English", "value": "Example Show English"}],
):
results = APP.search_anime("Example Show", "sub")
self.assertEqual(results[0]["alternative_titles"], [{"label": "English", "value": "Example Show English"}])
def test_pages_share_serial_polling_helper(self):
self.assertIn("function startSerialPoll(callback, intervalMs)", APP.QUEUE_HTML)
self.assertIn("function startSerialPoll(callback, intervalMs)", APP.WATCHLIST_HTML)
self.assertIn("startSerialPoll(() => pollVisibleJobs(), 2500)", APP.QUEUE_HTML)
self.assertIn("startSerialPoll(async () => {", APP.WATCHLIST_HTML)
self.assertIn("window.clearTimeout(timer);", APP.QUEUE_HTML)
self.assertIn('window.addEventListener("pagehide", stop, { once: true });', APP.QUEUE_HTML)
self.assertIn('window.addEventListener("beforeunload", stop, { once: true });', APP.WATCHLIST_HTML)
def test_watchlist_page_polls_when_queued_items_exist(self):
self.assertIn("queuedCount: 0", APP.WATCHLIST_HTML)
self.assertIn("else if (state.queuedCount > 0)", APP.WATCHLIST_HTML)
self.assertIn("fetchWatchlist(state.watchlistPage, { silent: true })", APP.WATCHLIST_HTML)
def test_watchlist_page_highlights_fully_ready_cards(self):
self.assertIn(".anime-card.fully-ready", APP.WATCHLIST_HTML)
self.assertIn("const isFullyReady = item.sub_complete && item.dub_complete;", APP.WATCHLIST_HTML)
self.assertIn("card.className = watchlistCardClassName(item);", APP.WATCHLIST_HTML)
def test_watchlist_page_download_all_replaces_open_action(self):
self.assertIn("Download all", APP.WATCHLIST_HTML)
self.assertIn('api("/api/watchlist/download-all"', APP.WATCHLIST_HTML)
self.assertIn('class="download-picker"', APP.WATCHLIST_HTML)
self.assertIn('downloadPicker.classList.toggle("visible")', APP.WATCHLIST_HTML)
self.assertIn('downloadWatchlistItem(item, "sub"', APP.WATCHLIST_HTML)
self.assertIn('downloadWatchlistItem(item, "dub"', APP.WATCHLIST_HTML)
self.assertIn("grid-template-columns: repeat(2, minmax(0, 1fr));", APP.WATCHLIST_HTML)
self.assertLess(APP.WATCHLIST_HTML.index(''), APP.WATCHLIST_HTML.index(''))
self.assertLess(APP.WATCHLIST_HTML.index(''), APP.WATCHLIST_HTML.index(''))
def test_watchlist_page_renders_alternative_titles_toggle(self):
self.assertIn("Alternative titles", APP.WATCHLIST_HTML)
self.assertIn('class="ghost alt-titles-toggle"', APP.WATCHLIST_HTML)
self.assertIn('id="altTitlesOverlay"', APP.WATCHLIST_HTML)
self.assertIn("openAltTitlesOverlay(item)", APP.WATCHLIST_HTML)
self.assertIn("closeAltTitlesOverlay()", APP.WATCHLIST_HTML)
self.assertIn("AniDB English alternative titles were not available for this show.", APP.WATCHLIST_HTML)
def test_watchlist_page_renders_auto_download_editor(self):
self.assertIn("Auto-download", APP.WATCHLIST_HTML)
self.assertIn('id="autoDownloadOverlay"', APP.WATCHLIST_HTML)
self.assertIn('api("/api/watchlist/update-auto-download"', APP.WATCHLIST_HTML)
self.assertIn("openAutoDownloadOverlay(item)", APP.WATCHLIST_HTML)
self.assertIn("closeAutoDownloadOverlay()", APP.WATCHLIST_HTML)
self.assertIn("Save auto-download", APP.WATCHLIST_HTML)
self.assertIn("Manual add does not trigger it", APP.WATCHLIST_HTML)
self.assertIn("Source name", APP.WATCHLIST_HTML)
self.assertIn('id="autoDownloadSourceName"', APP.WATCHLIST_HTML)
self.assertIn("Name source", APP.WATCHLIST_HTML)
self.assertIn('class="wide-half">Name source', APP.WATCHLIST_HTML)
self.assertIn('class="wide-half">Name', APP.WATCHLIST_HTML)
self.assertIn("Custom", APP.WATCHLIST_HTML)
self.assertIn("Series", APP.WATCHLIST_HTML)
self.assertIn("Episode offset", APP.WATCHLIST_HTML)
def test_watchlist_thumbnail_warmup_skips_repeated_attempts(self):
self.assertIn("thumbnailWarmupPending: new Set()", APP.WATCHLIST_HTML)
self.assertIn("function thumbnailWarmupToken(item)", APP.WATCHLIST_HTML)
self.assertIn("if (state.thumbnailWarmupTokens[item.show_id] === token) continue;", APP.WATCHLIST_HTML)
self.assertIn("state.thumbnailWarmupPending.delete(showId);", APP.WATCHLIST_HTML)
class AniDbCacheTests(unittest.TestCase):
def test_get_cached_anidb_titles_reuses_cached_entries(self):
original_cache = dict(APP.ANIDB_TITLES_CACHE)
try:
cached_entries = [{"aid": "1", "title": "Example", "normalized_title": "example"}]
APP.ANIDB_TITLES_CACHE.update(
{
"mtime": 123.0,
"entries": cached_entries,
"title_index": {"example": [0]},
"token_index": {"example": [0]},
"match_cache": {},
}
)
with mock.patch.object(APP, "ensure_anidb_titles_dump") as ensure_dump:
ensure_dump.return_value = mock.Mock(exists=lambda: True, stat=lambda: mock.Mock(st_mtime=123.0))
result = APP.get_cached_anidb_titles()
self.assertIs(result, cached_entries)
finally:
APP.ANIDB_TITLES_CACHE.clear()
APP.ANIDB_TITLES_CACHE.update(original_cache)
class ProviderDownloaderTests(unittest.TestCase):
def test_episode_key_keeps_episode_ten_distinct_from_one(self):
self.assertEqual(provider_downloader.episode_key(1), "1")
self.assertEqual(provider_downloader.episode_key(10), "10")
self.assertEqual(provider_downloader.episode_key("10.0"), "10")
self.assertEqual(provider_downloader.episode_key("10.5"), "10.5")
def test_wanted_episodes_preserves_range_order_and_existing_ids(self):
episodes = [
{"number": 10, "id": "ep10"},
{"number": 2, "id": "ep2"},
{"number": 11, "id": "ep11"},
{"number": 12, "id": "ep12"},
]
wanted = provider_downloader.wanted_episodes("10-12", episodes)
self.assertEqual([episode["number"] for episode in wanted], [10, 11, 12])
self.assertEqual([episode["id"] for episode in wanted], ["ep10", "ep11", "ep12"])
def test_best_hls_variant_url_selects_highest_bandwidth(self):
master = """#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
low/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5500000,RESOLUTION=1920x1080
high/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
mid/index.m3u8
"""
self.assertEqual(
provider_downloader.best_hls_variant_url("https://cdn.example/show/master.m3u8", master),
"https://cdn.example/show/high/index.m3u8",
)
def test_parse_hls_segments_resolves_plain_and_encrypted_segments(self):
playlist = """#EXTM3U
#EXT-X-MEDIA-SEQUENCE:7
#EXTINF:4.0,
seg-1
#EXT-X-KEY:METHOD=AES-128,URI="keys/key.bin"
#EXTINF:4.0,
https://cdn.example/raw-segment
#EXTINF:4.0,
seg-3.ts
"""
segments = provider_downloader.parse_hls_segments("https://cdn.example/path/index.m3u8", playlist)
self.assertEqual(segments[0], {"url": "https://cdn.example/path/seg-1"})
self.assertEqual(
segments[1],
{
"url": "https://cdn.example/raw-segment",
"key_url": "https://cdn.example/path/keys/key.bin",
"iv": "8",
},
)
self.assertEqual(segments[2]["iv"], "9")
def test_strip_png_header_removes_short_wrapper(self):
payload = b"\x89PNG\r\n\x1a\njunkIENDabcdVIDEO"
self.assertEqual(provider_downloader.strip_png_header(payload), b"VIDEO")
def test_hls_segments_need_native_download_for_extensionless_provider_urls(self):
self.assertTrue(
provider_downloader.hls_segments_need_native_download(
[{"url": "https://p16-ad-sg.ibyteimg.com/obj/ad-site-i18n/202606025d0da6f666bd3d7b4e7ab879"}]
)
)
self.assertFalse(
provider_downloader.hls_segments_need_native_download(
[{"url": "https://cdn.example/video/segment-1.ts"}]
)
)
if __name__ == "__main__":
unittest.main()