This commit is contained in:
2023-08-14 14:36:45 -04:00
commit b5966f98b2
94 changed files with 21124 additions and 0 deletions

11
common/src/index.ts Normal file
View File

@ -0,0 +1,11 @@
// Models
export * from './models/config';
export * from './models/job';
export * from './models/languages';
export * from './models/library';
export * from './models/metrics';
export * from './models/video';
// Utilities
export * from './utils/logger.utils';
export * from './utils/object.utils';

View File

@ -0,0 +1,45 @@
export type Config = {
/** What type of job type should be prioritized, leaving blank will automatically both queues */
priority: 'healthcheck' | 'transcode' | null;
/** Enable healthchecks using the given method, leaving blank disables */
healthcheck: 'quick' | 'verbose' | null;
/** Automatically delete unhealthy files */
deleteUnhealthy: boolean;
/** Require videos pass a healthcheck before being transcoded */
onlyTranscodeHealthy: boolean;
/** Delete original video file after it's been successfully transcoded */
deleteOriginal: boolean;
/** Desired video container/extension, leaving blank will accept any container type */
targetContainer: string | null;
/** Desired video codec, leaving blank will accept any codec */
targetVideoCodec: string | null;
/** Desired audio codec, leaving blank will accept any codec */
targetAudioCodec: string | null;
/** Only keep 1 audio track if multiple match */
singleAudioTrack: boolean;
/** Accepted audio track languages, leaving blank removes all */
audioWhitelist: string[];
/** Accepted subtitle languages, leaving blank removes all */
subtitleWhitelist: string[];
};
export const ConfigDefaults: Config = {
priority: null,
healthcheck: null,
deleteUnhealthy: false,
onlyTranscodeHealthy: false,
deleteOriginal: false,
targetContainer: null,
targetVideoCodec: null,
targetAudioCodec: null,
singleAudioTrack: false,
audioWhitelist: ['eng', 'unk'],
subtitleWhitelist: [],
}
export type KeyVal = {
/** Configuration key */
key: string;
/** Configuration value */
value: any;
}

8
common/src/models/job.ts Normal file
View File

@ -0,0 +1,8 @@
import {File} from 'buffer';
export type JobType = 'healthcheck' | 'transcode'
export type Job = {
type: JobType,
file: File
}

View File

@ -0,0 +1,6 @@
export enum Languages {
eng = 'English',
fre = 'French',
spa = 'Spanish',
unk = 'Unknown'
}

View File

@ -0,0 +1,10 @@
export type Library = {
/** Primary Key */
id?: number;
/** Human-readable name */
name: string;
/** Path to library folder */
path: string;
/** Monitor directory for changes */
watch: boolean;
}

View File

@ -0,0 +1,15 @@
export type Metrics = {
resolution: {[key: string]: number},
container: {[key: string]: number},
videoCodec: {[key: string]: number},
audioCodec: {[key: string]: number},
health: {
healthy: number,
unhealthy: number,
unknown: number
},
audioLang: {[key: string]: number},
subLang: {[key: string]: number},
size: number,
videos: number
}

View File

@ -0,0 +1,63 @@
export const Resolution = {
'240p': 240,
'360p': 360,
'480p': 480,
'720p': 720,
'1080p': 1080,
'4k': 2160,
'8k': 4320,
}
export enum Container {
avi = 'AVI',
mkv = 'MKV',
mp4 = 'MP4',
webm = 'WebM'
}
export enum VideoCodec {
h264 = 'h.264 (AVC)',
h265 = 'h.265 (HEVC)',
h266 = 'h.266 (VVC)',
mpeg2 = 'MPEG-2',
mpeg4 = 'MPEG-4'
}
export enum AudioCodec {
aac = 'AAC',
ac3 = 'AC3',
mp3 = 'MP3',
vorbis = 'Ogg Vorbis',
wav = 'WAV'
}
export type VideoMeta = {
/** Closest standard (NOT actual) video resolution (420p, 720p, 1080p, etc..) */
resolution: string;
/** Algorithm used to encode video */
videoCodec?: keyof VideoCodec;
/** Algorithm used to encode audio */
audioCodec?: keyof AudioCodec;
/** List of available audio tracks */
audioTracks?: string[];
/** List of available subtitle languages */
subtitleTracks?: string[];
}
export type Video = VideoMeta & {
id?: number;
/** Name of the file (extension included, path omitted: "sample.mp4") */
name: string;
/** Path to file */
path: string;
/** Library foreign key */
library: number;
/** Video container/File extension (Binds everything together) */
container?: keyof Container;
/** Whether the file is healthy or not; null if unchecked */
checksum?: string;
/** Size of file in bytes */
healthy?: boolean;
/** Checksum of file - useful for seeing if a file has changed */
size: number;
}

View File

@ -0,0 +1,65 @@
export const CliEffects = {
CLEAR: "\x1b[0m",
BRIGHT: "\x1b[1m",
DIM: "\x1b[2m",
UNDERSCORE: "\x1b[4m",
BLINK: "\x1b[5m",
REVERSE: "\x1b[7m",
HIDDEN: "\x1b[8m",
}
export const CliForeground = {
BLACK: "\x1b[30m",
RED: "\x1b[31m",
GREEN: "\x1b[32m",
YELLOW: "\x1b[33m",
BLUE: "\x1b[34m",
MAGENTA: "\x1b[35m",
CYAN: "\x1b[36m",
WHITE: "\x1b[37m",
GREY: "\x1b[90m",
}
export const CliBackground = {
BLACK: "\x1b[40m",
RED: "\x1b[41m",
GREEN: "\x1b[42m",
YELLOW: "\x1b[43m",
BLUE: "\x1b[44m",
MAGENTA: "\x1b[45m",
CYAN: "\x1b[46m",
WHITE: "\x1b[47m",
GREY: "\x1b[100m",
}
export class Logger {
constructor(public readonly namespace: string) { }
private format(...text: string[]): string {
return `${new Date().toISOString()} [${this.namespace}] ${text.join(' ')}`;
}
debug(...args: string[]) {
console.log(CliForeground.MAGENTA + this.format(...args) + CliEffects.CLEAR);
}
error(...args: string[]) {
console.log(CliForeground.RED + this.format(...args) + CliEffects.CLEAR);
}
info(...args: string[]) {
console.log(CliForeground.CYAN + this.format(...args) + CliEffects.CLEAR);
}
log(...args: string[]) {
console.log(CliEffects.CLEAR + this.format(...args));
}
warn(...args: string[]) {
console.log(CliForeground.YELLOW + this.format(...args) + CliEffects.CLEAR);
}
verbose(...args: string[]) {
console.log(CliForeground.WHITE + this.format(...args) + CliEffects.CLEAR);
}
}

View File

@ -0,0 +1,109 @@
/**
* Removes any null values from an object in-place
*
* @example
* ```ts
* let test = {a: 0, b: false, c: null, d: 'abc'}
* console.log(clean(test)); // Output: {a: 0, b: false, d: 'abc'}
* ```
*
* @param {T} obj Object reference that will be cleaned
* @returns {Partial<T>} Cleaned object
*/
export function clean<T>(obj: T): Partial<T> {
if(obj == null) throw new Error("Cannot clean a NULL value");
Object.entries(obj).forEach(([key, value]) => {
if(value == null) delete (<any>obj)[key];
});
return <any>obj;
}
/**
* Create a deep copy of an object (vs. a shallow copy of references)
*
* Should be replaced by `structuredClone` once released.
*
* @param {T} value Object to copy
* @returns {T} Type
*/
export function deepCopy<T>(value: T): T {
return JSON.parse(JSON.stringify(value));
}
/**
* Get/set a property of an object using dot notation
*
* @example
* ```ts
* // Get a value
* const name = dotNotation<string>(person, 'firstName');
* const familyCarMake = dotNotation(family, 'cars[0].make');
* // Set a value
* dotNotation(family, 'cars[0].make', 'toyota');
* ```
*
* @type T Return type
* @param {Object} obj source object to search
* @param {string} prop property name (Dot notation & indexing allowed)
* @param {any} set Set object property to value, omit to fetch value instead
* @return {T} property value
*/
export function dotNotation<T>(obj: any, prop: string, set: T): T;
export function dotNotation<T>(obj: any, prop: string): T | undefined;
export function dotNotation<T>(obj: any, prop: string, set?: T): T | undefined {
if(obj == null || !prop) return undefined;
// Split property string by '.' or [index]
return <T>prop.split(/[.[\]]/g).filter(prop => prop.length).reduce((obj, prop, i, arr) => {
if(prop[0] == '"' || prop[0] == "'") prop = prop.slice(1, -1); // Take quotes out
if(!obj?.hasOwnProperty(prop)) {
if(set == undefined) return undefined;
obj[prop] = {};
}
if(set !== undefined && i == arr.length - 1)
return obj[prop] = set;
return obj[prop];
}, obj);
}
/**
* Check that an object has the following values
*
* @example
* ```ts
* const test = {a: 2, b: 2};
* includes(test, {a: 1}); // true
* includes(test, {b: 1, c: 3}); // false
* ```
*
* @param target Object to search
* @param values Criteria to check against
* @param allowMissing Only check the keys that are available on the target
* @returns {boolean} Does target include all the values
*/
export function includes(target: any, values: any, allowMissing = false): boolean {
if(target == undefined) return allowMissing;
if(Array.isArray(values)) return values.findIndex((e, i) => !includes(target[i], values[i], allowMissing)) == -1;
const type = typeof values;
if(type != typeof target) return false;
if(type == 'object') {
return Object.keys(values).find(key => !includes(target[key], values[key], allowMissing)) == null;
}
if(type == 'function') return target.toString() == values.toString();
return target == values;
}
/**
* Deep check if two objects are equal
*
* @param {any} a - first item to compare
* @param {any} b - second item to compare
* @returns {boolean} True if they match
*/
export function isEqual(a: any, b: any): boolean {
const ta = typeof a, tb = typeof b;
if((ta != 'object' || a == null) || (tb != 'object' || b == null))
return ta == 'function' && tb == 'function' ? a.toString() == b.toString() : a === b;
const keys = Object.keys(a);
if(keys.length != Object.keys(b).length) return false;
return Object.keys(a).every(key => isEqual(a[key], b[key]));
}