parseCodecs = (format) => { const mimeType = format["mimeType"] if (!mimeType) { return {}; } const regex = /(?[^/]+\/[^;]+)(?:;\s*codecs="?(?[^"]+))?/; const match = mimeType.match(regex); if (!match) { return {}; } const codecs = match.groups.codecs; if (!codecs) { return {}; } const splitCodecs = codecs.trim().replace(/,$/, '').split(',').map(str => str.trim()).filter(Boolean); let vcodec = null; let acodec = null; for (const fullCodec of splitCodecs) { const codec = fullCodec.split('.')[0]; if (['avc1', 'avc2', 'avc3', 'avc4', 'vp9', 'vp8', 'hev1', 'hev2', 'h263', 'h264', 'mp4v', 'hvc1', 'av01', 'theora'].includes(codec)) { if (!vcodec) { vcodec = fullCodec; } } else if (['mp4a', 'opus', 'vorbis', 'mp3', 'aac', 'ac-3', 'ec-3', 'eac3', 'dtsc', 'dtse', 'dtsh', 'dtsl'].includes(codec)) { if (!acodec) { acodec = fullCodec; } } else { console.warn(`WARNING: Unknown codec ${fullCodec}`); } } if (!vcodec && !acodec) { if (splitCodecs.length === 2) { return { vcodec: splitCodecs[0], acodec: splitCodecs[1] }; } } else { return { vcodec: vcodec, acodec: acodec }; } return {}; } request = async (method, url, data = null, headers = {}, local) => { if (local) { 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))}`) if (local) { return fetch(url, { "method": method, "headers": headers, "body": data, }).then(res => res.text()) } return new Promise((resolve, reject) => { AF.request(url, method, data, headers, (data, err) => { if (err) { console.log(`请求失败: ${err}`) reject(err); } else { resolve(data); } }); }) } getStringBetween = (string, needleStart, needleEnd, offsetStart = 0, offsetEnd = 0) => { const x = string.indexOf(needleStart); const y = needleEnd ? string.indexOf(needleEnd, x) : string.length; return string.substring(x + needleStart.length + offsetEnd, y + offsetStart); } getDecipherFunction = (string) => { const js = string.replace("var _yt_player={}", ""); const top = getStringBetween(js, `a=a.split("")`, "};", 1, -28); const beginningOfFunction = "var " + getStringBetween(top, `a=a.split("")`, "(", 10, 1).split(".")[0] + "="; const side = getStringBetween(js, beginningOfFunction, "};", 2, -beginningOfFunction.length); console.log(`side: ${side}`); console.log(`top: ${top}`); return eval(side + top); }; const cache = {}; extractJSSignatureFunction = async (baseJsUrl, local) => { console.log(`解析baseUrl: ${baseJsUrl}`); const cacheKey = `js:${baseJsUrl}`; if (cache[cacheKey]) { console.log(`从缓存中获取JSSignatureFunction: ${baseJsUrl}`); return cache[cacheKey]; } 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', } const baseContent = await request('GET', baseJsUrl, null, headers, local); const decipher = getDecipherFunction(baseContent); if (decipher) { cache[cacheKey] = decipher; } return decipher; } getUrlFromSignature = async (signatureCipher, baseJsUrl, local) => { const decipher = await extractJSSignatureFunction(baseJsUrl, local); 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"]]; console.log(signatureCipher, url, signature, sp); return `${url}&${sp}=${decipher(signature)}`; } detail = async (url, local) => { try { console.log(`接受到解析请求: ${url}`); 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', } const html = await request('GET', url, null, headers, local); let regex = /var ytInitialPlayerResponse\s*=\s*({.*?});/; let match = html.match(regex); if (!match || !match.length) { console.log("解释失败: 无法找到json"); throw new Error('JSON not found.'); } const ytInitialPlayerResponse = JSON.parse(match[1]); const originVideoDetails = ytInitialPlayerResponse["videoDetails"]; console.log(`videoDetails: ${JSON.stringify(originVideoDetails)}`); const thumbnails = [] for (const item of originVideoDetails["thumbnail"]["thumbnails"]) { thumbnails.push({ "url": item["url"], "width": item["width"] + "", "height": item["height"] + "" }) } const formats = [] 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); } 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]); 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"] }) } } } const videoDetails = { "isLiveContent": originVideoDetails["isLiveContent"], "title": originVideoDetails["title"], "thumbnails": thumbnails, "description": originVideoDetails["shortDescription"], "lengthSeconds": originVideoDetails["lengthSeconds"], "viewCount": originVideoDetails["viewCount"], "keywords": originVideoDetails["keywords"], "author": originVideoDetails["author"], "channelID": originVideoDetails["channelId"], "recommendInfo": recommendInfo, "channelURL": `https://www.youtube.com/channel/${originVideoDetails["channelId"]}`, "videoId": originVideoDetails["videoId"] } const ret = { "code": 200, "msg": "", "data": { "videoDetails": videoDetails, "streamingData": { "formats": formats } }, "id": "MusicDetailViewModel_detail_url" } console.log(`解析结果: ${JSON.stringify(ret)}`); return ret; } catch (e) { const ret = { "code": -1, "msg": e.toString() } console.log(`解析失败: ${JSON.stringify(ret)}`); return ret; } } search = async (keyword, next, local) => { try { console.log(`接受到搜索请求 keyword: ${keyword}`); console.log(`接收到搜索请求 next: ${next}`); if (next) { const nextObject = JSON.parse(next); const key = nextObject["key"]; const body = { context: { client: { clientName: "WEB", clientVersion: "2.20240506.01.00", }, }, continuation: nextObject["continuation"] }; let res = await request('POST', `https://www.youtube.com/youtubei/v1/search?key=${key}`, JSON.stringify(body), {}, local); res = JSON.parse(res); const videos = []; for (const item of res["onResponseReceivedCommands"][0]["appendContinuationItemsAction"]["continuationItems"][0]["itemSectionRenderer"]["contents"]) { const video = item["videoRenderer"]; console.log(`搜索结果: ${JSON.stringify(video)}`); if (video && video["videoId"]) { videos.push({ "type": "videoWithContextRenderer", "data": { "videoId": video["videoId"], "title": video["title"]?.["runs"]?.[0]?.["text"], "thumbnails": video["thumbnail"]?.["thumbnails"], "channelName": video["longBylineText"]?.["runs"]?.[0]?.["text"], "publishedTimeText": video["publishedTimeText"]?.["simpleText"], "viewCountText": video["viewCountText"]?.["simpleText"], "shortViewCountText": video["shortViewCountText"]?.["simpleText"], "lengthText": video["lengthText"]?.["simpleText"] } }); } } const ret = { "code": 200, "msg": "", "data": { "data": videos, "next": JSON.stringify({ "key": nextObject["key"], "continuation": res["onResponseReceivedCommands"][0]["appendContinuationItemsAction"]["continuationItems"][1]["continuationItemRenderer"]["continuationEndpoint"]["continuationCommand"]["token"], }), }, "id": "MusicSearchResultViewModel_search_result" } console.log(`携带next搜索结果成功: ${JSON.stringify(ret)}`); return ret; } else { let url = `https://www.youtube.com/results?q=${encodeURIComponent(keyword)}&sp=EgIQAQ%253D%253D`; const html = await request('GET', url, null, {}, local); let regex = /var ytInitialData\s*=\s*({.*?});/; let match = html.match(regex); if (!match || !match.length) { console.log("搜索失败:无法找到ytInitialData"); throw new Error('JSON not found.'); } const ytInitialDataResp = JSON.parse(match[1]); const videos = []; 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)}`); if (video && video["videoId"]) { videos.push({ "type": "videoWithContextRenderer", "data": { "videoId": video["videoId"], "title": video["title"]?.["runs"]?.[0]?.["text"], "thumbnails": video["thumbnail"]?.["thumbnails"], "channelName": video["longBylineText"]?.["runs"]?.[0]?.["text"], "publishedTimeText": video["publishedTimeText"]?.["simpleText"], "viewCountText": video["viewCountText"]?.["simpleText"], "shortViewCountText": video["shortViewCountText"]?.["simpleText"], "lengthText": video["lengthText"]?.["simpleText"] } }); } } } let next = {}; if (html.split("innertubeApiKey").length > 0) { next["key"] = html .split("innertubeApiKey")[1] .trim() .split(",")[0] .split('"')[2]; } next["continuation"] = ytInitialDataResp["contents"]["twoColumnSearchResultsRenderer"]["primaryContents"]["sectionListRenderer"]["contents"][1]["continuationItemRenderer"]["continuationEndpoint"]["continuationCommand"]["token"] const ret = { "code": 200, "msg": "", "data": { "data": videos, "next": JSON.stringify(next), }, "id": "MusicSearchResultViewModel_search_result" } console.log(`未携带next搜索结果成功: ${JSON.stringify(ret)}`); return ret; } } catch (e) { const ret = { "code": -1, "msg": e.toString() } console.log(`搜索失败: ${JSON.stringify(ret)}`); return ret; } }