Initial MaterialSub release
Derived from pengchujin/jzsub at 222a90265d2a8797ca258eb1a980cee0863a8311; preserve the upstream MIT license and attribution.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import argparse
|
||||
from functools import partial
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "ingest_embedded_hls.py"
|
||||
sys.path.insert(0, str(SCRIPT.parent))
|
||||
SPEC = importlib.util.spec_from_file_location("ingest_embedded_hls", SCRIPT)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
ingest = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(ingest)
|
||||
|
||||
|
||||
class EmbeddedHlsTests(unittest.TestCase):
|
||||
def test_resource_map_must_be_private(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "resources.json"
|
||||
path.write_text(json.dumps({"schema_version": 1}), encoding="utf-8")
|
||||
os.chmod(path, 0o644)
|
||||
with self.assertRaisesRegex(ingest.IngestError, "chmod 600"):
|
||||
ingest._read_private_resource_map(path)
|
||||
|
||||
os.chmod(path, 0o600)
|
||||
self.assertEqual(
|
||||
ingest._read_private_resource_map(path), {"schema_version": 1}
|
||||
)
|
||||
|
||||
def test_private_map_can_be_the_only_job_entry_when_cleanup_is_enabled(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory)
|
||||
resource_map = output / "resource-map.json"
|
||||
resource_map.write_text('{"schema_version": 1}', encoding="utf-8")
|
||||
os.chmod(resource_map, 0o600)
|
||||
|
||||
self.assertEqual(
|
||||
ingest._prepare_output_dir(
|
||||
output,
|
||||
resource_map,
|
||||
resume=False,
|
||||
cleanup_resource_map=True,
|
||||
),
|
||||
output,
|
||||
)
|
||||
with self.assertRaisesRegex(ingest.IngestError, "not empty"):
|
||||
ingest._prepare_output_dir(
|
||||
output,
|
||||
resource_map,
|
||||
resume=False,
|
||||
cleanup_resource_map=False,
|
||||
)
|
||||
|
||||
def test_authorization_query_only_propagates_to_same_origin(self) -> None:
|
||||
root = "https://cdn.example/media/video.m3u8?Policy=secret&Key=pair"
|
||||
same = ingest._authorized_resource_url(
|
||||
"https://cdn.example/media/segment.ts?part=1", root
|
||||
)
|
||||
foreign = ingest._authorized_resource_url(
|
||||
"https://captions.example/subtitle.vtt", root
|
||||
)
|
||||
|
||||
self.assertIn("part=1", same)
|
||||
self.assertIn("Policy=secret", same)
|
||||
self.assertIn("Key=pair", same)
|
||||
self.assertEqual(foreign, "https://captions.example/subtitle.vtt")
|
||||
|
||||
def test_rewrites_segments_and_aes128_key(self) -> None:
|
||||
root = "https://cdn.example/hls/video.m3u8?Policy=secret"
|
||||
source = """#EXTM3U
|
||||
#EXT-X-KEY:METHOD=AES-128,URI="video.key"
|
||||
#EXTINF:10,
|
||||
segment-0.ts
|
||||
#EXT-X-ENDLIST
|
||||
"""
|
||||
rewritten = ingest.rewrite_media_playlist(source, root)
|
||||
|
||||
self.assertIn(
|
||||
'URI="https://cdn.example/hls/video.key?Policy=secret"', rewritten
|
||||
)
|
||||
self.assertIn(
|
||||
"https://cdn.example/hls/segment-0.ts?Policy=secret", rewritten
|
||||
)
|
||||
|
||||
def test_rejects_master_playlist(self) -> None:
|
||||
source = """#EXTM3U
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000
|
||||
high.m3u8
|
||||
"""
|
||||
with self.assertRaisesRegex(ingest.IngestError, "master playlist"):
|
||||
ingest.rewrite_media_playlist(source, "https://cdn.example/master.m3u8")
|
||||
|
||||
def test_rejects_drm_protection(self) -> None:
|
||||
source = """#EXTM3U
|
||||
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="key",KEYFORMAT="com.apple.streamingkeydelivery"
|
||||
#EXTINF:10,
|
||||
segment.ts
|
||||
"""
|
||||
with self.assertRaisesRegex(ingest.IngestError, "will not bypass DRM"):
|
||||
ingest.rewrite_media_playlist(source, "https://cdn.example/video.m3u8")
|
||||
|
||||
@unittest.skipUnless(
|
||||
shutil.which("ffmpeg") and shutil.which("ffprobe") and shutil.which("yt-dlp"),
|
||||
"FFmpeg and yt-dlp are required for the local integration test",
|
||||
)
|
||||
def test_local_media_playlist_reaches_standard_translation_stage(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / "source"
|
||||
output = root / "output"
|
||||
source.mkdir()
|
||||
output.mkdir()
|
||||
subprocess.run(
|
||||
[
|
||||
shutil.which("ffmpeg") or "ffmpeg",
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=blue:s=320x180:r=24:d=2",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"sine=frequency=440:duration=2",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-hls_time",
|
||||
"1",
|
||||
"-hls_playlist_type",
|
||||
"vod",
|
||||
str(source / "media.m3u8"),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
(source / "captions.vtt").write_text(
|
||||
"WEBVTT\n\n00:00:00.000 --> 00:00:01.500\nHello from the test video.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
handler = partial(SimpleHTTPRequestHandler, directory=str(source))
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
server_thread.start()
|
||||
try:
|
||||
port = server.server_address[1]
|
||||
resource_map = root / "resource-map.json"
|
||||
resource_map.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"page_url": f"http://127.0.0.1:{port}/page",
|
||||
"playlist_url": f"http://127.0.0.1:{port}/media.m3u8?Policy=test",
|
||||
"title": "Embedded test",
|
||||
"id": "fixture",
|
||||
"duration_seconds": 2,
|
||||
"width": 320,
|
||||
"height": 180,
|
||||
"subtitle": {
|
||||
"url": f"http://127.0.0.1:{port}/captions.vtt",
|
||||
"language": "en",
|
||||
"kind": "manual",
|
||||
"label": "English",
|
||||
"format": "vtt",
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.chmod(resource_map, 0o600)
|
||||
args = argparse.Namespace(
|
||||
resource_map=resource_map,
|
||||
output_dir=output,
|
||||
target_lang="zh-CN",
|
||||
deliver="full",
|
||||
concurrent_fragments=2,
|
||||
mp4_fallback=False,
|
||||
resume=False,
|
||||
cleanup_resource_map=False,
|
||||
)
|
||||
|
||||
exit_code = ingest.execute(args)
|
||||
|
||||
self.assertEqual(exit_code, 3)
|
||||
manifest = json.loads((output / "download-manifest.json").read_text())
|
||||
self.assertEqual(manifest["status"], "bilingual_required")
|
||||
self.assertTrue((output / "subtitles" / "subtitle-manifest.json").is_file())
|
||||
self.assertEqual(
|
||||
manifest["authentication"]["mode"],
|
||||
"browser-confirmed-resource-map",
|
||||
)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
server_thread.join(timeout=5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user