feat: resolve HTTP video URLs with bundled yt-dlp

This commit is contained in:
2026-10-22 17:05:29 +02:00
parent 6a4afc9f0e
commit b132bab393
14 changed files with 121 additions and 79 deletions
+11 -2
View File
@@ -36,6 +36,10 @@ struct PlayerCommandChecks {
let miruro = URL(string: "https://barelystarted.miruro.tv/watch/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/the-apothecary-diaries-season-3?ep=1")!
require(try opened([miruro.absoluteString]) == miruro, "Miruro positional URLs retain episode page identity")
require(try opened(["--url", miruro.absoluteString]) == miruro, "explicit Miruro URLs forward without network resolution")
let generic = URL(string: "https://vimeo.com/76979871?quality=original")!
require(try opened([generic.absoluteString]) == generic &&
opened(["--url", generic.absoluteString]) == generic,
"positional and explicit third-party URLs reach extraction unchanged")
require(try opened(["--new-instance"], newInstance: true) == nil, "an independent launch can omit media")
require(try opened(["--new-instance", movie.lastPathComponent], newInstance: true) == movie,
"independent local launches preserve relative paths and spaces")
@@ -171,6 +175,12 @@ struct PlayerCommandChecks {
require(parsedQueue.entries.map(\.url) == [miruro, canonical], "stream queue order is supplied, never filename-sorted")
require(parsedQueue.entries[0].identity.hasPrefix("miruro:") &&
parsedQueue.entries[1].identity == canonical.absoluteString, "stable identities exclude slugs and transport URLs")
try saveQueue(["version": 1, "items": [["url": generic.absoluteString, "progress_file": youtubeReport]]])
let genericQueue = try StreamQueue.load(from: queueFile)
require(genericQueue.entries[0].url == generic &&
genericQueue.entries[0].identity == generic.absoluteString &&
!genericQueue.entries[0].label.contains("YouTube"),
"third-party queues retain page identity without a YouTube label")
let duplicateYouTube = ["url": "https://youtu.be/jNQXAC9IVRw", "progress_file": miruroReport]
let duplicateMiruro = ["url": miruro.absoluteString.replacingOccurrences(of: "the-apothecary-diaries-season-3", with: "different-slug"),
"progress_file": youtubeReport]
@@ -183,7 +193,6 @@ struct PlayerCommandChecks {
["version": 1, "items": [entries[0], ["url": canonical.absoluteString, "progress_file": miruroReport]]],
["version": 1, "items": [entries[0], ["url": canonical.absoluteString,
"progress_file": privateDirectory.resolvingSymlinksInPath().appendingPathComponent("miruro.jsonl").path]]],
["version": 1, "items": [["url": "https://cdn.example/expired.m3u8", "progress_file": youtubeReport]]],
["version": 1, "items": [["url": canonical.absoluteString, "progress_file": "relative.jsonl"]]],
["version": 1, "items": [["url": canonical.absoluteString, "progress_file": movie.path]]]
]
@@ -221,7 +230,7 @@ struct PlayerCommandChecks {
for arguments in [["--url"], ["--url", canonical.absoluteString, movie.path], ["--"],
[movie.path, movie.path], ["--unknown"], ["missing.mp4"], [directory.path],
["file://remote-host/video.mp4"], ["https://example.com/video.mp4"],
["file://remote-host/video.mp4"], ["ftp://example.com/video.mp4"],
["--new-instance", "--new-instance"], ["--new-instance", "--url"],
["--new-instance", "--"], ["--new-instance", movie.path, movie.path],
["--new-instance", "--unknown"], ["--no-config", "--no-config"],
+21 -8
View File
@@ -55,7 +55,7 @@ static NSArray<NSDictionary *> *records(const std::string &path) {
"schema version is numeric one");
require([@[@"progress", @"ended", @"closed"] containsObject:row[@"event"]], "known event type");
require([row[@"media_url"] isKindOfClass:[NSString class]] &&
ProgressReporter::validMediaURL([row[@"media_url"] UTF8String]), "canonical consumer identity");
ProgressReporter::validMediaURL([row[@"media_url"] UTF8String]), "HTTP(S) consumer identity");
require([row[@"position_seconds"] isKindOfClass:[NSNumber class]] &&
std::isfinite([row[@"position_seconds"] doubleValue]) &&
[row[@"position_seconds"] doubleValue] >= 0, "position is finite and nonnegative");
@@ -240,13 +240,26 @@ int main(int argc, char **argv) {
for (NSUInteger i = 0; i < rows.count; ++i)
expect(rows[i], i % 2 ? @"closed" : @"progress", firstURL, 97, false, i < 8 ? 0 : 100);
require(ProgressReporter::validMediaURL(firstURL) &&
ProgressReporter::validMediaURL("https://www.youtube.com/watch?v=aA0_-123456"), "eleven ASCII ID characters are accepted");
for (const char *invalid : {"", "http://www.youtube.com/watch?v=AAAAAAAAAAA",
"https://youtube.com/watch?v=AAAAAAAAAAA", "https://www.youtube.com/watch?v=AAAAAAAAAA",
"https://www.youtube.com/watch?v=AAAAAAAAAAAA", "https://www.youtube.com/watch?v=AAAAAAAAAAA&x=1",
"https://www.youtube.com/watch?v=AAAAAAAAAA%", "https://www.youtube.com/watch?v=AAAAAAAAAA\n"})
require(!ProgressReporter::validMediaURL(invalid), "noncanonical URLs are rejected");
const std::string genericURL = "https://video.example/watch?label=\"quoted\"&id=123#part";
require(ProgressReporter::validMediaURL(genericURL) &&
ProgressReporter::validMediaURL("http://127.0.0.1:8080/video.mp4"),
"HTTP(S) page identities are accepted beyond YouTube");
for (const char *invalid : {"", "file:///tmp/video.mp4", "ftp://example.com/video",
"https://", "https://user:secret@example.com/video", "https://example.com:0/video",
"https://example.com:65536/video", "https://example.com/video\n"})
require(!ProgressReporter::validMediaURL(invalid), "unsafe page identities are rejected");
const auto genericPath = root + "/generic.jsonl";
{
ProgressReporter reporter(genericPath, [] { return 0.; });
reporter.begin(genericURL);
reporter.observe(sample(PLAYER_PLAYING, 25));
reporter.finish();
require(reporter.error().empty(), "generic page progress drains successfully");
}
rows = records(genericPath);
require(rows.count == 2, "generic page has initial and terminal snapshots");
expect(rows[0], @"progress", genericURL, 25, true);
expect(rows[1], @"closed", genericURL, 25, true);
require(ProgressReporter::validMediaIdentity("miruro:opaque_A-1:12") &&
ProgressReporter::validMediaIdentity("miruro:a:2147483647"), "strict Miruro identities accepted");
+6 -3
View File
@@ -52,9 +52,12 @@ struct StreamResolverChecks {
throw Failure(message: "YouTube URL classification")
}
try require(youtube.absoluteString == "https://www.youtube.com/watch?v=jNQXAC9IVRw", "existing YouTube normalization")
let other = "https://vimeo.com/76979871?quality=original"
guard case .youtube(let generic) = try StreamURL.parse(other) else {
throw Failure(message: "other HTTP(S) sites route through yt-dlp")
}
try require(generic.absoluteString == other, "other sites retain their URL and query")
for invalid in [
"https://example.com/video", "https://www.miruro.to/watch?id=21&ep=1",
page.replacingOccurrences(of: "barelystarted.miruro.tv", with: "barelystarted.miruro.tv.evil.example"),
page.replacingOccurrences(of: "barelystarted.miruro.tv", with: "barelystarted.miruro.tv@evil.example"),
page.replacingOccurrences(of: "https://", with: "http://"),
page.replacingOccurrences(of: "https://", with: "https://user:secret@"),
@@ -70,7 +73,7 @@ struct StreamResolverChecks {
page + "&ep=2", page + "&provider=bee", page + "#secret",
page.replacingOccurrences(of: ".tv/", with: ".tv:444/"),
page.replacingOccurrences(of: "?ep", with: "/?ep"),
"https://youtu.be/invalid", "file:///secret"
"file:///secret"
] {
try reject("unsupported or unsafe URL") { _ = try StreamURL.parse(invalid) }
}
+32 -18
View File
@@ -70,20 +70,20 @@ struct YouTubeResolverChecks {
let url = try YouTubeResolver.validateURL(input)
try require(url == page, "supported video URLs identify the same video")
}
for input in ["https://vimeo.com/76979871", "https://example.com/watch?v=jNQXAC9IVRw",
"https://youtube.com.evil.example/watch?v=jNQXAC9IVRw",
"https://evil.youtube.com/watch?v=jNQXAC9IVRw",
"http://127.0.0.1:8080/video.mp4?token=kept#fragment",
"https://www.youtube.com/playlist?list=PLexample"] {
try require(try YouTubeResolver.validateURL(input).absoluteString == input,
"HTTP(S) URLs outside canonical YouTube video forms reach yt-dlp unchanged")
}
let hostile = [
"", "not a URL", "javascript:alert(1)", "file:///watch?v=jNQXAC9IVRw",
"https://example.com/watch?v=jNQXAC9IVRw",
"https://youtube.com.evil.example/watch?v=jNQXAC9IVRw",
"https://evil.youtube.com/watch?v=jNQXAC9IVRw",
"ftp://example.com/video", "https://", "https://example.com:0/video",
"https://example.com:65536/video", "https://example.com/video\ninjected",
"https://youtube.com@evil.example/watch?v=jNQXAC9IVRw",
"https://user:password@www.youtube.com/watch?v=jNQXAC9IVRw",
"https://www.youtube.com/playlist?list=PLexample",
"https://www.youtube.com/watch?list=PLexample",
"https://www.youtube.com/watch?v=short",
"https://youtu.be/jNQXAC9IVRwextra", "https://youtu.be/jNQXAC9IVR!",
"https://www.youtube.com/shorts/", "https://youtu.be/jNQXAC9IVRw/extra",
"https://www.youtube.com/watch?v=jNQXAC9IVR%0A",
"https://www.youtube.com/watch?v=jNQXAC9IVR%C3%A9"
"https://user:password@www.youtube.com/watch?v=jNQXAC9IVRw"
]
for input in hostile {
try reject("hostile or malformed video URL") { _ = try YouTubeResolver.validateURL(input) }
@@ -202,6 +202,9 @@ struct YouTubeResolverChecks {
}
static func malformedMetadata() throws {
var collection = try fixture(combinedJSON)
collection["_type"] = "playlist"
try reject("collection result instead of a single video") { _ = try decode(collection) }
try reject("non-JSON extractor output") { _ = try decode("not JSON") }
for json in ["null", "[]", "{}", #"{"requested_formats":[]}"#] {
try reject("missing playable stream metadata") { _ = try decode(json) }
@@ -210,10 +213,11 @@ struct YouTubeResolverChecks {
_ = try decode(#"{"url":"https://media.example/video","vcodec":"h264","acodec":"aac","duration":1e999}"#)
}
let combined = try fixture(combinedJSON)
var missingURL = combined
missingURL.removeValue(forKey: "url")
try reject("missing combined stream URL") { _ = try decode(missingURL) }
for key in ["url", "vcodec", "acodec"] {
var object = combined
object.removeValue(forKey: key)
try reject("missing required combined \(key)") { _ = try decode(object) }
object[key] = 17
try reject("wrong type for combined \(key)") { _ = try decode(object) }
}
@@ -236,11 +240,9 @@ struct YouTubeResolverChecks {
var hdr = combined
hdr["dynamic_range"] = "HDR10"
try reject("HDR input unsupported by the SDR renderer") { _ = try decode(hdr) }
for value in ["none", NSNull()] as [Any] {
var object = combined
object["vcodec"] = value
try reject("combined input without video codec") { _ = try decode(object) }
}
var audioOnly = combined
audioOnly["vcodec"] = "none"
try reject("explicitly audio-only result") { _ = try decode(audioOnly) }
for transport in ["http_dash_segments", "mhtml", "rtmp"] {
var object = combined
object["protocol"] = transport
@@ -444,6 +446,18 @@ struct YouTubeResolverChecks {
try JSONSerialization.data(withJSONObject: object).write(to: extractor.marker("response.json"))
let lower = try await YouTubeResolver.resolve(page, executableURL: extractor.executable, runtimeURL: runtime)
try require(lower.height == 720, "default falls downward when 1080p is absent, never upward to available 4K")
let genericPage = URL(string: "https://video.example/watch?id=123")!
let generic: [String: Any] = [
"id": "123", "title": "Generic HTML5 video", "webpage_url": genericPage.absoluteString,
"extractor": "generic", "extractor_key": "Generic",
"formats": [["format_id": "0", "url": "https://media.example/unknown.mp4",
"ext": "mp4", "protocol": "https"]]
]
try JSONSerialization.data(withJSONObject: generic).write(to: extractor.marker("response.json"))
let unknown = try await YouTubeResolver.resolve(genericPage, executableURL: extractor.executable, runtimeURL: runtime)
try require(unknown.video.url == "https://media.example/unknown.mp4" &&
unknown.audio == nil && unknown.height == nil && unknown.webpageURL == genericPage,
"generic HTML5 video without codec or height hints remains playable and retains page identity")
}
static func bundledHelpers(root: URL) throws {