feat: add headless Zoko source checks

This commit is contained in:
2026-10-25 18:01:47 +01:00
parent e6a2759497
commit b95f7d251e
11 changed files with 329 additions and 12 deletions
+7 -5
View File
@@ -31,7 +31,7 @@ enum ZokoResolver {
completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) }
}
private static func fetch(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
private static func fetch(_ request: URLRequest, allowMissingPage: Bool = false) async throws -> (Data, HTTPURLResponse) {
let configuration = URLSessionConfiguration.ephemeral
configuration.httpShouldSetCookies = false
configuration.httpCookieStorage = nil
@@ -46,7 +46,7 @@ enum ZokoResolver {
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 }
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 {
@@ -62,14 +62,16 @@ enum ZokoResolver {
}
}
static func resolve(mediaID: String, episode: Int) async throws -> ResolvedSource {
static func resolve(mediaID: String, episode: Int, enrichTitle: Bool = true) 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)
let (data, response) = try await fetch(request, allowMissingPage: true)
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 else { return source }
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,
@@ -85,9 +87,9 @@ enum ZokoResolver {
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 }
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 }