123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- // 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 = /(?<mimetype>[^/]+\/[^;]+)(?:;\s*codecs="?(?<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
- };
- }
- }
|