Ben 7 months ago
parent
commit
224e3e27c7
3 changed files with 605 additions and 8 deletions
  1. 17 0
      js/nginx.conf
  2. 580 0
      js/youtube/youtube.bak.js
  3. 8 8
      js/youtube/youtubev1.js

+ 17 - 0
js/nginx.conf

@@ -74,4 +74,21 @@ http {
             proxy_pass https://www.xvideos.com/;
         }
     }
+
+        server {
+            listen       83;
+            proxy_hide_header 'Vary';
+            add_header 'Access-Control-Allow-Origin' '*' always;
+            add_header 'Access-Control-Allow-Methods' '*' always;
+            add_header 'Access-Control-Allow-Headers' '*' always;
+            location / {
+                if ($request_method = OPTIONS) {
+                    add_header 'Access-Control-Allow-Origin' '*' always;
+                    add_header 'Access-Control-Allow-Methods' '*' always;
+                    add_header 'Access-Control-Allow-Headers' '*' always;
+                    return 204;
+                }
+                proxy_pass https://music.youtube.com/;
+            }
+        }
 }

+ 580 - 0
js/youtube/youtube.bak.js

@@ -0,0 +1,580 @@
+console.log('bundle2!')
+
+printable = (platform) => {
+    return platform === "WEB";
+}
+
+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 {};
+}
+
+parseSetCookie = (headers) => {
+    if (!headers) {
+        return ""
+    }
+    const setCookie = headers['Set-Cookie']
+    if (!setCookie) {
+        return ""
+    }
+    console.log(`setCookie: ${setCookie}`)
+    let result = 'PREF=hl=en&tz=UTC; SOCS=CAI; GPS=1; ';
+    const needCookieNames = ['YSC', 'VISITOR_INFO1_LIVE', 'VISITOR_PRIVACY_METADATA'];
+    for (const i in needCookieNames) {
+        const cookieName = needCookieNames[i];
+        const regexp = new RegExp(`${cookieName}=([^;,]+)`)
+        const match = setCookie.match(regexp)
+        if (match && match.length === 2) {
+            const cookieValue = match[1]
+            if (i != needCookieNames.length - 1) {
+                result += `${cookieName}=${cookieValue}; `
+            } else {
+                result += `${cookieName}=${cookieValue}`
+            }
+        }
+    }
+    console.log(`current cookie: ${result}`)
+    return result;
+}
+
+request = async (method, url, data = null, headers = {}, platform) => {
+    if (platform === "WEB") {
+        url = url.replace("https://www.youtube.com/", "http://127.0.0.1:80/");
+        url = url.replace("https://music.youtube.com/", "http://127.0.0.1:80/");
+    }
+    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, (data, headers, err) => {
+            if (err) {
+                reject(err);
+            } else {
+                console.log(`response headers: ${headers}`);
+                resolve({
+                    'data': data,
+                    'headers': JSON.parse(headers)
+                });
+            }
+        });
+    })
+}
+
+findFunction = (jsCode, regexp, platform) => {
+    const match = jsCode.match(regexp)
+    if (!match && match.length <= 1) {
+        return null;
+    }
+    let result = "";
+    const dependencyMatches = match[0].match(/([$a-zA-Z0-9]+\.[$a-zA-Z0-9]+)/g)
+    const existDependencies = [];
+    if (dependencyMatches && dependencyMatches.length >= 1) {
+        for (let currentMatch of dependencyMatches) {
+            const varName = currentMatch.split('.')[0];
+            if (existDependencies.includes(varName)) {
+                continue
+            }
+            if (!/^[$A-Z|a-z]{2,}$/.test(varName)) {
+                continue
+            }
+            let reg = "var (\$)?" + varName + "={(.|\\n)*?};"
+            const varNameMatch = jsCode.match(new RegExp(reg), 'ig');
+            if (varNameMatch && varNameMatch.length >= 1) {
+                result += varNameMatch[0] + "\n";
+            }
+            existDependencies.push(varName);
+        }
+    }
+    result += `\n${match[0]}`;
+    if (printable(platform)) {
+        console.log(`findFunction result: ` + result);
+    }
+    return eval(result);
+};
+
+const cache = {};
+
+fetchBaseJSContent = async (baseJsUrl, platform) => {
+    const cacheKey = `jsContent:${baseJsUrl}`;
+    if (cache[cacheKey]) {
+        console.log(`baseContent from cache: ${baseJsUrl}`);
+        return cache[cacheKey];
+    }
+    console.log(`extract baseUrl: ${baseJsUrl}`);
+    const baseContentResp = await request('GET', baseJsUrl, null, {
+        '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',
+    }, platform);
+    const {data, _} = baseContentResp;
+    cache[cacheKey] = data;
+    return data;
+}
+
+extractJSSignatureFunction = async (baseJsUrl, platform) => {
+    const cacheKey = `jsSign:${baseJsUrl}`
+    if (cache[cacheKey]) {
+        console.log(`jsSign from cache: ${baseJsUrl}`);
+        return cache[cacheKey];
+    }
+    const baseJsContent = await fetchBaseJSContent(baseJsUrl, platform);
+    const result = findFunction(baseJsContent, /([a-zA-Z0-9]+)=function\([a-zA-Z0-9]+\)\{a=a\.split\(""\).*};/, platform);
+    cache[cacheKey] = result
+    return result
+}
+
+extractNJSFunction = async (baseJsUrl, platform) => {
+    const cacheKey = `jsN:${baseJsUrl}`
+    if (cache[cacheKey]) {
+        console.log(`jsN from cache: ${baseJsUrl}`);
+        return cache[cacheKey];
+    }
+    const baseJsContent = await fetchBaseJSContent(baseJsUrl, platform);
+    const result = findFunction(baseJsContent, /([a-zA-Z0-9]+)=function\([a-zA-Z0-9]+\)\{try{var b=a\.split\(","\)[\s\S]*?};/, platform);
+    cache[cacheKey] = result
+    return result
+}
+
+signUrl = async (signatureCipher, baseJsUrl, platform) => {
+    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']];
+    const decipher = await extractJSSignatureFunction(baseJsUrl, platform);
+    if (!decipher) {
+        return null;
+    }
+    if (printable(platform)) {
+        console.log(`signatureCipher=${signatureCipher}, url=${url}, signature=${signature}, sp=${sp}`)
+    }
+    let newUrl = `${url}&${sp}=${decipher(signature)}`;
+
+    function replaceUrlParam(url, paramName, paramValue) {
+        let pattern = new RegExp(`([?&])${paramName}=.*?(&|$)`, 'i');
+        let newUrl = url.replace(pattern, `$1${paramName}=${paramValue}$2`);
+
+        if (newUrl === url && url.indexOf('?') === -1) {
+            newUrl += `?${paramName}=${paramValue}`;
+        } else if (newUrl === url) {
+            newUrl += `&${paramName}=${paramValue}`;
+        }
+        return newUrl;
+    }
+
+    for (const item of url.split('&')) {
+        const [key, value] = item.split('=');
+        searchParams[decodeURIComponent(key)] = decodeURIComponent(value);
+    }
+
+    const nFunction = await extractNJSFunction(baseJsUrl, platform);
+    const n = searchParams['n']
+    if (n && nFunction) {
+        const newN = nFunction(n);
+        return replaceUrlParam(newUrl, 'n', newN);
+    }
+    return newUrl;
+}
+
+detail = async (url, platform) => {
+    try {
+        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',
+            'Cookie': 'PREF=hl=en&tz=UTC; SOCS=CAI'
+        }, platform);
+        let {data: html, headers: htmlHeaders} = htmlResp;
+
+        let regex = /var ytInitialPlayerResponse\s*=\s*({.*?});/;
+        let match = html.match(regex);
+        if (!match || !match.length) {
+            console.log('can not found JSON: ytInitialPlayerResponse');
+            throw new Error('JSON not found: ytInitialPlayerResponse');
+        }
+        const ytInitialPlayerResponse = JSON.parse(match[1]);
+        if (printable(platform)) {
+            console.log(ytInitialPlayerResponse);
+        }
+        const originVideoDetails = ytInitialPlayerResponse['videoDetails'];
+        const thumbnails = []
+        for (const item of originVideoDetails['thumbnail']['thumbnails']) {
+            thumbnails.push({
+                'url': item['url'],
+                'width': item['width'] + "",
+                'height': item['height'] + ""
+            })
+        }
+
+        let originFormats = [];
+
+        // android
+        try {
+            const apiUrl = `https://music.youtube.com/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8`;
+            const apiResp = await request('POST', apiUrl, JSON.stringify({
+                "context": {
+                    "client": {
+                        "clientName": "ANDROID",
+                        "hl": "en",
+                        "clientVersion": "18.49.37",
+                        "gl": "US"
+                    }
+                },
+                "videoId": url.replace('https://www.youtube.com/watch?v=', ''),
+                "params": "CgIQBg"
+            }), {
+                'Host': 'www.youtube.com',
+                'Connection': 'keep-alive',
+                'User-Agent': 'com.google.android.apps.youtube.music/17.31.35 (Linux; U; Android 11) gzip',
+                'Accept-Language': 'en-US,en',
+                'Cookie': parseSetCookie(htmlHeaders),
+                'Content-Type': 'application/json'
+            }, platform);
+            let {data: apiData, _} = apiResp;
+            console.log(`android api result: ${JSON.stringify(apiResp)}`);
+            const res = JSON.parse(apiData);
+            const currentFormats = [];
+            for (const format of [].concat(res["streamingData"]["formats"]).concat(res["streamingData"]["adaptiveFormats"])) {
+                if (format) {
+                    format["from"] = "android"
+                    currentFormats.push(format);
+                }
+            }
+            originFormats = originFormats.concat(currentFormats);
+        } catch (e) {
+            console.log(`can not found format android api error: ${e}`);
+        }
+        console.log(`after android api, format size:${originFormats.length}`);
+
+        // web
+        const currentFormats = [];
+        for (const format of ytInitialPlayerResponse["streamingData"]["formats"].concat(ytInitialPlayerResponse["streamingData"]["adaptiveFormats"])) {
+            if (format) {
+                format["from"] = "web"
+                currentFormats.push(format);
+            }
+        }
+        originFormats = originFormats.concat(currentFormats);
+        console.log(`after html, format size:${originFormats.length}`);
+
+        const baseJsUrl = `https://www.youtube.com${JSON.parse(html.match(/set\(({.+?})\);/)[1])["PLAYER_JS_URL"]}`
+        let audioUrl = ""
+        for (let format of originFormats) {
+            if (!format["url"]) {
+                format["url"] = await signUrl(format["signatureCipher"], baseJsUrl, platform);
+            }
+            if (format["url"]) {
+                const {vcodec, acodec} = parseCodecs(format)
+                if (!vcodec && acodec) {
+                    audioUrl = format["url"]
+                    break
+                }
+            }
+        }
+
+        let formatIds = [];
+        const formats = [];
+        for (let format of originFormats) {
+            if (printable(platform)) {
+                console.log(format);
+            }
+            if (format && formatIds.indexOf(format['itag']) === -1) {
+                if (!format["url"]) {
+                    format["url"] = await signUrl(format["signatureCipher"], baseJsUrl, platform);
+                }
+                if (format["url"]) {
+                    const {vcodec, acodec} = parseCodecs(format)
+                    if (vcodec && acodec) {
+                        const current = {
+                            "width": format["width"] + "",
+                            "height": format["height"] + "",
+                            "type": format["mimeType"],
+                            "quality": format["qualityLabel"],
+                            "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",
+                            "from": format["from"],
+                            "audioUrl": audioUrl
+                        }
+                        if (platform === "WEB") {
+                            current["source"] = format
+                        }
+                        formats.push(current)
+                        formatIds.push(format["itag"]);
+                    }
+                }
+            }
+        }
+
+        const ytInitialDataMatch = html.match(/var ytInitialData\s*=\s*({.*?});/);
+        const recommendInfo = [];
+        if (ytInitialDataMatch && ytInitialDataMatch.length === 2) {
+            const ytInitialData = JSON.parse(ytInitialDataMatch[1]);
+            if (printable(platform)) {
+                console.log(ytInitialData);
+            }
+            for (const item of ytInitialData["contents"]?.["twoColumnWatchNextResults"]?.["secondaryResults"]?.["secondaryResults"]?.["results"] || []) {
+                if (item["compactVideoRenderer"]) {
+                    const recommendVideo = item["compactVideoRenderer"];
+                    console.log(`recommend video: ${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"]
+                        })
+                    }
+                }
+            }
+        }
+
+        formats.sort((a, b) => parseInt(a["height"]) - parseInt(b["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": originVideoDetails["videoId"]
+        }
+        const ret = {
+            "code": 200,
+            "msg": "",
+            "data": {
+                "videoDetails": videoDetails,
+                "streamingData": {
+                    "formats": formats
+                }
+            },
+            "id": "MusicDetailViewModel_detail_url"
+        }
+        console.log(`detail result: ${JSON.stringify(ret)}`);
+        return ret;
+    } catch (e) {
+        const ret = {
+            "code": -1,
+            "msg": e.toString()
+        }
+        console.log(`detail2 result error: ${JSON.stringify(ret)}`);
+        console.log(e);
+        return ret;
+    }
+}
+
+search = async (keyword, next, platform) => {
+    try {
+        console.log(`search keyword: ${keyword}`);
+        console.log(`search 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), {}, platform);
+            const {data, _} = res;
+            res = JSON.parse(data);
+            const videos = [];
+            for (const item of res["onResponseReceivedCommands"][0]["appendContinuationItemsAction"]["continuationItems"][0]["itemSectionRenderer"]["contents"]) {
+                const video = item["videoRenderer"];
+                if (printable(platform)) {
+                    console.log(video);
+                }
+                if (video && video["videoId"] && video["lengthText"]) {
+                    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] search result: ${JSON.stringify(ret)}`);
+            return ret;
+        } else {
+            let url = `https://www.youtube.com/results?q=${encodeURIComponent(keyword)}&sp=EgIQAQ%253D%253D`;
+
+            const htmlRes = await request('GET', url, null, {}, platform);
+            const {data: html, _} = htmlRes;
+
+            let regex = /var ytInitialData\s*=\s*({.*?});/;
+            let match = html.match(regex);
+            if (!match || !match.length) {
+                console.log("can not found ytInitialData");
+                throw new Error('JSON not found: ytInitialData');
+            }
+
+            const ytInitialDataResp = JSON.parse(match[1]);
+            if (printable(platform)) {
+                console.log(ytInitialDataResp);
+            }
+            const videos = [];
+            const contents = ytInitialDataResp["contents"]?.["twoColumnSearchResultsRenderer"]?.["primaryContents"]?.["sectionListRenderer"]?.["contents"] || []
+            for (const content of contents) {
+                const currentContents = content["itemSectionRenderer"]?.["contents"]
+                if (Array.isArray(currentContents)) {
+                    for (const currentContent of currentContents) {
+                        if (currentContent["videoRenderer"]) {
+                            const video = currentContent["videoRenderer"];
+                            if (printable(platform)) {
+                                console.log(video);
+                            }
+                            if (video && video["videoId"] && video["lengthText"]) {
+                                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"] = contents[contents.length - 1]?.["continuationItemRenderer"]?.["continuationEndpoint"]?.["continuationCommand"]?.["token"]
+
+            const ret = {
+                "code": 200,
+                "msg": "",
+                "data": {
+                    "data": videos,
+                    "next": JSON.stringify(next),
+                },
+                "id": "MusicSearchResultViewModel_search_result"
+            }
+            console.log(`unnext search result: ${JSON.stringify(ret)}`);
+            return ret;
+        }
+    } catch (e) {
+        const ret = {
+            "code": -1,
+            "msg": e.toString()
+        }
+        console.log(`search result error: ${JSON.stringify(ret)}`);
+        return ret;
+    }
+}
+

+ 8 - 8
js/youtube/youtubev1.js

@@ -85,14 +85,14 @@ parseSetCookie = (headers) => {
 }
 
 request = async (method, url, data = null, headers = {}, requestId, platform) => {
-    if (platform === "WEB") {
-        url = url.replace("https://www.youtube.com/", "http://16.162.32.168:80/");
-        url = url.replace("https://music.youtube.com/", "http://16.162.32.168:80/");
-    }
-    if (platform === "LOCAL") {
-        url = url.replace("https://www.youtube.com/", "http://127.0.0.1:80/");
-        url = url.replace("https://music.youtube.com/", "http://127.0.0.1:80/");
-    }
+    // if (platform === "WEB") {
+    //     url = url.replace("https://www.youtube.com/", "http://43.198.215.27:80/");
+    //     url = url.replace("https://music.youtube.com/", "http://43.198.215.27:80/");
+    // }
+    // if (platform === "LOCAL") {
+    //     url = url.replace("https://www.youtube.com/", "http://127.0.0.1:80/");
+    //     url = url.replace("https://music.youtube.com/", "http://127.0.0.1:80/");
+    // }
     console.log(`request url:${url}`)
     console.log(`request data:${data}`)
     console.log(`request method:${method}`)