import Foundation enum ZokoError: String, LocalizedError { case unavailable, transport, protection, malformed var errorDescription: String? { switch self { case .unavailable: "ZokoAnime has no sub episode available for this catalog number. Catalog numbering does not confirm availability." case .transport: "ZokoAnime HTTPS request failed or timed out. No retry was attempted." case .protection: "ZokoAnime returned an access/protection page or refused the request." case .malformed: "ZokoAnime returned unsupported or malformed source data. Private source details are redacted." } } } // Narrow policy for the demonstrated deployment, not a general cross-origin grant. enum ZokoResolver { static let bodyLimit = 2 * 1024 * 1024 static let referer = "Referer: https://zokoanime.video/\r\n" static func validResource(_ url: URL, playlist: Bool = false) -> Bool { guard ResolvedSource.validURL(url.absoluteString), url.scheme == "https", url.port == nil || url.port == 443, let host = url.host, host == "hls.dramahot.top" || (!playlist && host == "hls1.drama1.cfd") else { return false } return true } private final class NoRedirect: NSObject, URLSessionTaskDelegate, Sendable { func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) } } private static func fetch(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) { let configuration = URLSessionConfiguration.ephemeral configuration.httpShouldSetCookies = false configuration.httpCookieStorage = nil configuration.urlCredentialStorage = nil configuration.urlCache = nil configuration.timeoutIntervalForRequest = 20 configuration.timeoutIntervalForResource = 25 let session = URLSession(configuration: configuration, delegate: NoRedirect(), delegateQueue: nil) defer { session.invalidateAndCancel() } do { let (bytes, response) = try await session.bytes(for: request) guard let response = response as? HTTPURLResponse, response.url == request.url else { throw ZokoError.transport } guard response.expectedContentLength <= bodyLimit else { throw ZokoError.malformed } if response.statusCode == 403 || response.statusCode == 429 || response.statusCode == 503 { throw ZokoError.protection } guard response.statusCode == 200 else { throw ZokoError.transport } var data = Data() for try await byte in bytes { try Task.checkCancellation() guard data.count < bodyLimit else { throw ZokoError.malformed } data.append(byte) } return (data, response) } catch { if Task.isCancelled || error is CancellationError || (error as? URLError)?.code == .cancelled { throw CancellationError() } if let error = error as? ZokoError { throw error } throw ZokoError.transport } } static func resolve(mediaID: String, episode: Int) async throws -> ResolvedSource { guard ResolvedSource.validAniListIdentity(mediaID: mediaID, episode: episode) else { throw ZokoError.malformed } var request = URLRequest(url: URL(string: "https://zokoanime.video/stream/ani/\(mediaID)/\(episode)/sub")!) request.timeoutInterval = 20 request.setValue("text/html", forHTTPHeaderField: "Accept") let (data, response) = try await fetch(request) guard response.mimeType == "text/html", let page = String(data: data, encoding: .utf8) else { throw ZokoError.malformed } let source = try parse(page: page, mediaID: mediaID, episode: episode) let title = (try? await Self.title(mediaID: mediaID, episode: episode)) ?? source.title try Task.checkCancellation() return ResolvedSource(url: source.url, headers: source.headers, identity: source.identity, title: title, subtitles: source.subtitles, requirements: source.requirements) } static func parse(page: String, mediaID: String, episode: Int) throws -> ResolvedSource { guard page.utf8.count <= bodyLimit, ResolvedSource.validAniListIdentity(mediaID: mediaID, episode: episode) else { throw ZokoError.malformed } let pattern = #"window\.__P\s*=\s*[\"']([A-Za-z0-9+/=]+)[\"']\s*;?\s*"# let regex = try NSRegularExpression(pattern: pattern) let matches = regex.matches(in: page, range: NSRange(page.startIndex..., in: page)) guard matches.count == 1, let match = matches.first, let range = Range(match.range(at: 1), in: page) else { guard !page.contains("window.__P") else { throw ZokoError.malformed } let lower = page.lowercased() let visible = lower.replacingOccurrences(of: "<[^>]*>", with: " ", options: .regularExpression) if visible.contains("error"), visible.contains("404"), visible.contains("couldn't find anything to play here") { throw ZokoError.unavailable } if lower.contains("cf-chl-") || lower.contains("challenge-platform") || lower.contains("just a moment") || lower.contains("captcha") || lower.contains("sorry, you have been blocked") { throw ZokoError.protection } throw ZokoError.malformed } guard let encoded = Data(base64Encoded: String(page[range])), !encoded.isEmpty else { throw ZokoError.malformed } let key = Array("otaku-embed-v1".utf8) var decoded = Array(encoded) for i in decoded.indices { decoded[i] ^= key[i % key.count] } struct Config: Decodable { struct Subtitle: Decodable { let src: String; let lang: String; let label: String; let `default`: Bool? } let src: String let subtitles: [Subtitle] } let config: Config do { config = try JSONDecoder().decode(Config.self, from: Data(decoded)) } catch { throw ZokoError.malformed } guard let url = URL(string: config.src), validResource(url, playlist: true), url.path.hasSuffix(".m3u8"), config.subtitles.count <= 32 else { throw ZokoError.malformed } let subtitles = try config.subtitles.map { row -> ResolvedSubtitle in guard let url = URL(string: row.src), validResource(url, playlist: true), url.path.hasSuffix(".vtt") else { throw ZokoError.malformed } return ResolvedSubtitle(url: row.src, format: "vtt", language: row.lang, label: row.label, isDefault: row.default ?? false, headers: ["Referer": "https://zokoanime.video/"]) } guard ResolvedSource.validSubtitles(subtitles) else { throw ZokoError.malformed } return ResolvedSource(url: config.src, headers: referer, identity: "anilist:\(mediaID):\(episode)", title: "AniList \(mediaID) · Episode \(episode) (sub)", subtitles: subtitles, requirements: .init(zoko: true)) } static func title(mediaID: String, episode: Int) async throws -> String { guard ResolvedSource.validAniListIdentity(mediaID: mediaID, episode: episode) else { throw ZokoError.malformed } var request = URLRequest(url: URL(string: "https://graphql.anilist.co")!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: ["query": "query($id:Int!){Media(id:$id,type:ANIME){id title{english romaji native}}}", "variables": ["id": Int(mediaID)!]]) let (data, response) = try await fetch(request) guard response.mimeType == "application/json" else { throw ZokoError.malformed } struct Metadata: Decodable { struct Body: Decodable { struct Media: Decodable { struct Title: Decodable { let english: String?; let romaji: String?; let native: String? } let id: Int; let title: Title } let media: Media enum CodingKeys: String, CodingKey { case media = "Media" } } let data: Body } let metadata = try JSONDecoder().decode(Metadata.self, from: data).data.media guard String(metadata.id) == mediaID, let name = [metadata.title.english, metadata.title.romaji, metadata.title.native].compactMap({ $0 }).first(where: { !$0.isEmpty }), ResolvedSource.validText(name, limit: 2048) else { throw ZokoError.malformed } return "\(name) · Episode \(episode) (sub)" } }