From aa9798b055792e7fa9db0a1eed1664cb6968e6fa Mon Sep 17 00:00:00 2001 From: xjbeta Date: Tue, 27 Aug 2024 13:36:04 +0800 Subject: [PATCH 1/6] fix: douyin uhd --- IINA+/Utils/VideoDecoder/DouYin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IINA+/Utils/VideoDecoder/DouYin.swift b/IINA+/Utils/VideoDecoder/DouYin.swift index cf26b90..1de791a 100644 --- a/IINA+/Utils/VideoDecoder/DouYin.swift +++ b/IINA+/Utils/VideoDecoder/DouYin.swift @@ -552,7 +552,7 @@ struct DouYinEnterData: Unmarshaling { var stream = Stream(url: u) if let fn = URL(string: u)?.lastPathComponent, - let q = qualities.first(where: { fn.subString(from: "_", to: ".").contains($0.sdkKey) }), + let q = qualities.filter({ fn.subString(from: "_", to: ".").contains($0.sdkKey) }).max(by: { $0.level < $1.level }), !q.disable { stream.quality = 900 + q.level json.streams[q.name] = stream From d1d4d9ddd94e5ed165efe2df6fe58be574f5063b Mon Sep 17 00:00:00 2001 From: xjbeta Date: Tue, 27 Aug 2024 15:21:24 +0800 Subject: [PATCH 2/6] fix: douyin qualities --- IINA+/Utils/VideoDecoder/DouYin.swift | 40 +++++++++++++++------------ 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/IINA+/Utils/VideoDecoder/DouYin.swift b/IINA+/Utils/VideoDecoder/DouYin.swift index 1de791a..d67b805 100644 --- a/IINA+/Utils/VideoDecoder/DouYin.swift +++ b/IINA+/Utils/VideoDecoder/DouYin.swift @@ -529,14 +529,22 @@ struct DouYinEnterData: Unmarshaling { isLiving = status == 2 roomId = try object.value(for: "id_str") - - urls = (try? object.value(for: "stream_url.flv_pull_url")) ?? [:] + qualities = (try? object.value(for: "stream_url.live_core_sdk_data.pull_data.options.qualities")) ?? [] + guard let streamData: String = try? object.value(for: "stream_url.live_core_sdk_data.pull_data.stream_data"), + let data = streamData.data(using: .utf8) else { +// #warning("FULL_HD1 only hls") + urls = (try? object.value(for: "stream_url.flv_pull_url")) ?? [:] + return + } + let jsonObj: JSONObject = try JSONParser.JSONObjectWithData(data) - #warning("FULL_HD1 only hls") - // https://live.douyin.com/208823316033 - // let hlsUrls: [String: String] = try object.value(for: "stream_url.hls_pull_url_map") + var urls = [String: String]() + qualities.forEach { q in + urls[q.sdkKey] = try? jsonObj.value(for: "data.\(q.sdkKey).main.flv") + } + self.urls = urls } func write(to yougetJson: YouGetJSON) -> YouGetJSON { @@ -548,14 +556,19 @@ struct DouYinEnterData: Unmarshaling { }.sorted { v0, v1 in v0.0 < v1.0 }.enumerated().forEach { - let u = $0.element.1 + let ku = $0.element + let u = ku.1 var stream = Stream(url: u) - if let fn = URL(string: u)?.lastPathComponent, - let q = qualities.filter({ fn.subString(from: "_", to: ".").contains($0.sdkKey) }).max(by: { $0.level < $1.level }), + if let q = qualities.first(where: { $0.sdkKey == ku.0 }), !q.disable { stream.quality = 900 + q.level json.streams[q.name] = stream + } else if let fn = URL(string: u)?.lastPathComponent, + let q = qualities.filter({ fn.subString(from: "_", to: ".").contains($0.sdkKey == "origin" ? "or" : $0.sdkKey) }).max(by: { $0.level < $1.level }), + !q.disable { + stream.quality = 900 + q.level + json.streams[q.name] = stream } else { stream.quality = 666 - $0.offset json.streams[$0.element.0] = stream @@ -574,16 +587,9 @@ struct DouYinEnterData: Unmarshaling { init(object: MarshaledObject) throws { level = try object.value(for: "level") - let sk: String = try object.value(for: "sdk_key") + sdkKey = try object.value(for: "sdk_key") disable = try object.value(for: "disable") - - if sk == "origin" { - name = "原画" - sdkKey = "or" - } else { - name = try object.value(for: "name") - sdkKey = sk - } + name = try object.value(for: "name") } } From 4c1d98127e4dfafe5349cd8d8505406c74177914 Mon Sep 17 00:00:00 2001 From: xjbeta Date: Tue, 27 Aug 2024 21:26:01 +0800 Subject: [PATCH 3/6] fix: douyin qualities --- IINA+/Utils/VideoDecoder/DouYin.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/IINA+/Utils/VideoDecoder/DouYin.swift b/IINA+/Utils/VideoDecoder/DouYin.swift index d67b805..d30688e 100644 --- a/IINA+/Utils/VideoDecoder/DouYin.swift +++ b/IINA+/Utils/VideoDecoder/DouYin.swift @@ -540,6 +540,13 @@ struct DouYinEnterData: Unmarshaling { } let jsonObj: JSONObject = try JSONParser.JSONObjectWithData(data) + qualities = [ + .init(name: "原画", level: 4, sdkKey: "origin"), + .init(name: "超清", level: 3, sdkKey: "hd"), + .init(name: "高清", level: 2, sdkKey: "sd"), + .init(name: "标清", level: 1, sdkKey: "ld"), + ] + var urls = [String: String]() qualities.forEach { q in urls[q.sdkKey] = try? jsonObj.value(for: "data.\(q.sdkKey).main.flv") @@ -585,13 +592,24 @@ struct DouYinEnterData: Unmarshaling { let sdkKey: String let disable: Bool + init(name: String, level: Int, sdkKey: String, disable: Bool = false) { + self.name = name + self.level = level + self.sdkKey = sdkKey + self.disable = disable + } + init(object: MarshaledObject) throws { level = try object.value(for: "level") sdkKey = try object.value(for: "sdk_key") disable = try object.value(for: "disable") + if sdkKey == "origin" { + name = "原画" + } else { name = try object.value(for: "name") } } + } init(object: MarshaledObject) throws { From 3e2267065071d648e006024d6a98c285a7705580 Mon Sep 17 00:00:00 2001 From: xjbeta Date: Wed, 28 Aug 2024 20:16:19 +0800 Subject: [PATCH 4/6] refactor: douyin cookies --- IINA+.xcodeproj/project.pbxproj | 12 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- IINA+/Utils/Danmaku/DouYinDM.swift | 16 +- IINA+/Utils/Extensions/TaskExtension.swift | 19 + IINA+/Utils/VideoDecoder/DouYin.swift | 311 +-------------- .../VideoDecoder/DouyinCookiesManager.swift | 355 ++++++++++++++++-- IINA+/Views/Identifiers.swift | 4 - 7 files changed, 377 insertions(+), 342 deletions(-) create mode 100644 IINA+/Utils/Extensions/TaskExtension.swift diff --git a/IINA+.xcodeproj/project.pbxproj b/IINA+.xcodeproj/project.pbxproj index a83a882..b45bb34 100644 --- a/IINA+.xcodeproj/project.pbxproj +++ b/IINA+.xcodeproj/project.pbxproj @@ -25,9 +25,11 @@ 011E4D3321E99CA400997633 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011E4D3221E99CA300997633 /* Log.swift */; }; 0120934227A40869002C7FD3 /* JSPlayerWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0120934127A40869002C7FD3 /* JSPlayerWindowController.swift */; }; 0120934427A4F632002C7FD3 /* JSPlayerWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0120934327A4F632002C7FD3 /* JSPlayerWebView.swift */; }; + 0120FDBF2C725E9800526079 /* DouyinCookiesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0120FDBE2C725E9800526079 /* DouyinCookiesManager.swift */; }; 01260171211948BF00C9C639 /* PreferencesTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01260170211948BF00C9C639 /* PreferencesTabViewController.swift */; }; 012601742119FBF400C9C639 /* BilibiliCardTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012601732119FBF400C9C639 /* BilibiliCardTableCellView.swift */; }; 01260178211ABD3B00C9C639 /* VideoViewsFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01260177211ABD3B00C9C639 /* VideoViewsFormatter.swift */; }; + 012738C12C731EEB0031E643 /* TaskExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012738C02C731EEB0031E643 /* TaskExtension.swift */; }; 0132B2E32123D05E001EB7DC /* BilibiliCardProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0132B2E22123D05E001EB7DC /* BilibiliCardProgressView.swift */; }; 0132B2E52123D68D001EB7DC /* BilibiliCardImageBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0132B2E42123D68D001EB7DC /* BilibiliCardImageBoxView.swift */; }; 013850FA214EA2AA003817CE /* huya.js in Resources */ = {isa = PBXBuildFile; fileRef = 013850F9214EA2AA003817CE /* huya.js */; }; @@ -46,7 +48,6 @@ 01479CD7210B35480046AAAD /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01479CD6210B35480046AAAD /* MainMenu.swift */; }; 014B447D210069FF00E7AA6A /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014B447B210069FF00E7AA6A /* Bookmark.swift */; }; 014B447E210069FF00E7AA6A /* BookmarkExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014B447C210069FF00E7AA6A /* BookmarkExtension.swift */; }; - 0153BFF12C3023F100AF5871 /* DouyinCookiesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0153BFF02C3023F100AF5871 /* DouyinCookiesManager.swift */; }; 015C19DC218B0D4F003B2F3A /* VideoGetStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015C19DB218B0D4F003B2F3A /* VideoGetStructs.swift */; }; 015C19F7218FC9A8003B2F3A /* Block-List-Plus.xml in Resources */ = {isa = PBXBuildFile; fileRef = 015C19F5218FC9A8003B2F3A /* Block-List-Plus.xml */; }; 015C19F8218FC9A8003B2F3A /* Block-List-Basic.xml in Resources */ = {isa = PBXBuildFile; fileRef = 015C19F6218FC9A8003B2F3A /* Block-List-Basic.xml */; }; @@ -198,9 +199,11 @@ 0120932A27A3B441002C7FD3 /* flvplayer.htm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = flvplayer.htm; sourceTree = ""; }; 0120934127A40869002C7FD3 /* JSPlayerWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSPlayerWindowController.swift; sourceTree = ""; }; 0120934327A4F632002C7FD3 /* JSPlayerWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSPlayerWebView.swift; sourceTree = ""; }; + 0120FDBE2C725E9800526079 /* DouyinCookiesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DouyinCookiesManager.swift; sourceTree = ""; }; 01260170211948BF00C9C639 /* PreferencesTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTabViewController.swift; sourceTree = ""; }; 012601732119FBF400C9C639 /* BilibiliCardTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BilibiliCardTableCellView.swift; sourceTree = ""; }; 01260177211ABD3B00C9C639 /* VideoViewsFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoViewsFormatter.swift; sourceTree = ""; }; + 012738C02C731EEB0031E643 /* TaskExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskExtension.swift; sourceTree = ""; }; 0132B2E22123D05E001EB7DC /* BilibiliCardProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BilibiliCardProgressView.swift; sourceTree = ""; }; 0132B2E42123D68D001EB7DC /* BilibiliCardImageBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BilibiliCardImageBoxView.swift; sourceTree = ""; }; 013850F9214EA2AA003817CE /* huya.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = huya.js; sourceTree = ""; }; @@ -219,7 +222,6 @@ 014B447B210069FF00E7AA6A /* Bookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bookmark.swift; sourceTree = ""; }; 014B447C210069FF00E7AA6A /* BookmarkExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkExtension.swift; sourceTree = ""; }; 014FD3A42115CB3000F05399 /* Bookmark v0.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Bookmark v0.2.xcdatamodel"; sourceTree = ""; }; - 0153BFF02C3023F100AF5871 /* DouyinCookiesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DouyinCookiesManager.swift; sourceTree = ""; }; 015C19DB218B0D4F003B2F3A /* VideoGetStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoGetStructs.swift; sourceTree = ""; }; 015C19F5218FC9A8003B2F3A /* Block-List-Plus.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "Block-List-Plus.xml"; sourceTree = ""; }; 015C19F6218FC9A8003B2F3A /* Block-List-Basic.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "Block-List-Basic.xml"; sourceTree = ""; }; @@ -485,7 +487,7 @@ 0199489D2742852E00C71708 /* SupportSites.swift */, 018F4F692812D95C0045B67C /* CC163.swift */, 01892C7727C0CDA400494AFD /* DouYin.swift */, - 0153BFF02C3023F100AF5871 /* DouyinCookiesManager.swift */, + 0120FDBE2C725E9800526079 /* DouyinCookiesManager.swift */, 018F4F652812C3EB0045B67C /* Douyu.swift */, 018F4F6028126FFD0045B67C /* Huya.swift */, 01A183DA2B789728006FA874 /* HuyaUrl.swift */, @@ -596,6 +598,7 @@ 01942A3527C49D850092FA5A /* WKWebViewExtension.swift */, 01010124211DBC27002F0F7F /* StringExtension.swift */, 0184D7112B2025FF00C7901A /* DateExtension.swift */, + 012738C02C731EEB0031E643 /* TaskExtension.swift */, ); path = Extensions; sourceTree = ""; @@ -778,12 +781,14 @@ 0189533F279FBC470011BAAC /* ColorPickButton.swift in Sources */, 01DCD2F9259710D800D286C8 /* BilibiliDMMessage.swift in Sources */, 01A183DB2B789728006FA874 /* HuyaUrl.swift in Sources */, + 012738C12C731EEB0031E643 /* TaskExtension.swift in Sources */, 010F0F1920FE4F0900F33553 /* DataModel.xcdatamodeld in Sources */, 0120934227A40869002C7FD3 /* JSPlayerWindowController.swift in Sources */, 013CE8F62185D992000271FB /* HttpServer.swift in Sources */, 016792E6212BDEE5003517A7 /* SelectVideoCollectionViewItem.swift in Sources */, 0101013921200DC5002F0F7F /* LiveUrlTableCellView.swift in Sources */, 018F4F6A2812D95C0045B67C /* CC163.swift in Sources */, + 0120FDBF2C725E9800526079 /* DouyinCookiesManager.swift in Sources */, 01C338B528E47B3F004CC0B8 /* MBGA.swift in Sources */, 01479CD3210AF5F40046AAAD /* DataManager.swift in Sources */, 01398985210F27A600B7042F /* PreferencesWindowController.swift in Sources */, @@ -853,7 +858,6 @@ 016D772427B0CD4300358E2F /* WaitTimer.swift in Sources */, 01E6A09C211AD62800C6EF98 /* VideoDurationFormatter.swift in Sources */, 018739EE2852135200156F3F /* QQLive.swift in Sources */, - 0153BFF12C3023F100AF5871 /* DouyinCookiesManager.swift in Sources */, 0169098021CF472F008CDF0E /* SideBarImageView.swift in Sources */, 0132B2E32123D05E001EB7DC /* BilibiliCardProgressView.swift in Sources */, 016792E1212BBB43003517A7 /* SelectVideoViewController.swift in Sources */, diff --git a/IINA+.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/IINA+.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0e4c1cf..a494452 100644 --- a/IINA+.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/IINA+.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9446deb32a3da7eb6f718bd9d122561507cc76295435ffdb46184cca0f53082f", + "originHash" : "002db082d67dd04ceef35a0e7fc98dc59d5b2928b472f339c49f8bce81f0f426", "pins" : [ { "identity" : "alamofire", diff --git a/IINA+/Utils/Danmaku/DouYinDM.swift b/IINA+/Utils/Danmaku/DouYinDM.swift index 45cf3e3..f1b868b 100644 --- a/IINA+/Utils/Danmaku/DouYinDM.swift +++ b/IINA+/Utils/Danmaku/DouYinDM.swift @@ -13,18 +13,15 @@ class DouYinDM: NSObject { var url = "" let douyin = Processes.shared.videoDecoder.douyin - var ua: String { - douyin.douyinUA - } var privateKeys: [String] { - douyin.privateKeys + douyin.cookiesManager.privateKeys } private var webView = WKWebView() var requestPrepared: ((URLRequest) -> Void)? - func initWS(_ roomId: String, cookies: [String: String]) async throws -> URLRequest { + func initWS(_ roomId: String, cookies: [String: String], ua: String) async throws -> URLRequest { let s = "bGl2ZV9pZD0xLGFpZD02MzgzLHZlcnNpb25fY29kZT0xODA4MDAsd2ViY2FzdF9zZGtfdmVyc2lvbj0xLjMuMCxyb29tX2lkPQ==".base64Decode() + roomId + "LHN1Yl9yb29tX2lkPSxzdWJfY2hhbm5lbF9pZD0sZGlkX3J1bGU9Myx1c2VyX3VuaXF1ZV9pZD0sZGV2aWNlX3BsYXRmb3JtPXdlYixkZXZpY2VfdHlwZT0sYWM9LGlkZW50aXR5PWF1ZGllbmNl".base64Decode() @@ -56,7 +53,7 @@ class DouYinDM: NSObject { req.setValue(cookieString, forHTTPHeaderField: "Cookie") req.setValue("https://live.douyin.com", forHTTPHeaderField: "referer") - req.setValue(self.ua, forHTTPHeaderField: "User-Agent") + req.setValue(ua, forHTTPHeaderField: "User-Agent") return req } @@ -89,7 +86,7 @@ class DouYinDM: NSObject { func prepareCookies() async { let storageDic = await MainActor.run { - return douyin.storageDic + return douyin.cookiesManager.storageDic } let kvs = [ @@ -112,10 +109,11 @@ class DouYinDM: NSObject { Task { do { let rid = try await getRoomId() - let cookies = await douyin.cookiesManager.cookies + let cookies = try await douyin.cookiesManager.cookies() + let ua = await douyin.cookiesManager.douyinUA() await prepareCookies() - let req = try await initWS(rid, cookies: cookies) + let req = try await initWS(rid, cookies: cookies, ua: ua) await MainActor.run { self.requestPrepared?(req) diff --git a/IINA+/Utils/Extensions/TaskExtension.swift b/IINA+/Utils/Extensions/TaskExtension.swift new file mode 100644 index 0000000..f56cd8e --- /dev/null +++ b/IINA+/Utils/Extensions/TaskExtension.swift @@ -0,0 +1,19 @@ +// +// TaskExtension.swift +// IINA+ +// +// Created by xjbeta on 2024/8/19. +// Copyright © 2024 xjbeta. All rights reserved. +// + +import Cocoa + +extension Task where Success == Never, Failure == Never { + public static func sleep(seconds duration: UInt64) async throws { + try await sleep(nanoseconds: duration * 1_000_000_000) + } + + public static func sleep(milliseconds duration: UInt64) async throws { + try await sleep(nanoseconds: duration * 1_000_000) + } +} diff --git a/IINA+/Utils/VideoDecoder/DouYin.swift b/IINA+/Utils/VideoDecoder/DouYin.swift index d30688e..75224c0 100644 --- a/IINA+/Utils/VideoDecoder/DouYin.swift +++ b/IINA+/Utils/VideoDecoder/DouYin.swift @@ -15,76 +15,10 @@ import Marshal class DouYin: NSObject, SupportSiteProtocol { // MARK: - DY Init - var webView: WKWebView? - - var dyFinishNotification: NSObjectProtocol? - - let douyinEmptyURL = URL(string: "https://live.douyin.com/1")! - var douyinUA = "" - let privateKeys = [ - "X2J5dGVkX3BhcmFtX3N3", - "dHRfc2NpZA==", - "Ynl0ZWRfYWNyYXdsZXI=", - "WC1Cb2d1cw==", - "X3NpZ25hdHVyZQ==" - ] - - @MainActor - lazy var webviewConfig: WKWebViewConfiguration = { - // https://gist.github.com/genecyber/e4a5f7c6f92eaef9ccb5 - let script = """ -function addXMLRequestCallback(callback) { - var oldSend, i; - if (XMLHttpRequest.callbacks) { - XMLHttpRequest.callbacks.push(callback); - } else { - XMLHttpRequest.callbacks = [callback]; - oldSend = XMLHttpRequest.prototype.send; - XMLHttpRequest.prototype.send = function () { - for (i = 0; i < XMLHttpRequest.callbacks.length; i++) { - XMLHttpRequest.callbacks[i](this); - } - oldSend.apply(this, arguments); - } - } -} - -addXMLRequestCallback(function (xhr) { - window.webkit.messageHandlers.fetch.postMessage(xhr._url); -}); -""" - - let scriptInjection = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: false) - - let config = WKWebViewConfiguration() - let contentController = WKUserContentController() - contentController.add(self, name: "fetch") - contentController.addUserScript(scriptInjection) - config.userContentController = contentController - - return config - }() - - enum DYState { - case none - case preparing - case checking - case finish - } - - @MainActor - var storageDic = [String: String]() - - @MainActor - private var cookiesTaskState: DYState = .none - - lazy var cookiesManager = DouyinCookiesManager(prepareArgs: prepareArgs) - - private var invalidCookiesCount = 0 + lazy var cookiesManager = DouyinCookiesManager() func liveInfo(_ url: String) async throws -> any LiveInfo { - let _ = try await cookiesManager.initCookies() let info = try await getEnterContent(url) return info } @@ -106,13 +40,8 @@ addXMLRequestCallback(function (xhr) { func getEnterContent(_ url: String) async throws -> LiveInfo { - let cookieString = await cookiesManager.cookiesString - - let headers = HTTPHeaders([ - "User-Agent": douyinUA, - "referer": url, - "Cookie": cookieString - ]) + var headers = try await cookiesManager.headers() + headers.add(name: "referer", value: url) guard let pc = NSURL(string: url)?.pathComponents, pc.count >= 2, @@ -131,7 +60,7 @@ addXMLRequestCallback(function (xhr) { }() let u = "https://live.douyin.com/webcast/room/web/enter/?aid=6383&app_name=douyin_web&live_id=1&device_platform=web&language=en-US&cookie_enabled=true&browser_language=en-US&browser_platform=Mac&browser_name=Safari&browser_version=16&web_rid=\(rid)&enter_source=&is_need_double_stream=true" - + let data = try await AF.request(u, headers: headers).serializingData().value let jsonObj: JSONObject = try JSONParser.JSONObjectWithData(data) let enterData = try DouYinEnterData(object: jsonObj) @@ -145,25 +74,13 @@ addXMLRequestCallback(function (xhr) { } } - + /* func getContent(_ url: String) async throws -> LiveInfo { - let cookieString = await cookiesManager.cookiesString - - let headers = HTTPHeaders([ - "User-Agent": douyinUA, - "referer": url, - "Cookie": cookieString - ]) + var headers = try await cookiesManager.headers() + headers.add(name: "referer", value: url) let text = try await AF.request(url, headers: headers).serializingString().value guard let json = getJSON(text) else { - self.invalidCookiesCount += 1 - if self.invalidCookiesCount == 5 { - self.invalidCookiesCount = 0 - await cookiesManager.removeAll() - - Log("Reload Douyin Cookies") - } throw VideoGetError.notFountData } @@ -227,215 +144,7 @@ addXMLRequestCallback(function (xhr) { return re.first?.data(using: .utf8) } } - - - @MainActor - func prepareArgs() async throws -> [String: String] { - deleteDouYinCookies() - - let config = webviewConfig - storageDic.removeAll() - cookiesTaskState = .preparing - webView = WKWebView(frame: .zero, configuration: config) - guard let webView else { throw VideoGetError.douyuSignError } - Log("DouYin Cookies start.") - -#if DEBUG - if #available(macOS 13.3, *) { - webView.isInspectable = true - } -#endif - - async let noti = Task { - let _ = await webcastUpdatedNotification() - await MainActor.run { - cookiesTaskState = .checking - } - } - - webView.load(.init(url: douyinEmptyURL)) - - var loadingCount = 0 - while loadingCount >= 0 { - loadingCount += 1 - try await Task.sleep(nanoseconds: 330_000_000) - let isLoading = webView.isLoading - guard !isLoading, - let title = try await webView.evaluateJavaScriptAsync("document.title") as? String else { - continue - } - - if loadingCount >= (3 * 120) { - Log("DouYin Cookies timeout, check cookies.") - loadingCount = -1 - break - } else if cookiesTaskState != .preparing { - Log("DouYin Cookies webcastUpdated.") - loadingCount = -2 - break - } else if title.contains("抖音直播") { - Log("Douyin cookies web load finish, \(title).") - loadingCount = -3 - break - } else if title.contains("验证") { - Log("Douyin cookies web reload.") - await self.deleteCookies() - webView.load(.init(url: self.douyinEmptyURL)) - } - } - - if loadingCount < -1 { - let _ = await noti - } - - cookiesTaskState = .checking - Log("Douyin cookies checking.") - - let cookies = try await loadCookies() - cookiesTaskState = .finish - Log("Douyin cookies finish.") - - await deinitWebView() - - return cookies - } - - - - func loadCookies() async throws -> [String: String] { - guard let webview = webView else { - throw VideoGetError.douyuSignError - } - let cid = "dHRjaWQ=".base64Decode() - - let allCookies = await getAllWKCookies() - - Log("Douyin getAllWKCookies") - var cookies = [String: String]() - - allCookies.filter { - $0.domain.contains("douyin") - }.forEach { - cookies[$0.name] = $0.value - } - - let re1 = try await webview.evaluateJavaScriptAsync("localStorage.\(cid)") - let re2 = try await webview.evaluateJavaScriptAsync("window.navigator.userAgent") - - - cookies[cid] = re1 as? String - guard let ua = re2 as? String else { - throw CookiesError.invalid - } - douyinUA = ua - - let re = try await webview.evaluateJavaScriptAsync("localStorage.\(self.privateKeys[0].base64Decode()) + ',' + localStorage.\(self.privateKeys[1].base64Decode())") - - Log("Douyin privateKeys") - - guard let values = (re as? String)?.split(separator: ",", maxSplits: 1).map(String.init) else { - throw VideoGetError.douyuSignError - } - - await MainActor.run { - storageDic = [ - self.privateKeys[0].base64Decode(): values[0], - self.privateKeys[1].base64Decode(): values[1] - ] - } - - await cookiesManager.setCookies(cookies) - - let info = try await getEnterContent(douyinEmptyURL.absoluteString) - - Log("Douyin test info \(info.title)") - - return cookies - } - - @MainActor - func deinitWebView() async { - Log("Douyin deinit webview") - - webView?.stopLoading() - webView?.configuration.userContentController.removeScriptMessageHandler(forName: "fetch") - webView?.removeFromSuperview() - webView = nil - } - - func deleteCookies() async { - let cookies = await getAllWKCookies() - - await withTaskGroup(of: Int.self) { group in - cookies.forEach { c in - group.addTask { - await self.deleteWKCookie(c) - return 0 - } - } - } - - deleteDouYinCookies() - } - - func deleteDouYinCookies() { - HTTPCookieStorage.shared.cookies?.filter { - $0.domain.contains("douyin") - }.forEach(HTTPCookieStorage.shared.deleteCookie) - } - - func webcastUpdatedNotification() async -> Notification { - await withCheckedContinuation { continuation in - dyFinishNotification = NotificationCenter.default.addObserver(forName: .douyinWebcastUpdated, object: nil, queue: nil) { n in - guard let noti = self.dyFinishNotification else { - return - } - NotificationCenter.default.removeObserver(noti) - self.dyFinishNotification = nil - continuation.resume(returning: n) - } - } - } - - - @MainActor - func getAllWKCookies() async -> [HTTPCookie] { - let all = await WKWebsiteDataStore.default().httpCookieStore.allCookies() - return all.filter({ $0.domain.contains("douyin") }) - } - - func deleteWKCookie(_ cookie: HTTPCookie) async { - await WKWebsiteDataStore.default().httpCookieStore.deleteCookie(cookie) - } - - deinit { -// prepareTask = nil - } - - enum CookiesError: Error { - case invalid, waintingForCookies - } -} - -extension DouYin: WKScriptMessageHandler { - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - guard let msg = message.body as? String else { return } - - func post() { - NotificationCenter.default.post(name: .douyinWebcastUpdated, object: nil) - } - - if msg.contains("webcast/im/push/v2") { - post() - } else if msg.contains("live.douyin.com/webcast/im/fetch"), - msg.contains("last_rtt=-1") { - post() - } else if msg.contains("live.douyin.com/aweme/v1/web/emoji/list") { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - post() - } - } - } + */ } struct DouYinInfo: Unmarshaling, LiveInfo { @@ -606,10 +315,10 @@ struct DouYinEnterData: Unmarshaling { if sdkKey == "origin" { name = "原画" } else { - name = try object.value(for: "name") + name = try object.value(for: "name") + } } } - } init(object: MarshaledObject) throws { diff --git a/IINA+/Utils/VideoDecoder/DouyinCookiesManager.swift b/IINA+/Utils/VideoDecoder/DouyinCookiesManager.swift index fc938dd..87068df 100644 --- a/IINA+/Utils/VideoDecoder/DouyinCookiesManager.swift +++ b/IINA+/Utils/VideoDecoder/DouyinCookiesManager.swift @@ -2,54 +2,363 @@ // DouyinCookiesManager.swift // IINA+ // -// Created by xjbeta on 2024/6/29. +// Created by xjbeta on 2024/8/19. // Copyright © 2024 xjbeta. All rights reserved. // import Cocoa +import WebKit +import Alamofire import Semaphore -actor DouyinCookiesManager { +class DouyinCookiesManager: NSObject { + private var webView: WKWebView? + + private let douyinEmptyURL = URL(string: "https://live.douyin.com/1")! + + private lazy var webviewConfig: WKWebViewConfiguration = { + // https://gist.github.com/genecyber/e4a5f7c6f92eaef9ccb5 + let script = """ +function addXMLRequestCallback(callback) { + var oldSend, i; + if (XMLHttpRequest.callbacks) { + XMLHttpRequest.callbacks.push(callback); + } else { + XMLHttpRequest.callbacks = [callback]; + oldSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.send = function () { + for (i = 0; i < XMLHttpRequest.callbacks.length; i++) { + XMLHttpRequest.callbacks[i](this); + } + oldSend.apply(this, arguments); + } + } +} +addXMLRequestCallback(function (xhr) { + window.webkit.messageHandlers.fetch.postMessage(xhr._url); +}); +""" + + let scriptInjection = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: false) + + let config = WKWebViewConfiguration() + let contentController = WKUserContentController() + contentController.add(self, name: "fetch") + contentController.addUserScript(scriptInjection) + config.userContentController = contentController + + return config + }() + + private var douyinWebcastUpdated: (() -> Void)? + + @MainActor + private let semaphore = AsyncSemaphore(value: 1) + + @MainActor private var _cookies = [String: String]() - var cookies: [String: String] { - _cookies + + @MainActor + private var shouldRetry = false + + enum DYState { + case none + case preparing + case checking + case finish } - var cookiesString: String { - cookies.map { - "\($0.key)=\($0.value)" - }.joined(separator: ";") + enum CookiesError: Error { + case invalid, waintingForCookies, timeout, unknown } - private var refreshCookies: Task<[String: String], Error> - private let prepareArgs: (() async throws -> [String: String]) + let privateKeys = [ + "X2J5dGVkX3BhcmFtX3N3", + "dHRfc2NpZA==", + "Ynl0ZWRfYWNyYXdsZXI=", + "WC1Cb2d1cw==", + "X3NpZ25hdHVyZQ==" + ] - private let semaphore = AsyncSemaphore(value: 1) + private var douyinUA = "" + var storageDic = [String: String]() - init(prepareArgs: @escaping (() async throws -> [String: String])) { - self.prepareArgs = prepareArgs - - refreshCookies = Task { () throws -> [String: String] in - try await prepareArgs() - } + func headers() async throws -> HTTPHeaders { + let cookie = try await cookiesString() + return await HTTPHeaders([ + "User-Agent": douyinUA(), + "Cookie": cookie + ]) + } + + func cookiesString() async throws -> String { + try await cookies().map { + "\($0.key)=\($0.value)" + }.joined(separator: ";") } - func initCookies() async throws -> [String: String] { + @MainActor + func douyinUA() async -> String { + douyinUA + } + + func cookies() async throws -> [String: String] { await semaphore.wait() defer { semaphore.signal() } - if cookies.count > 0 { + if await shouldRetry { + Log("retry 60s") + await updateInternalCookies([:]) + try await Task.sleep(seconds: 60) + await updateRetry(false) + } + + if await _cookies.count > 0 { +// Log("cached cookies") + return await _cookies + } + + do { + let cookies = try await withThrowingTaskGroup(of: [String: String].self) { group in + group.addTask { + try await self.prepareCookies() + } + + group.addTask { + try await Task.sleep(seconds: 120) + Log("timeout") + throw CookiesError.timeout + } + let result = try await group.next()! + group.cancelAll() + return result + } return cookies + } catch { + Log("should retry, catch \(error)") + await updateRetry(true) + await deinitWebViewAsync() + throw error } - return try await refreshCookies.value } - func setCookies(_ cookies: [String: String]) async { + @MainActor + func updateInternalCookies(_ cookies: [String: String]) { _cookies = cookies } - func removeAll() async { - _cookies.removeAll() + @MainActor + func updateRetry(_ retry: Bool) { + self.shouldRetry = retry + } + + @MainActor + func prepareCookies() async throws -> [String: String] { + Log("start") + deleteDouYinCookies() + storageDic.removeAll() + + let config = webviewConfig + webView = WKWebView(frame: .zero, configuration: config) + guard let webView else { throw CookiesError.unknown } + +#if DEBUG + if #available(macOS 13.3, *) { + webView.isInspectable = true + } +#endif + var loadingCount = 0 + + douyinWebcastUpdated = { + Log("douyinWebcastUpdated") + loadingCount = -99 + self.douyinWebcastUpdated = nil + } + + webView.load(.init(url: douyinEmptyURL)) + Log("load") + + while loadingCount >= 0 { + loadingCount += 1 + try await Task.sleep(milliseconds: 500) +// Log("time") + let isLoading = webView.isLoading + guard !isLoading, + let title = try await webView.evaluateJavaScriptAsync("document.title") as? String else { + continue + } + + if loadingCount >= (2 * 125) { + Log("timeout, check cookies.") + deinitWebViewAsync() + throw CookiesError.timeout + } else if title.contains("抖音直播") { + Log("web load finish, \(title).") + loadingCount = -98 + break + } else if title.contains("验证") { + Log("web reload.") + await self.deleteCookies() + webView.load(.init(url: self.douyinEmptyURL)) + } + } + + Log("loadCookies") + let cookies = try await loadCookies() + + Log("finish.") + deinitWebViewAsync() + + return cookies + } + + @MainActor + func loadCookies() async throws -> [String: String] { + guard let webview = webView else { + throw VideoGetError.douyuSignError + } + let cid = "dHRjaWQ=".base64Decode() + + let allCookies = await getAllWKCookies() + + Log("getAllWKCookies") + var cookies = [String: String]() + + allCookies.filter { + $0.domain.contains("douyin") + }.forEach { + cookies[$0.name] = $0.value + } + + let re1 = try await webview.evaluateJavaScriptAsync("localStorage.\(cid)") + let re2 = try await webview.evaluateJavaScriptAsync("window.navigator.userAgent") + + cookies[cid] = re1 as? String + Log("cid \(re1 ?? ""), ua \(re2 ?? "")") + + guard let ua = re2 as? String else { + Log("nil userAgent") + throw CookiesError.invalid + } + + let re = try await webview.evaluateJavaScriptAsync("localStorage.\(self.privateKeys[0].base64Decode()) + ',' + localStorage.\(self.privateKeys[1].base64Decode())") + + Log("privateKeys") + + guard let values = (re as? String)?.split(separator: ",", maxSplits: 1).map(String.init) else { + throw VideoGetError.douyuSignError + } + + storageDic = [ + self.privateKeys[0].base64Decode(): values[0], + self.privateKeys[1].base64Decode(): values[1] + ] + + try await verifyCookies(cookies, ua: ua) + + douyinUA = ua + updateInternalCookies(cookies) + + return cookies + } + + func verifyCookies(_ cookies: [String: String], ua: String) async throws { + Log("verifyCookies") + + let cookie = cookies.map { + "\($0.key)=\($0.value)" + }.joined(separator: ";") + + var headers = HTTPHeaders([ + "User-Agent": ua, + "Cookie": cookie + ]) + + let u = "https://live.douyin.com/webcast/room/web/enter/?aid=6383&app_name=douyin_web&live_id=1&device_platform=web&language=en-US&cookie_enabled=true&browser_language=en-US&browser_platform=Mac&browser_name=Safari&browser_version=16&web_rid=1&enter_source=&is_need_double_stream=true" + + do { + let _ = try await AF.request(u, headers: headers).serializingData().value + } catch { + switch error { + case AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength): + await updateRetry(true) + throw CookiesError.invalid + default: + Log(error) + } + } + } + + @MainActor + func deleteCookies() async { + let cookies = await getAllWKCookies() + + await withTaskGroup(of: Int.self) { group in + cookies.forEach { c in + group.addTask { + await self.deleteWKCookie(c) + return 0 + } + } + } + + deleteDouYinCookies() + } + + @MainActor + func deleteDouYinCookies() { + HTTPCookieStorage.shared.cookies?.filter { + $0.domain.contains("douyin") + }.forEach(HTTPCookieStorage.shared.deleteCookie) + } + + @MainActor + func getAllWKCookies() async -> [HTTPCookie] { + let all = await WKWebsiteDataStore.default().httpCookieStore.allCookies() + return all.filter({ $0.domain.contains("douyin") }) + } + + @MainActor + func deleteWKCookie(_ cookie: HTTPCookie) async { + await WKWebsiteDataStore.default().httpCookieStore.deleteCookie(cookie) + } + + @MainActor + func deinitWebViewAsync() { + deinitWebView() + } + + func deinitWebView() { + Log("Douyin deinit webview") + douyinWebcastUpdated = nil + webView?.stopLoading() + webView?.removeFromSuperview() + webView = nil + } + + deinit { + deinitWebView() + } +} + +extension DouyinCookiesManager: WKScriptMessageHandler { + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard let msg = message.body as? String else { return } + + func post() { + douyinWebcastUpdated?() + } + + if msg.contains("webcast/im/push/v2") { + post() + } else if msg.contains("live.douyin.com/webcast/im/fetch"), + msg.contains("last_rtt=-1") { + post() + } else if msg.contains("live.douyin.com/aweme/v1/web/emoji/list") { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + post() + } + } } } diff --git a/IINA+/Views/Identifiers.swift b/IINA+/Views/Identifiers.swift index 26a067a..074271d 100644 --- a/IINA+/Views/Identifiers.swift +++ b/IINA+/Views/Identifiers.swift @@ -39,8 +39,4 @@ extension Notification.Name { static let updateDanmakuWindow = Notification.Name("com.xjbeta.iina+.DanmakuWindow.Update") static let updateDanmukuFont = Notification.Name("com.xjbeta.iina+.DanmakuWindow.FontChanged") static let loadDanmaku = Notification.Name("com.xjbeta.iina+.LoadDanmaku") - - - static let douyinWebcastUpdated = Notification.Name("com.xjbeta.iina+.douyin.WebcastUpdated") - } From 914e93645bebb3cd04392513cb9394c63fe9b3bc Mon Sep 17 00:00:00 2001 From: xjbeta Date: Wed, 28 Aug 2024 20:34:58 +0800 Subject: [PATCH 5/6] misc: douyin cookies error --- IINA+/Utils/VideoDecoder/DouYin.swift | 29 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/IINA+/Utils/VideoDecoder/DouYin.swift b/IINA+/Utils/VideoDecoder/DouYin.swift index 75224c0..1571c41 100644 --- a/IINA+/Utils/VideoDecoder/DouYin.swift +++ b/IINA+/Utils/VideoDecoder/DouYin.swift @@ -61,16 +61,27 @@ class DouYin: NSObject, SupportSiteProtocol { let u = "https://live.douyin.com/webcast/room/web/enter/?aid=6383&app_name=douyin_web&live_id=1&device_platform=web&language=en-US&cookie_enabled=true&browser_language=en-US&browser_platform=Mac&browser_name=Safari&browser_version=16&web_rid=\(rid)&enter_source=&is_need_double_stream=true" - let data = try await AF.request(u, headers: headers).serializingData().value - let jsonObj: JSONObject = try JSONParser.JSONObjectWithData(data) - let enterData = try DouYinEnterData(object: jsonObj) - if let info = enterData.infos.first { - return info - } else if let info = try? DouYinEnterData2(object: jsonObj) { - return info - } else { - throw VideoGetError.notFountData + do { + let data = try await AF.request(u, headers: headers).serializingData().value + let jsonObj: JSONObject = try JSONParser.JSONObjectWithData(data) + let enterData = try DouYinEnterData(object: jsonObj) + + if let info = enterData.infos.first { + return info + } else if let info = try? DouYinEnterData2(object: jsonObj) { + return info + } else { + throw VideoGetError.notFountData + } + } catch { + switch error { + case AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength): + Log("douyin inputDataNilOrZeroLength") + default: + break + } + throw error } } From 1664880db26a62a747b0961ca7ab4fb39b17c0e7 Mon Sep 17 00:00:00 2001 From: xjbeta Date: Wed, 28 Aug 2024 20:36:21 +0800 Subject: [PATCH 6/6] chore: Bump Version to 0.8.3. --- IINA+.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IINA+.xcodeproj/project.pbxproj b/IINA+.xcodeproj/project.pbxproj index b45bb34..2f1a4da 100644 --- a/IINA+.xcodeproj/project.pbxproj +++ b/IINA+.xcodeproj/project.pbxproj @@ -1050,7 +1050,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 0.8.2; + MARKETING_VERSION = 0.8.3; PRODUCT_BUNDLE_IDENTIFIER = "com.xjbeta.iina-plus"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1077,7 +1077,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 0.8.2; + MARKETING_VERSION = 0.8.3; PRODUCT_BUNDLE_IDENTIFIER = "com.xjbeta.iina-plus"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "";