Compare commits
No commits in common. "e7d8a4b73707cac0952cd2b00df15be8736714d1" and "a119e41075c126d2f092c80b2690edd113c9fd31" have entirely different histories.
e7d8a4b737
...
a119e41075
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
@ -14,10 +14,7 @@
|
|||||||
"program": "${workspaceFolder}\\index.js",
|
"program": "${workspaceFolder}\\index.js",
|
||||||
"outFiles": [
|
"outFiles": [
|
||||||
"${workspaceFolder}/**/*.js"
|
"${workspaceFolder}/**/*.js"
|
||||||
],
|
]
|
||||||
"env": {
|
|
||||||
"D":"BUG"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -13,7 +13,7 @@ Get YouTube videos for a search query.
|
|||||||
### Required query parameters
|
### Required query parameters
|
||||||
|
|
||||||
- `pool`: id of the VRCUrl pool, only letters numbers hyphens or underscores, optionally followed by an integer for pool size.
|
- `pool`: id of the VRCUrl pool, only letters numbers hyphens or underscores, optionally followed by an integer for pool size.
|
||||||
- `input`: youtube search query. All chars up to and including this exact unicode char `→` are ignored, and then whitespace is trimmed. THIS MUST BE THE LAST QUERY PARAMETER AS ALL CHARS AFTER IT ARE CAPTURED VERBATIM (so you can type & etc without encoding)
|
- `input`: youtube search query. All chars up to and including this exact unicode char `→` are ignored, and then whitespace is trimmed.
|
||||||
|
|
||||||
### Optional query parameters
|
### Optional query parameters
|
||||||
|
|
||||||
@ -39,13 +39,13 @@ JSON object:
|
|||||||
- `description`: (string) short truncated description snippet i.e. `"http://nyan.cat/ Original song : http://momolabo.lolipop.jp/nyancatsong/Nyan/"`
|
- `description`: (string) short truncated description snippet i.e. `"http://nyan.cat/ Original song : http://momolabo.lolipop.jp/nyancatsong/Nyan/"`
|
||||||
- `lengthText`?: (string) i.e. `"3:37"`
|
- `lengthText`?: (string) i.e. `"3:37"`
|
||||||
- `longLengthText`?: (string) i.e. `"3 minutes, 37 seconds"`
|
- `longLengthText`?: (string) i.e. `"3 minutes, 37 seconds"`
|
||||||
- `viewCountText`?: (string) i.e. `"2,552,243 views"` or `"575 watching"` for live streams (playlist results don't have this)
|
- `viewCountText`: (string) i.e. `"2,552,243 views"` or `"575 watching"` for live streams
|
||||||
- `shortViewCountText`?: (string) i.e. `"2.5M views"` (streams don't have this)
|
- `shortViewCountText`?: (string) i.e. `"2.5M views"`
|
||||||
- `uploaded`: (string) i.e. `"12 years ago"`
|
- `uploaded`: (string) i.e. `"12 years ago"`
|
||||||
- `channel`: (object)
|
- `channel`: (object)
|
||||||
- `name`: (string) i.e. `"NyanCat"`
|
- `name`: (string) i.e. `"NyanCat"`
|
||||||
- `id`: (string) i.e. `"UCsW85RAS2_Twg_lEPyv7G8A"`
|
- `id`: (string) i.e. `"UCsW85RAS2_Twg_lEPyv7G8A"`
|
||||||
- `icon`?: (object) (playlist results don't have this)
|
- `icon`?: (object)
|
||||||
- `x`: (integer) px from left
|
- `x`: (integer) px from left
|
||||||
- `y`: (integer) px from top
|
- `y`: (integer) px from top
|
||||||
- `width`: (integer)
|
- `width`: (integer)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { searchYouTubeVideos, continueYouTubeVideoSearch, getYouTubePlaylist, continueYouTubePlaylist, getTrending } from "./youtube.js";
|
import { searchYouTubeVideos, continueYouTubeVideoSearch } from "./simpleYoutubeSearch.js";
|
||||||
import { putVrcUrl } from "./vrcurl.js";
|
import { putVrcUrl } from "./vrcurl.js";
|
||||||
import { makeImageSheetVrcUrl } from "./imagesheet.js";
|
import { makeImageSheetVrcUrl, thumbnailWidth, thumbnailHeight, iconWidth, iconHeight } from "./imagesheet.js";
|
||||||
|
import { getTrending } from "./trending.js";
|
||||||
|
|
||||||
var cache = {};
|
var cache = {};
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ export async function cachedVRCYoutubeSearch(pool, query, options) {
|
|||||||
|
|
||||||
|
|
||||||
async function VRCYoutubeSearch(pool, query, options = {}) {
|
async function VRCYoutubeSearch(pool, query, options = {}) {
|
||||||
console.log("search:", JSON.stringify(query));
|
console.debug("search:", JSON.stringify(query));
|
||||||
var data = {results: []};
|
var data = {results: []};
|
||||||
|
|
||||||
if (typeof query == "object") {
|
if (typeof query == "object") {
|
||||||
@ -37,50 +38,28 @@ async function VRCYoutubeSearch(pool, query, options = {}) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "continuation":
|
case "continuation":
|
||||||
//var {videos, continuationData} = await [query.for == "playlist" ? continueYouTubePlaylist : continueYouTubeVideoSearch](query.continuationData);
|
|
||||||
if (query.for == "playlist") {
|
|
||||||
var {videos, continuationData} = await continueYouTubePlaylist(query.continuationData);
|
|
||||||
} else {
|
|
||||||
var {videos, continuationData} = await continueYouTubeVideoSearch(query.continuationData);
|
var {videos, continuationData} = await continueYouTubeVideoSearch(query.continuationData);
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var playlistId = query.match(/list=(PL[a-zA-Z0-9-_]{32})/)?.[1];
|
var {videos, continuationData} = await searchYouTubeVideos(query);
|
||||||
if (playlistId) console.log("playlistId:", playlistId);
|
|
||||||
var {videos, continuationData} = playlistId ? await getYouTubePlaylist(playlistId) : await searchYouTubeVideos(query);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var images = [];
|
|
||||||
|
|
||||||
if (options.thumbnails) {
|
if (options.thumbnails) {
|
||||||
videos.forEach(video => {
|
var thumbnailUrls = videos.map(video => video.thumbnailUrl);
|
||||||
video.thumbnail = playlistId ? {
|
|
||||||
url: `https://i.ytimg.com/vi/${video.id}/default.jpg`,
|
|
||||||
width: 120,
|
|
||||||
height: 90
|
|
||||||
} : {
|
|
||||||
url: `https://i.ytimg.com/vi/${video.id}/mqdefault.jpg`,
|
|
||||||
width: 320,
|
|
||||||
height: 180
|
|
||||||
};
|
|
||||||
images.push(video.thumbnail);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.icons) {
|
if (options.icons) {
|
||||||
let iconUrls = new Set();
|
var iconUrls = new Set();
|
||||||
videos.forEach(video => video.channel.iconUrl && iconUrls.add(video.channel.iconUrl));
|
for (let video of videos) {
|
||||||
iconUrls.forEach(url => images.push({
|
iconUrls.add(video.channel.iconUrl);
|
||||||
width: 68,//todo pass from yt data not hardcode
|
}
|
||||||
height: 68,
|
iconUrls = [...iconUrls];
|
||||||
url
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (images.length) {
|
if (thumbnailUrls?.length || iconUrls?.length) {
|
||||||
try {
|
try {
|
||||||
var {vrcurl: imagesheet_vrcurl} = await makeImageSheetVrcUrl(pool, images);
|
var {vrcurl: imagesheet_vrcurl, thumbnails, icons} = await makeImageSheetVrcUrl(pool, thumbnailUrls, iconUrls);
|
||||||
data.imagesheet_vrcurl = imagesheet_vrcurl;
|
data.imagesheet_vrcurl = imagesheet_vrcurl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error.stack);
|
console.error(error.stack);
|
||||||
@ -89,30 +68,34 @@ async function VRCYoutubeSearch(pool, query, options = {}) {
|
|||||||
|
|
||||||
for (let video of videos) {
|
for (let video of videos) {
|
||||||
video.vrcurl = await putVrcUrl(pool, {type: "redirect", url: `https://www.youtube.com/watch?v=${video.id}`});
|
video.vrcurl = await putVrcUrl(pool, {type: "redirect", url: `https://www.youtube.com/watch?v=${video.id}`});
|
||||||
let thumbnail = images.find(image => image.url == video.thumbnail.url);
|
if (thumbnails?.length) {
|
||||||
video.thumbnail = thumbnail ? {
|
let thumbnail = thumbnails.find(x => x.url == video.thumbnailUrl);
|
||||||
|
video.thumbnail = {
|
||||||
x: thumbnail?.x,
|
x: thumbnail?.x,
|
||||||
y: thumbnail?.y,
|
y: thumbnail?.y,
|
||||||
width: thumbnail?.width,
|
width: thumbnailWidth,
|
||||||
height: thumbnail?.height
|
height: thumbnailHeight
|
||||||
} : undefined;
|
};
|
||||||
let icon = images.find(image => image.url == video.channel.iconUrl);
|
}
|
||||||
video.channel.icon = icon ? {
|
if (icons?.length) {
|
||||||
|
let icon = icons.find(x => x.url == video.channel.iconUrl);
|
||||||
|
video.channel.icon = {
|
||||||
x: icon?.x,
|
x: icon?.x,
|
||||||
y: icon?.y,
|
y: icon?.y,
|
||||||
width: icon?.width,
|
width: iconWidth,
|
||||||
height: icon?.height
|
height: iconHeight
|
||||||
} : undefined;
|
};
|
||||||
|
}
|
||||||
if (options.captions) {
|
if (options.captions) {
|
||||||
video.captions_vrcurl = await putVrcUrl(pool, {type: "captions", videoId: video.id});
|
video.captions_vrcurl = await putVrcUrl(pool, {type: "captions", videoId: video.id});
|
||||||
}
|
}
|
||||||
|
delete video.thumbnailUrl;
|
||||||
delete video.channel.iconUrl;
|
delete video.channel.iconUrl;
|
||||||
data.results.push(video);
|
data.results.push(video);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (continuationData) data.nextpage_vrcurl = await putVrcUrl(pool, {
|
if (continuationData) data.nextpage_vrcurl = await putVrcUrl(pool, {
|
||||||
type: "continuation",
|
type: "ytContinuation",
|
||||||
for: query.for || (playlistId ? "playlist" : "search"),
|
|
||||||
continuationData,
|
continuationData,
|
||||||
options
|
options
|
||||||
});
|
});
|
||||||
|
123
app.js
Normal file
123
app.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import Koa from "koa";
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import send from "koa-send";
|
||||||
|
import { cachedVRCYoutubeSearch } from "./VRCYoutubeSearch.js"
|
||||||
|
import { getImageSheet } from "./imagesheet.js";
|
||||||
|
import { resolveVrcUrl } from "./vrcurl.js";
|
||||||
|
import { getVideoCaptionsCached } from "./captions.js";
|
||||||
|
import { stringToBoolean } from "./util.js";
|
||||||
|
import shorturlmap from "./shorturlmap.json" assert { type: "json" };
|
||||||
|
|
||||||
|
export var app = new Koa();
|
||||||
|
var router = new Router();
|
||||||
|
|
||||||
|
|
||||||
|
router.get(["/search", "/trending"], async ctx => {
|
||||||
|
var query = ctx.path == "/trending" ? {"type":"trending"} : ctx.query.input?.replace(/^.*→/, '').trim();
|
||||||
|
if (!query) {
|
||||||
|
ctx.status = 400;
|
||||||
|
ctx.body = "missing search query";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.query.pool || !/^[a-z-_]+\d*$/.test(ctx.query.pool)) {
|
||||||
|
ctx.status = 400;
|
||||||
|
ctx.body = "invalid pool";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
thumbnails: stringToBoolean(ctx.query.thumbnails),
|
||||||
|
icons: stringToBoolean(ctx.query.icons),
|
||||||
|
captions: stringToBoolean(ctx.query.captions)
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.body = await cachedVRCYoutubeSearch(ctx.query.pool, query, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.get("/vrcurl/:pool/:num", async ctx => {
|
||||||
|
var dest = await resolveVrcUrl(ctx.params.pool, ctx.params.num);
|
||||||
|
if (!dest) {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (dest.type) {
|
||||||
|
case "redirect":
|
||||||
|
ctx.redirect(dest.url);
|
||||||
|
break;
|
||||||
|
case "imagesheet":
|
||||||
|
let buf = await getImageSheet(ctx.params.pool, ctx.params.num);
|
||||||
|
if (!buf) {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.body = buf;
|
||||||
|
ctx.type = "image/png";
|
||||||
|
break;
|
||||||
|
case "ytContinuation":
|
||||||
|
ctx.body = await cachedVRCYoutubeSearch(ctx.params.pool, {type: "continuation", continuationData: dest.continuationData}, dest.options);
|
||||||
|
break;
|
||||||
|
case "trending":
|
||||||
|
ctx.body = await cachedVRCYoutubeSearch(ctx.params.pool, {type: "trending", bp: dest.bp}, dest.options);
|
||||||
|
break;
|
||||||
|
case "captions":
|
||||||
|
ctx.body = await getVideoCaptionsCached(dest.videoId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("unknown vrcurl type", dest.type);
|
||||||
|
ctx.status = 500;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.get("/robots.txt", ctx => {
|
||||||
|
ctx.body = `User-agent: *\nDisallow: /`;
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/test.html", async ctx => {
|
||||||
|
await send(ctx, "test.html");
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/", ctx => {
|
||||||
|
ctx.redirect("https://www.u2b.cx/");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// short urls to work around https://feedback.vrchat.com/udon/p/vrcurlinputfield-incorrect-focus-issue-on-quest
|
||||||
|
app.use(async (ctx, next) => {
|
||||||
|
var subdomain = ctx.hostname.match(/(.*).u2b.cx$/i)?.[1];
|
||||||
|
if (subdomain && !["api","api2","dev"].includes(subdomain)) {
|
||||||
|
if (shorturlmap[subdomain]) {
|
||||||
|
ctx.url = shorturlmap[subdomain] + ctx.url.slice(1);
|
||||||
|
} else {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// work around vrchat json parser bug https://feedback.vrchat.com/udon/p/braces-inside-strings-in-vrcjson-can-fail-to-deserialize
|
||||||
|
app.use(async (ctx, next) => {
|
||||||
|
await next();
|
||||||
|
if (ctx.type != "application/json") return;
|
||||||
|
ctx.body = structuredClone(ctx.body);
|
||||||
|
(function iterateObject(obj) {
|
||||||
|
for (var key in obj) {
|
||||||
|
if (typeof obj[key] == "string") {
|
||||||
|
obj[key] = obj[key].replace(/[\[\]{}]/g, chr => "\\u" + chr.charCodeAt(0).toString(16).padStart(4, '0'));
|
||||||
|
} else if (typeof obj[key] == "object") {
|
||||||
|
iterateObject(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(ctx.body);
|
||||||
|
ctx.body = JSON.stringify(ctx.body).replaceAll("\\\\u", "\\u");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.use(router.routes());
|
||||||
|
app.use(router.allowedMethods());
|
@ -1,42 +1,76 @@
|
|||||||
import { createCanvas, loadImage } from 'canvas';
|
import { createCanvas, loadImage } from 'canvas';
|
||||||
import potpack from 'potpack';
|
|
||||||
import { putVrcUrl } from './vrcurl.js';
|
import { putVrcUrl } from './vrcurl.js';
|
||||||
|
|
||||||
var store = {};
|
var store = {};
|
||||||
|
|
||||||
async function createImageSheet(images /*[{width, height, url}]*/) {
|
|
||||||
images.forEach(image => {
|
export const thumbnailWidth = 360;
|
||||||
image.w = image.width;
|
export const thumbnailHeight = 202;
|
||||||
image.h = image.height;
|
export const iconWidth = 68;
|
||||||
|
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 maxIconRowLen = 3;
|
||||||
|
//const maxIconColLen = Math.floor(maxSheetHeight / iconHeight); // 30
|
||||||
|
|
||||||
|
|
||||||
|
async function createImageSheet(thumbnailUrls = [], iconUrls = []) {
|
||||||
|
|
||||||
|
|
||||||
|
var thumbnails = thumbnailUrls.map((url, index) => {
|
||||||
|
const x = index % maxThumbnailRowLen * thumbnailWidth;
|
||||||
|
const y = Math.floor(index / maxThumbnailRowLen) * thumbnailHeight;
|
||||||
|
return {x, y, url};
|
||||||
});
|
});
|
||||||
var {w, h, fill} = potpack(images);
|
|
||||||
if (w > 2048) {
|
const iconStartX = thumbnailWidth * Math.min(maxThumbnailRowLen, thumbnails.length);
|
||||||
console.warn("Imagesheet exceeded max width");
|
|
||||||
w = 2048;
|
var icons = iconUrls.map((url, index) => {
|
||||||
}
|
const x = iconStartX + index % maxIconRowLen * iconWidth;
|
||||||
if (h > 2048) {
|
const y = Math.floor(index / maxIconRowLen) * iconHeight;
|
||||||
console.warn("Imagesheet exceeded max height");
|
return {x, y, url};
|
||||||
h = 2048;
|
});
|
||||||
}
|
|
||||||
var canvas = createCanvas(w, h);
|
const canvasWidth = Math.max(
|
||||||
|
Math.min(thumbnails.length, maxThumbnailRowLen) * thumbnailWidth,
|
||||||
|
iconStartX + 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);
|
||||||
|
|
||||||
|
var canvas = createCanvas(Math.min(maxSheetWidth, canvasWidth), Math.min(maxSheetHeight, canvasHeight));
|
||||||
var ctx = canvas.getContext('2d');
|
var ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
await Promise.all(images.map(({x, y, w, h, url}) => (async function(){
|
var promises = [];
|
||||||
if (!url) return;
|
|
||||||
var image = await loadImage(url);
|
|
||||||
ctx.drawImage(image, x, y, w, h);
|
|
||||||
})().catch(error => console.error(error.stack))));
|
|
||||||
|
|
||||||
|
if (thumbnails.length) {
|
||||||
|
promises = promises.concat(thumbnails.map(({x, y, url}) => (async function(){
|
||||||
|
var image = await loadImage(url);
|
||||||
|
ctx.drawImage(image, x, y, thumbnailWidth, thumbnailHeight);
|
||||||
|
})().catch(error => console.error(error.stack))));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icons.length) {
|
||||||
|
promises = promises.concat(icons.map(({x, y, url}) => (async function(){
|
||||||
|
var image = await loadImage(url);
|
||||||
|
ctx.drawImage(image, x, y, iconWidth, iconHeight);
|
||||||
|
})().catch(error => console.error(error.stack))));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
return {
|
return {
|
||||||
imagesheet: canvas.toBuffer("image/png"),
|
imagesheet: canvas.toBuffer("image/png"),
|
||||||
images
|
thumbnails, icons
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function makeImageSheetVrcUrl(pool, images) {
|
|
||||||
|
export async function makeImageSheetVrcUrl(pool, thumbnailUrls, iconUrls) {
|
||||||
var num = await putVrcUrl(pool, {type: "imagesheet"});
|
var num = await putVrcUrl(pool, {type: "imagesheet"});
|
||||||
var key = `${pool}:${num}`;
|
var key = `${pool}:${num}`;
|
||||||
var promise = createImageSheet(images);
|
var promise = createImageSheet(thumbnailUrls, iconUrls);
|
||||||
store[key] = promise;
|
store[key] = promise;
|
||||||
promise.then(() => {
|
promise.then(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
130
index.js
130
index.js
@ -1,132 +1,4 @@
|
|||||||
if (process.env.D!="BUG") console.debug = () => {};
|
|
||||||
import "./util.js";
|
import "./util.js";
|
||||||
import Koa from "koa";
|
import { app } from "./app.js";
|
||||||
import Router from "@koa/router";
|
|
||||||
import send from "koa-send";
|
|
||||||
import { cachedVRCYoutubeSearch } from "./VRCYoutubeSearch.js"
|
|
||||||
import { getImageSheet } from "./imagesheet.js";
|
|
||||||
import { resolveVrcUrl } from "./vrcurl.js";
|
|
||||||
import { getVideoCaptionsCached } from "./youtube-captions.js";
|
|
||||||
import { stringToBoolean } from "./util.js";
|
|
||||||
import shorturlmap from "./shorturlmap.json" assert { type: "json" };
|
|
||||||
|
|
||||||
var app = new Koa();
|
|
||||||
var router = new Router();
|
|
||||||
|
|
||||||
|
|
||||||
router.get(["/search", "/trending"], async ctx => {
|
|
||||||
if (ctx.path == "/trending") {
|
|
||||||
var query = {"type":"trending"};
|
|
||||||
} else {
|
|
||||||
var query = ctx.querystring.match(/[?&]input=(.*)/i)?.[1];
|
|
||||||
if (!query) {
|
|
||||||
ctx.status = 400;
|
|
||||||
ctx.body = "missing search query";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
query = decodeURIComponent(query).replace(/^.*→/, '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.query.pool || !/^[a-z-_]+\d*$/.test(ctx.query.pool)) {
|
|
||||||
ctx.status = 400;
|
|
||||||
ctx.body = "invalid pool";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = {
|
|
||||||
thumbnails: stringToBoolean(ctx.query.thumbnails),
|
|
||||||
icons: stringToBoolean(ctx.query.icons),
|
|
||||||
captions: stringToBoolean(ctx.query.captions)
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.body = await cachedVRCYoutubeSearch(ctx.query.pool, query, options);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
router.get("/vrcurl/:pool/:num", async ctx => {
|
|
||||||
var dest = await resolveVrcUrl(ctx.params.pool, ctx.params.num);
|
|
||||||
if (!dest) {
|
|
||||||
ctx.status = 404;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (dest.type) {
|
|
||||||
case "redirect":
|
|
||||||
ctx.redirect(dest.url);
|
|
||||||
break;
|
|
||||||
case "imagesheet":
|
|
||||||
let buf = await getImageSheet(ctx.params.pool, ctx.params.num);
|
|
||||||
if (!buf) {
|
|
||||||
ctx.status = 404;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ctx.body = buf;
|
|
||||||
ctx.type = "image/png";
|
|
||||||
break;
|
|
||||||
case "continuation":
|
|
||||||
ctx.body = await cachedVRCYoutubeSearch(ctx.params.pool, {type: "continuation", for: dest.for, continuationData: dest.continuationData}, dest.options);
|
|
||||||
break;
|
|
||||||
case "trending":
|
|
||||||
ctx.body = await cachedVRCYoutubeSearch(ctx.params.pool, {type: "trending", bp: dest.bp}, dest.options);
|
|
||||||
break;
|
|
||||||
case "captions":
|
|
||||||
ctx.body = await getVideoCaptionsCached(dest.videoId);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.error("unknown vrcurl type", dest.type);
|
|
||||||
ctx.status = 500;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
router.get("/robots.txt", ctx => {
|
|
||||||
ctx.body = `User-agent: *\nDisallow: /`;
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/test.html", async ctx => {
|
|
||||||
await send(ctx, "test.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/", ctx => {
|
|
||||||
ctx.redirect("https://www.u2b.cx/");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// short urls to work around https://feedback.vrchat.com/udon/p/vrcurlinputfield-incorrect-focus-issue-on-quest
|
|
||||||
app.use(async (ctx, next) => {
|
|
||||||
var subdomain = ctx.hostname.match(/(.*).u2b.cx$/i)?.[1];
|
|
||||||
if (subdomain && !["api","api2","dev"].includes(subdomain)) {
|
|
||||||
if (shorturlmap[subdomain]) {
|
|
||||||
ctx.url = shorturlmap[subdomain] + ctx.url.slice(1);
|
|
||||||
} else {
|
|
||||||
ctx.status = 404;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// work around vrchat json parser bug https://feedback.vrchat.com/udon/p/braces-inside-strings-in-vrcjson-can-fail-to-deserialize
|
|
||||||
app.use(async (ctx, next) => {
|
|
||||||
await next();
|
|
||||||
if (ctx.type != "application/json") return;
|
|
||||||
ctx.body = structuredClone(ctx.body);
|
|
||||||
(function iterateObject(obj) {
|
|
||||||
for (var key in obj) {
|
|
||||||
if (typeof obj[key] == "string") {
|
|
||||||
obj[key] = obj[key].replace(/[\[\]{}]/g, chr => "\\u" + chr.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
||||||
} else if (typeof obj[key] == "object") {
|
|
||||||
iterateObject(obj[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})(ctx.body);
|
|
||||||
ctx.body = JSON.stringify(ctx.body).replaceAll("\\\\u", "\\u");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
app.use(router.allowedMethods());
|
|
||||||
|
|
||||||
app.listen(process.env.PORT || 8142, process.env.ADDRESS);
|
app.listen(process.env.PORT || 8142, process.env.ADDRESS);
|
8
package-lock.json
generated
8
package-lock.json
generated
@ -11,8 +11,7 @@
|
|||||||
"fast-xml-parser": "^4.3.4",
|
"fast-xml-parser": "^4.3.4",
|
||||||
"keyv": "^4.5.4",
|
"keyv": "^4.5.4",
|
||||||
"koa": "^2.14.2",
|
"koa": "^2.14.2",
|
||||||
"koa-send": "^5.0.1",
|
"koa-send": "^5.0.1"
|
||||||
"potpack": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
@ -1263,11 +1262,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
|
||||||
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
|
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
|
||||||
},
|
},
|
||||||
"node_modules/potpack": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw=="
|
|
||||||
},
|
|
||||||
"node_modules/promise-inflight": {
|
"node_modules/promise-inflight": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||||
|
@ -6,8 +6,7 @@
|
|||||||
"fast-xml-parser": "^4.3.4",
|
"fast-xml-parser": "^4.3.4",
|
||||||
"keyv": "^4.5.4",
|
"keyv": "^4.5.4",
|
||||||
"koa": "^2.14.2",
|
"koa": "^2.14.2",
|
||||||
"koa-send": "^5.0.1",
|
"koa-send": "^5.0.1"
|
||||||
"potpack": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
47
simpleYoutubeSearch.js
Normal file
47
simpleYoutubeSearch.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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`;
|
||||||
|
var html = await fetch(url).then(res => res.text());
|
||||||
|
|
||||||
|
var ytInitialData = html.match(/ytInitialData = ({.*});<\/script>/)[1];
|
||||||
|
ytInitialData = JSON.parse(ytInitialData);
|
||||||
|
|
||||||
|
var videos = ytInitialData?.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents?.find(x => x.itemSectionRenderer?.contents?.find(x => x.videoRenderer))?.itemSectionRenderer?.contents?.filter(x => x.videoRenderer).map(parseVideoRendererData);
|
||||||
|
if (!videos) return {videos: []};
|
||||||
|
|
||||||
|
try {
|
||||||
|
var ytcfg = html.match(/ytcfg.set\(({.*})\);/)[1];
|
||||||
|
ytcfg = JSON.parse(ytcfg);
|
||||||
|
var continuationData = {
|
||||||
|
context: ytcfg.INNERTUBE_CONTEXT,
|
||||||
|
continuation: ytInitialData.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.find(x => x.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {videos, continuationData};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function continueYouTubeVideoSearch(continuationData) {
|
||||||
|
var data = await fetch("https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(continuationData)
|
||||||
|
}).then(res => res.json());
|
||||||
|
|
||||||
|
var videos = data.onResponseReceivedCommands[0].appendContinuationItemsAction.continuationItems.find(x => x.itemSectionRenderer?.contents.find(x => x.videoRenderer)).itemSectionRenderer.contents.filter(x => x.videoRenderer).map(parseVideoRendererData);
|
||||||
|
var continuationToken = data.onResponseReceivedCommands[0].appendContinuationItemsAction.continuationItems.find(x => x.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token
|
||||||
|
|
||||||
|
return {
|
||||||
|
videos,
|
||||||
|
continuationData: {
|
||||||
|
context: continuationData.context,
|
||||||
|
continuation: continuationToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
trending.js
Normal file
42
trending.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { parseVideoRendererData } from "./util.js";
|
||||||
|
|
||||||
|
export async function getTrending(bp) {
|
||||||
|
var url = `https://www.youtube.com/feed/trending`;
|
||||||
|
if (bp) url += `?bp=${bp}`;
|
||||||
|
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
|
||||||
|
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.find(x => x.shelfRenderer)?.shelfRenderer;
|
||||||
|
if (shelfRenderer && !shelfRenderer.title) {
|
||||||
|
return shelfRenderer
|
||||||
|
.content
|
||||||
|
.expandedShelfContentsRenderer
|
||||||
|
.items
|
||||||
|
.map(parseVideoRendererData)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
|
||||||
|
return {tabs, videos};
|
||||||
|
}
|
23
util.js
23
util.js
@ -26,3 +26,26 @@ export function recursiveFind(object, fn) {
|
|||||||
})(object);
|
})(object);
|
||||||
return results;
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
192
youtube.js
192
youtube.js
@ -1,192 +0,0 @@
|
|||||||
|
|
||||||
export async function searchYouTubeVideos(query) {
|
|
||||||
var url = `https://www.youtube.com/results?search_query=${encodeURIComponent(query.replaceAll(' ', '+'))}&sp=EgIQAQ%253D%253D`;
|
|
||||||
var html = await fetch(url).then(res => res.text());
|
|
||||||
|
|
||||||
var ytInitialData = html.match(/ytInitialData = ({.*});<\/script>/)[1];
|
|
||||||
ytInitialData = JSON.parse(ytInitialData);
|
|
||||||
console.debug(ytInitialData);
|
|
||||||
|
|
||||||
var videos = ytInitialData?.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents?.find(x => x.itemSectionRenderer?.contents?.find(x => x.videoRenderer))?.itemSectionRenderer?.contents?.filterMap(x => x.videoRenderer)?.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: ytInitialData.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.find(x => x.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error.stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {videos, continuationData};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function continueYouTubeVideoSearch(continuationData) {
|
|
||||||
var data = await fetch("https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify(continuationData)
|
|
||||||
}).then(res => res.json());
|
|
||||||
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.find(x => x.continuationItemRenderer)?.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 html = await fetch("https://www.youtube.com/playlist?list=" + playlistId).then(res => res.text());
|
|
||||||
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.find(x => x.itemSectionRenderer).itemSectionRenderer.contents.find(x => x.playlistVideoListRenderer).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.find(x => x.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error.stack);
|
|
||||||
}
|
|
||||||
return {videos, continuationData};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function continueYouTubePlaylist(continuationData) {
|
|
||||||
var data = await fetch("https://www.youtube.com/youtubei/v1/browse?prettyPrint=false", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {"Content-Type": "application/json"},
|
|
||||||
body: JSON.stringify(continuationData)
|
|
||||||
}).then(res => res.json());
|
|
||||||
console.debug(data);
|
|
||||||
|
|
||||||
if (!data.onResponseReceivedActions) return {videos:[]};
|
|
||||||
var continuationItems = data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems;
|
|
||||||
var videos = continuationItems.find(x => x.itemSectionRenderer).itemSectionRenderer.contents.filterMap(x => x.playlistVideoListRenderer).map(parseVideoRendererData);
|
|
||||||
var continuationToken = continuationItems.find(x => x.continuationItemRenderer)?.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 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
|
|
||||||
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.find(x => x.shelfRenderer)?.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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user