Prototype
Signed-off-by: Sam Therapy <sam@samtherapy.net>
This commit is contained in:
parent
e8cbc911a4
commit
a63a3d990f
5 changed files with 237 additions and 0 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -130,3 +130,7 @@ dist
|
|||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
.dccache
|
||||
|
||||
result.json
|
||||
*.vtt
|
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug (deno)",
|
||||
"type": "pwa-node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "deno",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"--inspect-brk",
|
||||
"-A",
|
||||
"${workspaceFolder}/generator.ts",
|
||||
"./test.mp4",
|
||||
"./sodd.srt"
|
||||
],
|
||||
"attachSimplePort": 9229
|
||||
}
|
||||
]
|
||||
}
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"deno.enable": true,
|
||||
"deno.lint": true,
|
||||
"deno.unstable": true
|
||||
}
|
23
README.md
23
README.md
|
@ -1,2 +1,25 @@
|
|||
# cytube-json-generator
|
||||
|
||||
A [deno](https://deno.land) module to generate custom JSON for a
|
||||
[CyTube](https://github.com/calzoneman/sync) server.
|
||||
|
||||
## Installation
|
||||
|
||||
First, you'll need to
|
||||
[install deno](https://deno.land/manual/getting_started/installation).
|
||||
|
||||
Then, you can install the module with:
|
||||
|
||||
```bash
|
||||
deno install --allow-read --allow-write --allow-run "https://git.froth.zone/sam/cytube-json-generator/raw/branch/master/generator.ts"
|
||||
```
|
||||
|
||||
`deno install` installs the module to DENO_INSTALL_ROOT (default `~/.deno`), so
|
||||
you may need to add this to your PATH. An alternative I have is setting
|
||||
`DENO_INSTALL_ROOT` to `~/.local/bin`, which I have in my PATH.
|
||||
|
||||
## Usage
|
||||
|
||||
`generator video.mp4 subs.srt` will generate a JSON file for the video and
|
||||
subtitle file, and upload all three to
|
||||
[f.ruina.exposed](https://f.ruina.exposed). `generator -h` will show full usage.
|
||||
|
|
184
generator.ts
Normal file
184
generator.ts
Normal file
|
@ -0,0 +1,184 @@
|
|||
import { parse } from "https://deno.land/std@0.139.0/flags/mod.ts";
|
||||
import {
|
||||
bold,
|
||||
italic,
|
||||
underline,
|
||||
} from "https://deno.land/std@0.139.0/fmt/colors.ts";
|
||||
import { readableStreamFromReader } from "https://deno.land/std@0.139.0/streams/mod.ts";
|
||||
import srt2webvtt from "https://git.froth.zone/sam/srt-to-vtt/raw/branch/master/mod.ts";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// CLI stuff
|
||||
const args = parse(Deno.args, {
|
||||
alias: {
|
||||
h: "help",
|
||||
u: "upload",
|
||||
},
|
||||
boolean: ["help", "upload"],
|
||||
});
|
||||
if (args.help) {
|
||||
console.log(`
|
||||
${bold("cytube-json-generator")}: Generate a JSON file for Cytube
|
||||
|
||||
${underline("Usage")}:
|
||||
|
||||
generator ${italic("[options]")} ${underline("video")} ${
|
||||
underline(
|
||||
"subtitles",
|
||||
)
|
||||
}
|
||||
|
||||
- ${underline("video")}: The path to the video file. ${
|
||||
bold(
|
||||
"THIS FILE MUST BE AN MP4!",
|
||||
)
|
||||
}
|
||||
- Example: ${italic("./video.mp4")}
|
||||
|
||||
- ${underline("subtitles")}: The path to the subtitles file. ${
|
||||
bold(
|
||||
"THESE MUST BE IN SRT FORMAT!",
|
||||
)
|
||||
}
|
||||
- Example: ${italic("./subtitles.srt")}
|
||||
|
||||
${underline("Options")}:
|
||||
|
||||
--help, -h Show this help text
|
||||
`);
|
||||
Deno.exit(0);
|
||||
}
|
||||
if (args._.length < 2) {
|
||||
console.log(` ${bold("Error")}: video and subtitles must be specified!`);
|
||||
Deno.exit(1);
|
||||
} else if (args._.length > 2) {
|
||||
console.log(` ${bold("Error")}: too many arguments!`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
// End CLI stuff
|
||||
|
||||
// Get video file length
|
||||
const videoFile: string = args._[0] as string;
|
||||
const videoName = videoFile.split("/").pop()!.split(".")[0] as string;
|
||||
const stats = await ffprobe(videoFile).catch(() => {
|
||||
Deno.exit(1);
|
||||
});
|
||||
const videoLength = stats.duration;
|
||||
const videoHeight = stats.height; // Cytube wants this to be specific numbers, nmp
|
||||
|
||||
// Convert subtitles to webvtt
|
||||
const subtitlesFile: string = args._[1] as string;
|
||||
const subtitles = await Deno.readTextFile(subtitlesFile).catch(() => {
|
||||
console.log(` ${bold("Error")}: subtitles file not found!`);
|
||||
Deno.exit(1);
|
||||
});
|
||||
const webvtt = srt2webvtt(subtitles);
|
||||
// Write webvtt to file because idk I'm lazy and don't want to convert it to a stream
|
||||
await Deno.writeTextFile(`./${videoName}.vtt`, webvtt);
|
||||
|
||||
// Upload the video
|
||||
const videoUrl = await upload(videoFile);
|
||||
|
||||
// Upload the subtitles
|
||||
const subtitlesUrl = await upload(`./${videoName}.vtt`);
|
||||
|
||||
// Generate the JSON
|
||||
const json = {
|
||||
title: videoName,
|
||||
duration: videoLength,
|
||||
sources: [
|
||||
{
|
||||
url: videoUrl,
|
||||
contentType: "video/mp4",
|
||||
quality: videoHeight,
|
||||
},
|
||||
],
|
||||
textTracks: [
|
||||
{
|
||||
url: subtitlesUrl,
|
||||
contentType: "text/vtt",
|
||||
name: "Subs or shit IDK",
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
// Also write it to a file because lazy
|
||||
await Deno.writeTextFile("./result.json", JSON.stringify(json));
|
||||
const result = await upload("./result.json");
|
||||
|
||||
console.log(result);
|
||||
Deno.exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs ffprobe on the video file and returns the length in seconds
|
||||
* @param {string} videoFile The path to the video file.
|
||||
* @returns {Promise<Record<string, unknown>>} A JSON object containing the length of the video and its height.
|
||||
* @throws {Error} If ffprobe fails.
|
||||
*/
|
||||
async function ffprobe(videoFile: string): Promise<Record<string, unknown>> {
|
||||
const proc = Deno.run({
|
||||
cmd: [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=duration,height",
|
||||
"-of",
|
||||
"json",
|
||||
`${videoFile}`,
|
||||
],
|
||||
stdout: "piped",
|
||||
});
|
||||
const { code } = await proc.status();
|
||||
const rawOutput = await proc.output();
|
||||
const output = new TextDecoder().decode(rawOutput).trim();
|
||||
// Make the output into readable JSON
|
||||
let json: Record<string, unknown>;
|
||||
try {
|
||||
json = JSON.parse(output).streams[0];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
Deno.exit(1);
|
||||
}
|
||||
// Make the duration actually a number
|
||||
json.duration = Number(json.duration);
|
||||
if (code != 0) {
|
||||
throw new Error();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the specified file to "f.ruina.exposed"
|
||||
* @param location Location of the file
|
||||
* @returns the URL of the uploaded file directly
|
||||
*/
|
||||
async function upload(location: string): Promise<string> {
|
||||
const file = await Deno.open(location, { read: true });
|
||||
const stream = readableStreamFromReader(file);
|
||||
|
||||
const res = await fetch(`https://f.ruina.exposed/upload/`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Linx-Expiry": "259200", // 3 days
|
||||
"Linx-Randomize": "no",
|
||||
},
|
||||
body: stream,
|
||||
}).catch(() => {
|
||||
Deno.exit(1);
|
||||
});
|
||||
const jsondata = await res.json().catch(() => {
|
||||
Deno.exit(1);
|
||||
});
|
||||
return jsondata.direct_url;
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
if (import.meta.main) {
|
||||
await main();
|
||||
Deno.exit(0);
|
||||
}
|
Reference in a new issue