|
@@ -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);
|