import Foundation private struct StreamResolutionError: LocalizedError { let message: String var errorDescription: String? { message } } enum StreamURL: Sendable { case youtube(URL) case miruro(mediaID: String, episode: Int) case anilist(mediaID: String, episode: Int) static func parse(_ text: String) throws -> StreamURL { let text = text.trimmingCharacters(in: .whitespacesAndNewlines) let url = try YouTubeResolver.validateURL(text) if url.host?.lowercased() == "zokoanime.video" { let invalid = StreamResolutionError(message: "Enter https://zokoanime.video/stream/ani///sub with canonical positive numbers.") guard let c = URLComponents(string: text), c.scheme == "https", c.user == nil, c.password == nil, c.port == nil || c.port == 443, c.query == nil, c.fragment == nil, c.percentEncodedPath == c.path, !text.contains(where: { $0.isWhitespace || $0 == "\\" }) else { throw invalid } let p = c.path.split(separator: "/", omittingEmptySubsequences: false) guard p.count == 6, p[0].isEmpty, p[1] == "stream", p[2] == "ani", p[5] == "sub", let episode = Int(p[4]), String(episode) == p[4], ResolvedSource.validAniListIdentity(mediaID: String(p[3]), episode: episode) else { throw invalid } return .anilist(mediaID: String(p[3]), episode: episode) } if url.host?.lowercased() != "barelystarted.miruro.tv" { return .youtube(url) } let invalid = StreamResolutionError(message: "Enter a Miruro episode URL from barelystarted.miruro.tv with an explicit positive episode number.") guard !text.unicodeScalars.contains(where: { CharacterSet.whitespacesAndNewlines.contains($0) || CharacterSet.controlCharacters.contains($0) }), let components = URLComponents(string: text), components.scheme?.lowercased() == "https", components.host?.lowercased() == "barelystarted.miruro.tv", components.user == nil, components.password == nil, components.port == nil || components.port == 443, components.fragment == nil, components.percentEncodedPath == components.path, components.url != nil else { throw invalid } let path = components.path.split(separator: "/", omittingEmptySubsequences: false) guard path.count == 4, path[0].isEmpty, path[1] == "watch", !path[3].isEmpty, path[3].utf8.count <= 512, path[3].utf8.allSatisfy({ isSegmentByte($0) }), let query = components.queryItems, query.count == 1, query[0].name == "ep", let number = query[0].value, let episode = Int(number), components.percentEncodedQuery == "ep=\(episode)", ResolvedSource.validIdentity(mediaID: String(path[2]), episode: episode) else { throw invalid } return .miruro(mediaID: String(path[2]), episode: episode) } private static func isSegmentByte(_ byte: UInt8) -> Bool { (65...90).contains(byte) || (97...122).contains(byte) || (48...57).contains(byte) || byte == 45 || byte == 95 } } struct MiruroProxy: Decodable, Sendable { let rotate: Bool let segments: Bool enum CodingKeys: String, CodingKey { case rotate, segments } init(rotate: Bool, segments: Bool) { self.rotate = rotate self.segments = segments } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) rotate = try values.decodeIfPresent(Bool.self, forKey: .rotate) ?? false segments = try values.decodeIfPresent(Bool.self, forKey: .segments) ?? true } } struct MiruroPlaybackRequirements: Sendable { var proxy: MiruroProxy? = nil var segmentOrigin: String? = nil var playlistQuery: [String: String] = [:] var embed = false var zoko = false } struct MiruroSelection: Sendable { let source: ResolvedSource let videoProvider: String let captionProvider: String? } struct MiruroSources: Sendable { fileprivate struct Candidate: Sendable { let provider: String let track: String let source: ResolvedSource var id: String { "\(provider):\(track)" } } fileprivate struct Captions: Sendable { let provider: String let track: String let subtitles: [ResolvedSubtitle] var id: String { "\(provider):\(track)" } } fileprivate let videos: [Candidate] fileprivate let captions: [Captions] let videoProviders: [String] let captionProviders: [String] fileprivate init(videos: [Candidate], captions: [Captions]) { self.videos = videos self.captions = captions videoProviders = videos.map(\.id) captionProviders = captions.map(\.id) } func select(videoProvider: String? = nil, captionProvider: String? = nil) throws -> MiruroSelection { func requested(_ preference: String?, provider: String, track: String) -> Bool { preference == "\(provider):\(track)" || (preference == provider && track != "dub") } let japanese = videos.lazy.filter { $0.track != "dub" } guard let video = videos.first(where: { requested(videoProvider, provider: $0.provider, track: $0.track) }) ?? japanese.first(where: { $0.provider == "sun" }) ?? japanese.first else { throw StreamResolutionError(message: "Miruro Japanese source is unavailable. Select an available provider and track explicitly.") } let compatibleCaptions = captions.lazy.filter { $0.track == video.track || (video.track != "dub" && $0.track != "dub") } let caption = captions.first(where: { requested(captionProvider, provider: $0.provider, track: $0.track) }) ?? captions.first(where: { $0.id == video.id }) ?? compatibleCaptions.first(where: { $0.provider == video.provider }) ?? compatibleCaptions.first(where: { $0.provider == "sun" }) ?? compatibleCaptions.first let source = video.source.replacingSubtitles(caption?.subtitles ?? []) return MiruroSelection(source: source, videoProvider: video.id, captionProvider: caption?.id) } } enum MiruroResolver { static let bodyLimit = 2 * 1024 * 1024 static func resolve(mediaID: String, episode: Int) async throws -> MiruroSources { try Task.checkCancellation() guard ResolvedSource.validIdentity(mediaID: mediaID, episode: episode) else { throw StreamResolutionError(message: "Invalid Miruro episode identity.") } var components = URLComponents(string: "https://barelystarted.miruro.tv/api/sources")! components.queryItems = [URLQueryItem(name: "id", value: mediaID), URLQueryItem(name: "n", value: String(episode))] var request = URLRequest(url: components.url!) request.timeoutInterval = 25 request.setValue("application/json", forHTTPHeaderField: "Accept") let configuration = URLSessionConfiguration.ephemeral configuration.timeoutIntervalForRequest = 25 configuration.timeoutIntervalForResource = 25 configuration.httpShouldSetCookies = false configuration.httpCookieStorage = nil configuration.urlCache = nil let session = URLSession(configuration: configuration, delegate: MiruroRedirectPolicy(), delegateQueue: nil) defer { session.invalidateAndCancel() } do { let (bytes, response) = try await session.bytes(for: request) try validateResponse(response) var data = Data() if response.expectedContentLength > 0 { data.reserveCapacity(Int(response.expectedContentLength)) } for try await byte in bytes { try Task.checkCancellation() guard data.count < bodyLimit else { throw StreamResolutionError(message: "Miruro source response exceeded 2 MiB.") } data.append(byte) } try Task.checkCancellation() return try decode(data, mediaID: mediaID, episode: episode) } catch { if Task.isCancelled || error is CancellationError || (error as? URLError)?.code == .cancelled { throw CancellationError() } if let error = error as? StreamResolutionError { throw error } throw StreamResolutionError(message: "Miruro request failed or timed out. No retry was attempted.") } } static func validateResponse(_ response: URLResponse) throws { guard let response = response as? HTTPURLResponse else { throw StreamResolutionError(message: "Miruro returned an invalid HTTP response.") } guard response.statusCode == 200 else { throw StreamResolutionError(message: "Miruro HTTP \(response.statusCode). No retry was attempted.") } guard response.mimeType?.lowercased() == "application/json" else { throw StreamResolutionError(message: "Miruro returned an unexpected content type; protection pages are not supported.") } guard response.expectedContentLength <= Int64(bodyLimit) else { throw StreamResolutionError(message: "Miruro source response exceeded 2 MiB.") } } static func decode(_ data: Data, mediaID: String, episode: Int) throws -> MiruroSources { do { guard data.count <= bodyLimit, ResolvedSource.validIdentity(mediaID: mediaID, episode: episode) else { throw InvalidProjection() } let response = try JSONDecoder().decode(Sources.self, from: data) guard response.episodeNumber == episode else { throw InvalidProjection() } var videos: [MiruroSources.Candidate] = [] var captions: [MiruroSources.Captions] = [] var videoIDs = Set() var captionIDs = Set() for track in response.tracks.lazy.compactMap(\.value) where ["ssub", "sub", "dub"].contains(track.track) { for provider in track.providers.lazy.compactMap(\.value) { let name = provider.provider guard validProviderName(name) else { continue } let id = "\(name):\(track.track)" let config = response.config[name]?.value let servers = provider.servers.compactMap(\.value) // Prefer supplied server headers, but do not require a playable video to use them. let captionServer = servers.first { !($0.headers ?? [:]).isEmpty && ResolvedSource.validHeaders($0.headers ?? [:]) } ?? servers.first { ResolvedSource.validHeaders($0.headers ?? [:]) } if !captionIDs.contains(id), captionServer != nil || servers.isEmpty { var headers = captionServer?.headers ?? [:] var proxy = config?.proxy let returned = provider.subtitles.compactMap(\.value) // Iframe captions may be exact copies of a parent's resources. // Reuse only that verified caption transport, never its video rules. if headers.isEmpty, config?.player == "iframe", let parentName = config?.parent, let parent = track.providers.compactMap(\.value).first(where: { $0.provider == parentName }), parent.subtitles.compactMap(\.value) == returned, let parentServer = parent.servers.compactMap(\.value).first(where: { !($0.headers ?? [:]).isEmpty && ResolvedSource.validHeaders($0.headers ?? [:]) }) { headers = parentServer.headers ?? [:] proxy = response.config[parentName]?.value?.proxy } let subtitles = returned.lazy .filter { $0.format == "vtt" }.map { ResolvedSubtitle(url: $0.file, format: $0.format, language: $0.language, label: $0.label, isDefault: $0.default, headers: headers, proxy: proxy) } let values = Array(subtitles) if !values.isEmpty, ResolvedSource.validSubtitles(values) { captions.append(.init(provider: name, track: track.track, subtitles: values)) captionIDs.insert(id) } } for server in servers { let headers = server.headers ?? [:] guard ResolvedSource.validHeaders(headers) else { continue } guard !videoIDs.contains(id), let config, var requirements = try? config.requirements() else { continue } var locations: [(String, Bool)] = [] if config.player == "native", server.embed == nil { locations = server.streams.compactMap(\.value).compactMap { stream in guard stream.embed == nil, ["hls", "m3u8", "application/vnd.apple.mpegurl", "application/x-mpegurl"].contains(stream.format) else { return nil } return (stream.url, false) } } else if config.player == "iframe" { if let embed = server.embed { locations.append((embed.url, true)) } locations.append(contentsOf: server.streams.lazy.compactMap(\.value).compactMap { stream in stream.embed.map { ($0.url, true) } }) } for (url, embed) in locations { guard var source = try? ResolvedSource.validated(.init(version: 1, url: url, headers: headers, identity: .init(provider: "miruro", media_id: mediaID, episode: episode), title: nil, subtitles: [])) else { continue } requirements.embed = embed source.requirements = requirements videos.append(.init(provider: name, track: track.track, source: source)) videoIDs.insert(id) break } } } } // Provider order is advisory and deduplicated; each provider retains all usable tracks. var seen = Set() let providerOrder = ((response.providerOrder ?? []) + videos.map(\.provider) + captions.map(\.provider)) .filter { seen.insert($0).inserted } func variants(_ name: String) -> [String] { var seen = Set() return ((response.config[name]?.value?.variantOrder ?? []) + ["ssub", "sub", "dub"]) .filter { seen.insert($0).inserted } } let orderedVideos = providerOrder.flatMap { name in variants(name).compactMap { variant in videos.first { $0.provider == name && $0.track == variant } } } let orderedCaptions = providerOrder.flatMap { name in variants(name).compactMap { variant in captions.first { $0.provider == name && $0.track == variant } } } guard !orderedVideos.isEmpty || !orderedCaptions.isEmpty else { throw InvalidProjection() } return MiruroSources(videos: orderedVideos, captions: orderedCaptions) } catch { if let error = error as? StreamResolutionError { throw error } throw StreamResolutionError(message: "Miruro returned invalid or unsupported source metadata.") } } private static func validProviderName(_ name: String) -> Bool { !name.isEmpty && name.utf8.count <= 128 && name.utf8.allSatisfy { (65...90).contains($0) || (97...122).contains($0) || (48...57).contains($0) || $0 == 45 || $0 == 95 } } private struct InvalidProjection: Error {} // A malformed entry must not discard valid neighboring providers, servers or streams. private struct Entry: Decodable { let value: Value? init(from decoder: Decoder) throws { value = try? Value(from: decoder) } } private struct Sources: Decodable { let episodeNumber: Int let tracks: [Entry] let config: [String: Entry] let providerOrder: [String]? enum CodingKeys: String, CodingKey { case episodeNumber, tracks, config, providerOrder } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) episodeNumber = try values.decode(Int.self, forKey: .episodeNumber) tracks = try values.decode([Entry].self, forKey: .tracks) config = try values.decodeIfPresent([String: Entry].self, forKey: .config) ?? [:] providerOrder = try values.decodeIfPresent([String].self, forKey: .providerOrder) } } private struct ProviderConfig: Decodable { let player: String let proxy: MiruroProxy? let hls: HLS? let variantOrder: [String]? let parent: String? enum CodingKeys: String, CodingKey { case player, proxy, hls, variantOrder, parent } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) player = try values.decode(String.self, forKey: .player) if let flag = try? values.decode(Bool.self, forKey: .proxy) { guard !flag else { throw InvalidProjection() } proxy = nil } else { proxy = try values.decode(MiruroProxy.self, forKey: .proxy) } hls = try values.decodeIfPresent(HLS.self, forKey: .hls) variantOrder = try values.decodeIfPresent([String].self, forKey: .variantOrder) parent = try values.decodeIfPresent(String.self, forKey: .parent) } func requirements() throws -> MiruroPlaybackRequirements { guard player == "native" || player == "iframe" else { throw InvalidProjection() } var result = MiruroPlaybackRequirements(proxy: proxy) if let segments = hls?.segments, segments.enabled == true { guard let origin = segments.origin, ResolvedSource.validURL(origin) else { throw InvalidProjection() } result.segmentOrigin = origin } if let query = hls?.query, query.enabled == true { guard let params = query.params, params.count <= 64, params.allSatisfy({ !$0.key.isEmpty && ResolvedSource.validText($0.key, limit: 256) && ResolvedSource.validText($0.value, limit: 16384) }) else { throw InvalidProjection() } result.playlistQuery = params } return result } } private struct HLS: Decodable { struct Segments: Decodable { let enabled: Bool?; let origin: String? } struct Query: Decodable { let enabled: Bool?; let params: [String: String]? } let segments: Segments? let query: Query? } private struct Track: Decodable { let track: String let providers: [Entry] } private struct Provider: Decodable { let provider: String let servers: [Entry] let subtitles: [Entry] enum CodingKeys: String, CodingKey { case provider, servers, subtitles } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) provider = try values.decode(String.self, forKey: .provider) servers = try values.decodeIfPresent([Entry].self, forKey: .servers) ?? [] subtitles = try values.decodeIfPresent([Entry].self, forKey: .subtitles) ?? [] } } private struct Embed: Decodable { let url: String } private struct Server: Decodable { let headers: [String: String]? let streams: [Entry] let embed: Embed? enum CodingKeys: String, CodingKey { case headers, streams, embed } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) headers = try values.decodeIfPresent([String: String].self, forKey: .headers) streams = try values.decodeIfPresent([Entry].self, forKey: .streams) ?? [] embed = try values.decodeIfPresent(Embed.self, forKey: .embed) } } private struct Stream: Decodable { let url: String let format: String let embed: Embed? } private struct Subtitle: Decodable, Equatable { let file: String let format: String let language: String let label: String let `default`: Bool } } private final class MiruroRedirectPolicy: NSObject, URLSessionTaskDelegate, @unchecked Sendable { func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) } }