import Foundation enum ZokoError: String, LocalizedError { case unavailable, transport, protection, malformed, mapping 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." case .mapping: "AniList has no usable MAL mapping for this title. Source availability could not be checked." } } } // 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" private static let codecKey = Array("otaku-embed-v1".utf8) 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, allowMissingPage: Bool = false) 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 || (allowMissingPage && response.statusCode == 404) else { throw ZokoError.transport } 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 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, enrichTitle: Bool = true, fetchResponse: @Sendable (URLRequest) async throws -> (Data, HTTPURLResponse) = { try await fetch($0, allowMissingPage: $0.url?.host == "zokoanime.video") } ) async throws -> ResolvedSource { guard ResolvedSource.validAniListIdentity(mediaID: mediaID, episode: episode) else { throw ZokoError.malformed } let metadata = try await catalog(mediaID: mediaID, includeTitle: enrichTitle, fetchResponse: fetchResponse) guard let malID = metadata.idMal else { throw ZokoError.mapping } var request = URLRequest(url: URL(string: "https://zokoanime.video/stream/mal/\(malID)/\(episode)/sub")!) request.timeoutInterval = 20 request.setValue("text/html", forHTTPHeaderField: "Accept") let (data, response) = try await fetchResponse(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) guard response.statusCode == 200 else { throw ZokoError.transport } guard enrichTitle, let name = metadata.name else { return source } try Task.checkCancellation() return ResolvedSource(url: source.url, headers: source.headers, identity: source.identity, title: "\(name) · Episode \(episode) (sub)", subtitles: source.subtitles, intervals: source.intervals, requirements: source.requirements, playbackLifetime: source.playbackLifetime) } 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 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 } if visible.contains("error"), visible.contains("404"), visible.contains("couldn't find anything to play here") { throw ZokoError.unavailable } throw ZokoError.malformed } guard var decoded = Data(base64Encoded: String(page[range])), !decoded.isEmpty else { throw ZokoError.malformed } decoded.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) in let bytes = buffer.bindMemory(to: UInt8.self) for i in bytes.indices { bytes[i] ^= codecKey[i % codecKey.count] } } struct Config: Decodable { struct Subtitle: Decodable { let src: String; let lang: String; let label: String; let `default`: Bool? } struct Timing: Decodable { let start: Double let end: Double enum CodingKeys: CodingKey { case start, end } init(from decoder: any Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) start = try values.decode(Double.self, forKey: .start) end = try values.decode(Double.self, forKey: .end) guard start.isFinite, end.isFinite, start >= 0, end > start else { throw ZokoError.malformed } } } struct Skip: Decodable { let intro: Timing? let outro: Timing? enum CodingKeys: CodingKey { case intro, outro } init(from decoder: any Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) intro = try? values.decode(Timing.self, forKey: .intro) outro = try? values.decode(Timing.self, forKey: .outro) } } let src: String let subtitles: [Subtitle] let skip: Skip? enum CodingKeys: CodingKey { case src, subtitles, skip } init(from decoder: any Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) src = try values.decode(String.self, forKey: .src) subtitles = try values.decode([Subtitle].self, forKey: .subtitles) skip = try? values.decode(Skip.self, forKey: .skip) } } let config: Config do { config = try JSONDecoder().decode(Config.self, from: 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 } var intervals: [ProviderInterval] = [] if let intro = config.skip?.intro { intervals.append(ProviderInterval(kind: .intro, start: intro.start, end: intro.end)) } if let outro = config.skip?.outro { intervals.append(ProviderInterval(kind: .outro, start: outro.start, end: outro.end)) } return ResolvedSource(url: config.src, headers: referer, identity: "anilist:\(mediaID):\(episode)", title: "AniList \(mediaID) · Episode \(episode) (sub)", subtitles: subtitles, intervals: intervals, requirements: .init(zoko: true)) } struct Catalog: Decodable { struct Title: Decodable { let english: String?; let romaji: String?; let native: String? } let id: Int let idMal: Int? let title: Title? var name: String? { guard let title else { return nil } func usable(_ name: String?) -> String? { guard let name, !name.isEmpty, ResolvedSource.validText(name, limit: 2048) else { return nil } return name } return usable(title.english) ?? usable(title.romaji) ?? usable(title.native) } } static func decodeCatalog(_ data: Data, mediaID: String) throws -> Catalog { struct Response: Decodable { struct Body: Decodable { let media: Catalog? enum CodingKeys: String, CodingKey { case media = "Media" } } struct Problem: Decodable {} let data: Body? let errors: [Problem]? } let response: Response do { response = try JSONDecoder().decode(Response.self, from: data) } catch { throw ZokoError.malformed } guard response.errors?.isEmpty != false, let metadata = response.data?.media, String(metadata.id) == mediaID, metadata.idMal.map({ $0 > 0 && $0 <= Int(Int32.max) }) ?? true else { throw ZokoError.malformed } return metadata } private static func catalog( mediaID: String, includeTitle: Bool, fetchResponse: @Sendable (URLRequest) async throws -> (Data, HTTPURLResponse) ) async throws -> Catalog { var request = URLRequest(url: URL(string: "https://graphql.anilist.co/")!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let fields = includeTitle ? "id idMal title{english romaji native}" : "id idMal" request.httpBody = try JSONSerialization.data(withJSONObject: ["query": "query($id:Int!){Media(id:$id,type:ANIME){\(fields)}}", "variables": ["id": Int(mediaID)!]]) let (data, response) = try await fetchResponse(request) guard response.statusCode == 200, response.mimeType == "application/json" else { throw ZokoError.malformed } return try decodeCatalog(data, mediaID: mediaID) } static func title(mediaID: String, episode: Int) async throws -> String { guard ResolvedSource.validAniListIdentity(mediaID: mediaID, episode: episode) else { throw ZokoError.malformed } let metadata = try await catalog(mediaID: mediaID, includeTitle: true, fetchResponse: { try await fetch($0) }) guard let name = metadata.name else { throw ZokoError.malformed } return "\(name) · Episode \(episode) (sub)" } }