Ben před 11 měsíci
rodič
revize
8ca11e8d8e
4 změnil soubory, kde provedl 179 přidání a 8699 odebrání
  1. 0 8639
      js/base.js
  2. 114 46
      js/info.js
  3. 48 5
      js/main.swift
  4. 17 9
      js/test.js

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 8639
js/base.js


+ 114 - 46
js/info.js

@@ -1,5 +1,5 @@
 parseCodecs = (format) => {
-    const mimeType = format["mimeType"]
+    const mimeType = format['mimeType']
     if (!mimeType) {
         return {};
     }
@@ -50,6 +50,27 @@ parseCodecs = (format) => {
     return {};
 }
 
+parseSetCookie = (headers) => {
+    if (!headers) {
+        return ""
+    }
+    const setCookie = headers['Set-Cookie']
+    if (!setCookie) {
+        return ""
+    }
+    console.log(`setCookie: ${setCookie}`)
+    let result = 'PREF=hl=en&tz=UTC; SOCS=CAI; GPS=1; ';
+    for (const cookieName of ['YSC', 'VISITOR_INFO1_LIVE', 'VISITOR_PRIVACY_METADATA']) {
+        const regexp = new RegExp(`${cookieName}=([^;,]+)`)
+        const match = setCookie.match(regexp)
+        if (match && match.length === 2) {
+            const cookieValue = match[1]
+            result += `${cookieName}=${cookieValue}; `
+        }
+    }
+    return result;
+}
+
 request = async (method, url, data = null, headers = {}, platform) => {
     if (platform === "WEB") {
         url = url.replace("https://www.youtube.com", "http://127.0.0.1");
@@ -59,18 +80,28 @@ request = async (method, url, data = null, headers = {}, platform) => {
     console.log(`request method:${method}`)
     console.log(`request headers:${JSON.stringify((headers))}`)
     if (platform === "WEB") {
-        return fetch(url, {
-            "method": method,
-            "headers": headers,
-            "body": data
-        }).then(res => res.text())
+        const res = await fetch(url, {
+            'mode': 'cors',
+            'method': method,
+            'headers': headers,
+            'body': data
+        })
+        const resData = await res.text()
+        return Promise.resolve({
+            'data': resData,
+            'headers': res.headers
+        });
     }
     return new Promise((resolve, reject) => {
-        AF.request(url, method, data, headers, (data, err) => {
+        AF.request(url, method, data, headers, (data, headers, err) => {
             if (err) {
                 reject(err);
             } else {
-                resolve(data);
+                console.log(`响应头: ${headers}`);
+                resolve({
+                    'data': data,
+                    'headers': JSON.parse(headers)
+                });
             }
         });
     })
@@ -88,10 +119,10 @@ getDecipherFunction = (jsCode) => {
         return null;
     }
     let result = "";
-    const dependencyMatchs = match[0].match(/([$a-zA-Z0-9]+\.[$a-zA-Z0-9]+)/g)
+    const dependencyMatches = match[0].match(/([$a-zA-Z0-9]+\.[$a-zA-Z0-9]+)/g)
     const existDependencies = [];
-    if (dependencyMatchs && dependencyMatchs.length >= 1) {
-        for (let currentMatch of dependencyMatchs) {
+    if (dependencyMatches && dependencyMatches.length >= 1) {
+        for (let currentMatch of dependencyMatches) {
             const varName = currentMatch.split('.')[0];
             if (existDependencies.includes(varName)) {
                 continue
@@ -110,16 +141,17 @@ getDecipherFunction = (jsCode) => {
 
 const cache = {};
 extractJSSignatureFunction = async (baseJsUrl, platform) => {
-    console.log(`extract baseUrl: ${baseJsUrl}`);
     const cacheKey = `js:${baseJsUrl}`;
     if (cache[cacheKey]) {
         console.log(`from cache JSSignatureFunction: ${baseJsUrl}`);
         return cache[cacheKey];
     }
-    const baseContent = await request('GET', baseJsUrl, null, {
+    console.log(`extract baseUrl: ${baseJsUrl}`);
+    const baseContentResp = await request('GET', baseJsUrl, null, {
         'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36',
     }, platform);
-    const decipher = getDecipherFunction(baseContent);
+    const {data, headers} = baseContentResp;
+    const decipher = getDecipherFunction(data);
     if (decipher) {
         cache[cacheKey] = decipher;
     }
@@ -129,37 +161,41 @@ extractJSSignatureFunction = async (baseJsUrl, platform) => {
 getUrlFromSignature = async (signatureCipher, baseJsUrl, platform) => {
     const decipher = await extractJSSignatureFunction(baseJsUrl, platform);
     const searchParams = {}
-    for (const item of signatureCipher.split("&")) {
+    for (const item of signatureCipher.split('&')) {
         const [key, value] = item.split('=');
         searchParams[decodeURIComponent(key)] = decodeURIComponent(value);
     }
-    const [url, signature, sp] = [searchParams["url"], searchParams["s"], searchParams["sp"]];
+    const [url, signature, sp] = [searchParams['url'], searchParams['s'], searchParams['sp']];
     console.log(`signatureCipher=${signatureCipher}, url=${url}, signature=${signature}, sp=${sp}`)
     return `${url}&${sp}=${decipher(signature)}`;
 }
 
 detail = async (url, platform) => {
     try {
-        let html = await request('GET', `${url}&bpctr=9999999999&has_verified=1`, null, {
-            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
+        const htmlResp = await request('GET', `${url}&bpctr=9999999999&has_verified=1`, null, {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36',
+            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+            'Accept-Language': 'en-us,en;q=0.5',
+            'Sec-Fetch-Mode': 'navigate',
+            'Cookie': 'PREF=hl=en&tz=UTC; SOCS=CAI',
         }, platform);
+        let {data: html, headers: htmlHeaders} = htmlResp;
 
         let regex = /var ytInitialPlayerResponse\s*=\s*({.*?});/;
         let match = html.match(regex);
         if (!match || !match.length) {
-            console.log("can not found JSON: ytInitialPlayerResponse");
+            console.log('can not found JSON: ytInitialPlayerResponse');
             throw new Error('JSON not found: ytInitialPlayerResponse');
         }
         const ytInitialPlayerResponse = JSON.parse(match[1]);
-        console.log(ytInitialPlayerResponse);
-        const originVideoDetails = ytInitialPlayerResponse["videoDetails"];
+        const originVideoDetails = ytInitialPlayerResponse['videoDetails'];
         console.log(`videoDetails: ${JSON.stringify(originVideoDetails)}`);
         const thumbnails = []
-        for (const item of originVideoDetails["thumbnail"]["thumbnails"]) {
+        for (const item of originVideoDetails['thumbnail']['thumbnails']) {
             thumbnails.push({
-                "url": item["url"],
-                "width": item["width"] + "",
-                "height": item["height"] + ""
+                'url': item['url'],
+                'width': item['width'] + "",
+                'height': item['height'] + ""
             })
         }
 
@@ -167,13 +203,17 @@ detail = async (url, platform) => {
         // android
         try {
             const apiKey = 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39'
-            const data = {
+            const apiUrl = `https://www.youtube.com/youtubei/v1/player?key=${apiKey}&prettyPrint=false`;
+            const apiResp = await request('POST', apiUrl, JSON.stringify({
                 "context": {
                     "client": {
                         "clientName": "ANDROID",
                         "clientVersion": "19.09.37",
                         "androidSdkVersion": 30,
-                        "userAgent": "com.google.android.youtube/19.09.37 (Linux; U; Android 11) gzip",
+                        'userAgent': 'com.google.android.youtube/19.09.37 (Linux; U; Android 11) gzip',
+                        "hl": "en",
+                        "timeZone": "UTC",
+                        "utcOffsetMinutes": 0
                     }
                 },
                 'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
@@ -185,17 +225,24 @@ detail = async (url, platform) => {
                 "params": "CgIIAQ==",
                 "contentCheckOk": true,
                 "racyCheckOk": true
-            }
-            const apiUrl = `https://www.youtube.com/youtubei/v1/player?key=${apiKey}&prettyPrint=false`;
-            let apiResp = await request('POST', apiUrl, JSON.stringify(data), {
+            }), {
                 'X-YouTube-Client-Name': '5',
                 'X-YouTube-Client-Version': '19.09.3',
                 'User-Agent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
-                'Content-Type': 'application/json'
+                'Content-Type': 'application/json',
+                'Cookie': parseSetCookie(htmlHeaders)
             }, platform);
+            let {data: apiData, _} = apiResp;
             console.log(`android api result: ${JSON.stringify(apiResp)}`);
-            const res = JSON.parse(apiResp);
-            originFormats = originFormats.concat([].concat(res["streamingData"]["formats"]).concat(res["streamingData"]["adaptiveFormats"]));
+            const res = JSON.parse(apiData);
+            const currentFormats = [];
+            for (const format of [].concat(res["streamingData"]["formats"]).concat(res["streamingData"]["adaptiveFormats"])) {
+                if (format) {
+                    format["from"] = "android"
+                    currentFormats.push(format);
+                }
+            }
+            originFormats = originFormats.concat(currentFormats);
         } catch (e) {
             console.log(`can not found format android api error: ${e}`);
         }
@@ -203,13 +250,17 @@ detail = async (url, platform) => {
         // ios
         try {
             const apiKey = 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc'
-            const data = {
+            const apiUrl = `https://www.youtube.com/youtubei/v1/player?key=${apiKey}&prettyPrint=false`;
+            let apiResp = await request('POST', apiUrl, JSON.stringify({
                 "context": {
                     "client": {
                         'clientName': 'IOS',
                         'clientVersion': '19.09.3',
                         'deviceModel': 'iPhone14,3',
-                        'userAgent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
+                        'userAgent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
+                        "hl": "en",
+                        "timeZone": "UTC",
+                        "utcOffsetMinutes": 0
                     }
                 },
                 'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
@@ -220,23 +271,37 @@ detail = async (url, platform) => {
                 },
                 "contentCheckOk": true,
                 "racyCheckOk": true
-            }
-            const apiUrl = `https://www.youtube.com/youtubei/v1/player?key=${apiKey}&prettyPrint=false`;
-            let apiResp = await request('POST', apiUrl, JSON.stringify(data), {
+            }), {
                 'X-YouTube-Client-Name': '5',
                 'X-YouTube-Client-Version': '19.09.3',
                 'User-Agent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
-                'Content-Type': 'application/json'
+                'Content-Type': 'application/json',
+                'Cookie': parseSetCookie(htmlHeaders)
             }, platform);
+            let {data: apiData, _} = apiResp;
             console.log(`ios api result: ${JSON.stringify(apiResp)}`);
-            const res = JSON.parse(apiResp);
-            originFormats = originFormats.concat([].concat(res["streamingData"]["formats"]).concat(res["streamingData"]["adaptiveFormats"]));
+            const res = JSON.parse(apiData);
+            const currentFormats = [];
+            for (const format of [].concat(res["streamingData"]["formats"]).concat(res["streamingData"]["adaptiveFormats"])) {
+                if (format) {
+                    format["from"] = "ios"
+                    currentFormats.push(format);
+                }
+            }
+            originFormats = originFormats.concat(currentFormats);
         } catch (e) {
             console.log(`can not found format ios api error: ${e}`);
         }
-        console.log(`after android api, format size:${originFormats.length}`);
+        console.log(`after ios api, format size:${originFormats.length}`);
 
-        originFormats = originFormats.concat(ytInitialPlayerResponse["streamingData"]["formats"]).concat(ytInitialPlayerResponse["streamingData"]["adaptiveFormats"]);
+        const currentFormats = [];
+        for (const format of ytInitialPlayerResponse["streamingData"]["formats"].concat(ytInitialPlayerResponse["streamingData"]["adaptiveFormats"])) {
+            if (format) {
+                format["from"] = "web"
+                currentFormats.push(format);
+            }
+        }
+        originFormats = originFormats.concat(currentFormats);
         console.log(`after html, format size:${originFormats.length}`);
 
         const baseJsUrl = `https://www.youtube.com${JSON.parse(html.match(/set\(({.+?})\);/)[1])["PLAYER_JS_URL"]}`
@@ -265,7 +330,8 @@ detail = async (url, platform) => {
                             "acodec": acodec,
                             "vbr": "0",
                             "abr": "0",
-                            "container": "mp4_dash"
+                            "container": "mp4_dash",
+                            "from": format["from"]
                         })
                         formatIds.push(format["itag"]);
                     }
@@ -354,7 +420,8 @@ search = async (keyword, next, platform) => {
                 continuation: nextObject["continuation"]
             };
             let res = await request('POST', `https://www.youtube.com/youtubei/v1/search?key=${key}`, JSON.stringify(body), {}, platform);
-            res = JSON.parse(res);
+            const {data, _} = res;
+            res = JSON.parse(data);
             const videos = [];
             for (const item of res["onResponseReceivedCommands"][0]["appendContinuationItemsAction"]["continuationItems"][0]["itemSectionRenderer"]["contents"]) {
                 const video = item["videoRenderer"];
@@ -392,7 +459,8 @@ search = async (keyword, next, platform) => {
         } else {
             let url = `https://www.youtube.com/results?q=${encodeURIComponent(keyword)}&sp=EgIQAQ%253D%253D`;
 
-            const html = await request('GET', url, null, {}, platform);
+            const htmlRes = await request('GET', url, null, {}, platform);
+            const {data: html, _} = htmlRes;
 
             let regex = /var ytInitialData\s*=\s*({.*?});/;
             let match = html.match(regex);

+ 48 - 5
js/main.swift

@@ -1,6 +1,38 @@
 import JavaScriptCore
 import Alamofire
 
+
+import Alamofire
+import Foundation
+ 
+//// 扩展SessionManager以支持多个Cookies
+//extension SessionManager {
+//    static let multipleCookiesSessionManager: SessionManager = {
+//        let configuration = URLSessionConfiguration.default
+//        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
+//        
+//        let manager = SessionManager(configuration: configuration)
+//        
+//        // 自定义处理Cookies的方式
+//        manager.delegate.taskWillPerformHTTPRedirection = { session, task, response, request in
+//            if let headers = response.allHeaderFields as? [String: String],
+//               let setCookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: request.url!) {
+//                for cookie in setCookies {
+//                    HTTPCookieStorage.shared.setCookie(cookie)
+//                }
+//            }
+//            return request
+//        }
+//        
+//        return manager
+//    }()
+//}
+// 
+//// 使用扩展的SessionManager发送请求
+//SessionManager.multipleCookiesSessionManager.request("https://www.youtube.com/watch?v=5GJWxDKyk3A").response { response in
+//    debugPrint(response)
+//}
+
 func downloadJSFile(url: URL, completion: @escaping (Result<String, Error>) -> Void) {
     AF.download(url).responseData { (response) in
         switch response.result {
@@ -45,11 +77,22 @@ func createJSContext() -> JSContext {
         
         AF.request(request).response { response in
             if let data = response.data {
-                callback?.call(withArguments: [String(data: data, encoding: .utf8), nil])
+                var headerJsonString = ""
+                if let headers = response.response?.allHeaderFields as? [String: String] {
+                    do {
+                        let jsonData = try JSONSerialization.data(withJSONObject: headers, options: [])
+                        if let jsonString = String(data: jsonData, encoding: .utf8) {
+                            headerJsonString = jsonString
+                        }
+                    } catch {
+                        
+                    }
+                }
+                callback?.call(withArguments: [String(data: data, encoding: .utf8), headerJsonString, nil])
             }
             if let error = response.error {
                 debugPrint(response)
-                callback?.call(withArguments: [nil, error.localizedDescription])
+                callback?.call(withArguments: [nil, nil, error.localizedDescription])
             }
         }
     }
@@ -90,14 +133,14 @@ func testSearch(keyword: String, ctx: JSContext) -> Void {
 
 let ctx = createJSContext()
 
-if let url = URL(string: "http://hubgit.cn/ben/be-ytb/raw/master/js/info.js") {
+if let url = URL(string: "file:///Users/ben/Desktop/app/be/be-ytb/js/info.js") {
     downloadJSFile(url: url) { result in
         switch result {
         case .success(let jsString):
             print("下载远程JS成功")
             ctx.evaluateScript(jsString)
-            testDetail(url: "https://www.youtube.com/watch?v=5GJWxDKyk3A", ctx: ctx)
-//            testSearch(keyword: "周杰伦", ctx: ctx)
+//            testDetail(url: "https://www.youtube.com/watch?v=IXuhdnB2dAY", ctx: ctx)
+            testSearch(keyword: "周杰伦", ctx: ctx)
         case .failure(let error):
             print("Download Error: \(error)")
         }

+ 17 - 9
js/test.js

@@ -1,15 +1,23 @@
-detail(`https://www.youtube.com/watch?v=yBjnGz4S8FU`, 'WEB')
-    .then(res => {
-        console.log(res);
-    })
-    .catch(e => {
-        console.log(e);
-    })
-
-// search("周 杰伦", null, "WEB")
+// detail(`https://www.youtube.com/watch?v=yBjnGz4S8FU`, 'WEB')
 //     .then(res => {
 //         console.log(res);
 //     })
 //     .catch(e => {
 //         console.log(e);
 //     })
+
+search("周 杰伦", null, "WEB")
+    .then(res => {
+        console.log(res);
+
+        search("周 杰伦", res["data"]["next"], "WEB")
+            .then(res => {
+                console.log(res);
+            })
+            .catch(e => {
+                console.log(e);
+            })
+    })
+    .catch(e => {
+        console.log(e);
+    })

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů