feat: select Miruro playback and caption providers
This commit is contained in:
@@ -128,6 +128,31 @@ struct ConfigurationChecks {
|
||||
"printed TOML is directly usable and preserves all default behavior and bindings")
|
||||
try require(!defaults.showBufferInfo && !defaults.automaticQuality && defaults.snapToEdges,
|
||||
"built-in defaults hide buffer info, disable automatic quality, and enable snapping")
|
||||
try require(defaults.miruroVideoProvider.isEmpty && defaults.miruroCaptionProvider.isEmpty,
|
||||
"Miruro defaults select automatic video and captions independently")
|
||||
let miruro = root.appendingPathComponent("miruro.toml")
|
||||
for (settings, video, captions) in [
|
||||
("video_provider = \"new-Host_2\"", "new-Host_2", ""),
|
||||
("caption_provider = \"moon\"", "", "moon"),
|
||||
("video_provider = \"sun\"\ncaption_provider = \"moon\"", "sun", "moon"),
|
||||
("video_provider = \"\"\ncaption_provider = \"\"", "", ""),
|
||||
("video_provider = \"\(String(repeating: "a", count: 128))\"", String(repeating: "a", count: 128), "")
|
||||
] {
|
||||
try Data("[miruro]\n\(settings)\n".utf8).write(to: miruro)
|
||||
try require(try PlayerConfiguration.load(from: miruro)
|
||||
== PlayerConfiguration(miruroVideoProvider: video, miruroCaptionProvider: captions),
|
||||
"partial Miruro settings preserve omitted defaults and accept runtime provider names")
|
||||
}
|
||||
for key in ["video_provider", "caption_provider"] {
|
||||
for name in ["with space", " sun", "sun ", "é", "host.name", "host/name", String(repeating: "a", count: 129)] {
|
||||
try Data("[miruro]\n\(key) = \"\(name)\"\n".utf8).write(to: miruro)
|
||||
try requireLoadError(at: miruro, "invalid Miruro provider")
|
||||
}
|
||||
for value in ["true", "1", "[\"sun\"]", "{}"] {
|
||||
try Data("[miruro]\n\(key) = \(value)\n".utf8).write(to: miruro)
|
||||
try requireLoadError(at: miruro, "non-string Miruro provider")
|
||||
}
|
||||
}
|
||||
for (value, expected) in [("time", CompactTitleProgress.time), ("percentage", .percentage), ("none", .none)] {
|
||||
let progress = root.appendingPathComponent("compact-progress-\(value).toml")
|
||||
try Data("[ui]\ncompact_title_progress = \"\(value)\"\n".utf8).write(to: progress)
|
||||
@@ -204,7 +229,7 @@ struct ConfigurationChecks {
|
||||
try require(try PlayerConfiguration.load(from: nested).keys == expectedKeys && keySession.keys == defaults.keys,
|
||||
"key session overrides never persist")
|
||||
let emptySections = root.appendingPathComponent("empty-sections.toml")
|
||||
try Data("[ui]\n[keys]\n[playback]\n".utf8).write(to: emptySections)
|
||||
try Data("[ui]\n[keys]\n[playback]\n[miruro]\n".utf8).write(to: emptySections)
|
||||
try require(try PlayerConfiguration.load(from: emptySections) == defaults, "empty nested sections use defaults")
|
||||
let reordered = root.appendingPathComponent("reordered.toml")
|
||||
try Data("""
|
||||
@@ -226,6 +251,9 @@ struct ConfigurationChecks {
|
||||
("syntax", "[playback]\nautomatic_quality = [\n", ["automatic_quality"]),
|
||||
("duplicate", "[playback]\nautomatic_quality = true\n\"automatic_quality\" = false\n", ["automatic_quality"]),
|
||||
("unknown", "unknown_setting = true\n", ["unknown_setting"]),
|
||||
("miruro-scalar", "miruro = true\n", ["miruro"]),
|
||||
("miruro-unknown", "[miruro]\nprovider = \"sun\"\n", ["miruro.provider"]),
|
||||
("miruro-nested", "[miruro.video_provider]\nname = \"sun\"\n", []),
|
||||
("legacy-buffer", "show_buffer_info = true\n", ["show_buffer_info"]),
|
||||
("legacy-snap", "snap_to_edges = false\n", ["snap_to_edges"]),
|
||||
("legacy-quality", "automatic_quality = true\n", ["automatic_quality"]),
|
||||
|
||||
@@ -20,7 +20,11 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
path = urlsplit(self.path).path
|
||||
seen.append(path)
|
||||
expected = "subtitle-private" if path == "/english.jpeg" else "stream-private"
|
||||
alternate = path.startswith("/alternate/")
|
||||
captions = path.startswith("/captions/")
|
||||
if alternate or captions:
|
||||
path = "/" + path.split("/", 2)[2]
|
||||
expected = "alternate-private" if alternate else "subtitle-private" if captions or path == "/english.jpeg" else "stream-private"
|
||||
if self.headers.get("X-Resource") != expected:
|
||||
errors.append("wrong resource headers: " + path)
|
||||
self.send_error(403)
|
||||
@@ -34,7 +38,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
errors.append("variant token lost")
|
||||
data = (fixtures / "resolved-vod.m3u8").read_bytes()
|
||||
elif path == "/english.jpeg":
|
||||
data = b'WEBVTT\n\n00:00.000 --> 00:02.000\nFirst English cue\n\n00:02.000 --> 00:04.000\nSecond English cue\n'
|
||||
data = (b'WEBVTT\n\n00:00.000 --> 00:10.000\nAlternate English cue\n' if alternate else
|
||||
b'WEBVTT\n\n00:00.000 --> 00:02.000\nFirst English cue\n\n00:02.000 --> 00:04.000\nSecond English cue\n')
|
||||
elif path.startswith("/resolved-vod-") and path.endswith(".ts"):
|
||||
data = (fixtures / path[1:]).read_bytes()
|
||||
else:
|
||||
@@ -71,6 +76,7 @@ try:
|
||||
assert "stream-private" not in progress.read_text() and "token=" not in progress.read_text(), "progress has no transport secrets"
|
||||
assert not errors, errors
|
||||
assert {"/master.m3u8", "/variant.m3u8", "/english.jpeg"}.issubset(seen), "master, nested playlist and external VTT fetched"
|
||||
assert {"/alternate/master.m3u8", "/alternate/english.jpeg"}.issubset(seen), "alternate video host and independent caption provider fetched"
|
||||
assert any(path.endswith(".ts") for path in seen), "media segment fetched"
|
||||
print("PASS: model HLS/VTT playback, independent resource headers, tokens, stable progress")
|
||||
finally:
|
||||
|
||||
+115
-10
@@ -37,7 +37,7 @@ struct ResolvedPlaybackCheck {
|
||||
guard case .miruro(let mediaID, let episode) = try StreamURL.parse(input) else {
|
||||
fatalError("FAIL: URL smoke requires a Miruro episode")
|
||||
}
|
||||
source = try await MiruroResolver.resolve(mediaID: mediaID, episode: episode)
|
||||
source = try await MiruroResolver.resolve(mediaID: mediaID, episode: episode).select().source
|
||||
} else {
|
||||
source = try ResolvedSource.load(from: file)
|
||||
}
|
||||
@@ -106,6 +106,7 @@ struct ResolvedPlaybackCheck {
|
||||
if !input.hasPrefix("https://") { try await segmentSkips(model, clock: clock, file: file) }
|
||||
if !input.hasPrefix("https://") {
|
||||
model.shutdown()
|
||||
try await providerLifecycle(source: source)
|
||||
try await queueMetadataLifecycle(source: source)
|
||||
try await streamQueueLifecycle(source: source)
|
||||
try await enqueueLifecycle(source: source)
|
||||
@@ -113,6 +114,111 @@ struct ResolvedPlaybackCheck {
|
||||
}
|
||||
}
|
||||
|
||||
static func providerSources(_ source: ResolvedSource, alternate: Bool = false) throws -> MiruroSources {
|
||||
func headers(_ text: String) -> [String: String] {
|
||||
Dictionary(uniqueKeysWithValues: text.components(separatedBy: "\r\n").filter { !$0.isEmpty }.map {
|
||||
let parts = $0.split(separator: ":", maxSplits: 1)
|
||||
return (String(parts[0]), String(parts[1]).trimmingCharacters(in: .whitespaces))
|
||||
})
|
||||
}
|
||||
func provider(_ name: String, url: String, header: [String: String], subtitles: [[String: Any]]) -> [String: Any] {
|
||||
["provider": name, "servers": [["headers": header, "streams": [["url": url, "format": "hls"]]]],
|
||||
"subtitles": subtitles]
|
||||
}
|
||||
let subtitles: [[String: Any]] = source.subtitles.map {
|
||||
["file": $0.url, "format": $0.format, "language": $0.language, "label": $0.label, "default": $0.isDefault]
|
||||
}
|
||||
// Source-file fixtures allow independent headers; Miruro returns each set on its own provider.
|
||||
let video = provider("sun", url: source.url, header: headers(source.headers), subtitles: [])
|
||||
let captions = provider("captions", url: source.url.replacingOccurrences(of: "/master.m3u8", with: "/captions/master.m3u8"),
|
||||
header: headers(source.subtitles[0].headers), subtitles: subtitles)
|
||||
var providers = [video, captions]
|
||||
var config: [String: Any] = ["sun": ["player": "native", "proxy": false],
|
||||
"captions": ["player": "native", "proxy": false]]
|
||||
if alternate {
|
||||
providers.append(provider("alternate", url: source.url.replacingOccurrences(of: "/master.m3u8", with: "/alternate/master.m3u8"),
|
||||
header: ["X-Resource": "alternate-private"],
|
||||
subtitles: [["file": source.subtitles[0].url.replacingOccurrences(of: "/english.jpeg", with: "/alternate/english.jpeg"),
|
||||
"format": "vtt", "language": "en", "label": "Alternate English", "default": true]]))
|
||||
config["alternate"] = ["player": "native", "proxy": false]
|
||||
}
|
||||
return try MiruroResolver.decode(JSONSerialization.data(withJSONObject: [
|
||||
"episodeNumber": 1, "providerOrder": ["sun", "captions", "alternate"], "config": config,
|
||||
"tracks": [["track": "ssub", "providers": providers]]
|
||||
]), mediaID: "fixture-ID", episode: 1)
|
||||
}
|
||||
|
||||
static func providerLifecycle(source: ResolvedSource) async throws {
|
||||
let providers = try providerSources(source, alternate: true)
|
||||
let suite = "terebi.provider-check." + UUID().uuidString
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
var requests = 0
|
||||
var pending: CheckedContinuation<MiruroSources, Never>?
|
||||
var completed = 0
|
||||
let model = PlayerModel(defaults: defaults,
|
||||
configuration: .init(miruroVideoProvider: "unavailable", miruroCaptionProvider: "captions"),
|
||||
miruroResolver: { _, _ in
|
||||
requests += 1
|
||||
defer { completed += 1 }
|
||||
if requests == 4 { throw CocoaError(.fileReadCorruptFile) }
|
||||
if requests == 5 { return await withCheckedContinuation { pending = $0 } }
|
||||
return providers
|
||||
}, miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
|
||||
defer { model.shutdown() }
|
||||
let url = "https://barelystarted.miruro.tv/watch/fixture-ID/episode?ep=1"
|
||||
model.openStream(url)
|
||||
try await until("configured missing host falls back with independent captions", model: model) {
|
||||
model.canChangeMiruroProvider && model.state.position > 0.2 && !model.subtitleText.isEmpty
|
||||
}
|
||||
require(model.selectedMiruroVideoProvider == "sun" && model.selectedMiruroCaptionProvider == "captions",
|
||||
"startup preferences select independent providers with host fallback")
|
||||
require(model.setPlayback(playing: false), "pause before provider switches")
|
||||
try await until("provider pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
|
||||
let position = model.state.position
|
||||
let revision = model.state.revision
|
||||
model.setMiruroCaptionProvider("alternate")
|
||||
try await until("alternate provider's own VTT is loaded", model: model) { model.subtitleText == "Alternate English cue" }
|
||||
require(model.state.revision == revision && model.state.position == position && requests == 1,
|
||||
"caption selection does not reopen video, seek or refetch sources")
|
||||
model.setMiruroVideoProvider("alternate")
|
||||
try await until("alternate host preserves paused position", model: model) {
|
||||
model.canChangeMiruroProvider && model.state.status == PLAYER_PAUSED &&
|
||||
model.state.revision != revision && abs(model.state.position - position) < 0.2
|
||||
}
|
||||
require(model.selectedMiruroVideoProvider == "alternate" && model.selectedMiruroCaptionProvider == "alternate",
|
||||
"host and captions remain independently selected")
|
||||
model.selectSubtitle(nil)
|
||||
model.setMiruroCaptionProvider("")
|
||||
require(model.selectedSubtitle == nil && model.subtitleText.isEmpty, "caption provider respects explicit Off")
|
||||
require(model.setPlayback(playing: true), "resume before automatic host choice")
|
||||
try await until("playback resumes before host switch", model: model) { model.state.status == PLAYER_PLAYING }
|
||||
model.setMiruroVideoProvider("")
|
||||
try await until("automatic host returns to Sun and preserves playing intent", model: model) {
|
||||
model.canChangeMiruroProvider && model.state.status == PLAYER_PLAYING &&
|
||||
model.selectedMiruroVideoProvider == "sun"
|
||||
}
|
||||
require(model.selectedSubtitle == nil, "host changes preserve explicit subtitle Off")
|
||||
model.setMiruroVideoProvider("alternate")
|
||||
let deadline = ContinuousClock.now.advanced(by: .seconds(5))
|
||||
while (model.resolvingStream || model.state.status != PLAYER_PLAYING), ContinuousClock.now < deadline {
|
||||
try await Task.sleep(for: .milliseconds(10))
|
||||
}
|
||||
require(model.commandError != nil && model.selectedMiruroVideoProvider == "sun" &&
|
||||
model.state.status == PLAYER_PLAYING && model.canChangeMiruroProvider,
|
||||
"failed host fetch retains previous source and resumes playback")
|
||||
model.setMiruroVideoProvider("")
|
||||
try await until("replacement provider request is held", model: model) { pending != nil }
|
||||
model.closeFile()
|
||||
pending!.resume(returning: providers)
|
||||
try await until("closed player discards late provider results", model: model) {
|
||||
completed == requests && model.state.status == PLAYER_EMPTY
|
||||
}
|
||||
require(model.miruroVideoProviders.isEmpty && model.selectedMiruroCaptionProvider == nil,
|
||||
"closing clears episode provider state")
|
||||
print("PASS: provider config fallback, independent caption headers, in-place captions, paused/playing host switches and failure retention")
|
||||
}
|
||||
|
||||
static func queueMetadataLifecycle(source: ResolvedSource) async throws {
|
||||
let suite = "terebi.queue-metadata-check." + UUID().uuidString
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
@@ -263,7 +369,7 @@ struct ResolvedPlaybackCheck {
|
||||
return video
|
||||
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in
|
||||
episodeRequests += 1
|
||||
return source
|
||||
return try providerSources(source)
|
||||
}, miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
|
||||
defer { model.shutdown() }
|
||||
require(model.progressInitializationError == nil, "queue report registration")
|
||||
@@ -281,7 +387,7 @@ struct ResolvedPlaybackCheck {
|
||||
stale = nil
|
||||
// Yield to the canceled resolver; it must not open or redirect its report.
|
||||
for _ in 0..<10 { await Task.yield() }
|
||||
require(model.queue.currentID == secondID && model.fileName == source.title &&
|
||||
require(model.queue.currentID == secondID && !model.miruroVideoProviders.isEmpty &&
|
||||
!FileManager.default.fileExists(atPath: firstReport.path), "stale extraction cannot replace selected media or report")
|
||||
model.advanceQueue(-1)
|
||||
try await until("previous resolves YouTube afresh", model: model) {
|
||||
@@ -297,7 +403,7 @@ struct ResolvedPlaybackCheck {
|
||||
require(model.setPlayback(playing: true), "play first stream to natural end")
|
||||
try await until("natural end advances and freshly resolves second stream", model: model) {
|
||||
model.queue.currentID == secondID && !model.isBusy && model.state.status == PLAYER_PLAYING &&
|
||||
model.fileName == source.title && episodeRequests == 2
|
||||
!model.miruroVideoProviders.isEmpty && episodeRequests == 2
|
||||
}
|
||||
require(model.setPlayback(playing: false), "pause second stream for restoration")
|
||||
try await until("second paused", model: model) { model.state.status == PLAYER_PAUSED }
|
||||
@@ -311,7 +417,7 @@ struct ResolvedPlaybackCheck {
|
||||
}
|
||||
model.advanceQueue(1)
|
||||
try await until("second revisit resumes acknowledged bookmark", model: model) {
|
||||
!model.isBusy && model.fileName == source.title && model.state.status == PLAYER_PLAYING &&
|
||||
!model.isBusy && model.queue.currentID == secondID && model.state.status == PLAYER_PLAYING &&
|
||||
model.state.position >= 0.7
|
||||
}
|
||||
require(youtubeRequests == 3 && episodeRequests == 3, "every revisit resolves fresh transport metadata")
|
||||
@@ -361,7 +467,7 @@ struct ResolvedPlaybackCheck {
|
||||
return YouTubeVideo(title: url.absoluteString,
|
||||
video: .init(url: source.url, headers: source.headers), audio: nil,
|
||||
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
|
||||
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in episodeRequests += 1; return source },
|
||||
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in episodeRequests += 1; return try providerSources(source) },
|
||||
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
|
||||
defer { model.shutdown() }
|
||||
func deliver(_ items: [(URL, URL)]) throws {
|
||||
@@ -415,7 +521,7 @@ struct ResolvedPlaybackCheck {
|
||||
}
|
||||
require(model.setPlayback(playing: true), "resume original through natural end")
|
||||
try await until("natural end advances to enqueued Miruro item", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == source.title &&
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && !model.miruroVideoProviders.isEmpty &&
|
||||
model.queue.currentURL == second
|
||||
}
|
||||
model.advanceQueue(1)
|
||||
@@ -424,7 +530,7 @@ struct ResolvedPlaybackCheck {
|
||||
}
|
||||
model.advanceQueue(-1)
|
||||
try await until("enqueued Miruro report revisits", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == source.title
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.queue.currentURL == second
|
||||
}
|
||||
model.advanceQueue(-1)
|
||||
try await until("original single video remains navigable", model: model) {
|
||||
@@ -510,7 +616,7 @@ struct ResolvedPlaybackCheck {
|
||||
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
|
||||
if url == cancelledURL { return await withCheckedContinuation { pending = $0 } }
|
||||
return video
|
||||
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in source },
|
||||
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in try providerSources(source) },
|
||||
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
|
||||
defer { model.shutdown() }
|
||||
try await model.addStreamToQueue(first.absoluteString)
|
||||
@@ -527,7 +633,6 @@ struct ResolvedPlaybackCheck {
|
||||
try await model.addStreamToQueue(episode.absoluteString)
|
||||
require(model.queue.items.map(\.url) == [first, second, episode] &&
|
||||
model.queue.items[1].displayName == "Sidebar second" &&
|
||||
model.queue.items[2].displayName == source.title &&
|
||||
model.queue.currentID == selected && model.state.revision == revision &&
|
||||
model.state.position == position && model.state.status == PLAYER_PAUSED,
|
||||
"fetched YouTube and Miruro additions preserve paused playback, order and titles")
|
||||
|
||||
@@ -40,7 +40,17 @@ struct StreamResolverChecks {
|
||||
throw Failure(message: "Accepted \(message)")
|
||||
}
|
||||
static func project(_ json: String) throws -> ResolvedSource {
|
||||
try MiruroResolver.decode(Data(json.utf8), mediaID: mediaID, episode: 1)
|
||||
try MiruroResolver.decode(Data(json.utf8), mediaID: mediaID, episode: 1).select().source
|
||||
}
|
||||
static func catalog(_ document: [String: Any]) throws -> MiruroSources {
|
||||
try MiruroResolver.decode(JSONSerialization.data(withJSONObject: document), mediaID: mediaID, episode: 1)
|
||||
}
|
||||
static func provider(_ name: String, captions: Bool = true) -> [String: Any] {
|
||||
["provider": name,
|
||||
"servers": [["headers": ["Referer": "https://\(name).example/"],
|
||||
"streams": [["url": "https://\(name).example/master.m3u8", "format": "hls"]]]],
|
||||
"subtitles": captions ? [["file": "https://\(name).example/english.vtt", "format": "vtt",
|
||||
"language": "en", "label": "English", "default": true]] : []]
|
||||
}
|
||||
|
||||
static func checks() async throws {
|
||||
@@ -84,6 +94,79 @@ struct StreamResolverChecks {
|
||||
try require(source.subtitles.count == 2 && source.subtitles[0].isDefault, "only external VTT retained with default")
|
||||
try require(source.subtitles[0].url.hasSuffix(".jpeg") && source.subtitles[1].label == "Spanish (Latin America)", "subtitle format and labels independent of file extension")
|
||||
try require(source.subtitles.allSatisfy { $0.headers == source.headers }, "verified Sun subtitle headers retained")
|
||||
let defaults = try MiruroResolver.decode(Data(fixture.utf8), mediaID: mediaID, episode: 1)
|
||||
let defaultSelection = try defaults.select()
|
||||
try require(defaults.videoProviders == ["sun"] && defaults.captionProviders == ["sun"], "menus contain usable native providers")
|
||||
try require(defaultSelection.videoProvider == "sun" && defaultSelection.captionProvider == "sun", "existing Sun defaults")
|
||||
let native: [String: Any] = ["player": "native", "proxy": false]
|
||||
var document: [String: Any] = [
|
||||
"episodeNumber": 1, "providerOrder": ["cloud", "river", "sun", "cloud", "missing"],
|
||||
"config": ["sun": native, "river": native, "cloud": native],
|
||||
"tracks": [["track": "ssub", "providers": [provider("river"), provider("sun"), provider("cloud")]]]
|
||||
]
|
||||
let choices = try catalog(document)
|
||||
try require(choices.videoProviders == ["cloud", "river", "sun"] &&
|
||||
choices.captionProviders == ["cloud", "river", "sun"], "runtime names follow deduplicated provider order")
|
||||
let automatic = try choices.select()
|
||||
try require(automatic.videoProvider == "sun" && automatic.captionProvider == "sun", "Auto prefers usable Sun")
|
||||
let independent = try choices.select(videoProvider: "river", captionProvider: "cloud")
|
||||
try require(independent.videoProvider == "river" && independent.captionProvider == "cloud", "explicit independent choices")
|
||||
try require(independent.source.url == "https://river.example/master.m3u8" &&
|
||||
independent.source.headers == "Referer: https://river.example/\r\n", "selected host retains its URL and headers")
|
||||
try require(independent.source.subtitles.first?.url == "https://cloud.example/english.vtt" &&
|
||||
independent.source.subtitles.first?.headers == "Referer: https://cloud.example/\r\n", "captions retain their own provider headers")
|
||||
let sameHost = try choices.select(videoProvider: "river")
|
||||
try require(sameHost.captionProvider == "river", "Auto captions prefer chosen video provider")
|
||||
let absent = try choices.select(videoProvider: "absent", captionProvider: "unsupported")
|
||||
let empty = try choices.select(videoProvider: "", captionProvider: "")
|
||||
try require(absent.videoProvider == "sun" && absent.captionProvider == "sun" &&
|
||||
empty.videoProvider == "sun" && empty.captionProvider == "sun", "absent and empty preferences fall back to Auto")
|
||||
document["tracks"] = [["track": "ssub", "providers": [provider("river"), provider("cloud")]]]
|
||||
let withoutSun = try catalog(document).select()
|
||||
try require(withoutSun.videoProvider == "cloud" && withoutSun.captionProvider == "cloud", "provider order applies without Sun")
|
||||
document["providerOrder"] = []
|
||||
let returnedOrder = try catalog(document).select()
|
||||
try require(returnedOrder.videoProvider == "river", "returned order is deterministic when no provider order applies")
|
||||
document["tracks"] = [["track": "ssub", "providers": [provider("river", captions: false), provider("cloud")]]]
|
||||
let otherCaptions = try catalog(document).select()
|
||||
try require(otherCaptions.videoProvider == "river" && otherCaptions.captionProvider == "cloud", "Auto captions fall back independently")
|
||||
document["tracks"] = [["track": "sub", "providers": [provider("river", captions: false)]]]
|
||||
let noCaptions = try catalog(document)
|
||||
let bare = try noCaptions.select(captionProvider: "cloud")
|
||||
try require(noCaptions.captionProviders.isEmpty && bare.captionProvider == nil && bare.source.subtitles.isEmpty, "Japanese sub without captions remains playable")
|
||||
var soft = provider("river")
|
||||
soft["servers"] = [["streams": [["url": "https://soft.example/master.m3u8", "format": "m3u8"]]]]
|
||||
document["tracks"] = [["track": "sub", "providers": [provider("river")]],
|
||||
["track": "ssub", "providers": [soft]],
|
||||
["track": "dub", "providers": [provider("cloud")]]]
|
||||
let preferredVariant = try catalog(document)
|
||||
let preferredSelection = try preferredVariant.select()
|
||||
try require(preferredVariant.videoProviders == ["river"] &&
|
||||
preferredSelection.source.url == "https://soft.example/master.m3u8", "ssub wins per provider without dub fallback")
|
||||
var mixedConfigs: [String: Any] = ["river": native, "proxy": ["player": "native", "proxy": ["segments": true]],
|
||||
"iframe": ["player": "iframe", "proxy": false], "broken": ["player": 42], "bad name": native]
|
||||
mixedConfigs["rewrite"] = ["player": "native", "proxy": false, "hls": ["query": ["enabled": true]]]
|
||||
document["config"] = mixedConfigs
|
||||
var resilient = provider("river")
|
||||
resilient["servers"] = [
|
||||
["streams": [["url": "https://embed.example/master.m3u8", "format": "hls"]], "embed": ["url": "https://embed.example/"]],
|
||||
["streams": [42, ["url": "file:///secret", "format": "hls"],
|
||||
["url": "https://river.example/master.m3u8", "format": "application/vnd.apple.mpegurl"]] as [Any],
|
||||
"headers": ["Referer": "https://river.example/"]]] as [[String: Any]]
|
||||
document["tracks"] = [["track": "ssub", "providers": [
|
||||
42, ["provider": "broken", "servers": "invalid"], provider("proxy"), provider("iframe"),
|
||||
provider("rewrite"), provider("bad name"), resilient] as [Any]]]
|
||||
let resilientSources = try catalog(document)
|
||||
let resilientChoice = try resilientSources.select(videoProvider: "proxy", captionProvider: "iframe")
|
||||
try require(resilientSources.videoProviders == ["river"] && resilientSources.captionProviders == ["river"] &&
|
||||
resilientChoice.source.url == "https://river.example/master.m3u8", "malformed and unsupported entries do not poison usable alternatives")
|
||||
for (old, replacement) in [
|
||||
("https://vault.example/english.jpeg", "https://user:secret@vault.example/english.jpeg"),
|
||||
("\"default\": false", "\"default\": true")
|
||||
] {
|
||||
let safeVideo = try project(fixture.replacingOccurrences(of: old, with: replacement))
|
||||
try require(safeVideo.url == source.url && safeVideo.subtitles.isEmpty, "unsafe caption sets are excluded without losing safe video")
|
||||
}
|
||||
let renewed = try project(fixture.replacingOccurrences(of: "token=secret", with: "token=renewed"))
|
||||
try require(renewed.identity == source.identity && renewed.url != source.url, "token rotation does not change stable identity")
|
||||
for (old, replacement) in [
|
||||
@@ -92,14 +175,13 @@ struct StreamResolverChecks {
|
||||
("\"enabled\": false", "\"enabled\": true"),
|
||||
("\"player\": \"native\"", "\"player\": \"iframe\""),
|
||||
("\"track\": \"ssub\"", "\"track\": \"dub\""),
|
||||
("\"provider\": \"sun\"", "\"provider\": \"bee\""),
|
||||
("\"proxy\": false", "\"proxy\": {\"segments\": true}"),
|
||||
("\"format\": \"hls\"", "\"format\": \"mp4\""),
|
||||
("\"embed\": null", "\"embed\": {\"url\": \"https://embed.example/secret\"}"),
|
||||
("https://vault.example/master.m3u8?token=secret", "file:///secret"),
|
||||
("https://vault.example/english.jpeg", "https://user:secret@vault.example/english.jpeg"),
|
||||
("\"embed\": null", "\"embed\": false"),
|
||||
("https://ultracloud.cc/\"", "https://ultracloud.cc/\\r\\nX-Injected: secret\""),
|
||||
("\"Origin\":", "\"origin\": \"secret\", \"Origin\":"),
|
||||
("\"default\": false", "\"default\": true")
|
||||
("\"Origin\":", "\"origin\": \"secret\", \"Origin\":")
|
||||
] {
|
||||
try reject("unsafe/unsupported source projection") {
|
||||
_ = try project(fixture.replacingOccurrences(of: old, with: replacement))
|
||||
@@ -122,7 +204,7 @@ struct StreamResolverChecks {
|
||||
_ = try await cancelled.value
|
||||
throw Failure(message: "Cancelled resolution was accepted")
|
||||
} catch is CancellationError {}
|
||||
print("PASS: streaming URL security, Sun projection, shared validation, bounded responses and cancellation")
|
||||
print("PASS: streaming URL security, runtime native providers, independent captions, bounded responses and cancellation")
|
||||
}
|
||||
|
||||
static func main() async {
|
||||
@@ -132,12 +214,14 @@ struct StreamResolverChecks {
|
||||
guard case .miruro(let id, let episode) = try StreamURL.parse(args[1]) else {
|
||||
throw Failure(message: "--live requires a Miruro episode URL")
|
||||
}
|
||||
let source = try await MiruroResolver.resolve(mediaID: id, episode: episode)
|
||||
print("PASS: live \(source.identity); \(source.subtitles.count) VTT tracks")
|
||||
let sources = try await MiruroResolver.resolve(mediaID: id, episode: episode)
|
||||
let selection = try sources.select()
|
||||
print("PASS: live video=\(selection.videoProvider); captions=\(selection.captionProvider ?? "none"); \(sources.videoProviders.count) hosts; \(sources.captionProviders.count) caption providers; \(selection.source.subtitles.count) VTT tracks")
|
||||
} else if args.count == 2, args[0] == "--fixture" {
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: args[1]))
|
||||
let source = try MiruroResolver.decode(data, mediaID: mediaID, episode: 1)
|
||||
print("PASS: fixture \(source.identity); \(source.subtitles.count) VTT tracks")
|
||||
let sources = try MiruroResolver.decode(data, mediaID: mediaID, episode: 1)
|
||||
let selection = try sources.select()
|
||||
print("PASS: fixture video=\(selection.videoProvider); captions=\(selection.captionProvider ?? "none"); \(sources.videoProviders.count) hosts; \(sources.captionProviders.count) caption providers; \(selection.source.subtitles.count) VTT tracks")
|
||||
} else if args.isEmpty {
|
||||
try await checks()
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user