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 { // 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(`JSON file uploaded to: ${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 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); }