From 4cdead16a77cfd06cab52ea1ae2d9a3120335437 Mon Sep 17 00:00:00 2001 From: Toby Milner-Gulland Date: Sun, 20 Oct 2024 22:58:52 +0100 Subject: [PATCH] feat: refactored cli script and made CLI colors nicer --- src/cli/process.ts | 72 ++++++++++++++++++++++++++++++------- src/fs.ts | 4 ++- src/log.ts | 10 +++++- src/sustainability.ts | 14 ++++---- src/video.ts | 83 ++++++++----------------------------------- 5 files changed, 92 insertions(+), 91 deletions(-) diff --git a/src/cli/process.ts b/src/cli/process.ts index bbf79bd..db2b299 100755 --- a/src/cli/process.ts +++ b/src/cli/process.ts @@ -1,10 +1,13 @@ #!/usr/bin/env bun -import { isSuccessfulResult, processVideos } from '../video' +import { isFFmpegInstalled, isSuccessfulResult, processVideos } from '../video' import { name } from '../../package.json' import { parseArgs } from 'util' -import { print } from '../log' +import { logo, print } from '../log' import { logCO2eReport } from '../sustainability' +import { access, mkdir } from 'fs/promises' +import { basename } from 'path' +import { fileExists, getFilesInDirectory, isDirectory } from '../fs' const { values } = parseArgs({ options: { @@ -28,18 +31,55 @@ const input = values.src const outputFolder = values.output const baseDir = values.baseDir -processVideos({ input, outputFolder, baseDir, loglevel: 'quiet' }) - .then(async (videos) => { +const FILE_TYPES = ['.mp4', '.mov'] +const main = async () => { + try { + print.log({ + message: logo.split('\n'), + color: 'lime green' + }) + + await access(input) + + const isDir = await isDirectory(input) + let files: string[] = [] + + if (isDir) { + files = await getFilesInDirectory(input, FILE_TYPES) + } else { + const existingFile = await fileExists(input, basename(input), FILE_TYPES) + if (existingFile) { + files.push(existingFile) + } else { + print.error({ + message: [`Error: The file "${input}" is not a supported video format (.mp4 or .mov).`] + }) + process.exit(1) + } + } + + const hasFFmpeg = await isFFmpegInstalled() + + if (!hasFFmpeg) { + print.error({ message: ['Error: ffmpeg is not installed.'] }) + process.exit(1) + } + + await mkdir(outputFolder, { recursive: true }) + + const videos = await processVideos({ + files, + outputFolder, + baseDir, + loglevel: 'quiet' + }) if (videos.success.length > 0) { print.log({ - message: [`> Generated ${videos.success.length} videos`], + message: [`Processed ${videos.success.length} videos`], color: 'lime green' }) print.log({ - message: videos.success - .filter(isSuccessfulResult) - .map((video) => `> ${video.manifest?.id}`), - indent: 2 + message: videos.success.filter(isSuccessfulResult).map((video) => `∟ ${video.manifest?.id}`) }) logCO2eReport(videos.success) @@ -56,8 +96,16 @@ processVideos({ input, outputFolder, baseDir, loglevel: 'quiet' }) color: 'orange' }) } - }) - .catch((error) => { + } catch (error) { + print.error({ + message: [`Error`] + }) + console.error(error) process.exit(1) - }) + } +} + +if (import.meta.main) { + main() +} diff --git a/src/fs.ts b/src/fs.ts index a728613..b5df430 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -9,7 +9,9 @@ export const getFilesInDirectory = async ( if (!extensions || extensions.length === 0) { return files } - return files.filter((file) => extensions.includes(extname(file).toLowerCase())) + return files + .filter((file) => extensions.includes(extname(file).toLowerCase())) + .map((file) => join(directory, file)) } export const isDirectory = async (path: string): Promise => { diff --git a/src/log.ts b/src/log.ts index 7b403ba..27e7cf6 100644 --- a/src/log.ts +++ b/src/log.ts @@ -4,7 +4,7 @@ type LogOptions = { color?: string } -export const log = ({ message, indent = 0, color = 'grey' }: LogOptions) => { +export const log = ({ message, indent = 2, color = 'grey' }: LogOptions) => { const indentation = ' '.repeat(indent) const colorCode = getColorCode(color) @@ -38,3 +38,11 @@ export const print = { error, log } + +export const logo = ` + \\ | / + +— F I G U R E — + + / | \\ +` diff --git a/src/sustainability.ts b/src/sustainability.ts index 562329f..57a156d 100644 --- a/src/sustainability.ts +++ b/src/sustainability.ts @@ -1,8 +1,8 @@ import { co2 } from '@tgwf/co2' +import { isNumber } from '@figureland/kit/ts/guards' import type { VideoManifest } from './schema' import type { VideoProcessingSuccessResult } from './video' import { print } from './log' -import { isNumber } from '@figureland/kit/ts/guards' const swd = new co2({ model: 'swd' }) const co2e = (bytes: number) => Number(swd.perByte(bytes)) @@ -87,17 +87,15 @@ export const logCO2eReport = (videos: VideoProcessingSuccessResult[]) => { print.log({ message: [ - `Saved ${formatBytes(deltaSize)} of data, or an estimated ${formatCO2e(deltaCO2e)} in emissions` + `:) Saved ${formatBytes(deltaSize)} of data, or an estimated ${formatCO2e(deltaCO2e)} in emissions` ], - color: 'lime green', - indent: 0 + color: 'lime green' }) print.log({ message: [ - `For an example website with 1000 visitors per month`, - `this could save ${formatCO2e(calculation)}/yr` + `You could save ${formatCO2e(calculation)}/yr`, + `based on a website with 1000 visitors/month` ], - color: 'lime green', - indent: 0 + color: 'forest green' }) } diff --git a/src/video.ts b/src/video.ts index a8224d0..4b702fe 100644 --- a/src/video.ts +++ b/src/video.ts @@ -1,11 +1,11 @@ import { $ } from 'bun' -import { mkdir, access } from 'fs/promises' +import { mkdir } from 'fs/promises' import { parse, join, basename, relative } from 'path' import type { VideoManifest } from './schema' import { generateManifest } from './manifest' import { getFileHash } from './hash' import { getVideoManifest } from './api' -import { fileExists, fileSize, getFilesInDirectory, isDirectory } from './fs' +import { fileExists, fileSize } from './fs' import { print } from './log' export const getVideoFPS = async (inputFile: string) => { @@ -196,14 +196,12 @@ export const processVideo = async ({ const endTime = performance.now() const elapsedTime = ((endTime - startTime) / 1000).toFixed(2) - print.log({ message: [`Finished processing ${filename} (${elapsedTime}s)`] }) - print.log({ message: [`${manifestPath}`], indent: 2 }) + print.log({ message: [`Optimised ${filename} (${elapsedTime}s)`] }) for (const source of manifest.sources) { const sizeInMB = (source.size / (1024 * 1024)).toFixed(2) const percentReduction = ((1 - source.size / metadata.size) * 100).toFixed(0) print.log({ - message: [`${source.type} (${sizeInMB}mb, ${percentReduction}% smaller)`], - indent: 4 + message: [`∟ ${source.type} (${sizeInMB}mb, ${percentReduction}% smaller)`] }) } return { @@ -227,88 +225,35 @@ export const isFFmpegInstalled = async (): Promise => { } export const processVideos = async ({ - input, + files, outputFolder, baseDir = '/', loglevel = 'info' }: { - input: string + files: string[] outputFolder: string baseDir?: string loglevel?: FFMpegLogLevel }) => { - try { - await access(input) - } catch (error) { - print.error({ message: [`Error: The input "${input}" does not exist or is not accessible.`] }) - process.exit(1) - } - - const hasFFmpeg = await isFFmpegInstalled() - - if (!hasFFmpeg) { - print.error({ message: ['Error: ffmpeg is not installed.'] }) - process.exit(1) - } - - await mkdir(outputFolder, { recursive: true }) - const results: VideoProcessingResult[] = [] - const isDir = await isDirectory(input) - - if (isDir) { - const videoFiles = await getFilesInDirectory(input, ['.mp4', '.mov']) - - if (videoFiles.length === 0) { - print.error({ - message: [ - `No video files found in "${input}". Please make sure the directory contains .mp4 or .mov files.` - ] - }) - process.exit(0) - } - - for (const f of videoFiles) { - const file = join(input, f) - const result = await processVideo({ - outputFolder, - file, - baseDir, - overwrite: true, - loglevel - }) - if (result) { - results.push(result) - } - } - - print.log({ message: ['All videos processed.'] }) - } else { - const singleFileExists = await fileExists(input, basename(input), ['.mp4', '.mov']) - if (!singleFileExists) { - print.error({ - message: [`Error: The file "${input}" is not a supported video format (.mp4 or .mov).`] - }) - process.exit(1) - } + print.log({ + message: [`Processing ${files.length} videos`] + }) + for (const file of files) { const result = await processVideo({ outputFolder, - file: input, - baseDir: '/converted', + file, + baseDir, overwrite: true, - loglevel: 'info' + loglevel }) - if (result) { results.push(result) } - - print.log({ - message: ['Video processed.'] - }) } + return collectResults(results) }