console.log('v1') printable = (platform) => { return platform === "WEB"; } 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 {}; } 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 = {}, requestId, platform) => { // 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}`) console.log(`request headers:${JSON.stringify((headers))}`) if (platform === "WEB" || platform === "LOCAL") { 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) }); } }); }) } detail = async (url, requestId, platform) => { try { // fetch recommend const recommendInfo = []; 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' }, requestId, platform); let {data: html, headers: htmlHeaders} = htmlResp; let regex = /var ytInitialPlayerResponse\s*=\s*({.*?});/; let match = html.match(regex); if (match) { const ytInitialPlayerResponse = JSON.parse(match[1]); console.log(ytInitialPlayerResponse); const originVideoDetails = ytInitialPlayerResponse['videoDetails']; const ytInitialDataMatch = html.match(/var ytInitialData\s*=\s*({.*?});/); if (ytInitialDataMatch && ytInitialDataMatch.length === 2) { const ytInitialData = JSON.parse(ytInitialDataMatch[1]); 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"] }) } } } } } // android let thumbnails = []; let originFormats = []; let originVideoDetails = undefined; try { const apiUrl = `https://www.youtube.com/youtubei/v1/player`; const apiResp = await request('POST', apiUrl, JSON.stringify({ "context": { "client": { "clientName": "IOS", "clientVersion": "19.29.1", "deviceMake": "Apple", "deviceModel": "iPhone16,2", "hl": "en", "osName": "iPhone", "osVersion": "17.5.1.21F90", "timeZone": "UTC", "userAgent": "com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X;)", "gl": "US", "utcOffsetMinutes": 0 } }, "videoId": url.replace('https://www.youtube.com/watch?v=', '') }), { 'Host': 'www.youtube.com', 'Connection': 'keep-alive', 'User-Agent': 'com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X;)', 'Accept-Language': 'en-US,en', 'Cookie': parseSetCookie(htmlHeaders), 'Content-Type': 'application/json', 'X-YouTube-Client-Version': '19.29.1' }, requestId, platform); let {data: apiData, _} = apiResp; console.log(`android api result: ${JSON.stringify(apiResp)}`); console.log(`${JSON.stringify(apiResp)}`); const res = JSON.parse(apiData); const currentFormats = []; originVideoDetails = res["videoDetails"]; 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}`); const ret = { "code": -1, "msg": e.toString(), "requestId": requestId } console.log(`detail2 result error: ${JSON.stringify(ret)}`); return ret; } console.log(`after android api, format size:${originFormats.length}`); let audioUrl = "" for (let format of originFormats) { if (format["url"]) { const {vcodec, acodec} = parseCodecs(format) if (!vcodec && acodec) { audioUrl = format["url"] break } } } let itags = []; const formats = []; for (let format of originFormats) { if (printable(platform)) { console.log(format); } if (format && itags.indexOf(format['itag']) === -1) { if (format["url"]) { const {vcodec, acodec} = parseCodecs(format) if (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"], "source": format, "audioUrl": audioUrl, "videoUrl": format["url"] } formats.push(current) itags.push(format["itag"]); } } } } for (const item of originVideoDetails['thumbnail']['thumbnails']) { thumbnails.push({ 'url': item['url'], 'width': item['width'] + "", 'height': item['height'] + "" }) } 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", "requestId": requestId } console.log(`detail result: ${JSON.stringify(ret)}`); return ret; } catch (e) { const ret = { "code": -1, "msg": e.toString(), "requestId": requestId } console.log(`detail2 result error: ${JSON.stringify(ret)}`); console.log(e); return ret; } } search = async (keyword, next, requestId, 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), {}, requestId, 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", "requestId": requestId } 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, {}, requestId, 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", "requestId": requestId } console.log(`unnext search result: ${JSON.stringify(ret)}`); return ret; } } catch (e) { const ret = { "code": -1, "msg": e.toString(), "requestId": requestId } console.log(`search result error: ${JSON.stringify(ret)}`); return ret; } }