init
This commit is contained in:
commit
743bf31a79
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
vrcurl.sqlite
|
20
.vscode/launch.json
vendored
Normal file
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
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
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
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
1542
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
package.json
Normal file
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
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
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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user