diff --git a/BilibiliLive/Request/WebRequest.swift b/BilibiliLive/Request/WebRequest.swift index b4be79e..1259dad 100644 --- a/BilibiliLive/Request/WebRequest.swift +++ b/BilibiliLive/Request/WebRequest.swift @@ -6,6 +6,7 @@ // import Alamofire +import CommonCrypto import Foundation import SwiftProtobuf import SwiftyJSON @@ -38,8 +39,8 @@ enum WebRequest { static let like = "https://api.bilibili.com/x/web-interface/archive/like" static let likeStatus = "https://api.bilibili.com/x/web-interface/archive/has/like" static let coin = "https://api.bilibili.com/x/web-interface/coin/add" - static let playerInfo = "https://api.bilibili.com/x/player/v2" - static let playUrl = "https://api.bilibili.com/x/player/playurl" + static let playerInfo = "https://api.bilibili.com/x/player/wbi/v2" + static let playUrl = "https://api.bilibili.com/x/player/wbi/playurl" static let pcgPlayUrl = "https://api.bilibili.com/pgc/player/web/playurl" static let bangumiSeason = "https://bangumi.bilibili.com/view/web_api/season" static let userEpisodeInfo = "https://api.bilibili.com/pgc/season/episode/web/info" @@ -83,21 +84,63 @@ enum WebRequest { } session.sessionConfiguration.timeoutIntervalForResource = 10 session.sessionConfiguration.timeoutIntervalForRequest = 10 - session.request(url, - method: method, - parameters: parameters, - encoding: URLEncoding.default, - headers: afheaders, - interceptor: nil) - .responseData { response in - switch response.result { - case let .success(data): - complete?(.success(data)) - case let .failure(err): - print(err) - complete?(.failure(.networkFail)) + + let completionHandler: (AFDataResponse) -> Void = { response in + switch response.result { + case let .success(data): + complete?(.success(data)) + case let .failure(err): + print(err) + complete?(.failure(.networkFail)) + } + } + + addWbiSign(method: method, url: url, parameters: parameters) { wbiSign in + if let wbiSign { + session.request(wbiSign, + method: method, + encoding: URLEncoding.default, + headers: afheaders) + .responseData(completionHandler: completionHandler) + } else { + session.request(url, + method: method, + parameters: parameters, + encoding: URLEncoding.default, + headers: afheaders) + .responseData(completionHandler: completionHandler) + } + } + } + + private static func addWbiSign(method: HTTPMethod = .get, + url: URLConvertible, + parameters: Parameters = [:], + onComplete: @escaping (String?) -> Void) + { + do { + let urlObj = try url.asURL() + if urlObj.absoluteString.contains("/wbi/") == true, method == .get { + var request = URLRequest(url: urlObj) + request.method = .get + request = try URLEncoding.queryString.encode(request, with: parameters) + if let query = request.url?.query(percentEncoded: true) { + biliWbiSign(param: query) { res in + if let res { + let urlString = urlObj.absoluteString + "?" + res + onComplete(urlString) + return + } else { + onComplete(nil) + } + } + return } } + onComplete(nil) + } catch { + onComplete(nil) + } } static func requestJSON(method: HTTPMethod = .get, @@ -507,6 +550,104 @@ extension WebRequest { } } +// MARK: - Wbi + +extension WebRequest { + // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md#Swift + static func biliWbiSign(param: String, completion: @escaping (String?) -> Void) { + func getMixinKey(orig: String) -> String { + return String(mixinKeyEncTab.map { orig[orig.index(orig.startIndex, offsetBy: $0)] }.prefix(32)) + } + + func encWbi(params: [String: Any], imgKey: String, subKey: String) -> [String: Any] { + var params = params + let mixinKey = getMixinKey(orig: imgKey + subKey) + let currTime = round(Date().timeIntervalSince1970) + params["wts"] = currTime + params = params.sorted { $0.key < $1.key }.reduce(into: [:]) { $0[$1.key] = $1.value } + params = params.mapValues { String(describing: $0).filter { !"!'()*".contains($0) } } + let query = params.map { "\($0.key)=\($0.value)" }.joined(separator: "&") + let wbiSign = calculateMD5(string: query + mixinKey) + params["w_rid"] = wbiSign + return params + } + + func getWbiKeys(completion: @escaping (Result<(imgKey: String, subKey: String), Error>) -> Void) { + class Cache { + var imgKey: String? + var subKey: String? + var lastUpdate: Date? + + static let shared = Cache() + } + + if let imgKey = Cache.shared.imgKey, let subKey = Cache.shared.subKey, let lastUpdate = Cache.shared.lastUpdate, Date().timeIntervalSince(lastUpdate) < 60 * 60 * 12 { + completion(.success((imgKey, subKey))) + return + } + + let headers: HTTPHeaders = [ + "User-Agent": Keys.userAgent, + "Referer": Keys.referer, + ] + + AF.request("https://api.bilibili.com/x/web-interface/nav", headers: headers).responseData { response in + switch response.result { + case let .success(value): + let json = JSON(value) + let imgURL = json["data"]["wbi_img"]["img_url"].string ?? "" + let subURL = json["data"]["wbi_img"]["sub_url"].string ?? "" + let imgKey = imgURL.components(separatedBy: "/").last?.components(separatedBy: ".").first ?? "" + let subKey = subURL.components(separatedBy: "/").last?.components(separatedBy: ".").first ?? "" + Cache.shared.imgKey = imgKey + Cache.shared.subKey = subKey + Cache.shared.lastUpdate = Date() + completion(.success((imgKey, subKey))) + case let .failure(error): + completion(.failure(error)) + } + } + } + + func calculateMD5(string: String) -> String { + let data = Data(string.utf8) + var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) + _ = data.withUnsafeBytes { + CC_MD5($0.baseAddress, CC_LONG(data.count), &digest) + } + return digest.map { String(format: "%02hhx", $0) }.joined() + } + + let mixinKeyEncTab = [ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, + 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, + 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, + 36, 20, 34, 44, 52, + ] + + getWbiKeys { result in + switch result { + case let .success(keys): + let spdParam = param.components(separatedBy: "&") + var spdDicParam = [String: String]() + spdParam.forEach { pair in + let components = pair.components(separatedBy: "=") + if components.count == 2 { + spdDicParam[components[0]] = components[1] + } + } + + let signedParams = encWbi(params: spdDicParam, imgKey: keys.imgKey, subKey: keys.subKey) + let query = signedParams.map { "\($0.key)=\($0.value)" }.joined(separator: "&") + completion(query) + case let .failure(error): + print("Error getting keys: \(error)") + completion(nil) + } + } + } +} + struct HistoryData: DisplayData, Codable { struct HistoryPage: Codable, Hashable { let cid: Int