|
@@ -50,25 +50,24 @@ parseCodecs = (format) => {
|
|
return {};
|
|
return {};
|
|
}
|
|
}
|
|
|
|
|
|
-request = async (method, url, data = null, headers = {}, local) => {
|
|
|
|
- if (local) {
|
|
|
|
|
|
+request = async (method, url, data = null, headers = {}, platform) => {
|
|
|
|
+ if (platform === "WEB") {
|
|
url = url.replace("https://www.youtube.com", "http://127.0.0.1");
|
|
url = url.replace("https://www.youtube.com", "http://127.0.0.1");
|
|
}
|
|
}
|
|
console.log(`请求url:${url}`)
|
|
console.log(`请求url:${url}`)
|
|
console.log(`请求data:${data}`)
|
|
console.log(`请求data:${data}`)
|
|
console.log(`请求method:${method}`)
|
|
console.log(`请求method:${method}`)
|
|
console.log(`请求headers:${JSON.stringify((headers))}`)
|
|
console.log(`请求headers:${JSON.stringify((headers))}`)
|
|
- if (local) {
|
|
|
|
|
|
+ if (platform === "WEB") {
|
|
return fetch(url, {
|
|
return fetch(url, {
|
|
"method": method,
|
|
"method": method,
|
|
"headers": headers,
|
|
"headers": headers,
|
|
- "body": data,
|
|
|
|
|
|
+ "body": data
|
|
}).then(res => res.text())
|
|
}).then(res => res.text())
|
|
}
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
return new Promise((resolve, reject) => {
|
|
AF.request(url, method, data, headers, (data, err) => {
|
|
AF.request(url, method, data, headers, (data, err) => {
|
|
if (err) {
|
|
if (err) {
|
|
- console.log(`请求失败: ${err}`)
|
|
|
|
reject(err);
|
|
reject(err);
|
|
} else {
|
|
} else {
|
|
resolve(data);
|
|
resolve(data);
|
|
@@ -95,7 +94,7 @@ getDecipherFunction = (string) => {
|
|
};
|
|
};
|
|
|
|
|
|
const cache = {};
|
|
const cache = {};
|
|
-extractJSSignatureFunction = async (baseJsUrl, local) => {
|
|
|
|
|
|
+extractJSSignatureFunction = async (baseJsUrl, platform) => {
|
|
console.log(`解析baseUrl: ${baseJsUrl}`);
|
|
console.log(`解析baseUrl: ${baseJsUrl}`);
|
|
const cacheKey = `js:${baseJsUrl}`;
|
|
const cacheKey = `js:${baseJsUrl}`;
|
|
if (cache[cacheKey]) {
|
|
if (cache[cacheKey]) {
|
|
@@ -105,7 +104,7 @@ extractJSSignatureFunction = async (baseJsUrl, local) => {
|
|
const headers = {
|
|
const headers = {
|
|
'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',
|
|
'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, local);
|
|
|
|
|
|
+ const baseContent = await request('GET', baseJsUrl, null, headers, platform);
|
|
const decipher = getDecipherFunction(baseContent);
|
|
const decipher = getDecipherFunction(baseContent);
|
|
if (decipher) {
|
|
if (decipher) {
|
|
cache[cacheKey] = decipher;
|
|
cache[cacheKey] = decipher;
|
|
@@ -113,8 +112,8 @@ extractJSSignatureFunction = async (baseJsUrl, local) => {
|
|
return decipher;
|
|
return decipher;
|
|
}
|
|
}
|
|
|
|
|
|
-getUrlFromSignature = async (signatureCipher, baseJsUrl, local) => {
|
|
|
|
- const decipher = await extractJSSignatureFunction(baseJsUrl, local);
|
|
|
|
|
|
+getUrlFromSignature = async (signatureCipher, baseJsUrl, platform) => {
|
|
|
|
+ const decipher = await extractJSSignatureFunction(baseJsUrl, platform);
|
|
const searchParams = {}
|
|
const searchParams = {}
|
|
for (const item of signatureCipher.split("&")) {
|
|
for (const item of signatureCipher.split("&")) {
|
|
const [key, value] = item.split('=');
|
|
const [key, value] = item.split('=');
|
|
@@ -125,55 +124,11 @@ getUrlFromSignature = async (signatureCipher, baseJsUrl, local) => {
|
|
return `${url}&${sp}=${decipher(signature)}`;
|
|
return `${url}&${sp}=${decipher(signature)}`;
|
|
}
|
|
}
|
|
|
|
|
|
-detail = async (url, local) => {
|
|
|
|
|
|
+detail = async (url, platform) => {
|
|
try {
|
|
try {
|
|
- if (url.includes('?')) {
|
|
|
|
- url = `${url}&bpctr=9999999999&has_verified=1`;
|
|
|
|
- } else {
|
|
|
|
- url = `${url}?bpctr=9999999999&has_verified=1`;
|
|
|
|
- }
|
|
|
|
- console.log(`接受到解析请求: ${url}`);
|
|
|
|
- const headers = {
|
|
|
|
- 'User-Agent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
|
|
|
|
- }
|
|
|
|
- const html = await request('GET', url, null, headers, local);
|
|
|
|
-
|
|
|
|
- // 尝试找到更好的
|
|
|
|
- try {
|
|
|
|
- let regex = /ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;/
|
|
|
|
- let match = html.match(regex);
|
|
|
|
- if (match == null || match.length !== 2) {
|
|
|
|
- console.log(`无法找到更好的format`);
|
|
|
|
- } else {
|
|
|
|
- const masterYtConfig = JSON.parse(match[1]);
|
|
|
|
- const headers = {
|
|
|
|
- 'Origin': 'https://www.youtube.com',
|
|
|
|
- 'X-YouTube-Client-Name': `${masterYtConfig['INNERTUBE_CONTEXT_CLIENT_NAME']}`,
|
|
|
|
- 'X-YouTube-Client-Version': `${masterYtConfig['INNERTUBE_CLIENT_VERSION']}`,
|
|
|
|
- 'User-Agent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
|
|
|
|
- 'Content-Type': 'application/json'
|
|
|
|
- }
|
|
|
|
- const apiKey = masterYtConfig['INNERTUBE_API_KEY']
|
|
|
|
- const data = {
|
|
|
|
- 'context': masterYtConfig['INNERTUBE_CONTEXT'],
|
|
|
|
- 'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
|
|
|
|
- 'playbackContext': {
|
|
|
|
- 'contentPlaybackContext': {
|
|
|
|
- 'html5Preference': 'HTML5_PREF_WANTS',
|
|
|
|
- 'signatureTimestamp': 0
|
|
|
|
- }
|
|
|
|
- },
|
|
|
|
- 'contentCheckOk': true,
|
|
|
|
- 'racyCheckOk': true
|
|
|
|
- }
|
|
|
|
- const jsUrl = `https://www.youtube.com/youtubei/v1/player?key=${apiKey}&prettyPrint=false`;
|
|
|
|
- const res = await request('POST', jsUrl, JSON.stringify(data), headers, local);
|
|
|
|
- console.log(`找到了更好的`);
|
|
|
|
- console.log(JSON.stringify(res));
|
|
|
|
- }
|
|
|
|
- } catch (e) {
|
|
|
|
- console.log(`无法找到更好的format,并且报错了: ${e}`);
|
|
|
|
- }
|
|
|
|
|
|
+ let html = await request('GET', url, 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'
|
|
|
|
+ }, platform);
|
|
|
|
|
|
let regex = /var ytInitialPlayerResponse\s*=\s*({.*?});/;
|
|
let regex = /var ytInitialPlayerResponse\s*=\s*({.*?});/;
|
|
let match = html.match(regex);
|
|
let match = html.match(regex);
|
|
@@ -194,62 +149,133 @@ detail = async (url, local) => {
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
- const formats = []
|
|
|
|
const baseJsUrl = `https://www.youtube.com${JSON.parse(html.match(/set\(({.+?})\);/)[1])["PLAYER_JS_URL"]}`
|
|
const baseJsUrl = `https://www.youtube.com${JSON.parse(html.match(/set\(({.+?})\);/)[1])["PLAYER_JS_URL"]}`
|
|
- for (let format of [].concat(ytInitialPlayerResponse["streamingData"]["formats"]).concat(ytInitialPlayerResponse["streamingData"]["adaptiveFormats"])) {
|
|
|
|
- console.log(`current format: ${JSON.stringify(format)}`);
|
|
|
|
- if (!format["url"]) {
|
|
|
|
- format["url"] = await getUrlFromSignature(format["signatureCipher"], baseJsUrl, local);
|
|
|
|
|
|
+ const formats = [];
|
|
|
|
+ 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"
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
- 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"
|
|
|
|
- })
|
|
|
|
|
|
+ } catch (e) {
|
|
|
|
+ console.log(`无法从api中解析format,并且报错了: ${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);
|
|
|
|
+ }
|
|
|
|
+ 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"
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- regex = /var ytInitialData\s*=\s*({.*?});/;
|
|
|
|
- match = html.match(regex);
|
|
|
|
- if (!match || !match.length) {
|
|
|
|
- console.log(`解析失败,无法找到 ytInitialData`);
|
|
|
|
- throw new Error('JSON not found.');
|
|
|
|
- }
|
|
|
|
- const ytInitialData = JSON.parse(match[1]);
|
|
|
|
|
|
+ match = html.match(/var ytInitialData\s*=\s*({.*?});/);
|
|
const recommendInfo = [];
|
|
const recommendInfo = [];
|
|
- for (const item of ytInitialData["contents"]["twoColumnWatchNextResults"]["secondaryResults"]["secondaryResults"]["results"]) {
|
|
|
|
- if (item["compactVideoRenderer"]) {
|
|
|
|
- const recommendVideo = item["compactVideoRenderer"];
|
|
|
|
- console.log(`推荐视频: ${JSON.stringify(recommendVideo)}`);
|
|
|
|
- if (recommendVideo["videoId"]) {
|
|
|
|
- recommendInfo.push({
|
|
|
|
- "type": "gridVideoRenderer",
|
|
|
|
- "videoId": recommendVideo["videoId"],
|
|
|
|
- "title": recommendVideo["title"]?.["simpleText"],
|
|
|
|
- "thumbnails": recommendVideo["thumbnail"]?.["thumbnails"],
|
|
|
|
- "channelName": recommendVideo["longBylineText"]?.["runs"]?.[0]?.["text"],
|
|
|
|
- "publishedTimeText": recommendVideo["publishedTimeText"]?.["simpleText"],
|
|
|
|
- "viewCountText": recommendVideo["viewCountText"]?.["simpleText"],
|
|
|
|
- "shortViewCountText": recommendVideo["shortViewCountText"]?.["simpleText"],
|
|
|
|
- "lengthText": recommendVideo["lengthText"]?.["simpleText"]
|
|
|
|
- })
|
|
|
|
|
|
+ if (match && match.length === 2) {
|
|
|
|
+ const ytInitialData = JSON.parse(match[1]);
|
|
|
|
+ for (const item of ytInitialData["contents"]?.["twoColumnWatchNextResults"]?.["secondaryResults"]?.["secondaryResults"]?.["results"] || []) {
|
|
|
|
+ if (item["compactVideoRenderer"]) {
|
|
|
|
+ const recommendVideo = item["compactVideoRenderer"];
|
|
|
|
+ console.log(`推荐视频: ${JSON.stringify(recommendVideo)}`);
|
|
|
|
+ if (recommendVideo["videoId"]) {
|
|
|
|
+ recommendInfo.push({
|
|
|
|
+ "type": "gridVideoRenderer",
|
|
|
|
+ "videoId": recommendVideo["videoId"],
|
|
|
|
+ "title": recommendVideo["title"]?.["simpleText"],
|
|
|
|
+ "thumbnails": recommendVideo["thumbnail"]?.["thumbnails"],
|
|
|
|
+ "channelName": recommendVideo["longBylineText"]?.["runs"]?.[0]?.["text"],
|
|
|
|
+ "publishedTimeText": recommendVideo["publishedTimeText"]?.["simpleText"],
|
|
|
|
+ "viewCountText": recommendVideo["viewCountText"]?.["simpleText"],
|
|
|
|
+ "shortViewCountText": recommendVideo["shortViewCountText"]?.["simpleText"],
|
|
|
|
+ "lengthText": recommendVideo["lengthText"]?.["simpleText"]
|
|
|
|
+ })
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+ } else {
|
|
|
|
+ console.log(`解析失败,无法找到 ytInitialData,无法获取推荐视频`);
|
|
}
|
|
}
|
|
|
|
|
|
const videoDetails = {
|
|
const videoDetails = {
|
|
@@ -272,7 +298,7 @@ detail = async (url, local) => {
|
|
"data": {
|
|
"data": {
|
|
"videoDetails": videoDetails,
|
|
"videoDetails": videoDetails,
|
|
"streamingData": {
|
|
"streamingData": {
|
|
- "formats": formats
|
|
|
|
|
|
+ "formats": formats.reverse()
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"id": "MusicDetailViewModel_detail_url"
|
|
"id": "MusicDetailViewModel_detail_url"
|
|
@@ -285,11 +311,12 @@ detail = async (url, local) => {
|
|
"msg": e.toString()
|
|
"msg": e.toString()
|
|
}
|
|
}
|
|
console.log(`解析失败: ${JSON.stringify(ret)}`);
|
|
console.log(`解析失败: ${JSON.stringify(ret)}`);
|
|
|
|
+ console.log(e);
|
|
return ret;
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
-search = async (keyword, next, local) => {
|
|
|
|
|
|
+search = async (keyword, next, platform) => {
|
|
try {
|
|
try {
|
|
console.log(`接受到搜索请求 keyword: ${keyword}`);
|
|
console.log(`接受到搜索请求 keyword: ${keyword}`);
|
|
console.log(`接收到搜索请求 next: ${next}`);
|
|
console.log(`接收到搜索请求 next: ${next}`);
|
|
@@ -305,7 +332,7 @@ search = async (keyword, next, local) => {
|
|
},
|
|
},
|
|
continuation: nextObject["continuation"]
|
|
continuation: nextObject["continuation"]
|
|
};
|
|
};
|
|
- let res = await request('POST', `https://www.youtube.com/youtubei/v1/search?key=${key}`, JSON.stringify(body), {}, local);
|
|
|
|
|
|
+ let res = await request('POST', `https://www.youtube.com/youtubei/v1/search?key=${key}`, JSON.stringify(body), {}, platform);
|
|
res = JSON.parse(res);
|
|
res = JSON.parse(res);
|
|
const videos = [];
|
|
const videos = [];
|
|
for (const item of res["onResponseReceivedCommands"][0]["appendContinuationItemsAction"]["continuationItems"][0]["itemSectionRenderer"]["contents"]) {
|
|
for (const item of res["onResponseReceivedCommands"][0]["appendContinuationItemsAction"]["continuationItems"][0]["itemSectionRenderer"]["contents"]) {
|
|
@@ -344,7 +371,7 @@ search = async (keyword, next, local) => {
|
|
} else {
|
|
} else {
|
|
let url = `https://www.youtube.com/results?q=${encodeURIComponent(keyword)}&sp=EgIQAQ%253D%253D`;
|
|
let url = `https://www.youtube.com/results?q=${encodeURIComponent(keyword)}&sp=EgIQAQ%253D%253D`;
|
|
|
|
|
|
- const html = await request('GET', url, null, {}, local);
|
|
|
|
|
|
+ const html = await request('GET', url, null, {}, platform);
|
|
|
|
|
|
let regex = /var ytInitialData\s*=\s*({.*?});/;
|
|
let regex = /var ytInitialData\s*=\s*({.*?});/;
|
|
let match = html.match(regex);
|
|
let match = html.match(regex);
|