|
@@ -54,10 +54,10 @@ request = async (method, url, data = null, headers = {}, platform) => {
|
|
|
if (platform === "WEB") {
|
|
|
url = url.replace("https://www.youtube.com", "http://127.0.0.1");
|
|
|
}
|
|
|
- console.log(`请求url:${url}`)
|
|
|
- console.log(`请求data:${data}`)
|
|
|
- console.log(`请求method:${method}`)
|
|
|
- console.log(`请求headers:${JSON.stringify((headers))}`)
|
|
|
+ console.log(`request url:${url}`)
|
|
|
+ console.log(`request data:${data}`)
|
|
|
+ console.log(`request method:${method}`)
|
|
|
+ console.log(`request headers:${JSON.stringify((headers))}`)
|
|
|
if (platform === "WEB") {
|
|
|
return fetch(url, {
|
|
|
"method": method,
|
|
@@ -95,16 +95,15 @@ getDecipherFunction = (string) => {
|
|
|
|
|
|
const cache = {};
|
|
|
extractJSSignatureFunction = async (baseJsUrl, platform) => {
|
|
|
- console.log(`解析baseUrl: ${baseJsUrl}`);
|
|
|
+ console.log(`extract baseUrl: ${baseJsUrl}`);
|
|
|
const cacheKey = `js:${baseJsUrl}`;
|
|
|
if (cache[cacheKey]) {
|
|
|
- console.log(`从缓存中获取JSSignatureFunction: ${baseJsUrl}`);
|
|
|
+ console.log(`from cache JSSignatureFunction: ${baseJsUrl}`);
|
|
|
return cache[cacheKey];
|
|
|
}
|
|
|
- const headers = {
|
|
|
+ const baseContent = 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',
|
|
|
- }
|
|
|
- const baseContent = await request('GET', baseJsUrl, null, headers, platform);
|
|
|
+ }, platform);
|
|
|
const decipher = getDecipherFunction(baseContent);
|
|
|
if (decipher) {
|
|
|
cache[cacheKey] = decipher;
|
|
@@ -133,8 +132,8 @@ detail = async (url, platform) => {
|
|
|
let regex = /var ytInitialPlayerResponse\s*=\s*({.*?});/;
|
|
|
let match = html.match(regex);
|
|
|
if (!match || !match.length) {
|
|
|
- console.log("解释失败: 无法找到json");
|
|
|
- throw new Error('JSON not found.');
|
|
|
+ console.log("can not found JSON: ytInitialPlayerResponse");
|
|
|
+ throw new Error('JSON not found: ytInitialPlayerResponse');
|
|
|
}
|
|
|
const ytInitialPlayerResponse = JSON.parse(match[1]);
|
|
|
console.log(ytInitialPlayerResponse);
|
|
@@ -149,116 +148,125 @@ detail = async (url, platform) => {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- const baseJsUrl = `https://www.youtube.com${JSON.parse(html.match(/set\(({.+?})\);/)[1])["PLAYER_JS_URL"]}`
|
|
|
- const formats = [];
|
|
|
+ let originFormats = [];
|
|
|
+ // android
|
|
|
try {
|
|
|
- let regex = /ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;/
|
|
|
- let match = html.match(regex);
|
|
|
- if (match != null && match.length === 2) {
|
|
|
- const masterYtConfig = JSON.parse(match[1]);
|
|
|
- const apiKey = masterYtConfig['INNERTUBE_API_KEY'] || 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39'
|
|
|
- const data = {
|
|
|
- "context": {
|
|
|
- "client": {
|
|
|
- "clientName": "ANDROID",
|
|
|
- "clientVersion": "19.09.37",
|
|
|
- "androidSdkVersion": 30,
|
|
|
- "userAgent": "com.google.android.youtube/19.09.37 (Linux; U; Android 11) gzip",
|
|
|
- }
|
|
|
- },
|
|
|
- 'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
|
|
|
- "playbackContext": {
|
|
|
- "contentPlaybackContext": {
|
|
|
- "html5Preference": "HTML5_PREF_WANTS"
|
|
|
- }
|
|
|
- },
|
|
|
- "params": "CgIIAQ==",
|
|
|
- "contentCheckOk": true,
|
|
|
- "racyCheckOk": true
|
|
|
- }
|
|
|
- const apiUrl = `https://www.youtube.com/youtubei/v1/player?key=${apiKey}&prettyPrint=false`;
|
|
|
- let res = 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'
|
|
|
- }, platform);
|
|
|
- console.log(`api结果: ${res}`);
|
|
|
- res = JSON.parse(res);
|
|
|
- console.log(res);
|
|
|
- for (let format of [].concat(res["streamingData"]["formats"]).concat(res["streamingData"]["adaptiveFormats"])) {
|
|
|
- if (format) {
|
|
|
- console.log(`current format: ${JSON.stringify(format)}`);
|
|
|
- if (!format["url"]) {
|
|
|
- format["url"] = await getUrlFromSignature(format["signatureCipher"], baseJsUrl, platform);
|
|
|
- }
|
|
|
- if (format["url"]) {
|
|
|
- const {vcodec, acodec} = parseCodecs(format)
|
|
|
- if (vcodec && acodec) {
|
|
|
- formats.push({
|
|
|
- "width": format["width"] + "",
|
|
|
- "height": format["height"] + "",
|
|
|
- "type": format["mimeType"],
|
|
|
- "quality": format["quality"],
|
|
|
- "itag": format["itag"],
|
|
|
- "fps": format["fps"] + "",
|
|
|
- "bitrate": format["bitrate"] + "",
|
|
|
- "url": format["url"],
|
|
|
- "ext": "mp4",
|
|
|
- "vcodec": vcodec,
|
|
|
- "acodec": acodec,
|
|
|
- "vbr": "0",
|
|
|
- "abr": "0",
|
|
|
- "container": "mp4_dash"
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
+ const apiKey = 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39'
|
|
|
+ const data = {
|
|
|
+ "context": {
|
|
|
+ "client": {
|
|
|
+ "clientName": "ANDROID",
|
|
|
+ "clientVersion": "19.09.37",
|
|
|
+ "androidSdkVersion": 30,
|
|
|
+ "userAgent": "com.google.android.youtube/19.09.37 (Linux; U; Android 11) gzip",
|
|
|
}
|
|
|
- }
|
|
|
+ },
|
|
|
+ 'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
|
|
|
+ "playbackContext": {
|
|
|
+ "contentPlaybackContext": {
|
|
|
+ "html5Preference": "HTML5_PREF_WANTS"
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "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'
|
|
|
+ }, platform);
|
|
|
+ console.log(`android api result: ${JSON.stringify(apiResp)}`);
|
|
|
+ const res = JSON.parse(apiResp);
|
|
|
+ originFormats = originFormats.concat([].concat(res["streamingData"]["formats"]).concat(res["streamingData"]["adaptiveFormats"]));
|
|
|
} catch (e) {
|
|
|
- console.log(`无法从api中解析format,并且报错了: ${e}`);
|
|
|
+ console.log(`can not found format android api error: ${e}`);
|
|
|
}
|
|
|
- if (formats.length === 0) {
|
|
|
- for (let format of [].concat(ytInitialPlayerResponse["streamingData"]["formats"]).concat(ytInitialPlayerResponse["streamingData"]["adaptiveFormats"])) {
|
|
|
- console.log(`current format: ${JSON.stringify(format)}`);
|
|
|
- if (format) {
|
|
|
- if (!format["url"]) {
|
|
|
- format["url"] = await getUrlFromSignature(format["signatureCipher"], baseJsUrl, platform);
|
|
|
+ console.log(`after android api, format size:${originFormats.length}`);
|
|
|
+ // ios
|
|
|
+ try {
|
|
|
+ const apiKey = 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc'
|
|
|
+ const data = {
|
|
|
+ "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)'
|
|
|
}
|
|
|
- if (format["url"]) {
|
|
|
- const {vcodec, acodec} = parseCodecs(format)
|
|
|
- if (vcodec && acodec) {
|
|
|
- formats.push({
|
|
|
- "width": format["width"] + "",
|
|
|
- "height": format["height"] + "",
|
|
|
- "type": format["mimeType"],
|
|
|
- "quality": format["quality"],
|
|
|
- "itag": format["itag"],
|
|
|
- "fps": format["fps"] + "",
|
|
|
- "bitrate": format["bitrate"] + "",
|
|
|
- "url": format["url"],
|
|
|
- "ext": "mp4",
|
|
|
- "vcodec": vcodec,
|
|
|
- "acodec": acodec,
|
|
|
- "vbr": "0",
|
|
|
- "abr": "0",
|
|
|
- "container": "mp4_dash"
|
|
|
- })
|
|
|
- }
|
|
|
+ },
|
|
|
+ 'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
|
|
|
+ "playbackContext": {
|
|
|
+ "contentPlaybackContext": {
|
|
|
+ "html5Preference": "HTML5_PREF_WANTS"
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "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'
|
|
|
+ }, platform);
|
|
|
+ console.log(`ios api result: ${JSON.stringify(apiResp)}`);
|
|
|
+ const res = JSON.parse(apiResp);
|
|
|
+ originFormats = originFormats.concat([].concat(res["streamingData"]["formats"]).concat(res["streamingData"]["adaptiveFormats"]));
|
|
|
+ } catch (e) {
|
|
|
+ console.log(`can not found format ios api error: ${e}`);
|
|
|
+ }
|
|
|
+ console.log(`after android api, format size:${originFormats.length}`);
|
|
|
+
|
|
|
+ originFormats = originFormats.concat(ytInitialPlayerResponse["streamingData"]["formats"]).concat(ytInitialPlayerResponse["streamingData"]["adaptiveFormats"]);
|
|
|
+ console.log(`after html, format size:${originFormats.length}`);
|
|
|
+
|
|
|
+ const baseJsUrl = `https://www.youtube.com${JSON.parse(html.match(/set\(({.+?})\);/)[1])["PLAYER_JS_URL"]}`
|
|
|
+ let formatIds = [];
|
|
|
+ const formats = [];
|
|
|
+ for (let format of originFormats) {
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ if (format["url"]) {
|
|
|
+ const {vcodec, acodec} = parseCodecs(format)
|
|
|
+ if (vcodec && acodec) {
|
|
|
+ formats.push({
|
|
|
+ "width": format["width"] + "",
|
|
|
+ "height": format["height"] + "",
|
|
|
+ "type": format["mimeType"],
|
|
|
+ "quality": format["quality"],
|
|
|
+ "itag": format["itag"],
|
|
|
+ "fps": format["fps"] + "",
|
|
|
+ "bitrate": format["bitrate"] + "",
|
|
|
+ "url": format["url"],
|
|
|
+ "ext": "mp4",
|
|
|
+ "vcodec": vcodec,
|
|
|
+ "acodec": acodec,
|
|
|
+ "vbr": "0",
|
|
|
+ "abr": "0",
|
|
|
+ "container": "mp4_dash"
|
|
|
+ })
|
|
|
+ formatIds.push(format["itag"]);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- match = html.match(/var ytInitialData\s*=\s*({.*?});/);
|
|
|
+ const ytInitialDataMatch = html.match(/var ytInitialData\s*=\s*({.*?});/);
|
|
|
const recommendInfo = [];
|
|
|
- if (match && match.length === 2) {
|
|
|
- const ytInitialData = JSON.parse(match[1]);
|
|
|
+ if (ytInitialDataMatch && ytInitialDataMatch.length === 2) {
|
|
|
+ const ytInitialData = JSON.parse(ytInitialDataMatch[1]);
|
|
|
+ console.log(`ytInitialData: ${JSON.stringify(ytInitialData)}`);
|
|
|
for (const item of ytInitialData["contents"]?.["twoColumnWatchNextResults"]?.["secondaryResults"]?.["secondaryResults"]?.["results"] || []) {
|
|
|
if (item["compactVideoRenderer"]) {
|
|
|
const recommendVideo = item["compactVideoRenderer"];
|
|
|
- console.log(`推荐视频: ${JSON.stringify(recommendVideo)}`);
|
|
|
+ console.log(`recommend video: ${JSON.stringify(recommendVideo)}`);
|
|
|
if (recommendVideo["videoId"]) {
|
|
|
recommendInfo.push({
|
|
|
"type": "gridVideoRenderer",
|
|
@@ -274,8 +282,6 @@ detail = async (url, platform) => {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- } else {
|
|
|
- console.log(`解析失败,无法找到 ytInitialData,无法获取推荐视频`);
|
|
|
}
|
|
|
|
|
|
const videoDetails = {
|
|
@@ -303,14 +309,14 @@ detail = async (url, platform) => {
|
|
|
},
|
|
|
"id": "MusicDetailViewModel_detail_url"
|
|
|
}
|
|
|
- console.log(`解析结果: ${JSON.stringify(ret)}`);
|
|
|
+ console.log(`detail result: ${JSON.stringify(ret)}`);
|
|
|
return ret;
|
|
|
} catch (e) {
|
|
|
const ret = {
|
|
|
"code": -1,
|
|
|
"msg": e.toString()
|
|
|
}
|
|
|
- console.log(`解析失败: ${JSON.stringify(ret)}`);
|
|
|
+ console.log(`detail result error: ${JSON.stringify(ret)}`);
|
|
|
console.log(e);
|
|
|
return ret;
|
|
|
}
|
|
@@ -318,8 +324,8 @@ detail = async (url, platform) => {
|
|
|
|
|
|
search = async (keyword, next, platform) => {
|
|
|
try {
|
|
|
- console.log(`接受到搜索请求 keyword: ${keyword}`);
|
|
|
- console.log(`接收到搜索请求 next: ${next}`);
|
|
|
+ console.log(`search keyword: ${keyword}`);
|
|
|
+ console.log(`search next: ${next}`);
|
|
|
if (next) {
|
|
|
const nextObject = JSON.parse(next);
|
|
|
const key = nextObject["key"];
|
|
@@ -337,7 +343,7 @@ search = async (keyword, next, platform) => {
|
|
|
const videos = [];
|
|
|
for (const item of res["onResponseReceivedCommands"][0]["appendContinuationItemsAction"]["continuationItems"][0]["itemSectionRenderer"]["contents"]) {
|
|
|
const video = item["videoRenderer"];
|
|
|
- console.log(`搜索结果: ${JSON.stringify(video)}`);
|
|
|
+ console.log(`search result video: ${JSON.stringify(video)}`);
|
|
|
if (video && video["videoId"]) {
|
|
|
videos.push({
|
|
|
"type": "videoWithContextRenderer",
|
|
@@ -361,12 +367,12 @@ search = async (keyword, next, platform) => {
|
|
|
"data": videos,
|
|
|
"next": JSON.stringify({
|
|
|
"key": nextObject["key"],
|
|
|
- "continuation": res["onResponseReceivedCommands"][0]["appendContinuationItemsAction"]["continuationItems"][1]["continuationItemRenderer"]["continuationEndpoint"]["continuationCommand"]["token"],
|
|
|
+ "continuation": res["onResponseReceivedCommands"]?.[0]?.["appendContinuationItemsAction"]?.["continuationItems"]?.[1]?.["continuationItemRenderer"]?.["continuationEndpoint"]?.["continuationCommand"]?.["token"],
|
|
|
}),
|
|
|
},
|
|
|
"id": "MusicSearchResultViewModel_search_result"
|
|
|
}
|
|
|
- console.log(`携带next搜索结果成功: ${JSON.stringify(ret)}`);
|
|
|
+ console.log(`[next] search result: ${JSON.stringify(ret)}`);
|
|
|
return ret;
|
|
|
} else {
|
|
|
let url = `https://www.youtube.com/results?q=${encodeURIComponent(keyword)}&sp=EgIQAQ%253D%253D`;
|
|
@@ -376,16 +382,16 @@ search = async (keyword, next, platform) => {
|
|
|
let regex = /var ytInitialData\s*=\s*({.*?});/;
|
|
|
let match = html.match(regex);
|
|
|
if (!match || !match.length) {
|
|
|
- console.log("搜索失败:无法找到ytInitialData");
|
|
|
- throw new Error('JSON not found.');
|
|
|
+ console.log("can not found ytInitialData");
|
|
|
+ throw new Error('JSON not found: ytInitialData');
|
|
|
}
|
|
|
|
|
|
const ytInitialDataResp = JSON.parse(match[1]);
|
|
|
const videos = [];
|
|
|
- for (const item of ytInitialDataResp["contents"]["twoColumnSearchResultsRenderer"]["primaryContents"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]["contents"]) {
|
|
|
+ for (const item of ytInitialDataResp["contents"]?.["twoColumnSearchResultsRenderer"]?.["primaryContents"]?.["sectionListRenderer"]?.["contents"]?.[0]?.["itemSectionRenderer"]?.["contents"]) {
|
|
|
if (item["videoRenderer"]) {
|
|
|
const video = item["videoRenderer"];
|
|
|
- console.log(`搜索结果: ${JSON.stringify(video)}`);
|
|
|
+ console.log(`search result video: ${JSON.stringify(video)}`);
|
|
|
if (video && video["videoId"]) {
|
|
|
videos.push({
|
|
|
"type": "videoWithContextRenderer",
|
|
@@ -412,7 +418,7 @@ search = async (keyword, next, platform) => {
|
|
|
.split(",")[0]
|
|
|
.split('"')[2];
|
|
|
}
|
|
|
- next["continuation"] = ytInitialDataResp["contents"]["twoColumnSearchResultsRenderer"]["primaryContents"]["sectionListRenderer"]["contents"][1]["continuationItemRenderer"]["continuationEndpoint"]["continuationCommand"]["token"]
|
|
|
+ next["continuation"] = ytInitialDataResp["contents"]?.["twoColumnSearchResultsRenderer"]?.["primaryContents"]?.["sectionListRenderer"]?.["contents"]?.[1]?.["continuationItemRenderer"]?.["continuationEndpoint"]?.["continuationCommand"]?.["token"]
|
|
|
|
|
|
const ret = {
|
|
|
"code": 200,
|
|
@@ -423,7 +429,7 @@ search = async (keyword, next, platform) => {
|
|
|
},
|
|
|
"id": "MusicSearchResultViewModel_search_result"
|
|
|
}
|
|
|
- console.log(`未携带next搜索结果成功: ${JSON.stringify(ret)}`);
|
|
|
+ console.log(`unnext search result: ${JSON.stringify(ret)}`);
|
|
|
return ret;
|
|
|
}
|
|
|
} catch (e) {
|
|
@@ -431,7 +437,7 @@ search = async (keyword, next, platform) => {
|
|
|
"code": -1,
|
|
|
"msg": e.toString()
|
|
|
}
|
|
|
- console.log(`搜索失败: ${JSON.stringify(ret)}`);
|
|
|
+ console.log(`search result error: ${JSON.stringify(ret)}`);
|
|
|
return ret;
|
|
|
}
|
|
|
}
|