import importlib.util import json import subprocess import sys import tempfile import unittest from pathlib import Path from unittest import mock MODULE_PATH = Path(__file__).resolve().parents[1] / "bili_video_download_bridge.py" SPEC = importlib.util.spec_from_file_location("bili_video_download_bridge", MODULE_PATH) bridge = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None sys.modules[SPEC.name] = bridge SPEC.loader.exec_module(bridge) def completed(stdout="", stderr="", returncode=0): return subprocess.CompletedProcess([], returncode, stdout, stderr) class BatchInputTests(unittest.TestCase): def test_example_loads_two_exact_canonical_targets(self): example = MODULE_PATH.with_name("bili_video_download_bridge.example.json") batch = bridge.load_batch(example) self.assertEqual("qingfengpushangq-20260805", batch.batch_id) self.assertEqual(["BV1DVMX6XEPq", "BV1HA3o6oEJJ"], [item.bvid for item in batch.items]) self.assertTrue(all(not url.endswith(".webp") for url in (item.source_url for item in batch.items))) def test_rejects_noncanonical_query_cover_and_duplicate(self): base = { "schema_version": "1.0", "batch_id": "batch-1", "items": [ { "bvid": "BV1DVMX6XEPq", "source_url": "https://www.bilibili.com/video/BV1DVMX6XEPq?token=secret", "published_at": "2026-08-03T19:39:03+08:00", } ], } with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "batch.json" path.write_text(json.dumps(base), encoding="utf-8") with self.assertRaises(bridge.InputError): bridge.load_batch(path) base["items"][0]["source_url"] = "https://example.test/cover.webp" path.write_text(json.dumps(base), encoding="utf-8") with self.assertRaises(bridge.InputError): bridge.load_batch(path) base["items"][0]["source_url"] = "https://www.bilibili.com/video/BV1DVMX6XEPq" base["items"].append(dict(base["items"][0])) path.write_text(json.dumps(base), encoding="utf-8") with self.assertRaises(bridge.InputError): bridge.load_batch(path) class CommandTests(unittest.TestCase): def setUp(self): self.item = bridge.VideoItem( bvid="BV1DVMX6XEPq", source_url="https://www.bilibili.com/video/BV1DVMX6XEPq", published_at="2026-08-03T19:39:03+08:00", ) def test_probe_command_forbids_implicit_credentials_and_download(self): command = bridge.build_probe_command("yt-dlp", self.item) self.assertIn("--ignore-config", command) self.assertIn("--no-cookies", command) self.assertIn("--no-cookies-from-browser", command) self.assertIn("--simulate", command) self.assertNotIn("--netrc", command) self.assertNotIn("--cookies", command) self.assertNotIn("--cookies-from-browser", command) self.assertEqual(self.item.source_url, command[-1]) def test_download_command_requests_complete_merged_media_in_staging(self): with tempfile.TemporaryDirectory() as tmp: stage = Path(tmp) / "stage" result = stage / "completed-path.txt" command = bridge.build_download_command("yt-dlp", "ffmpeg", self.item, stage, result) self.assertEqual("bv*+ba/b", command[command.index("--format") + 1]) self.assertEqual("mp4", command[command.index("--merge-output-format") + 1]) self.assertEqual("2", command[command.index("--retries") + 1]) self.assertIn("--no-overwrites", command) self.assertIn("after_move:filepath", command) self.assertNotIn("m4s", " ".join(command).casefold()) def test_probe_rejects_remote_identity_mismatch(self): runner = mock.Mock(return_value=completed(json.dumps({"id": "BV0000000000", "duration": 12}))) with self.assertRaises(bridge.AccessError): bridge.probe_item(self.item, "yt-dlp", runner=runner) def test_successful_probe_is_labeled_metadata_only(self): runner = mock.Mock( return_value=completed( json.dumps({"id": self.item.bvid, "duration": 1206.067, "is_live": False}) ) ) result = bridge.probe_item(self.item, "yt-dlp", runner=runner) self.assertEqual("METADATA_PASS_ONLY", result["probe"]) def test_probe_rejects_playlist_and_auth_boundaries(self): cases = [ {"id": self.item.bvid, "duration": 100, "_type": "playlist"}, {"id": self.item.bvid, "duration": 100, "entries": []}, {"id": self.item.bvid, "duration": 100, "availability": "needs_auth"}, {"id": self.item.bvid, "duration": 100, "availability": "premium_only"}, ] for metadata in cases: with self.subTest(metadata=metadata): runner = mock.Mock(return_value=completed(json.dumps(metadata))) with self.assertRaises(bridge.AccessError): bridge.probe_item(self.item, "yt-dlp", runner=runner) def test_process_diagnostic_redacts_urls(self): safe = bridge._safe_process_error("failed https://signed.example/x?token=secret") self.assertNotIn("secret", safe) self.assertIn("[URL_REDACTED]", safe) def test_browser_file_command_has_no_cookie_or_download_switch(self): args = bridge._parser().parse_args( [ "--input", "batch.json", "accept-browser-file", "--bvid", self.item.bvid, "--media-file", "complete.mp4", "--destination", "destination", ] ) self.assertEqual("accept-browser-file", args.command) self.assertFalse(hasattr(args, "browser_profile")) self.assertFalse(hasattr(args, "cookies")) class ValidationAndPublishTests(unittest.TestCase): def setUp(self): self.item = bridge.VideoItem( bvid="BV1DVMX6XEPq", source_url="https://www.bilibili.com/video/BV1DVMX6XEPq", published_at="2026-08-03T19:39:03+08:00", ) def test_ffprobe_requires_video_and_audio_and_rejects_cover(self): with tempfile.TemporaryDirectory() as tmp: video = Path(tmp) / "x.mp4" video.write_bytes(b"media") valid = { "streams": [ {"codec_type": "video", "codec_name": "h264"}, {"codec_type": "audio", "codec_name": "aac"}, ], "format": {"duration": "10.5", "format_name": "mov,mp4"}, } facts = bridge.probe_media_file( video, "ffprobe", runner=mock.Mock(return_value=completed(json.dumps(valid))) ) self.assertEqual("h264", facts.video_codec) valid["streams"] = valid["streams"][:1] with self.assertRaises(bridge.ValidationError): bridge.probe_media_file( video, "ffprobe", runner=mock.Mock(return_value=completed(json.dumps(valid))) ) cover = Path(tmp) / "cover.webp" cover.write_bytes(b"RIFF") with self.assertRaises(bridge.ValidationError): bridge.probe_media_file(cover, "ffprobe", runner=mock.Mock()) def test_publish_create_new_hash_mapping_and_collision(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) source = root / "staging.mp4" source.write_bytes(b"complete-video-and-audio") destination = root / "destination" destination.mkdir() facts = bridge.MediaFacts(12.5, "mov,mp4", "h264", "aac") result = bridge.publish_validated_media( source, destination, self.item, {"title": "test title", "duration_seconds": 12.5}, facts, ) media = destination / "BV1DVMX6XEPq.mp4" mapping = destination / "BV1DVMX6XEPq.download.json" self.assertTrue(media.is_file()) self.assertTrue(mapping.is_file()) self.assertEqual(bridge.sha256_file(source), result["sha256"]) self.assertEqual(result, json.loads(mapping.read_text(encoding="utf-8"))) with self.assertRaises(bridge.CollisionError): bridge.publish_validated_media( source, destination, self.item, {"title": "test title", "duration_seconds": 12.5}, facts, ) def test_publish_rolls_back_media_when_mapping_create_fails(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) source = root / "staging.mp4" source.write_bytes(b"complete-video-and-audio") destination = root / "destination" destination.mkdir() facts = bridge.MediaFacts(12.5, "mov,mp4", "h264", "aac") with mock.patch.object(bridge, "_write_json_create_new", side_effect=KeyboardInterrupt): with self.assertRaises(KeyboardInterrupt): bridge.publish_validated_media( source, destination, self.item, {"title": "test title", "duration_seconds": 12.5}, facts, ) def test_duration_consistency_allows_mux_drift_and_rejects_preview(self): accepted = bridge.validate_duration_consistency(1206.067, 1206.067664) self.assertLess(accepted["duration_delta_seconds"], accepted["duration_tolerance_seconds"]) tolerance = max(3.0, 4000.0 * 0.001) bridge.validate_duration_consistency(4000.0, 4000.0 + tolerance) with self.assertRaises(bridge.ValidationError): bridge.validate_duration_consistency(4000.0, 4000.0 + tolerance + 0.000001) with self.assertRaisesRegex(bridge.ValidationError, "possible preview or wrong part"): bridge.validate_duration_consistency(3133.95, 600.133313) self.assertEqual([], list(destination.iterdir())) class BatchFlowTests(unittest.TestCase): def test_download_item_stub_closes_complete_media_mapping_and_cleanup(self): item = bridge.VideoItem( "BV1DVMX6XEPq", "https://www.bilibili.com/video/BV1DVMX6XEPq", "2026-08-03T19:39:03+08:00", ) def runner(command, **_kwargs): if "--dump-single-json" in command: return completed( json.dumps( { "id": item.bvid, "title": "A股缩量磨底阶段。", "duration": 1206.067, "is_live": False, } ) ) if "--format" in command: stage = Path(command[command.index("--paths") + 1]) media = stage / f"{item.bvid}.mp4" media.write_bytes(b"stub-complete-media") path_file = Path(command[command.index("--print-to-file") + 2]) path_file.write_text(str(media), encoding="utf-8") return completed() if "-show_entries" in command: return completed( json.dumps( { "streams": [ {"codec_type": "video", "codec_name": "h264"}, {"codec_type": "audio", "codec_name": "aac"}, ], "format": {"duration": "1206.067", "format_name": "mov,mp4"}, } ) ) raise AssertionError(command) with tempfile.TemporaryDirectory() as tmp: destination = Path(tmp) result = bridge.download_item( item, destination, "batch-1", "yt-dlp", "ffmpeg", "ffprobe", runner=runner, ) self.assertEqual("COMPLETE", result["status"]) self.assertTrue((destination / f"{item.bvid}.mp4").is_file()) self.assertTrue((destination / f"{item.bvid}.download.json").is_file()) self.assertFalse((destination / ".bili-download-staging").exists()) def test_download_batch_continues_after_one_item_failure(self): items = ( bridge.VideoItem("BV1DVMX6XEPq", "https://www.bilibili.com/video/BV1DVMX6XEPq", "2026-08-03T19:39:03+08:00"), bridge.VideoItem("BV1HA3o6oEJJ", "https://www.bilibili.com/video/BV1HA3o6oEJJ", "2026-08-02T21:11:11+08:00"), ) batch = bridge.BatchSpec("batch-1", items) with tempfile.TemporaryDirectory() as tmp: with mock.patch.object( bridge, "download_item", side_effect=[bridge.AccessError("public access denied"), {"status": "COMPLETE"}], ) as mocked: results, failures = bridge.download_batch( batch, Path(tmp), "yt-dlp", "ffmpeg", "ffprobe", runner=mock.Mock() ) self.assertEqual(2, mocked.call_count) self.assertEqual(1, failures) self.assertEqual(["FAIL", "COMPLETE"], [item["status"] for item in results]) def test_preview_duration_mismatch_never_publishes_and_cleans_staging(self): item = bridge.VideoItem( "BV1HA3o6oEJJ", "https://www.bilibili.com/video/BV1HA3o6oEJJ", "2026-08-02T21:11:11+08:00", ) def runner(command, **_kwargs): if "--dump-single-json" in command: return completed( json.dumps( { "id": item.bvid, "title": "full remote video", "duration": 3133.95, "is_live": False, } ) ) if "--format" in command: stage = Path(command[command.index("--paths") + 1]) media = stage / f"{item.bvid}.mp4" media.write_bytes(b"premium-preview-only") path_file = Path(command[command.index("--print-to-file") + 2]) path_file.write_text(str(media), encoding="utf-8") return completed() if "-show_entries" in command: return completed( json.dumps( { "streams": [ {"codec_type": "video", "codec_name": "h264"}, {"codec_type": "audio", "codec_name": "aac"}, ], "format": {"duration": "600.133313", "format_name": "mov,mp4"}, } ) ) raise AssertionError(command) with tempfile.TemporaryDirectory() as tmp: destination = Path(tmp) with self.assertRaisesRegex(bridge.ValidationError, "possible preview or wrong part"): bridge.download_item( item, destination, "batch-1", "yt-dlp", "ffmpeg", "ffprobe", runner=runner, ) self.assertEqual([], list(destination.iterdir())) class BrowserFileHandoffTests(unittest.TestCase): def setUp(self): self.item = bridge.VideoItem( "BV1HA3o6oEJJ", "https://www.bilibili.com/video/BV1HA3o6oEJJ", "2026-08-02T21:11:11+08:00", ) def runner_with_duration(self, local_duration): def runner(command, **_kwargs): if "--dump-single-json" in command: self.assertIn("--no-cookies", command) self.assertIn("--no-cookies-from-browser", command) return completed( json.dumps( { "id": self.item.bvid, "title": "流水淘沙不暂停,前波未灭后波生。(周复盘)", "duration": 3133.95, "is_live": False, } ) ) if "-show_entries" in command: return completed( json.dumps( { "streams": [ {"codec_type": "video", "codec_name": "h264"}, {"codec_type": "audio", "codec_name": "aac"}, ], "format": { "duration": str(local_duration), "format_name": "mov,mp4", }, } ) ) raise AssertionError(command) return mock.Mock(side_effect=runner) def test_accept_browser_file_publishes_copy_and_keeps_source_unchanged(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) source = root / "browser-download.mp4" source.write_bytes(b"complete-authorized-video-with-audio") source_before = (source.read_bytes(), source.stat().st_mtime_ns) destination = root / "destination" destination.mkdir() runner = self.runner_with_duration(3133.9504) result = bridge.accept_browser_file( self.item, source, destination, "batch-1", "yt-dlp", "ffprobe", runner=runner, sleeper=lambda _seconds: None, ) self.assertEqual("COMPLETE", result["status"]) self.assertEqual("authorized_browser_file_handoff", result["acquisition_mode"]) self.assertEqual(result["sha256"], result["handoff_source_sha256"]) self.assertEqual(source_before, (source.read_bytes(), source.stat().st_mtime_ns)) published = destination / f"{self.item.bvid}.mp4" mapping = destination / f"{self.item.bvid}.download.json" self.assertTrue(published.is_file()) self.assertTrue(mapping.is_file()) self.assertEqual(source.read_bytes(), published.read_bytes()) self.assertFalse((destination / ".bili-download-staging").exists()) commands = [call.args[0] for call in runner.call_args_list] self.assertEqual(2, len(commands)) self.assertTrue(all("--cookies-from-browser" not in command for command in commands)) self.assertTrue(all("--cookies" not in command for command in commands)) def test_rejects_partial_and_growing_browser_files(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) partial = root / "video.mp4.crdownload" partial.write_bytes(b"partial") with self.assertRaisesRegex(bridge.ValidationError, "incomplete media suffix"): bridge.validate_stable_local_media(partial, sleeper=lambda _seconds: None) growing = root / "growing-video.mp4" growing.write_bytes(b"first") def grow(_seconds): with growing.open("ab") as target: target.write(b"more") with self.assertRaisesRegex(bridge.ValidationError, "not stable"): bridge.validate_stable_local_media(growing, sleeper=grow) def test_preview_handoff_fails_before_publication_and_cleans_stage(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) source = root / "browser-download.mp4" source.write_bytes(b"preview-only") destination = root / "destination" destination.mkdir() with self.assertRaisesRegex(bridge.ValidationError, "possible preview or wrong part"): bridge.accept_browser_file( self.item, source, destination, "batch-1", "yt-dlp", "ffprobe", runner=self.runner_with_duration(600.133313), sleeper=lambda _seconds: None, ) self.assertEqual([], list(destination.iterdir())) self.assertEqual(b"preview-only", source.read_bytes()) def test_base_exception_after_copy_cleans_stage_and_keeps_source(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) source = root / "browser-download.mp4" source.write_bytes(b"complete-media") destination = root / "destination" destination.mkdir() runner = self.runner_with_duration(3133.95) with mock.patch.object(bridge, "probe_media_file", side_effect=KeyboardInterrupt): with self.assertRaises(KeyboardInterrupt): bridge.accept_browser_file( self.item, source, destination, "batch-1", "yt-dlp", "ffprobe", runner=runner, sleeper=lambda _seconds: None, ) self.assertEqual([], list(destination.iterdir())) self.assertEqual(b"complete-media", source.read_bytes()) def test_browser_handoff_requires_exact_batch_bvid(self): batch = bridge.BatchSpec("batch-1", (self.item,)) self.assertIs(self.item, bridge._find_batch_item(batch, self.item.bvid)) with self.assertRaises(bridge.InputError): bridge._find_batch_item(batch, "BV1DVMX6XEPq") if __name__ == "__main__": unittest.main()