This commit is contained in:
Lamp 2023-12-07 01:00:01 -08:00
commit 743bf31a79
9 changed files with 1709 additions and 0 deletions

2
.gitignore vendored Normal file

@ -0,0 +1,2 @@
node_modules
vrcurl.sqlite

20
.vscode/launch.json vendored Normal file

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\index.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}

42
README.md Normal file

@ -0,0 +1,42 @@
pre-fill VRCUrlInputField with user-friendly prefix:
```
https://api.u2b.cx/search/{domain}/ Type YouTube search query here → {user input}
```
{domain} must be short unique alphabetical string representing the world, and {user input} is end of string where user types. You can customize everything between / and → for cosmetic purposes, user input is everything after → with whitespace trimmed.
Server will return JSON string of array of YouTube search results in this format:
- `id`: (string) YouTube video id
- `vrcurl`: (number) index of VRCUrl that will redirect to the youtube url
- `title`: (string)
- `duration`: (number) video duration in ms
- `durationString`: (string) formatted duration
- `uploaded`: (string) when the video was uploaded (i.e. "12 years ago")
- `views`: (number)
- `thumbnail_vrcurl`: (number) index of VRCUrl that will redirect to thumbnail image url
- `channel` (object)
- `name`: (string)
- `id`: (string)
- todo should we include icon?
### VRCUrls
Since VRCUrls are immutable you must define an array of at LEAST 10,000 VRCUrl instances like so:
```csharp
VRCUrl[] vrcurl_pool = [
new VRCUrl("https://api.u2b.cx/vrcurl/{domain}/0"),
new VRCUrl("https://api.u2b.cx/vrcurl/{domain}/1"),
new VRCUrl("https://api.u2b.cx/vrcurl/{domain}/2"),
// etc...
]
//todo: provide tool to auto generate
```
The server converts all URLs in the JSON response such that the VRCUrl at the Nth index will be 302 redirected to its substitute, that is until the list of VRCUrls are cycled through.
{domain} must be the same as in the search url, it allows different worlds to use the same api without using the same pool of VRCUrls.
If you want a different size pool you can specify by suffixing an integer to {domain}, i.e. if the domain is `foobar1000` the server will cycle through 0-999. Your pool size must be equal or larger than this value.

21
app.js Normal file

@ -0,0 +1,21 @@
import Koa from "koa";
import Router from "@koa/router";
import { cachedYoutubeSearch } from "./ytsearch.js";
import { resolveVrcUrl } from "./vrcurl.js";
export var app = new Koa();
var router = new Router();
router.get("/search/:domain/:query", async ctx => {
var query = ctx.params.query.replace(/^.*→/, '').trim();
ctx.body = await cachedYoutubeSearch(ctx.params.domain, query);
});
router.get("/vrcurl/:domain/:num", async ctx => {
var url = await resolveVrcUrl(ctx.params.domain, ctx.params.num);
if (url) ctx.redirect(url);
else ctx.status = 404;
});
app.use(router.routes());
app.use(router.allowedMethods());

3
index.js Normal file

@ -0,0 +1,3 @@
import { app } from "./app.js";
app.listen(process.env.PORT || 8142, process.env.ADDRESS);

1542
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

10
package.json Normal file

@ -0,0 +1,10 @@
{
"dependencies": {
"@keyv/sqlite": "^3.6.6",
"@koa/router": "^12.0.1",
"keyv": "^4.5.4",
"koa": "^2.14.2",
"youtube-sr": "^4.3.10"
},
"type": "module"
}

29
vrcurl.js Normal file

@ -0,0 +1,29 @@
import Keyv from "keyv";
var keyv = new Keyv('sqlite://vrcurl.sqlite');
async function nextNum(domain) {
var num = await keyv.get(`${domain}:nextnum`);
num ||= 0;
var max = domain.match(/\d+$/)?.[0];
if (max) max = Number(max);
else max = 10000;
if (num >= max) num = 0;
keyv.set(`${domain}:nextnum`, num + 1);
return num;
}
export async function toVrcUrl(domain, url) {
var num = await nextNum(domain);
await keyv.set(`${domain}:${num}`, url);
return num;
};
export async function resolveVrcUrl(domain, num) {
return await keyv.get(`${domain}:${num}`);
};

40
ytsearch.js Normal file

@ -0,0 +1,40 @@
import { YouTube } from "youtube-sr";
import { toVrcUrl } from "./vrcurl.js";
var cache = {};
export async function cachedYoutubeSearch(domain, query) {
var key = `${domain}:${query}`;
if (!cache[key]) {
cache[key] = youtubeSearch(domain, query);
setTimeout(() => {
delete cache[key];
}, 3.6e6); // cache results for an hour
}
return await cache[key];
}
async function youtubeSearch(domain, query) {
console.debug("search:", query);
var _results = await YouTube.search(query, {safeSearch: true});
console.debug(`raw:`, _results);
var results = [];
for (var result of _results) {
results.push({
id: result.id,
vrcurl: await toVrcUrl(domain, result.url),
title: result.title,
duration: result.duration,
durationString: result.durationFormatted,
uploaded: result.uploadedAt,
views: result.views,
thumbnail_vrcurl: await toVrcUrl(domain, result.thumbnail.url),
channel: {
name: result.channel.name,
id: result.channel.id
//todo icon?
}
});
}
return results;
}