diff --git a/Types.ts b/Types.ts index 26dadb9..3c71868 100644 --- a/Types.ts +++ b/Types.ts @@ -4,9 +4,32 @@ export type Session = { ApiGatewayVersion: string; } + export type Metadata = { date: string; title: string; playbackUrl: string; posterImage: string; -} \ No newline at end of file +} + + +interface Errors { + [key: number]: string +} + +// I didn't use an enum because there is no real advantage that i can find and +// we can't use multiline string for long errors +// TODO: create better errors descriptions +export const Errors: Errors = { + 22: 'FFmpeg is missing. \n' + + 'Destreamer requires a fairly recent release of FFmpeg to work properly. \n' + + 'Please install it with your preferred package manager or copy FFmpeg binary in destreamer root directory. \n', + + 33: 'cannot split videoID from videUrl \n', + + 44: 'couldn\'t evaluate sessionInfo in the page \n', + + 55: 'running in an elevated shell \n', + + 66: 'no valid URL in the input \n' +} diff --git a/destreamer.ts b/destreamer.ts index b98ecc2..f6eba2a 100644 --- a/destreamer.ts +++ b/destreamer.ts @@ -1,7 +1,7 @@ import { sleep, parseVideoUrls, checkRequirements, makeUniqueTitle } from './utils'; import { TokenCache } from './TokenCache'; import { getVideoMetadata } from './Metadata'; -import { Metadata, Session } from './Types'; +import { Metadata, Session, Errors } from './Types'; import { drawThumbnail } from './Thumbnail'; import isElevated from 'is-elevated'; @@ -14,14 +14,6 @@ import yargs from 'yargs'; import sanitize from 'sanitize-filename'; -/** - * exitCode 22 = ffmpeg not found in $PATH - * exitCode 25 = cannot split videoID from videUrl - * exitCode 27 = no hlsUrl in the API response - * exitCode 29 = invalid response from API - * exitCode 88 = error extracting cookies - */ - let tokenCache = new TokenCache(); const argv = yargs.options({ @@ -66,16 +58,23 @@ const argv = yargs.options({ }).argv; async function init() { - const isValidUser = !(await isElevated()); - if (!isValidUser) { - const usrName = process.platform === 'win32' ? 'Admin':'root'; + process.on('unhandledRejection', (reason) => { + console.error(colors.red('Unhandled error!\nTimeout or fatal error, please check your downloads and try again if necessary.\n')); + console.error(colors.red(reason as string)); + }); - console.error(colors.red( - '\nERROR: Destreamer does not run as '+usrName+'!\nPlease run destreamer with a non-privileged user.\n' - )); - process.exit(-1); - } + process.on('exit', (code) => { + if (code === 0) + console.log(colors.bgGreen('\n\nDestreamer finished successfully! \n')) + else if (code in Errors) + console.error(colors.bgRed(`\n\nError: ${Errors[code]} \n`)) + else + console.error(colors.bgRed(`\n\nUnknown exit code ${code} \n`)) + }); + + if (await isElevated()) + process.exit(55); // create output directory if (!fs.existsSync(argv.outputDirectory)) { @@ -100,9 +99,7 @@ async function init() { async function DoInteractiveLogin(url: string, username?: string): Promise { - let videoId = url.split("/").pop() ?? ( - console.log('Couldn\'t split the video Id from the first videoUrl'), process.exit(25) - ); + let videoId = url.split("/").pop() ?? process.exit(33) console.log('Launching headless Chrome to perform the OpenID Connect dance...'); const browser = await puppeteer.launch({ @@ -126,7 +123,6 @@ async function DoInteractiveLogin(url: string, username?: string): Promise { - console.error(colors.red('Unhandled error!\nTimeout or fatal error, please check your downloads and try again if necessary.\n')); - console.error(colors.red(reason as string)); - throw new Error('Killing process..\n'); -}); + async function main() { - checkRequirements(); + checkRequirements() ?? process.exit(22); await init(); - const videoUrls: string[] = parseVideoUrls(argv.videoUrls); - - if (videoUrls.length === 0) { - console.error(colors.red('\nERROR: No valid URL has been found!\n')); - process.exit(-1); - } + const videoUrls: string[] = parseVideoUrls(argv.videoUrls) ?? process.exit(66); let session = tokenCache.Read(); @@ -254,5 +240,5 @@ async function main() { downloadVideo(videoUrls, argv.outputDirectory, session); } -// run + main(); diff --git a/test/test.ts b/test/test.ts index 3bd6a51..003fd65 100644 --- a/test/test.ts +++ b/test/test.ts @@ -53,7 +53,7 @@ describe('Destreamer', () => { fs.writeFileSync(tmpFile.fd, testIn.join('\r\n')); - testOut = parseVideoUrls([tmpFile.name]); + testOut = parseVideoUrls([tmpFile.name])!; if (testOut.length !== expectedOut.length) assert.strictEqual(testOut, expectedOut, "URL list not sanitized"); diff --git a/utils.ts b/utils.ts index bb60ce8..f8095d0 100644 --- a/utils.ts +++ b/utils.ts @@ -3,6 +3,7 @@ import colors from 'colors'; import fs from 'fs'; import path from 'path'; + function sanitizeUrls(urls: string[]) { const rex = new RegExp(/(?:https:\/\/)?.*\/video\/[a-z0-9]{8}-(?:[a-z0-9]{4}\-){3}[a-z0-9]{12}$/, 'i'); const sanitized: string[] = []; @@ -25,9 +26,10 @@ function sanitizeUrls(urls: string[]) { sanitized.push(url+query); } - return sanitized; + return sanitized.length ? sanitized : null; } + export function parseVideoUrls(videoUrls: any) { const t = videoUrls[0] as string; const isPath = t.substring(t.length-4) === '.txt'; @@ -41,24 +43,25 @@ export function parseVideoUrls(videoUrls: any) { return sanitizeUrls(urls); } + export function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } + export function checkRequirements() { try { const ffmpegVer = execSync('ffmpeg -version').toString().split('\n')[0]; console.info(colors.green(`Using ${ffmpegVer}\n`)); } catch (e) { - console.error(colors.red( - 'FFmpeg is missing.\nDestreamer requires a fairly recent release of FFmpeg to work properly.\n' + - 'Please install it with your preferred package manager or copy FFmpeg binary in destreamer root directory.\n' - )); - process.exit(22); + return null; } + + return true; } + export function makeUniqueTitle(title: string, outDir: string) { let ntitle = title; let k = 0; @@ -67,4 +70,4 @@ export function makeUniqueTitle(title: string, outDir: string) { ntitle = title + ' - ' + (++k).toString(); return ntitle; -} \ No newline at end of file +}