Compare commits
2 Commits
d413e7d1b8
...
5c00a0444b
Author | SHA1 | Date | |
---|---|---|---|
5c00a0444b | |||
18f01ea323 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
vrcurl.sqlite
|
||||
*.sqlite
|
||||
*.json
|
@ -1,6 +1,7 @@
|
||||
import { searchYouTubeVideos, continueYouTubeVideoSearch } from "./simpleYoutubeSearch.js";
|
||||
import { putVrcUrl } from "./vrcurl.js";
|
||||
import { makeImageSheetVrcUrl, thumbnailWidth, thumbnailHeight, iconWidth, iconHeight } from "./imagesheet.js";
|
||||
import { getTrending } from "./trending.js";
|
||||
|
||||
var cache = {};
|
||||
|
||||
@ -24,7 +25,18 @@ async function VRCYoutubeSearch(pool, query, options = {}) {
|
||||
console.debug("search:", JSON.stringify(query));
|
||||
var data = {results: []};
|
||||
|
||||
var {videos, continuationData} = typeof query == "object" ? await continueYouTubeVideoSearch(query) : await searchYouTubeVideos(query);
|
||||
if (query == "trending") {
|
||||
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) {
|
||||
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();
|
||||
|
||||
|
||||
router.get("/search", async ctx => {
|
||||
var query = ctx.query.input?.replace(/^.*→/, '').trim();
|
||||
router.get(["/search", "/trending"], async ctx => {
|
||||
var query = ctx.path == "/trending" ? "trending" : ctx.query.input?.replace(/^.*→/, '').trim();
|
||||
if (!query) {
|
||||
ctx.status = 400;
|
||||
ctx.body = "missing search query";
|
||||
|
@ -11,9 +11,10 @@ export const iconHeight = 68;
|
||||
const maxSheetWidth = 2048;
|
||||
const maxSheetHeight = 2048;
|
||||
const maxThumbnailRowLen = Math.floor(maxSheetWidth / thumbnailWidth); // 5
|
||||
const maxThumbnailColLen = Math.floor(maxSheetHeight / thumbnailHeight); // 10
|
||||
const maxIconRowLen = Math.floor(maxSheetWidth / iconWidth); // 30
|
||||
const maxIconColLen = Math.floor(maxSheetHeight / iconHeight); // 30
|
||||
//const maxThumbnailColLen = Math.floor(maxSheetHeight / thumbnailHeight); // 10
|
||||
//const maxIconRowLen = Math.floor(maxSheetWidth / iconWidth); // 30
|
||||
const maxIconRowLen = 3;
|
||||
//const maxIconColLen = Math.floor(maxSheetHeight / iconHeight); // 30
|
||||
|
||||
|
||||
async function createImageSheet(thumbnailUrls = [], iconUrls = []) {
|
||||
@ -25,19 +26,19 @@ async function createImageSheet(thumbnailUrls = [], iconUrls = []) {
|
||||
return {x, y, url};
|
||||
});
|
||||
|
||||
const iconStartY = thumbnails.length ? thumbnails.at(-1).y + thumbnailHeight : 0;
|
||||
const iconStartX = thumbnailWidth * Math.min(maxThumbnailRowLen, thumbnails.length);
|
||||
|
||||
var icons = iconUrls.map((url, index) => {
|
||||
const x = index % maxIconRowLen * iconWidth;
|
||||
const y = iconStartY + Math.floor(index / maxIconRowLen);
|
||||
const x = iconStartX + index % maxIconRowLen * iconWidth;
|
||||
const y = Math.floor(index / maxIconRowLen) * iconHeight;
|
||||
return {x, y, url};
|
||||
});
|
||||
|
||||
const canvasWidth = Math.max(
|
||||
Math.min(thumbnails.length, maxThumbnailRowLen) * thumbnailWidth,
|
||||
Math.min(icons.length, maxIconRowLen) * iconWidth
|
||||
iconStartX + Math.min(icons.length, maxIconRowLen) * iconWidth
|
||||
);
|
||||
const canvasHeight = icons.length ? icons.at(-1).y + iconHeight : thumbnails.length ? thumbnails.at(-1).y + thumbnailHeight : 0;
|
||||
const canvasHeight = Math.max(thumbnails.length ? thumbnails.at(-1).y + thumbnailHeight : 0, icons.length ? icons.at(-1)?.y + iconHeight : 0);
|
||||
|
||||
var canvas = createCanvas(Math.min(maxSheetWidth, canvasWidth), Math.min(maxSheetHeight, canvasHeight));
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
1
index.js
1
index.js
@ -1,3 +1,4 @@
|
||||
import "./util.js";
|
||||
import { app } from "./app.js";
|
||||
|
||||
app.listen(process.env.PORT || 8142, process.env.ADDRESS);
|
1
package-lock.json
generated
1
package-lock.json
generated
@ -4,7 +4,6 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vrchat-youtube-search-api",
|
||||
"dependencies": {
|
||||
"@keyv/sqlite": "^3.6.6",
|
||||
"@koa/router": "^12.0.1",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { parseVideoRendererData } from "./util.js";
|
||||
|
||||
export async function searchYouTubeVideos(query) {
|
||||
var url = `https://www.youtube.com/results?search_query=${encodeURIComponent(query.replaceAll(' ', '+'))}&sp=EgIQAQ%253D%253D`;
|
||||
@ -44,26 +45,3 @@ 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
Normal file
39
trending.js
Normal file
@ -0,0 +1,39 @@
|
||||
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};
|
||||
}
|
45
util.js
45
util.js
@ -1,6 +1,51 @@
|
||||
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) {
|
||||
if (str) {
|
||||
if (!["0", "false", "off", "no", "null", "undefined", "nan"].includes(str.toLowerCase())) return true;
|
||||
}
|
||||
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…
Reference in New Issue
Block a user