Browse Source

Mod: filter youtube live

Ben 1 month ago
parent
commit
96a9b70e08

File diff suppressed because it is too large
+ 0 - 0
js/pornhub.bundle.js


File diff suppressed because it is too large
+ 0 - 0
js/tiktok.bundle.js


File diff suppressed because it is too large
+ 0 - 0
js/xvideos.bundle.js


File diff suppressed because it is too large
+ 0 - 0
js/youtube.bundle.js


+ 18 - 17
js/youtube/test.js

@@ -6,26 +6,27 @@
 //         console.log(e);
 //     })
 
-detail(`https://www.youtube.com/watch?v=b1kbLwvqugk`, '1111', 'WEB')
-    .then(res => {
-        console.log(res);
-    })
-    .catch(e => {
-        console.log(e);
-    })
-
-// search("billboard pop songs 2024", null, "WEB")
+// detail(`https://www.youtube.com/watch?v=b1kbLwvqugk`, '1111', 'WEB')
 //     .then(res => {
 //         console.log(res);
-//
-//         search("trump gets mercilessly booed at speech", res["data"]["next"], "WEB")
-//             .then(res => {
-//                 console.log(res);
-//             })
-//             .catch(e => {
-//                 console.log(e);
-//             })
 //     })
 //     .catch(e => {
 //         console.log(e);
 //     })
+
+query = "Billboard Pop Songs 2025 Playlist +"
+search(query, null, "", "WEB")
+    .then(res => {
+        console.log(res);
+
+        search(query, res["data"]["next"], "", "WEB")
+            .then(res => {
+                console.log(res);
+            })
+            .catch(e => {
+                console.log(e);
+            })
+    })
+    .catch(e => {
+        console.log(e);
+    })

+ 316 - 0
js/youtube/youtube_extractor.js

@@ -0,0 +1,316 @@
+// 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
+        };
+    }
+}

+ 13 - 2
js/youtube/youtubev1.js

@@ -368,6 +368,17 @@ detail = async (url, requestId, platform) => {
 }
 
 search = async (keyword, next, requestId, platform) => {
+    isLiveContent = video => {
+        const badgets = video["badges"]
+        if (badgets && badgets.length > 0) {
+            for (const badge of badgets) {
+                if (badge["metadataBadgeRenderer"] && badge["metadataBadgeRenderer"]["style"] === "BADGE_STYLE_TYPE_LIVE_NOW") {
+                    return true
+                }
+            }
+        }
+        return false
+    }
     try {
         console.log(`search keyword: ${keyword}`);
         console.log(`search next: ${next}`);
@@ -392,7 +403,7 @@ search = async (keyword, next, requestId, platform) => {
                 if (printable(platform)) {
                     console.log(video);
                 }
-                if (video && video["videoId"] && video["lengthText"]) {
+                if (video && video["videoId"] && video["lengthText"] && !isLiveContent(video)) {
                     videos.push({
                         "type": "videoWithContextRenderer",
                         "data": {
@@ -451,7 +462,7 @@ search = async (keyword, next, requestId, platform) => {
                             if (printable(platform)) {
                                 console.log(video);
                             }
-                            if (video && video["videoId"] && video["lengthText"]) {
+                            if (video && video["videoId"] && video["lengthText"] && !isLiveContent(video)) {
                                 videos.push({
                                     "type": "videoWithContextRenderer",
                                     "data": {

Some files were not shown because too many files changed in this diff