UNPKG

videory

Version:

CLI for transcoding videos

241 lines (187 loc) 5.8 kB
import {JSONFile, Memory, Low} from "lowdb" import log from 'log'; import assert from "assert" import fs from "fs" import {fileURLToPath} from 'url' import dayjs from "dayjs"; import {dirname, join} from 'path' const debug = log.get("videory:db") let db; /** * Initialize the database * @return {Promise<*>} */ export const initializeDb = async (inMemory) => { let adapter; if (inMemory) { // use memory db adapter = new Memory() debug.notice(`Using memory database`) } else { // use json database on fs const __dirname = dirname(fileURLToPath(import.meta.url)); const file = join(__dirname, 'videory.db.json') adapter = new JSONFile(file) debug.notice(`Using database on filesystem path ${file}`) } // initialize db db = new Low(adapter); // init movies table await db.read() db.data ||= {videos: []}; await db.write() //todo: remove transcoding Bit on every video after startup //init settings tables }; /** * Return video matching given hash and fs path * @param hash * @param path * @returns {*[]} */ async function findVideoByHash({hash, path}) { const {videos} = await getVideos(); return videos.find(vid => vid.hash === hash && vid.path === path) } /** * Return the videos array from database * @returns {Promise<{videos: [], findByHash({hash: *, path: *}): *}|*[]>} */ const getVideos = async () => { await db.read(); return { videos: db.data.videos }; } /** * Update video entity * @param {Object} video * @return {Promise<any>} */ export async function updateVideo(video) { assert.ok(video.hash); const i = db.data.videos.findIndex(v => v.hash === video.hash && v.path === video.path) db.data.videos[i] = video return db.write(); } /** * Insert video to database * @param {string} hash - video file hash * @param {string} name - video name * @param {string} path - video fs path * @param {Date} created - video creation date * @param {string} length - video length in seconds * @param {number} fps - video fps * @param {number} frames - amount of frames in video file * @return {Promise<any | never>} */ export const insertVideo = async (hash, name, path, created, length, fps, frames) => { const m = await findVideoByHash({hash, path}) if (m) { debug.info(`video hash ${hash} already exists in path ${path}`); return; } // video does not exist db.data.videos.push({ hash, name, path, created, length, frames, codec: null, transcoded: null, fps: fps, bitrate: null, failed: null, indexed: dayjs(new Date()), isTranscoding: false, transcodedPath: null, }); debug.info(`video hash ${hash} with path ${path} added to database`); return db.write(); }; /** * Delete a video from db * @param {Object} video * @return {Promise<any>} */ export const deleteVideo = async ({hash, path}) => { let {videos} = await getVideos(); videos = videos.filter(vid => vid.hash !== hash && vid.path !== path) // remove matching element from array return db.write(); }; /** * Return videos that are yet to be encoded * @return {Promise<*>} */ export const findNotTranscodedVideos = async () => { const {videos} = await getVideos(); const notTranscoded = videos .filter(n => n.transcodedPath === null && n.isTranscoding === false /*&& n.failed === null*/) .slice(0, 4) ; if (!notTranscoded.length) { debug.notice("Found no videos to transcode. Will retry shortly"); const failed = await findFailedTranscodings(); if (failed.length) { debug.notice(`Found ${failed.length} failed transcodings: `, failed); } return []; } debug.notice("Found videos to transcode:", notTranscoded.map(n => n.path).join()); const toDelete = []; const foundNotTranscoded = notTranscoded.filter(async n => { const exists = await fs.existsSync(n.path); if (exists) { return n; } debug.notice(`Could not locate DB entry for video ${n.path} on the filesystem. Video entry will be deleted`); toDelete.push(n); }); await Promise.all(toDelete.map(d => deleteVideo(d))); return foundNotTranscoded .map(n => ({ ...n, async setTranscodePath(path) { n.transcodedPath = path; n.transcoded = path ? dayjs(new Date()) : null; return updateVideo(n); }, async setFailed(error) { n.failed = error.message; return updateVideo(n); }, async setIsTrancoding(isTranscoding = true, codec, preset, crf, bitrate) { n = {...n, codec, preset, crf, bitrate}; return updateVideo(n); } })); }; /** * Return videos from db that are marked as isTranscoding * @return {Promise<void>} */ const findStalledTranscodings = async () => { const {videos} = await getVideos(); return videos.filter(vid => vid.transcodedPath === null && vid.isTranscoding === true); }; /** * Return videos from db that are marked as failed * @return {Promise<void>} */ const findFailedTranscodings = async () => { const {videos} = await getVideos(); return videos.filter(v => v.failed === false); }; /** * Delete videos from db that are marked as isTranscoding * @return {Promise<void>} */ export const removeStalledTranscodings = async () => { const videos = await findStalledTranscodings(); videos.map((v) => { v.isTranscoding = false; }) return db.write(); };