Compare commits
2 Commits
d413e7d1b8
...
5c00a0444b
Author | SHA1 | Date | |
---|---|---|---|
5c00a0444b | |||
18f01ea323 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
vrcurl.sqlite
|
*.sqlite
|
||||||
|
*.json
|
@ -1,6 +1,7 @@
|
|||||||
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 = {};
|
||||||
|
|
||||||
@ -24,8 +25,19 @@ async function VRCYoutubeSearch(pool, query, options = {}) {
|
|||||||
console.debug("search:", JSON.stringify(query));
|
console.debug("search:", JSON.stringify(query));
|
||||||
var data = {results: []};
|
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) {
|
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", async ctx => {
|
router.get(["/search", "/trending"], async ctx => {
|
||||||
var query = ctx.query.input?.replace(/^.*→/, '').trim();
|
var query = ctx.path == "/trending" ? "trending" : 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,9 +11,10 @@ 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 maxIconColLen = Math.floor(maxSheetHeight / iconHeight); // 30
|
const maxIconRowLen = 3;
|
||||||
|
//const maxIconColLen = Math.floor(maxSheetHeight / iconHeight); // 30
|
||||||
|
|
||||||
|
|
||||||
async function createImageSheet(thumbnailUrls = [], iconUrls = []) {
|
async function createImageSheet(thumbnailUrls = [], iconUrls = []) {
|
||||||
@ -25,19 +26,19 @@ async function createImageSheet(thumbnailUrls = [], iconUrls = []) {
|
|||||||
return {x, y, url};
|
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) => {
|
var icons = iconUrls.map((url, index) => {
|
||||||
const x = index % maxIconRowLen * iconWidth;
|
const x = iconStartX + index % maxIconRowLen * iconWidth;
|
||||||
const y = iconStartY + Math.floor(index / maxIconRowLen);
|
const y = Math.floor(index / maxIconRowLen) * iconHeight;
|
||||||
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,
|
||||||
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 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,3 +1,4 @@
|
|||||||
|
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,7 +4,6 @@
|
|||||||
"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,3 +1,4 @@
|
|||||||
|
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`;
|
||||||
@ -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};
|
||||||
|
}
|
47
util.js
47
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) {
|
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…
Reference in New Issue
Block a user