From 292c72aa1fb9f467ea0e300d6977d90b41be7c69 Mon Sep 17 00:00:00 2001 From: lukaarma Date: Wed, 12 Aug 2020 17:10:04 +0100 Subject: [PATCH] Title template (#194) * added template option and validation * update comment link to element list * get author info when fetching video info * added template elements to video object * minor function naming changes * better exit message for template error * changed template elements for better substitution * implemented video title template * removed trailing decimals on duration * added template description * removed hashing from uniqueId removed debug logger.warn() * fixed typos in default template added elements to template fail message * moved ffmpeg version logging to verbose --- README.md | 25 +++++++++++ src/CommandLineParser.ts | 47 +++++++++++++++++++- src/Types.ts | 20 ++++++++- src/Utils.ts | 2 +- src/VideoUtils.ts | 92 +++++++++++++++++++++++++++++++--------- src/destreamer.ts | 4 +- 6 files changed, 165 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 496c76b..eb3cf30 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ This release would not have been possible without the code and time contributed - [Università della Calabria][unical]: fork over at https://github.com/peppelongo96/UnicalDown ## What's new +### v2.2 + +- Added title template + +### v2.1 - Major code refactoring (all credits to @lukaarma) - Destreamer is now able to refresh the session's access token. Use this with `-k` (keep cookies) and tick "Remember Me" on login. @@ -138,6 +143,26 @@ https://web.microsoftstream.com/video/xxxxxxxx-aaaa-xxxx-xxxx-xxxxxxxxxxxx -dir=videos/lessons/week2" ``` +### Title template +The `-t` option allows users to input a template string for the output file names. +In the template you have to use 1 or more of the following special sequence that will be substituted at runtime. +The special sequences must be surrounded by curly brackets like this '{title} {publishDate}' + +- `title`: the video title +- `duration`: the video duration in HH:MM:SS format +- `publishDate`: the date when the video was first published in YYYY-MM-DD format +- `publishTime`: the time when the video was first published in HH:MM:SS format +- `author`: the video publisher's name +- `authorEmail`: the video publisher's email +- `uniqueId`: a (almost) unique ID generated from the video informations + +Example - +``` +Input: +-t '{title} - {duration} - {publishDate} - {publishTime} - {author} - {authorEmail} - {uniqueId}' +Expected filename: +This is an example - 0:16:18 - 2020-07-30 - 10:30:13 - lukaarma - example@domain.org - #3c6ca929.mkv +``` ## Expected output diff --git a/src/CommandLineParser.ts b/src/CommandLineParser.ts index 55c36db..5c15b5f 100644 --- a/src/CommandLineParser.ts +++ b/src/CommandLineParser.ts @@ -1,9 +1,11 @@ import { CLI_ERROR, ERROR_CODE } from './Errors'; import { checkOutDir } from './Utils'; import { logger } from './Logger'; +import { templateElements } from './Types'; import fs from 'fs'; import readlineSync from 'readline-sync'; +import sanitize from 'sanitize-filename'; import yargs from 'yargs'; @@ -33,6 +35,13 @@ export const argv: any = yargs.options({ default: 'videos', demandOption: false }, + outputTemplate: { + alias: 't', + describe: 'The template for the title. See the README for more info.', + type: 'string', + default: '{title} - {publishDate} {uniqueId}', + demandOption: false + }, keepLoginCookies: { alias: 'k', describe: 'Let Chromium cache identity provider cookies so you can use "Remember me" during login', @@ -102,7 +111,7 @@ export const argv: any = yargs.options({ }) .wrap(120) .check(() => noArguments()) -.check((argv: any) => inputConflicts(argv.videoUrls, argv.inputFile)) +.check((argv: any) => checkInputConflicts(argv.videoUrls, argv.inputFile)) .check((argv: any) => { if (checkOutDir(argv.outputDirectory)) { return true; @@ -113,6 +122,7 @@ export const argv: any = yargs.options({ throw new Error(' '); } }) +.check((argv: any) => isOutputTemplateValid(argv)) .argv; @@ -129,7 +139,7 @@ function noArguments(): boolean { } -function inputConflicts(videoUrls: Array | undefined, +function checkInputConflicts(videoUrls: Array | undefined, inputFile: string | undefined): boolean { // check if both inputs are declared if ((videoUrls !== undefined) && (inputFile !== undefined)) { @@ -162,6 +172,39 @@ function inputConflicts(videoUrls: Array | undefined, } +function isOutputTemplateValid(argv: any): boolean { + let finalTemplate: string = argv.outputTemplate; + const elementRegEx = RegExp(/{(.*?)}/g); + let match = elementRegEx.exec(finalTemplate); + + // if no template elements this fails + if (match) { + // keep iterating untill we find no more elements + while (match) { + if (!templateElements.includes(match[1])) { + logger.error( + `'${match[0]}' is not aviable as a template element \n` + + `Aviable templates elements: '${templateElements.join("', '")}' \n`, + { fatal: true } + ); + + process.exit(1); + } + match = elementRegEx.exec(finalTemplate); + } + } + // bad template from user, switching to default + else { + logger.warn('Empty output template provided, using default one \n'); + finalTemplate = '{title} - {publishDate} {uniqueId}'; + } + + argv.outputTemplate = sanitize(finalTemplate.trim()); + + return true; +} + + export function promptUser(choices: Array): number { let index: number = readlineSync.keyInSelect(choices, 'Which resolution/format do you prefer?'); diff --git a/src/Types.ts b/src/Types.ts index e75a536..4fc5076 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -6,11 +6,29 @@ export type Session = { export type Video = { - date: string; title: string; + duration: string; + publishDate: string; + publishTime: string; + author: string; + authorEmail: string; + uniqueId: string; outPath: string; totalChunks: number; // Abstraction of FFmpeg timemark playbackUrl: string; posterImageUrl: string; captionsUrl?: string } + + +/* TODO: expand this template once we are all on board with a list +see https://github.com/snobu/destreamer/issues/190#issuecomment-663718010 for list*/ +export const templateElements: Array = [ + 'title', + 'duration', + 'publishDate', + 'publishTime', + 'author', + 'authorEmail', + 'uniqueId' +]; diff --git a/src/Utils.ts b/src/Utils.ts index d643d10..500b92f 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -182,7 +182,7 @@ export function checkOutDir(directory: string): boolean { export function checkRequirements(): void { try { const ffmpegVer: string = execSync('ffmpeg -version').toString().split('\n')[0]; - logger.info(`Using ${ffmpegVer}\n`); + logger.verbose(`Using ${ffmpegVer}\n`); } catch (e) { process.exit(ERROR_CODE.MISSING_FFMPEG); diff --git a/src/VideoUtils.ts b/src/VideoUtils.ts index e2b0040..a9592f5 100644 --- a/src/VideoUtils.ts +++ b/src/VideoUtils.ts @@ -5,10 +5,9 @@ import { Video, Session } from './Types'; import { AxiosResponse } from 'axios'; import fs from 'fs'; -import { parse } from 'iso8601-duration'; +import { parse as parseDuration, Duration } from 'iso8601-duration'; import path from 'path'; -import sanitize from 'sanitize-filename'; - +import sanitizeWindowsName from 'sanitize-filename'; function publishedDateToString(date: string): string { const dateJs: Date = new Date(date); @@ -19,8 +18,25 @@ function publishedDateToString(date: string): string { } +function publishedTimeToString(date: string): string { + const dateJs: Date = new Date(date); + const hours: string = dateJs.getHours().toString(); + const minutes: string = dateJs.getMinutes().toString(); + const seconds: string = dateJs.getSeconds().toString(); + + return `${hours}:${minutes}:${seconds}`; +} + + +function isoDurationToString(time: string): string { + const duration: Duration = parseDuration(time); + + return `${duration.hours ?? '00'}:${duration.minutes ?? '00'}:${duration.seconds?.toFixed(0) ?? '00'}`; +} + + function durationToTotalChunks(duration: string): number { - const durationObj: any = parse(duration); + const durationObj: any = parseDuration(duration); const hrs: number = durationObj.hours ?? 0; const mins: number = durationObj.minutes ?? 0; const secs: number = Math.ceil(durationObj.seconds ?? 0); @@ -32,7 +48,13 @@ function durationToTotalChunks(duration: string): number { export async function getVideoInfo(videoGuids: Array, session: Session, subtitles?: boolean): Promise> { let metadata: Array