vrchat-youtube-search-api/youtube.js
2024-07-06 00:22:31 -07:00

275 lines
7.7 KiB
JavaScript

import { gotw } from "./util.js";
export async function searchYouTubeVideos(query, sp = "EgIQAQ%253D%253D") {
console.debug("sp", sp);
var url = `https://www.youtube.com/results?search_query=${encodeURIComponent(query.replaceAll(' ', '+'))}${sp ? `$sp=${sp}` : ''}`;
var res = await gotw(url), html = res.body;
var ytInitialData = html.match(/ytInitialData = ({.*});<\/script>/)?.[1];
if (!ytInitialData) {
console.error("missing ytInitialData", query, res.status, html);
}
ytInitialData = JSON.parse(ytInitialData);
console.debug(ytInitialData);
var sectionListRendererContents = ytInitialData
.contents
.twoColumnSearchResultsRenderer
.primaryContents
.sectionListRenderer
.contents;
var videos = sectionListRendererContents
?.find(x => x.itemSectionRenderer?.contents?.find(x => x.videoRenderer))
?.itemSectionRenderer
.contents
.filterMap(x => x.videoRenderer)
.map(parseVideoRendererData);
if (!videos) return {videos: []};
var latest = sectionListRendererContents
.find(x => x.itemSectionRenderer?.contents?.find(x => x.shelfRenderer))
?.itemSectionRenderer
.contents
.find(x => x.shelfRenderer?.title?.simpleText?.startsWith("Latest from"))
?.shelfRenderer
.content
.verticalListRenderer
.items
.filterMap(x => x.videoRenderer)
.map(parseVideoRendererData);
console.debug("latest", latest);
if (latest) videos = [...latest, ...videos];
console.debug(videos.length, "results");
try {
var ytcfg = html.match(/ytcfg.set\(({.*})\);/)[1];
ytcfg = JSON.parse(ytcfg);
var continuationData = {
context: ytcfg.INNERTUBE_CONTEXT,
continuation: sectionListRendererContents.findMap(x => x.continuationItemRenderer).continuationEndpoint.continuationCommand.token
}
} catch (error) {
console.error(error.stack);
}
return {videos, continuationData};
}
export async function continueYouTubeVideoSearch(continuationData) {
var res = await gotw("https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false", {
method: "POST",
json: continuationData,
responseType: "json"
});
var data = res.body;
console.debug(data);
var continuationItems = data
.onResponseReceivedCommands[0]
.appendContinuationItemsAction
.continuationItems;
var videos = continuationItems
.find(x => x.itemSectionRenderer?.contents.find(x => x.videoRenderer))
.itemSectionRenderer
.contents
.filterMap(x => x.videoRenderer)
.map(parseVideoRendererData);
var continuationToken = continuationItems
.findMap(x => x.continuationItemRenderer)
?.continuationEndpoint
.continuationCommand
.token;
console.debug(videos.length, "results");
return {
videos,
continuationData: continuationToken ? {
context: continuationData.context,
continuation: continuationToken
} : null
}
}
export async function getYouTubePlaylist(playlistId) {
var res = await gotw("https://www.youtube.com/playlist?list=" + playlistId);
var html = res.body;
var ytInitialData = html.match(/ytInitialData = ({.*});<\/script>/)[1];
ytInitialData = JSON.parse(ytInitialData);
console.debug(ytInitialData);
var sectionListRendererContents = ytInitialData
.contents
.twoColumnBrowseResultsRenderer
.tabs
.find(tab => tab.tabRenderer.selected)
.tabRenderer
.content
.sectionListRenderer
.contents;
var videos = sectionListRendererContents
.findMap(x => x.itemSectionRenderer?.contents)
.findMap(x => x.playlistVideoListRenderer?.contents)
.filterMap(x => x.playlistVideoRenderer)
.map(parseVideoRendererData);
if (!videos) return {videos: []};
console.debug(videos.length, "results");
try {
var ytcfg = html.match(/ytcfg.set\(({.*})\);/)[1];
ytcfg = JSON.parse(ytcfg);
var continuationData = {
context: ytcfg.INNERTUBE_CONTEXT,
continuation: sectionListRendererContents.findMap(x => x.continuationItemRenderer).continuationEndpoint.continuationCommand.token
}
} catch (error) {
console.error(error.stack);
}
return {videos, continuationData};
}
export async function continueYouTubePlaylist(continuationData) {
var res = await gotw("https://www.youtube.com/youtubei/v1/browse?prettyPrint=false", {
method: "POST",
json: continuationData,
responseType: "json"
});
var data = res.body;
console.debug(data);
if (!data.onResponseReceivedActions) return {videos:[]};
var continuationItems = data
.onResponseReceivedActions[0]
.appendContinuationItemsAction
.continuationItems;
var videos = continuationItems
.findMap(x => x.itemSectionRenderer?.contents)
.filterMap(x => x.playlistVideoListRenderer)
.map(parseVideoRendererData);
var continuationToken = continuationItems
.findMap(x => x.continuationItemRenderer)
?.continuationEndpoint
.continuationCommand
.token;
console.debug(videos.length, "results");
return {
videos,
continuationData: continuationToken ? {
context: continuationData.context,
continuation: continuationToken
} : null
}
}
export async function getTrending(bp) {
var url = `https://www.youtube.com/feed/trending`;
if (bp) url += `?bp=${bp}`;
var res = await gotw(url);
var html = res.body;
var ytInitialData = html.match(/ytInitialData = ({.*});<\/script>/)[1];
ytInitialData = JSON.parse(ytInitialData);
var tabs = ytInitialData.contents.twoColumnBrowseResultsRenderer.tabs.map(t => {
return {
name: t.tabRenderer.title,
//url: `https://www.youtube.com` + t.tabRenderer.endpoint.commandMetadata.webCommandMetadata.url
bp: t.tabRenderer.endpoint.browseEndpoint.params
}
});
var videos = ytInitialData
.contents
.twoColumnBrowseResultsRenderer
.tabs
.find(tab => tab.tabRenderer.selected)
.tabRenderer
.content
.sectionListRenderer
.contents
// regular trending in sections with shelfRenderer without title
.filterMap(x => {
var shelfRenderer = x.itemSectionRenderer.contents.findMap(x => x.shelfRenderer);
if (shelfRenderer && !shelfRenderer.title) {
return shelfRenderer
.content
.expandedShelfContentsRenderer
.items
.filterMap(x => x.videoRenderer)
.map(parseVideoRendererData)
};
})
.flat();
return {tabs, videos};
}
Object.prototype.concatRunsText = function concatRunsText() {
return this.reduce((str, obj) => str += obj.text, "");
};
function parseVideoRendererData(data) {
return {
id: data.videoId,
live: Boolean(data.badges?.find(x => x.metadataBadgeRenderer?.style == "BADGE_STYLE_TYPE_LIVE_NOW")),
title: data.title?.runs?.concatRunsText(),
description: data.detailedMetadataSnippets?.[0]?.snippetText?.runs?.concatRunsText()
|| data.descriptionSnippet?.runs?.concatRunsText(),
//thumbnailUrl: data.thumbnail?.thumbnails?.find(x => (x.width == 360 && x.height == 202) || (x.width == 246 && x.height == 138))?.url || data.thumbnail?.thumbnails?.[0]?.url,
thumbnail: data.thumbnail?.thumbnails?.find(x => (x.width == 360 && x.height == 202) || (x.width == 246 && x.height == 138)) || data.thumbnail?.thumbnails?.[0],
/*thumbnail: {
url: `https://i.ytimg.com/vi/${data.videoId}/mqdefault.jpg`,
width: 320,
height: 180
},*/
uploaded: data.publishedTimeText?.simpleText || data.videoInfo?.runs?.[2]?.text,
lengthText: data.lengthText?.simpleText,
longLengthText: data.lengthText?.accessibility?.accessibilityData?.label,
viewCountText: data.viewCountText?.runs ? data.viewCountText.runs.concatRunsText() : data.viewCountText?.simpleText,
shortViewCountText: data.shortViewCountText?.simpleText || data.videoInfo?.runs?.[0]?.text,
channel: {
name: (data.ownerText || data.shortBylineText)?.runs?.concatRunsText(),
id: (data.ownerText || data.shortBylineText)?.runs?.[0]?.navigationEndpoint?.browseEndpoint?.browseId,
iconUrl: data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails?.[0]?.url
}
};
}