From a63a3d990f24b1181c365549c605be0b9ff4cbee Mon Sep 17 00:00:00 2001 From: Sam Therapy Date: Fri, 13 May 2022 13:21:32 +0200 Subject: [PATCH] Prototype Signed-off-by: Sam Therapy --- .gitignore | 4 + .vscode/launch.json | 21 +++++ .vscode/settings.json | 5 ++ README.md | 23 ++++++ generator.ts | 184 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 237 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 generator.ts diff --git a/.gitignore b/.gitignore index ceaea36..c640434 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,7 @@ dist .yarn/install-state.gz .pnp.* +.dccache + +result.json +*.vtt \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..cc581e1 --- /dev/null +++ b/.vscode/launch.json @@ -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 + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e40716f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": true +} diff --git a/README.md b/README.md index 1f82c24..9ada1f3 100644 --- a/README.md +++ b/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. diff --git a/generator.ts b/generator.ts new file mode 100644 index 0000000..e844fb7 --- /dev/null +++ b/generator.ts @@ -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 { + // 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>} A JSON object containing the length of the video and its height. + * @throws {Error} If ffprobe fails. + */ +async function ffprobe(videoFile: string): Promise> { + 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; + 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 { + 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); +}