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)
                });
            }
        });
    })
}

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);
}

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]+$/.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]+\)\{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 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"]
                        }
                        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(`detail 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"]) {
                    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]);
            const videos = [];
            for (const item of ytInitialDataResp["contents"]?.["twoColumnSearchResultsRenderer"]?.["primaryContents"]?.["sectionListRenderer"]?.["contents"]?.[0]?.["itemSectionRenderer"]?.["contents"]) {
                if (item["videoRenderer"]) {
                    const video = item["videoRenderer"];
                    if (printable(platform)) {
                        console.log(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(`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;
    }
}
