|
@@ -120,8 +120,8 @@ getStringBetween = (string, needleStart, needleEnd, offsetStart = 0, offsetEnd =
|
|
|
return string.substring(x + needleStart.length + offsetEnd, y + offsetStart);
|
|
|
}
|
|
|
|
|
|
-getDecipherFunction = (jsCode) => {
|
|
|
- const match = jsCode.match(/([a-zA-Z0-9]+)=function\([a-zA-Z0-9]+\)\{a=a\.split\(""\).*};/)
|
|
|
+findFunction = (jsCode, regexp) => {
|
|
|
+ const match = jsCode.match(regexp)
|
|
|
if (!match && match.length <= 1) {
|
|
|
return null;
|
|
|
}
|
|
@@ -147,34 +147,68 @@ getDecipherFunction = (jsCode) => {
|
|
|
};
|
|
|
|
|
|
const cache = {};
|
|
|
-extractJSSignatureFunction = async (baseJsUrl, platform) => {
|
|
|
- const cacheKey = `js:${baseJsUrl}`;
|
|
|
+fetchBaseJSContent = async (baseJsUrl, platform) => {
|
|
|
+ const cacheKey = `jsContent:${baseJsUrl}`;
|
|
|
if (cache[cacheKey]) {
|
|
|
- console.log(`from cache JSSignatureFunction: ${baseJsUrl}`);
|
|
|
+ console.log(`from cache: ${baseJsUrl}`);
|
|
|
return cache[cacheKey];
|
|
|
}
|
|
|
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 {data, headers} = baseContentResp;
|
|
|
- const decipher = getDecipherFunction(data);
|
|
|
- if (decipher) {
|
|
|
- cache[cacheKey] = decipher;
|
|
|
- }
|
|
|
- return decipher;
|
|
|
+ const {data, _} = baseContentResp;
|
|
|
+ cache[cacheKey] = data;
|
|
|
+ return data;
|
|
|
+}
|
|
|
+
|
|
|
+extractJSSignatureFunction = async (baseJsUrl, platform) => {
|
|
|
+ const baseJsContent = await fetchBaseJSContent(baseJsUrl, platform);
|
|
|
+ return findFunction(baseJsContent, /([a-zA-Z0-9]+)=function\([a-zA-Z0-9]+\)\{a=a\.split\(""\).*};/);
|
|
|
+}
|
|
|
+extractNJSFunction = async (baseJsUrl, platform) => {
|
|
|
+ const baseJsContent = await fetchBaseJSContent(baseJsUrl, platform);
|
|
|
+ return findFunction(baseJsContent, /([a-zA-Z0-9]+)=function\([a-zA-Z0-9]+\)\{var b=a\.split\(""\)[\s\S]*?};/);
|
|
|
}
|
|
|
|
|
|
-getUrlFromSignature = async (signatureCipher, baseJsUrl, platform) => {
|
|
|
- const decipher = await extractJSSignatureFunction(baseJsUrl, platform);
|
|
|
+signUrl = async (signatureCipher, baseJsUrl, platfrom) => {
|
|
|
const searchParams = {}
|
|
|
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 decipher = await extractJSSignatureFunction(baseJsUrl, platfrom);
|
|
|
+ if (!decipher) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
console.log(`signatureCipher=${signatureCipher}, url=${url}, signature=${signature}, sp=${sp}`)
|
|
|
- return `${url}&${sp}=${decipher(signature)}`;
|
|
|
+ let newUrl = `${url}&${sp}=${decipher(signature)}`;
|
|
|
+
|
|
|
+ function replaceUrlParam(url, paramName, paramValue) {
|
|
|
+ let pattern = new RegExp(`([?&])${paramName}=.*?(&|$)`, 'i');
|
|
|
+ let newUrl = url.replace(pattern, `$1${paramName}=${paramValue}$2`);
|
|
|
+
|
|
|
+ if (newUrl === url && url.indexOf('?') === -1) {
|
|
|
+ newUrl += `?${paramName}=${paramValue}`;
|
|
|
+ } else if (newUrl === url) {
|
|
|
+ newUrl += `&${paramName}=${paramValue}`;
|
|
|
+ }
|
|
|
+ return newUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const item of url.split('&')) {
|
|
|
+ const [key, value] = item.split('=');
|
|
|
+ searchParams[decodeURIComponent(key)] = decodeURIComponent(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ const nFunction = await extractNJSFunction(baseJsUrl, platfrom);
|
|
|
+ const n = searchParams['n']
|
|
|
+ if (n && nFunction) {
|
|
|
+ const newN = nFunction(n);
|
|
|
+ return replaceUrlParam(newUrl, 'n', newN);
|
|
|
+ }
|
|
|
+ return newUrl;
|
|
|
}
|
|
|
|
|
|
detail = async (url, platform) => {
|
|
@@ -219,113 +253,113 @@ detail = async (url, platform) => {
|
|
|
originFormats = originFormats.concat(currentFormats);
|
|
|
console.log(`after html, format size:${originFormats.length}`);
|
|
|
|
|
|
- // android
|
|
|
- try {
|
|
|
- const apiKey = 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w'
|
|
|
- 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',
|
|
|
- "hl": "en",
|
|
|
- "timeZone": "UTC",
|
|
|
- "utcOffsetMinutes": 0
|
|
|
- }
|
|
|
- },
|
|
|
- 'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
|
|
|
- "params": "CgIIAQ==",
|
|
|
- "playbackContext": {
|
|
|
- "contentPlaybackContext": {
|
|
|
- "html5Preference": "HTML5_PREF_WANTS"
|
|
|
- }
|
|
|
- },
|
|
|
- "contentCheckOk": true,
|
|
|
- "racyCheckOk": true
|
|
|
- }), {
|
|
|
- 'Host': 'www.youtube.com',
|
|
|
- 'Connection': 'keep-alive',
|
|
|
- 'User-Agent': 'com.google.android.youtube/19.09.37 (Linux; U; Android 11) gzip',
|
|
|
- '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',
|
|
|
- 'X-YouTube-Client-Name': '3',
|
|
|
- 'X-YouTube-Client-Version': '19.09.37',
|
|
|
- 'Origin': 'https://www.youtube.com',
|
|
|
- 'Accept-Encoding': 'gzip, deflate, br',
|
|
|
- 'Cookie': parseSetCookie(htmlHeaders),
|
|
|
- 'Content-Type': 'application/json'
|
|
|
- }, platform);
|
|
|
- let {data: apiData, _} = apiResp;
|
|
|
- console.log(`android api result: ${JSON.stringify(apiResp)}`);
|
|
|
- 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}`);
|
|
|
- }
|
|
|
- console.log(`after android api, format size:${originFormats.length}`);
|
|
|
- // ios
|
|
|
- try {
|
|
|
- const apiKey = 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc'
|
|
|
- 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)',
|
|
|
- "hl": "en",
|
|
|
- "timeZone": "UTC",
|
|
|
- "utcOffsetMinutes": 0
|
|
|
- }
|
|
|
- },
|
|
|
- 'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
|
|
|
- "playbackContext": {
|
|
|
- "contentPlaybackContext": {
|
|
|
- "html5Preference": "HTML5_PREF_WANTS"
|
|
|
- }
|
|
|
- },
|
|
|
- "contentCheckOk": true,
|
|
|
- "racyCheckOk": true
|
|
|
- }), {
|
|
|
- 'Host': 'www.youtube.com',
|
|
|
- 'Connection': 'keep-alive',
|
|
|
- 'User-Agent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
|
|
|
- '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',
|
|
|
- 'X-YouTube-Client-Name': '5',
|
|
|
- 'X-YouTube-Client-Version': '19.09.3',
|
|
|
- 'Origin': 'https://www.youtube.com',
|
|
|
- 'Accept-Encoding': 'gzip, deflate, br',
|
|
|
- 'Cookie': parseSetCookie(htmlHeaders),
|
|
|
- 'Content-Type': 'application/json'
|
|
|
- }, platform);
|
|
|
- let {data: apiData, _} = apiResp;
|
|
|
- console.log(`ios api result: ${JSON.stringify(apiResp)}`);
|
|
|
- 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 ios api, format size:${originFormats.length}`);
|
|
|
+ // // android
|
|
|
+ // try {
|
|
|
+ // const apiKey = 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w'
|
|
|
+ // 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',
|
|
|
+ // "hl": "en",
|
|
|
+ // "timeZone": "UTC",
|
|
|
+ // "utcOffsetMinutes": 0
|
|
|
+ // }
|
|
|
+ // },
|
|
|
+ // 'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
|
|
|
+ // "params": "CgIIAQ==",
|
|
|
+ // "playbackContext": {
|
|
|
+ // "contentPlaybackContext": {
|
|
|
+ // "html5Preference": "HTML5_PREF_WANTS"
|
|
|
+ // }
|
|
|
+ // },
|
|
|
+ // "contentCheckOk": true,
|
|
|
+ // "racyCheckOk": true
|
|
|
+ // }), {
|
|
|
+ // 'Host': 'www.youtube.com',
|
|
|
+ // 'Connection': 'keep-alive',
|
|
|
+ // 'User-Agent': 'com.google.android.youtube/19.09.37 (Linux; U; Android 11) gzip',
|
|
|
+ // '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',
|
|
|
+ // 'X-YouTube-Client-Name': '3',
|
|
|
+ // 'X-YouTube-Client-Version': '19.09.37',
|
|
|
+ // 'Origin': 'https://www.youtube.com',
|
|
|
+ // 'Accept-Encoding': 'gzip, deflate, br',
|
|
|
+ // 'Cookie': parseSetCookie(htmlHeaders),
|
|
|
+ // 'Content-Type': 'application/json'
|
|
|
+ // }, platform);
|
|
|
+ // let {data: apiData, _} = apiResp;
|
|
|
+ // console.log(`android api result: ${JSON.stringify(apiResp)}`);
|
|
|
+ // 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}`);
|
|
|
+ // }
|
|
|
+ // console.log(`after android api, format size:${originFormats.length}`);
|
|
|
+ // // ios
|
|
|
+ // try {
|
|
|
+ // const apiKey = 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc'
|
|
|
+ // 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)',
|
|
|
+ // "hl": "en",
|
|
|
+ // "timeZone": "UTC",
|
|
|
+ // "utcOffsetMinutes": 0
|
|
|
+ // }
|
|
|
+ // },
|
|
|
+ // 'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
|
|
|
+ // "playbackContext": {
|
|
|
+ // "contentPlaybackContext": {
|
|
|
+ // "html5Preference": "HTML5_PREF_WANTS"
|
|
|
+ // }
|
|
|
+ // },
|
|
|
+ // "contentCheckOk": true,
|
|
|
+ // "racyCheckOk": true
|
|
|
+ // }), {
|
|
|
+ // 'Host': 'www.youtube.com',
|
|
|
+ // 'Connection': 'keep-alive',
|
|
|
+ // 'User-Agent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
|
|
|
+ // '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',
|
|
|
+ // 'X-YouTube-Client-Name': '5',
|
|
|
+ // 'X-YouTube-Client-Version': '19.09.3',
|
|
|
+ // 'Origin': 'https://www.youtube.com',
|
|
|
+ // 'Accept-Encoding': 'gzip, deflate, br',
|
|
|
+ // 'Cookie': parseSetCookie(htmlHeaders),
|
|
|
+ // 'Content-Type': 'application/json'
|
|
|
+ // }, platform);
|
|
|
+ // let {data: apiData, _} = apiResp;
|
|
|
+ // console.log(`ios api result: ${JSON.stringify(apiResp)}`);
|
|
|
+ // 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 ios api, format size:${originFormats.length}`);
|
|
|
|
|
|
const baseJsUrl = `https://www.youtube.com${JSON.parse(html.match(/set\(({.+?})\);/)[1])["PLAYER_JS_URL"]}`
|
|
|
let formatIds = [];
|
|
@@ -334,7 +368,7 @@ detail = async (url, platform) => {
|
|
|
console.log(`current format: ${JSON.stringify(format)}`);
|
|
|
if (format && formatIds.indexOf(format['itag']) === -1) {
|
|
|
if (!format["url"]) {
|
|
|
- format["url"] = await getUrlFromSignature(format["signatureCipher"], baseJsUrl, platform);
|
|
|
+ format["url"] = await signUrl(format["signatureCipher"], baseJsUrl, platform);
|
|
|
}
|
|
|
if (format["url"]) {
|
|
|
const {vcodec, acodec} = parseCodecs(format)
|