Compare commits
	
		
			No commits in common. "5c00a0444b611a88057e0a81a59600d49b2cf477" and "d413e7d1b8aaf83b375112d7f196cd4b6e8759fc" have entirely different histories.
		
	
	
		
			5c00a0444b
			...
			d413e7d1b8
		
	
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,3 +1,2 @@
 | 
				
			|||||||
node_modules
 | 
					node_modules
 | 
				
			||||||
*.sqlite
 | 
					vrcurl.sqlite
 | 
				
			||||||
*.json
 | 
					 | 
				
			||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
import { searchYouTubeVideos, continueYouTubeVideoSearch } from "./simpleYoutubeSearch.js";
 | 
					import { searchYouTubeVideos, continueYouTubeVideoSearch } from "./simpleYoutubeSearch.js";
 | 
				
			||||||
import { putVrcUrl } from "./vrcurl.js";
 | 
					import { putVrcUrl } from "./vrcurl.js";
 | 
				
			||||||
import { makeImageSheetVrcUrl, thumbnailWidth, thumbnailHeight, iconWidth, iconHeight } from "./imagesheet.js";
 | 
					import { makeImageSheetVrcUrl, thumbnailWidth, thumbnailHeight, iconWidth, iconHeight } from "./imagesheet.js";
 | 
				
			||||||
import { getTrending } from "./trending.js";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
var cache = {};
 | 
					var cache = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -25,19 +24,8 @@ async function VRCYoutubeSearch(pool, query, options = {}) {
 | 
				
			|||||||
	console.debug("search:", JSON.stringify(query));
 | 
						console.debug("search:", JSON.stringify(query));
 | 
				
			||||||
	var data = {results: []};
 | 
						var data = {results: []};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (query == "trending") {
 | 
						var {videos, continuationData} = typeof query == "object" ? await continueYouTubeVideoSearch(query) : await searchYouTubeVideos(query);
 | 
				
			||||||
		var {videos, tabs} = await getTrending();
 | 
					
 | 
				
			||||||
		data.tabs = [];
 | 
					 | 
				
			||||||
		for (let tab of tabs) {
 | 
					 | 
				
			||||||
			data.tabs.push({
 | 
					 | 
				
			||||||
				name: tab.name,
 | 
					 | 
				
			||||||
				vrcurl: await putVrcUrl(pool, {type: "trending", url: tab.url})
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		var {videos, continuationData} = typeof query == "object" ? await continueYouTubeVideoSearch(query) : await searchYouTubeVideos(query);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	if (options.thumbnails) {
 | 
						if (options.thumbnails) {
 | 
				
			||||||
		var thumbnailUrls = videos.map(video => video.thumbnailUrl);
 | 
							var thumbnailUrls = videos.map(video => video.thumbnailUrl);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								app.js
									
									
									
									
									
								
							@ -11,8 +11,8 @@ export var app = new Koa();
 | 
				
			|||||||
var router = new Router();
 | 
					var router = new Router();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.get(["/search", "/trending"], async ctx => {
 | 
					router.get("/search", async ctx => {
 | 
				
			||||||
	var query = ctx.path == "/trending" ? "trending" : ctx.query.input?.replace(/^.*→/, '').trim();
 | 
						var query = ctx.query.input?.replace(/^.*→/, '').trim();
 | 
				
			||||||
	if (!query) {
 | 
						if (!query) {
 | 
				
			||||||
		ctx.status = 400;
 | 
							ctx.status = 400;
 | 
				
			||||||
		ctx.body = "missing search query";
 | 
							ctx.body = "missing search query";
 | 
				
			||||||
 | 
				
			|||||||
@ -11,10 +11,9 @@ export const iconHeight = 68;
 | 
				
			|||||||
const maxSheetWidth = 2048;
 | 
					const maxSheetWidth = 2048;
 | 
				
			||||||
const maxSheetHeight = 2048;
 | 
					const maxSheetHeight = 2048;
 | 
				
			||||||
const maxThumbnailRowLen = Math.floor(maxSheetWidth / thumbnailWidth); // 5
 | 
					const maxThumbnailRowLen = Math.floor(maxSheetWidth / thumbnailWidth); // 5
 | 
				
			||||||
//const maxThumbnailColLen = Math.floor(maxSheetHeight / thumbnailHeight); // 10
 | 
					const maxThumbnailColLen = Math.floor(maxSheetHeight / thumbnailHeight); // 10
 | 
				
			||||||
//const maxIconRowLen = Math.floor(maxSheetWidth / iconWidth); // 30
 | 
					const maxIconRowLen = Math.floor(maxSheetWidth / iconWidth); // 30
 | 
				
			||||||
const maxIconRowLen = 3;
 | 
					const maxIconColLen = Math.floor(maxSheetHeight / iconHeight); // 30
 | 
				
			||||||
//const maxIconColLen = Math.floor(maxSheetHeight / iconHeight); // 30
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function createImageSheet(thumbnailUrls = [], iconUrls = []) {
 | 
					async function createImageSheet(thumbnailUrls = [], iconUrls = []) {
 | 
				
			||||||
@ -26,19 +25,19 @@ async function createImageSheet(thumbnailUrls = [], iconUrls = []) {
 | 
				
			|||||||
		return {x, y, url};
 | 
							return {x, y, url};
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const iconStartX = thumbnailWidth * Math.min(maxThumbnailRowLen, thumbnails.length);
 | 
						const iconStartY = thumbnails.length ? thumbnails.at(-1).y + thumbnailHeight : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var icons = iconUrls.map((url, index) => {
 | 
						var icons = iconUrls.map((url, index) => {
 | 
				
			||||||
		const x = iconStartX + index % maxIconRowLen * iconWidth;
 | 
							const x = index % maxIconRowLen * iconWidth;
 | 
				
			||||||
		const y = Math.floor(index / maxIconRowLen) * iconHeight;
 | 
							const y = iconStartY + Math.floor(index / maxIconRowLen);
 | 
				
			||||||
		return {x, y, url};
 | 
							return {x, y, url};
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const canvasWidth = Math.max(
 | 
						const canvasWidth = Math.max(
 | 
				
			||||||
		Math.min(thumbnails.length, maxThumbnailRowLen) * thumbnailWidth,
 | 
							Math.min(thumbnails.length, maxThumbnailRowLen) * thumbnailWidth,
 | 
				
			||||||
		iconStartX + Math.min(icons.length, maxIconRowLen) * iconWidth
 | 
							Math.min(icons.length, maxIconRowLen) * iconWidth
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
	const canvasHeight = Math.max(thumbnails.length ? thumbnails.at(-1).y + thumbnailHeight : 0, icons.length ? icons.at(-1)?.y + iconHeight : 0);
 | 
						const canvasHeight = icons.length ? icons.at(-1).y + iconHeight : thumbnails.length ? thumbnails.at(-1).y + thumbnailHeight : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var canvas = createCanvas(Math.min(maxSheetWidth, canvasWidth), Math.min(maxSheetHeight, canvasHeight));
 | 
						var canvas = createCanvas(Math.min(maxSheetWidth, canvasWidth), Math.min(maxSheetHeight, canvasHeight));
 | 
				
			||||||
	var ctx = canvas.getContext('2d');
 | 
						var ctx = canvas.getContext('2d');
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								index.js
									
									
									
									
									
								
							@ -1,4 +1,3 @@
 | 
				
			|||||||
import "./util.js";
 | 
					 | 
				
			||||||
import { app } from "./app.js";
 | 
					import { app } from "./app.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.listen(process.env.PORT || 8142, process.env.ADDRESS);
 | 
					app.listen(process.env.PORT || 8142, process.env.ADDRESS);
 | 
				
			||||||
							
								
								
									
										1
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -4,6 +4,7 @@
 | 
				
			|||||||
	"requires": true,
 | 
						"requires": true,
 | 
				
			||||||
	"packages": {
 | 
						"packages": {
 | 
				
			||||||
		"": {
 | 
							"": {
 | 
				
			||||||
 | 
								"name": "vrchat-youtube-search-api",
 | 
				
			||||||
			"dependencies": {
 | 
								"dependencies": {
 | 
				
			||||||
				"@keyv/sqlite": "^3.6.6",
 | 
									"@keyv/sqlite": "^3.6.6",
 | 
				
			||||||
				"@koa/router": "^12.0.1",
 | 
									"@koa/router": "^12.0.1",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,3 @@
 | 
				
			|||||||
import { parseVideoRendererData } from "./util.js";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function searchYouTubeVideos(query) {
 | 
					export async function searchYouTubeVideos(query) {
 | 
				
			||||||
	var url = `https://www.youtube.com/results?search_query=${encodeURIComponent(query.replaceAll(' ', '+'))}&sp=EgIQAQ%253D%253D`;
 | 
						var url = `https://www.youtube.com/results?search_query=${encodeURIComponent(query.replaceAll(' ', '+'))}&sp=EgIQAQ%253D%253D`;
 | 
				
			||||||
@ -45,3 +44,26 @@ export async function continueYouTubeVideoSearch(continuationData) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function parseVideoRendererData(data) {
 | 
				
			||||||
 | 
						data = data.videoRenderer;
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							id: data.videoId,
 | 
				
			||||||
 | 
							live: Boolean(data.badges?.find(x => x.metadataBadgeRenderer?.style == "BADGE_STYLE_TYPE_LIVE_NOW")),
 | 
				
			||||||
 | 
							title: data.title?.runs?.[0]?.text,
 | 
				
			||||||
 | 
							description: data.detailedMetadataSnippets?.[0]?.snippetText?.runs?.reduce((str, obj) => str += obj.text, ""),
 | 
				
			||||||
 | 
							thumbnailUrl: data.thumbnail?.thumbnails?.find(x => x.width == 360 && x.height == 202)?.url || data.thumbnail?.thumbnails?.[0]?.url,
 | 
				
			||||||
 | 
							uploaded: data.publishedTimeText?.simpleText,
 | 
				
			||||||
 | 
							lengthText: data.lengthText?.simpleText,
 | 
				
			||||||
 | 
							longLengthText: data.lengthText?.accessibility?.accessibilityData?.label,
 | 
				
			||||||
 | 
							viewCountText: data.viewCountText?.runs ? data.viewCountText.runs.reduce((str, obj) => str += obj.text, "") : data.viewCountText?.simpleText,
 | 
				
			||||||
 | 
							shortViewCountText: data.shortViewCountText?.simpleText,
 | 
				
			||||||
 | 
							channel: {
 | 
				
			||||||
 | 
								name: data.ownerText?.runs?.[0]?.text,
 | 
				
			||||||
 | 
								id: data.ownerText?.runs?.[0]?.navigationEndpoint?.browseEndpoint?.browseId,
 | 
				
			||||||
 | 
								iconUrl: data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails?.[0]?.url
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										39
									
								
								trending.js
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								trending.js
									
									
									
									
									
								
							@ -1,39 +0,0 @@
 | 
				
			|||||||
import { parseVideoRendererData } from "./util.js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function getTrending() {
 | 
					 | 
				
			||||||
	var url = `https://www.youtube.com/feed/trending`;
 | 
					 | 
				
			||||||
	var html = await fetch(url).then(res => res.text());
 | 
					 | 
				
			||||||
	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
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var videos = ytInitialData
 | 
					 | 
				
			||||||
		.contents
 | 
					 | 
				
			||||||
		.twoColumnBrowseResultsRenderer
 | 
					 | 
				
			||||||
		.tabs[0] //Now
 | 
					 | 
				
			||||||
		.tabRenderer
 | 
					 | 
				
			||||||
		.content
 | 
					 | 
				
			||||||
		.sectionListRenderer
 | 
					 | 
				
			||||||
		.contents
 | 
					 | 
				
			||||||
		// regular trending in sections with shelfRenderer without title
 | 
					 | 
				
			||||||
		.filterMap(x => {
 | 
					 | 
				
			||||||
			var shelfRenderer = x.itemSectionRenderer.contents.find(x => x.shelfRenderer)?.shelfRenderer;
 | 
					 | 
				
			||||||
			if (shelfRenderer && !shelfRenderer.title) {
 | 
					 | 
				
			||||||
				return shelfRenderer
 | 
					 | 
				
			||||||
					.content
 | 
					 | 
				
			||||||
					.expandedShelfContentsRenderer
 | 
					 | 
				
			||||||
					.items
 | 
					 | 
				
			||||||
					.map(parseVideoRendererData)
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
		.flat();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return {tabs, videos};
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										47
									
								
								util.js
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								util.js
									
									
									
									
									
								
							@ -1,51 +1,6 @@
 | 
				
			|||||||
Array.prototype.filterMap = function(fn) {
 | 
					 | 
				
			||||||
	var newarray = [];
 | 
					 | 
				
			||||||
	for (var item of this) {
 | 
					 | 
				
			||||||
		var ret = fn(item);
 | 
					 | 
				
			||||||
		if (ret) newarray.push(ret);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return newarray;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function stringToBoolean(str) {
 | 
					export function stringToBoolean(str) {
 | 
				
			||||||
	if (str) {
 | 
						if (str) {
 | 
				
			||||||
		if (!["0", "false", "off", "no", "null", "undefined", "nan"].includes(str.toLowerCase())) return true;
 | 
							if (!["0", "false", "off", "no", "null", "undefined", "nan"].includes(str.toLowerCase())) return true;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return false;
 | 
						return false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export function recursiveFind(object, fn) {
 | 
					 | 
				
			||||||
	var results = [];
 | 
					 | 
				
			||||||
	(function crawlObject(object) {
 | 
					 | 
				
			||||||
		for (var key in object) {
 | 
					 | 
				
			||||||
			var value = object[key];
 | 
					 | 
				
			||||||
			var test = fn(value);
 | 
					 | 
				
			||||||
			if (test) results.push(test);
 | 
					 | 
				
			||||||
			if (typeof value == "object") crawlObject(value);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})(object);
 | 
					 | 
				
			||||||
	return results;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function parseVideoRendererData(data) {
 | 
					 | 
				
			||||||
	data = data.videoRenderer;
 | 
					 | 
				
			||||||
	return {
 | 
					 | 
				
			||||||
		id: data.videoId,
 | 
					 | 
				
			||||||
		live: Boolean(data.badges?.find(x => x.metadataBadgeRenderer?.style == "BADGE_STYLE_TYPE_LIVE_NOW")),
 | 
					 | 
				
			||||||
		title: data.title?.runs?.[0]?.text,
 | 
					 | 
				
			||||||
		description: data.detailedMetadataSnippets?.[0]?.snippetText?.runs?.reduce((str, obj) => str += obj.text, "") 
 | 
					 | 
				
			||||||
			|| data.descriptionSnippet?.runs?.reduce((str, obj) => str += obj.text, ""),
 | 
					 | 
				
			||||||
		thumbnailUrl: data.thumbnail?.thumbnails?.find(x => x.width == 360 && x.height == 202)?.url || data.thumbnail?.thumbnails?.[0]?.url,
 | 
					 | 
				
			||||||
		uploaded: data.publishedTimeText?.simpleText,
 | 
					 | 
				
			||||||
		lengthText: data.lengthText?.simpleText,
 | 
					 | 
				
			||||||
		longLengthText: data.lengthText?.accessibility?.accessibilityData?.label,
 | 
					 | 
				
			||||||
		viewCountText: data.viewCountText?.runs ? data.viewCountText.runs.reduce((str, obj) => str += obj.text, "") : data.viewCountText?.simpleText,
 | 
					 | 
				
			||||||
		shortViewCountText: data.shortViewCountText?.simpleText,
 | 
					 | 
				
			||||||
		channel: {
 | 
					 | 
				
			||||||
			name: data.ownerText?.runs?.[0]?.text,
 | 
					 | 
				
			||||||
			id: data.ownerText?.runs?.[0]?.navigationEndpoint?.browseEndpoint?.browseId,
 | 
					 | 
				
			||||||
			iconUrl: data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails?.[0]?.url
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user