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) static func parse(_ text: String) throws -> StreamURL { let text = text.trimmingCharacters(in: .whitespacesAndNewlines) if let url = try? YouTubeResolver.validateURL(text) { return .youtube(url) } let invalid = StreamResolutionError(message: "Enter a YouTube video URL or a Miruro episode URL from barelystarted.miruro.tv.") 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 } } enum MiruroResolver { static let bodyLimit = 2 * 1024 * 1024 static func resolve(mediaID: String, episode: Int) async throws -> ResolvedSource { 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.") } } // Match Tubular's direct ssub/Sun projection, without provider or server retry. static func decode(_ data: Data, mediaID: String, episode: Int) throws -> ResolvedSource { 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() } let config = response.config.sun guard config.player == "native", !config.proxy, config.hls?.segments?.enabled != true, config.hls?.query?.enabled != true else { throw StreamResolutionError(message: "Miruro Sun requires unsupported proxy or HLS rewriting.") } guard let provider = response.tracks.first(where: { $0.track == "ssub" })? .providers.first(where: { $0.provider == "sun" }), let server = provider.servers.first(where: { !$0.streams.isEmpty }), let stream = server.streams.first else { throw StreamResolutionError(message: "Direct Miruro Sun soft-subtitle source is unavailable.") } guard ["hls", "m3u8", "application/vnd.apple.mpegurl", "application/x-mpegurl"].contains(stream.format), !stream.hasEmbed else { throw InvalidProjection() } let headers = server.headers ?? [:] var subtitles: [ResolvedSubtitle] = [] for subtitle in provider.subtitles where subtitle.format == "vtt" { guard subtitles.count < 32 else { throw InvalidProjection() } subtitles.append(ResolvedSubtitle(url: subtitle.file, format: subtitle.format, language: subtitle.language, label: subtitle.label, isDefault: subtitle.default, headers: headers)) } return try ResolvedSource.validated(.init(version: 1, url: stream.url, headers: headers, identity: .init(provider: "miruro", media_id: mediaID, episode: episode), title: nil, subtitles: subtitles)) } catch { if let error = error as? StreamResolutionError { throw error } throw StreamResolutionError(message: "Miruro returned invalid or unsupported source metadata.") } } private struct InvalidProjection: Error {} private struct Sources: Decodable { let episodeNumber: Int let tracks: [Track] let config: Config } private struct Config: Decodable { let sun: ProviderConfig } private struct ProviderConfig: Decodable { let player: String let proxy: Bool let hls: HLS? } private struct HLS: Decodable { struct Rule: Decodable { let enabled: Bool? } let segments: Rule? let query: Rule? } private struct Track: Decodable { let track: String let providers: [Provider] } private struct Provider: Decodable { let provider: String let servers: [Server] let subtitles: [Subtitle] } private struct Server: Decodable { let headers: [String: String]? let streams: [Stream] } private struct Stream: Decodable { let url: String let format: String let hasEmbed: Bool enum CodingKeys: String, CodingKey { case url, format, embed } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) url = try values.decode(String.self, forKey: .url) format = try values.decode(String.self, forKey: .format) if values.contains(.embed) { hasEmbed = try !values.decodeNil(forKey: .embed) } else { hasEmbed = false } } } private struct Subtitle: Decodable { 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) } }