// YouTube 视频信息提取器 // 处理网络请求 request = async (method, url, data = null, headers = {}, requestId, platform) => { 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") { 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, requestId, (data, headers, err) => { if (err) { console.log(`request error: ${err}`); reject(err); } else { console.log(`response headers: ${headers}`); resolve({ 'data': data, 'headers': JSON.parse(headers) }); } }); }); } // 解析视频编码信息 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.log(`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 {}; } // 从播放器JS中提取解密函数 async function extractDecryptFunction(playerUrl, requestId, platform) { // 函数内部缓存 const cache = extractDecryptFunction.cache || (extractDecryptFunction.cache = {}); const cacheKey = `jsFunction:${playerUrl}`; if (cache[cacheKey]) { console.log(`从缓存获取解密函数: ${playerUrl}`); return cache[cacheKey]; } const playerResp = await request('GET', playerUrl, null, {}, requestId, platform); const playerJs = playerResp.data; // 提取签名函数名 const signatureFunctionName = playerJs.match(/\bc\s*&&\s*d\.set\([^,]+\s*,\s*\([^)]*\)\s*=>\s*([a-zA-Z$_][a-zA-Z$_0-9]*)\(/)[1]; // 提取ncode函数名 const ncodeFunctionName = playerJs.match(/\bc\s*&&\s*d\.set\([^,]+\s*,\s*\([^)]*\)\s*=>\s*([a-zA-Z$_][a-zA-Z$_0-9]*)\(\))/)[1]; // 提取函数定义 const functionPattern = new RegExp(`${signatureFunctionName}=function\\(\\w+\\)\\{[^\\}]+\\}`); const signatureFunction = playerJs.match(functionPattern)[0]; const ncodeFunctionPattern = new RegExp(`${ncodeFunctionName}=function\\(\\)\\{[^\\}]+\\}`); const ncodeFunction = playerJs.match(ncodeFunctionPattern)[0]; // 存入函数内部缓存 const result = { signatureFunction, ncodeFunction }; cache[cacheKey] = result; return result; } // 解析并执行解密函数 function executeDecryptFunction(code, input) { const fn = new Function('a', code.replace(/^[^=]+=function/, 'return function')); return fn(input); } // 解密签名 decryptSignature = async (signatureEncrypted, playerUrl, requestId, platform) => { try { // 提取解密函数 const {signatureFunction, ncodeFunction} = await extractDecryptFunction(playerUrl, requestId, platform); // 执行签名解密 const decryptedSignature = executeDecryptFunction(signatureFunction, signatureEncrypted); // 执行ncode处理 const ncode = executeDecryptFunction(ncodeFunction, ''); return { signature: decryptedSignature, ncode: ncode }; } catch (e) { console.error('签名解密失败:', e); return { signature: signatureEncrypted, ncode: '' }; } } // 获取视频详情 detail = async (url, requestId, platform) => { try { // 获取视频页面 HTML 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', 'Accept-Encoding': 'gzip, deflate, br', }, requestId, platform); let {data: html, headers: htmlHeaders} = htmlResp; // 解析初始播放器响应 const playerMatch = html.match(/var ytInitialPlayerResponse\s*=\s*({.*?});/); if (!playerMatch) { throw new Error('无法找到播放器数据'); } const ytInitialPlayerResponse = JSON.parse(playerMatch[1]); const originVideoDetails = ytInitialPlayerResponse['videoDetails']; // 获取推荐视频 const recommendInfo = []; const ytInitialDataMatch = html.match(/var ytInitialData\s*=\s*({.*?});/); if (ytInitialDataMatch) { const ytInitialData = JSON.parse(ytInitialDataMatch[1]); const recommendations = ytInitialData.contents?.twoColumnWatchNextResults?.secondaryResults?.secondaryResults?.results || []; for (const item of recommendations) { if (item.compactVideoRenderer) { const video = item.compactVideoRenderer; if (video.videoId) { recommendInfo.push({ type: "gridVideoRenderer", videoId: video.videoId, title: video.title?.simpleText, 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 formats = []; const qualities = []; // 从 HTML 中获取格式 const streamingData = ytInitialPlayerResponse.streamingData; const allFormats = [ ...(streamingData.formats || []), ...(streamingData.adaptiveFormats || []) ]; for (const format of allFormats) { if (format.height && parseInt(format.height) >= 720) { continue; } if (format && !qualities.includes(format.qualityLabel)) { const {vcodec, acodec} = parseCodecs(format); let finalUrl = format.url; if (!finalUrl && format.signatureCipher) { const urlParams = new URLSearchParams(format.signatureCipher); const url = urlParams.get('url'); const s = urlParams.get('s'); if (url && s) { const playerUrl = `https://www.youtube.com${html.match(/"(?:PLAYER_JS_URL|jsUrl)"\s*:\s*"([^"]+)"/)?.at(1)}`; const {signature, ncode} = await decryptSignature(s, playerUrl, requestId, platform); finalUrl = `${url}&sig=${signature}&n=${ncode}`; } } if (finalUrl && vcodec && acodec) { formats.push({ width: format.width + "", height: format.height + "", type: format.mimeType, quality: format.qualityLabel, itag: format.itag, fps: format.fps + "", bitrate: format.bitrate + "", ext: "mp4", vcodec: vcodec, acodec: acodec, vbr: "0", abr: "0", container: "mp4_dash", from: "web", url: format.url, videoUrl: "", audioUrl: "" }); qualities.push(format.qualityLabel); } } } // 按高度排序 formats.sort((a, b) => parseInt(a.height) - parseInt(b.height)); // 构建缩略图列表 const thumbnails = originVideoDetails.thumbnail.thumbnails.map(item => ({ url: item.url, width: item.width + "", height: item.height + "" })); // 构建视频详情 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: url.replace('https://www.youtube.com/watch?v=', '') }; return { code: 200, msg: "", requestId: requestId, data: { videoDetails: videoDetails, streamingData: { formats: formats } }, id: "MusicDetailViewModel_detail_url" }; } catch (e) { console.error(e); return { code: -1, msg: e.toString(), requestId: requestId }; } }