forked from sam/cytube-json-generator
202 lines
5.5 KiB
TypeScript
202 lines
5.5 KiB
TypeScript
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 Kia from "https://deno.land/x/kia@0.4.1/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;
|
|
|
|
//preserve file name for JSON file and strip file extension and final .
|
|
const videoName = videoFile.split('/').pop()!.slice(0,videoFile.length-4) as string;
|
|
const stats = await ffprobe(videoFile).catch(() => {
|
|
Deno.exit(1);
|
|
});
|
|
const videoLength = stats.duration;
|
|
let videoHeight = stats.height as number; // Cytube wants this to be specific numbers, nmp
|
|
//cytube freaks out if the height isn't a specific expected number so do some cleanup below
|
|
if (videoHeight <= 480) {
|
|
videoHeight = 480;
|
|
}
|
|
if (videoHeight <= 720 && videoHeight > 480) {
|
|
videoHeight = 720;
|
|
}
|
|
if (videoHeight <= 1080 && videoHeight > 720) {
|
|
videoHeight = 1080;
|
|
}
|
|
|
|
// 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`);
|
|
const subtitleName = `${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: subtitleName,
|
|
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 kia: Kia = new Kia(`Uploading ${location}, this will take time...`);
|
|
kia.start();
|
|
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(() => {
|
|
kia.fail(`Error uploading ${location}: fetch failed`);
|
|
Deno.exit(1);
|
|
});
|
|
const jsondata = await res.json().catch(() => {
|
|
kia.fail(`Error uploading ${location}: JSON response malformed`);
|
|
Deno.exit(1);
|
|
});
|
|
kia.succeed(`Successfully uploaded ${location} to ${jsondata.url}.\n`);
|
|
return jsondata.direct_url;
|
|
}
|
|
|
|
// Run the main function
|
|
if (import.meta.main) {
|
|
await main();
|
|
Deno.exit(0);
|
|
}
|