diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js
index 3f81d116..102dd030 100644
--- a/server/controllers/ToolsController.js
+++ b/server/controllers/ToolsController.js
@@ -2,9 +2,17 @@ const Logger = require('../Logger')
const Database = require('../Database')
class ToolsController {
- constructor() { }
+ constructor() {}
- // POST: api/tools/item/:id/encode-m4b
+ /**
+ * POST: /api/tools/item/:id/encode-m4b
+ * Start an audiobook merge to m4b task
+ *
+ * @this import('../routers/ApiRouter')
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
async encodeM4b(req, res) {
if (req.libraryItem.isMissing || req.libraryItem.isInvalid) {
Logger.error(`[MiscController] encodeM4b: library item not found or invalid ${req.params.id}`)
@@ -27,7 +35,15 @@ class ToolsController {
res.sendStatus(200)
}
- // DELETE: api/tools/item/:id/encode-m4b
+ /**
+ * DELETE: /api/tools/item/:id/encode-m4b
+ * Cancel a running m4b merge task
+ *
+ * @this import('../routers/ApiRouter')
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
async cancelM4bEncode(req, res) {
const workerTask = this.abMergeManager.getPendingTaskByLibraryItemId(req.params.id)
if (!workerTask) return res.sendStatus(404)
@@ -37,7 +53,15 @@ class ToolsController {
res.sendStatus(200)
}
- // POST: api/tools/item/:id/embed-metadata
+ /**
+ * POST: /api/tools/item/:id/embed-metadata
+ * Start audiobook embed task
+ *
+ * @this import('../routers/ApiRouter')
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
async embedAudioFileMetadata(req, res) {
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
Logger.error(`[ToolsController] Invalid library item`)
@@ -57,7 +81,15 @@ class ToolsController {
res.sendStatus(200)
}
- // POST: api/tools/batch/embed-metadata
+ /**
+ * POST: /api/tools/batch/embed-metadata
+ * Start batch audiobook embed task
+ *
+ * @this import('../routers/ApiRouter')
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
async batchEmbedMetadata(req, res) {
const libraryItemIds = req.body.libraryItemIds || []
if (!libraryItemIds.length) {
@@ -99,6 +131,12 @@ class ToolsController {
res.sendStatus(200)
}
+ /**
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ * @param {import('express').NextFunction} next
+ */
async middleware(req, res, next) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user)
@@ -120,4 +158,4 @@ class ToolsController {
next()
}
}
-module.exports = new ToolsController()
\ No newline at end of file
+module.exports = new ToolsController()
diff --git a/server/libs/fluentFfmpeg/index.d.ts b/server/libs/fluentFfmpeg/index.d.ts
new file mode 100644
index 00000000..ee9f4923
--- /dev/null
+++ b/server/libs/fluentFfmpeg/index.d.ts
@@ -0,0 +1,498 @@
+///
+
+import * as events from "events";
+import * as stream from "stream";
+
+declare namespace Ffmpeg {
+ interface FfmpegCommandLogger {
+ error(...data: any[]): void;
+ warn(...data: any[]): void;
+ info(...data: any[]): void;
+ debug(...data: any[]): void;
+ }
+
+ interface FfmpegCommandOptions {
+ logger?: FfmpegCommandLogger | undefined;
+ niceness?: number | undefined;
+ priority?: number | undefined;
+ presets?: string | undefined;
+ preset?: string | undefined;
+ stdoutLines?: number | undefined;
+ timeout?: number | undefined;
+ source?: string | stream.Readable | undefined;
+ cwd?: string | undefined;
+ }
+
+ interface FilterSpecification {
+ filter: string;
+ inputs?: string | string[] | undefined;
+ outputs?: string | string[] | undefined;
+ options?: any | string | any[] | undefined;
+ }
+
+ type PresetFunction = (command: FfmpegCommand) => void;
+
+ interface Filter {
+ description: string;
+ input: string;
+ multipleInputs: boolean;
+ output: string;
+ multipleOutputs: boolean;
+ }
+ interface Filters {
+ [key: string]: Filter;
+ }
+ type FiltersCallback = (err: Error, filters: Filters) => void;
+
+ interface Codec {
+ type: string;
+ description: string;
+ canDecode: boolean;
+ canEncode: boolean;
+ drawHorizBand?: boolean | undefined;
+ directRendering?: boolean | undefined;
+ weirdFrameTruncation?: boolean | undefined;
+ intraFrameOnly?: boolean | undefined;
+ isLossy?: boolean | undefined;
+ isLossless?: boolean | undefined;
+ }
+ interface Codecs {
+ [key: string]: Codec;
+ }
+ type CodecsCallback = (err: Error, codecs: Codecs) => void;
+
+ interface Encoder {
+ type: string;
+ description: string;
+ frameMT: boolean;
+ sliceMT: boolean;
+ experimental: boolean;
+ drawHorizBand: boolean;
+ directRendering: boolean;
+ }
+ interface Encoders {
+ [key: string]: Encoder;
+ }
+ type EncodersCallback = (err: Error, encoders: Encoders) => void;
+
+ interface Format {
+ description: string;
+ canDemux: boolean;
+ canMux: boolean;
+ }
+ interface Formats {
+ [key: string]: Format;
+ }
+ type FormatsCallback = (err: Error, formats: Formats) => void;
+
+ interface FfprobeData {
+ streams: FfprobeStream[];
+ format: FfprobeFormat;
+ chapters: any[];
+ }
+
+ interface FfprobeStream {
+ [key: string]: any;
+ index: number;
+ codec_name?: string | undefined;
+ codec_long_name?: string | undefined;
+ profile?: number | undefined;
+ codec_type?: string | undefined;
+ codec_time_base?: string | undefined;
+ codec_tag_string?: string | undefined;
+ codec_tag?: string | undefined;
+ width?: number | undefined;
+ height?: number | undefined;
+ coded_width?: number | undefined;
+ coded_height?: number | undefined;
+ has_b_frames?: number | undefined;
+ sample_aspect_ratio?: string | undefined;
+ display_aspect_ratio?: string | undefined;
+ pix_fmt?: string | undefined;
+ level?: string | undefined;
+ color_range?: string | undefined;
+ color_space?: string | undefined;
+ color_transfer?: string | undefined;
+ color_primaries?: string | undefined;
+ chroma_location?: string | undefined;
+ field_order?: string | undefined;
+ timecode?: string | undefined;
+ refs?: number | undefined;
+ id?: string | undefined;
+ r_frame_rate?: string | undefined;
+ avg_frame_rate?: string | undefined;
+ time_base?: string | undefined;
+ start_pts?: number | undefined;
+ start_time?: number | undefined;
+ duration_ts?: string | undefined;
+ duration?: string | undefined;
+ bit_rate?: string | undefined;
+ max_bit_rate?: string | undefined;
+ bits_per_raw_sample?: string | undefined;
+ nb_frames?: string | undefined;
+ nb_read_frames?: string | undefined;
+ nb_read_packets?: string | undefined;
+ sample_fmt?: string | undefined;
+ sample_rate?: number | undefined;
+ channels?: number | undefined;
+ channel_layout?: string | undefined;
+ bits_per_sample?: number | undefined;
+ disposition?: FfprobeStreamDisposition | undefined;
+ rotation?: string | number | undefined;
+ }
+
+ interface FfprobeStreamDisposition {
+ [key: string]: any;
+ default?: number | undefined;
+ dub?: number | undefined;
+ original?: number | undefined;
+ comment?: number | undefined;
+ lyrics?: number | undefined;
+ karaoke?: number | undefined;
+ forced?: number | undefined;
+ hearing_impaired?: number | undefined;
+ visual_impaired?: number | undefined;
+ clean_effects?: number | undefined;
+ attached_pic?: number | undefined;
+ timed_thumbnails?: number | undefined;
+ }
+
+ interface FfprobeFormat {
+ [key: string]: any;
+ filename?: string | undefined;
+ nb_streams?: number | undefined;
+ nb_programs?: number | undefined;
+ format_name?: string | undefined;
+ format_long_name?: string | undefined;
+ start_time?: number | undefined;
+ duration?: number | undefined;
+ size?: number | undefined;
+ bit_rate?: number | undefined;
+ probe_score?: number | undefined;
+ tags?: Record | undefined;
+ }
+
+ interface ScreenshotsConfig {
+ count?: number | undefined;
+ folder?: string | undefined;
+ filename?: string | undefined;
+ timemarks?: number[] | string[] | undefined;
+ timestamps?: number[] | string[] | undefined;
+ fastSeek?: boolean | undefined;
+ size?: string | undefined;
+ }
+
+ interface AudioVideoFilter {
+ filter: string;
+ options: string | string[] | {};
+ }
+
+ // static methods
+ function setFfmpegPath(path: string): FfmpegCommand;
+ function setFfprobePath(path: string): FfmpegCommand;
+ function setFlvtoolPath(path: string): FfmpegCommand;
+ function availableFilters(callback: FiltersCallback): void;
+ function getAvailableFilters(callback: FiltersCallback): void;
+ function availableCodecs(callback: CodecsCallback): void;
+ function getAvailableCodecs(callback: CodecsCallback): void;
+ function availableEncoders(callback: EncodersCallback): void;
+ function getAvailableEncoders(callback: EncodersCallback): void;
+ function availableFormats(callback: FormatsCallback): void;
+ function getAvailableFormats(callback: FormatsCallback): void;
+
+ class FfmpegCommand extends events.EventEmitter {
+ constructor(options?: FfmpegCommandOptions);
+ constructor(input?: string | stream.Readable, options?: FfmpegCommandOptions);
+
+ // options/inputs
+ mergeAdd(source: string | stream.Readable): FfmpegCommand;
+ addInput(source: string | stream.Readable): FfmpegCommand;
+ input(source: string | stream.Readable): FfmpegCommand;
+ withInputFormat(format: string): FfmpegCommand;
+ inputFormat(format: string): FfmpegCommand;
+ fromFormat(format: string): FfmpegCommand;
+ withInputFps(fps: number): FfmpegCommand;
+ withInputFPS(fps: number): FfmpegCommand;
+ withFpsInput(fps: number): FfmpegCommand;
+ withFPSInput(fps: number): FfmpegCommand;
+ inputFPS(fps: number): FfmpegCommand;
+ inputFps(fps: number): FfmpegCommand;
+ fpsInput(fps: number): FfmpegCommand;
+ FPSInput(fps: number): FfmpegCommand;
+ nativeFramerate(): FfmpegCommand;
+ withNativeFramerate(): FfmpegCommand;
+ native(): FfmpegCommand;
+ setStartTime(seek: string | number): FfmpegCommand;
+ seekInput(seek: string | number): FfmpegCommand;
+ loop(duration?: string | number): FfmpegCommand;
+
+ // options/audio
+ withNoAudio(): FfmpegCommand;
+ noAudio(): FfmpegCommand;
+ withAudioCodec(codec: string): FfmpegCommand;
+ audioCodec(codec: string): FfmpegCommand;
+ withAudioBitrate(bitrate: string | number): FfmpegCommand;
+ audioBitrate(bitrate: string | number): FfmpegCommand;
+ withAudioChannels(channels: number): FfmpegCommand;
+ audioChannels(channels: number): FfmpegCommand;
+ withAudioFrequency(freq: number): FfmpegCommand;
+ audioFrequency(freq: number): FfmpegCommand;
+ withAudioQuality(quality: number): FfmpegCommand;
+ audioQuality(quality: number): FfmpegCommand;
+ withAudioFilter(filters: string | string[] | AudioVideoFilter[]): FfmpegCommand;
+ withAudioFilters(filters: string | string[] | AudioVideoFilter[]): FfmpegCommand;
+ audioFilter(filters: string | string[] | AudioVideoFilter[]): FfmpegCommand;
+ audioFilters(filters: string | string[] | AudioVideoFilter[]): FfmpegCommand;
+
+ // options/video;
+ withNoVideo(): FfmpegCommand;
+ noVideo(): FfmpegCommand;
+ withVideoCodec(codec: string): FfmpegCommand;
+ videoCodec(codec: string): FfmpegCommand;
+ withVideoBitrate(bitrate: string | number, constant?: boolean): FfmpegCommand;
+ videoBitrate(bitrate: string | number, constant?: boolean): FfmpegCommand;
+ withVideoFilter(filters: string | string[] | AudioVideoFilter[]): FfmpegCommand;
+ withVideoFilters(filters: string | string[] | AudioVideoFilter[]): FfmpegCommand;
+ videoFilter(filters: string | string[] | AudioVideoFilter[]): FfmpegCommand;
+ videoFilters(filters: string | string[] | AudioVideoFilter[]): FfmpegCommand;
+ withOutputFps(fps: number): FfmpegCommand;
+ withOutputFPS(fps: number): FfmpegCommand;
+ withFpsOutput(fps: number): FfmpegCommand;
+ withFPSOutput(fps: number): FfmpegCommand;
+ withFps(fps: number): FfmpegCommand;
+ withFPS(fps: number): FfmpegCommand;
+ outputFPS(fps: number): FfmpegCommand;
+ outputFps(fps: number): FfmpegCommand;
+ fpsOutput(fps: number): FfmpegCommand;
+ FPSOutput(fps: number): FfmpegCommand;
+ fps(fps: number): FfmpegCommand;
+ FPS(fps: number): FfmpegCommand;
+ takeFrames(frames: number): FfmpegCommand;
+ withFrames(frames: number): FfmpegCommand;
+ frames(frames: number): FfmpegCommand;
+
+ // options/videosize
+ keepPixelAspect(): FfmpegCommand;
+ keepDisplayAspect(): FfmpegCommand;
+ keepDisplayAspectRatio(): FfmpegCommand;
+ keepDAR(): FfmpegCommand;
+ withSize(size: string): FfmpegCommand;
+ setSize(size: string): FfmpegCommand;
+ size(size: string): FfmpegCommand;
+ withAspect(aspect: string | number): FfmpegCommand;
+ withAspectRatio(aspect: string | number): FfmpegCommand;
+ setAspect(aspect: string | number): FfmpegCommand;
+ setAspectRatio(aspect: string | number): FfmpegCommand;
+ aspect(aspect: string | number): FfmpegCommand;
+ aspectRatio(aspect: string | number): FfmpegCommand;
+ applyAutopadding(pad?: boolean, color?: string): FfmpegCommand;
+ applyAutoPadding(pad?: boolean, color?: string): FfmpegCommand;
+ applyAutopad(pad?: boolean, color?: string): FfmpegCommand;
+ applyAutoPad(pad?: boolean, color?: string): FfmpegCommand;
+ withAutopadding(pad?: boolean, color?: string): FfmpegCommand;
+ withAutoPadding(pad?: boolean, color?: string): FfmpegCommand;
+ withAutopad(pad?: boolean, color?: string): FfmpegCommand;
+ withAutoPad(pad?: boolean, color?: string): FfmpegCommand;
+ autoPad(pad?: boolean, color?: string): FfmpegCommand;
+ autopad(pad?: boolean, color?: string): FfmpegCommand;
+
+ // options/output
+ addOutput(target: string | stream.Writable, pipeopts?: { end?: boolean | undefined }): FfmpegCommand;
+ output(target: string | stream.Writable, pipeopts?: { end?: boolean | undefined }): FfmpegCommand;
+ seekOutput(seek: string | number): FfmpegCommand;
+ seek(seek: string | number): FfmpegCommand;
+ withDuration(duration: string | number): FfmpegCommand;
+ setDuration(duration: string | number): FfmpegCommand;
+ duration(duration: string | number): FfmpegCommand;
+ toFormat(format: string): FfmpegCommand;
+ withOutputFormat(format: string): FfmpegCommand;
+ outputFormat(format: string): FfmpegCommand;
+ format(format: string): FfmpegCommand;
+ map(spec: string): FfmpegCommand;
+ updateFlvMetadata(): FfmpegCommand;
+ flvmeta(): FfmpegCommand;
+
+ // options/custom
+ addInputOption(options: string[]): FfmpegCommand;
+ addInputOption(...options: string[]): FfmpegCommand;
+ addInputOptions(options: string[]): FfmpegCommand;
+ addInputOptions(...options: string[]): FfmpegCommand;
+ withInputOption(options: string[]): FfmpegCommand;
+ withInputOption(...options: string[]): FfmpegCommand;
+ withInputOptions(options: string[]): FfmpegCommand;
+ withInputOptions(...options: string[]): FfmpegCommand;
+ inputOption(options: string[]): FfmpegCommand;
+ inputOption(...options: string[]): FfmpegCommand;
+ inputOptions(options: string[]): FfmpegCommand;
+ inputOptions(...options: string[]): FfmpegCommand;
+ addOutputOption(options: string[]): FfmpegCommand;
+ addOutputOption(...options: string[]): FfmpegCommand;
+ addOutputOptions(options: string[]): FfmpegCommand;
+ addOutputOptions(...options: string[]): FfmpegCommand;
+ addOption(options: string[]): FfmpegCommand;
+ addOption(...options: string[]): FfmpegCommand;
+ addOptions(options: string[]): FfmpegCommand;
+ addOptions(...options: string[]): FfmpegCommand;
+ withOutputOption(options: string[]): FfmpegCommand;
+ withOutputOption(...options: string[]): FfmpegCommand;
+ withOutputOptions(options: string[]): FfmpegCommand;
+ withOutputOptions(...options: string[]): FfmpegCommand;
+ withOption(options: string[]): FfmpegCommand;
+ withOption(...options: string[]): FfmpegCommand;
+ withOptions(options: string[]): FfmpegCommand;
+ withOptions(...options: string[]): FfmpegCommand;
+ outputOption(options: string[]): FfmpegCommand;
+ outputOption(...options: string[]): FfmpegCommand;
+ outputOptions(options: string[]): FfmpegCommand;
+ outputOptions(...options: string[]): FfmpegCommand;
+ filterGraph(
+ spec: string | FilterSpecification | Array,
+ map?: string[] | string,
+ ): FfmpegCommand;
+ complexFilter(
+ spec: string | FilterSpecification | Array,
+ map?: string[] | string,
+ ): FfmpegCommand;
+
+ // options/misc
+ usingPreset(preset: string | PresetFunction): FfmpegCommand;
+ preset(preset: string | PresetFunction): FfmpegCommand;
+
+ // processor
+ renice(niceness: number): FfmpegCommand;
+ kill(signal: string): FfmpegCommand;
+ _getArguments(): string[];
+
+ // capabilities
+ setFfmpegPath(path: string): FfmpegCommand;
+ setFfprobePath(path: string): FfmpegCommand;
+ setFlvtoolPath(path: string): FfmpegCommand;
+ availableFilters(callback: FiltersCallback): void;
+ getAvailableFilters(callback: FiltersCallback): void;
+ availableCodecs(callback: CodecsCallback): void;
+ getAvailableCodecs(callback: CodecsCallback): void;
+ availableEncoders(callback: EncodersCallback): void;
+ getAvailableEncoders(callback: EncodersCallback): void;
+ availableFormats(callback: FormatsCallback): void;
+ getAvailableFormats(callback: FormatsCallback): void;
+
+ // ffprobe
+ ffprobe(callback: (err: any, data: FfprobeData) => void): void;
+ ffprobe(index: number, callback: (err: any, data: FfprobeData) => void): void;
+ ffprobe(options: string[], callback: (err: any, data: FfprobeData) => void): void; // tslint:disable-line unified-signatures
+ ffprobe(index: number, options: string[], callback: (err: any, data: FfprobeData) => void): void;
+
+ // event listeners
+ /**
+ * Emitted just after ffmpeg has been spawned.
+ *
+ * @event FfmpegCommand#start
+ * @param {String} command ffmpeg command line
+ */
+ on(event: "start", listener: (command: string) => void): this;
+
+ /**
+ * Emitted when ffmpeg reports progress information
+ *
+ * @event FfmpegCommand#progress
+ * @param {Object} progress progress object
+ * @param {Number} progress.frames number of frames transcoded
+ * @param {Number} progress.currentFps current processing speed in frames per second
+ * @param {Number} progress.currentKbps current output generation speed in kilobytes per second
+ * @param {Number} progress.targetSize current output file size
+ * @param {String} progress.timemark current video timemark
+ * @param {Number} [progress.percent] processing progress (may not be available depending on input)
+ */
+ on(
+ event: "progress",
+ listener: (progress: {
+ frames: number;
+ currentFps: number;
+ currentKbps: number;
+ targetSize: number;
+ timemark: string;
+ percent?: number | undefined;
+ }) => void,
+ ): this;
+
+ /**
+ * Emitted when ffmpeg outputs to stderr
+ *
+ * @event FfmpegCommand#stderr
+ * @param {String} line stderr output line
+ */
+ on(event: "stderr", listener: (line: string) => void): this;
+
+ /**
+ * Emitted when ffmpeg reports input codec data
+ *
+ * @event FfmpegCommand#codecData
+ * @param {Object} codecData codec data object
+ * @param {String} codecData.format input format name
+ * @param {String} codecData.audio input audio codec name
+ * @param {String} codecData.audio_details input audio codec parameters
+ * @param {String} codecData.video input video codec name
+ * @param {String} codecData.video_details input video codec parameters
+ */
+ on(
+ event: "codecData",
+ listener: (codecData: {
+ format: string;
+ audio: string;
+ audio_details: string;
+ video: string;
+ video_details: string;
+ }) => void,
+ ): this;
+
+ /**
+ * Emitted when an error happens when preparing or running a command
+ *
+ * @event FfmpegCommand#error
+ * @param {Error} error error object, with optional properties 'inputStreamError' / 'outputStreamError' for errors on their respective streams
+ * @param {String|null} stdout ffmpeg stdout, unless outputting to a stream
+ * @param {String|null} stderr ffmpeg stderr
+ */
+ on(event: "error", listener: (error: Error, stdout: string | null, stderr: string | null) => void): this;
+
+ /**
+ * Emitted when a command finishes processing
+ *
+ * @event FfmpegCommand#end
+ * @param {Array|String|null} [filenames|stdout] generated filenames when taking screenshots, ffmpeg stdout when not outputting to a stream, null otherwise
+ * @param {String|null} stderr ffmpeg stderr
+ */
+ on(event: "end", listener: (filenames: string[] | string | null, stderr: string | null) => void): this;
+
+ // recipes
+ saveToFile(output: string): FfmpegCommand;
+ save(output: string): FfmpegCommand;
+ writeToStream(stream: stream.Writable, options?: { end?: boolean | undefined }): stream.Writable;
+ pipe(stream?: stream.Writable, options?: { end?: boolean | undefined }): stream.Writable | stream.PassThrough;
+ stream(stream: stream.Writable, options?: { end?: boolean | undefined }): stream.Writable;
+ takeScreenshots(config: number | ScreenshotsConfig, folder?: string): FfmpegCommand;
+ thumbnail(config: number | ScreenshotsConfig, folder?: string): FfmpegCommand;
+ thumbnails(config: number | ScreenshotsConfig, folder?: string): FfmpegCommand;
+ screenshot(config: number | ScreenshotsConfig, folder?: string): FfmpegCommand;
+ screenshots(config: number | ScreenshotsConfig, folder?: string): FfmpegCommand;
+ mergeToFile(target: string | stream.Writable, tmpFolder: string): FfmpegCommand;
+ concatenate(target: string | stream.Writable, options?: { end?: boolean | undefined }): FfmpegCommand;
+ concat(target: string | stream.Writable, options?: { end?: boolean | undefined }): FfmpegCommand;
+ clone(): FfmpegCommand;
+ run(): void;
+ }
+
+ function ffprobe(file: string, callback: (err: any, data: FfprobeData) => void): void;
+ function ffprobe(file: string, index: number, callback: (err: any, data: FfprobeData) => void): void;
+ function ffprobe(file: string, options: string[], callback: (err: any, data: FfprobeData) => void): void; // tslint:disable-line unified-signatures
+ function ffprobe(
+ file: string,
+ index: number,
+ options: string[],
+ callback: (err: any, data: FfprobeData) => void,
+ ): void;
+}
+declare function Ffmpeg(options?: Ffmpeg.FfmpegCommandOptions): Ffmpeg.FfmpegCommand;
+declare function Ffmpeg(input?: string | stream.Readable, options?: Ffmpeg.FfmpegCommandOptions): Ffmpeg.FfmpegCommand;
+
+export = Ffmpeg;
diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js
index 5d3fea8f..e0780cc4 100644
--- a/server/managers/AbMergeManager.js
+++ b/server/managers/AbMergeManager.js
@@ -3,29 +3,53 @@ const fs = require('../libs/fsExtra')
const Logger = require('../Logger')
const TaskManager = require('./TaskManager')
const Task = require('../objects/Task')
-const { writeConcatFile } = require('../utils/ffmpegHelpers')
const ffmpegHelpers = require('../utils/ffmpegHelpers')
const Ffmpeg = require('../libs/fluentFfmpeg')
const SocketAuthority = require('../SocketAuthority')
const { isWritable, copyToExisting } = require('../utils/fileUtils')
const TrackProgressMonitor = require('../objects/TrackProgressMonitor')
+/**
+ * @typedef AbMergeEncodeOptions
+ * @property {string} codec
+ * @property {string} channels
+ * @property {string} bitrate
+ */
+
class AbMergeManager {
constructor() {
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
+ /** @type {Task[]} */
this.pendingTasks = []
}
+ /**
+ *
+ * @param {string} libraryItemId
+ * @returns {Task|null}
+ */
getPendingTaskByLibraryItemId(libraryItemId) {
return this.pendingTasks.find((t) => t.task.data.libraryItemId === libraryItemId)
}
+ /**
+ * Cancel and fail running task
+ *
+ * @param {Task} task
+ * @returns {Promise}
+ */
cancelEncode(task) {
task.setFailed('Task canceled by user')
return this.removeTask(task, true)
}
+ /**
+ *
+ * @param {import('../objects/user/User')} user
+ * @param {import('../objects/LibraryItem')} libraryItem
+ * @param {AbMergeEncodeOptions} [options={}]
+ */
async startAudiobookMerge(user, libraryItem, options = {}) {
const task = new Task()
@@ -63,6 +87,12 @@ class AbMergeManager {
this.runAudiobookMerge(libraryItem, task, options || {})
}
+ /**
+ *
+ * @param {import('../objects/LibraryItem')} libraryItem
+ * @param {Task} task
+ * @param {AbMergeEncodeOptions} encodingOptions
+ */
async runAudiobookMerge(libraryItem, task, encodingOptions) {
// Make sure the target directory is writable
if (!(await isWritable(libraryItem.path))) {
@@ -178,6 +208,12 @@ class AbMergeManager {
Logger.info(`[AbMergeManager] Ab task finished ${task.id}`)
}
+ /**
+ * Remove ab merge task
+ *
+ * @param {Task} task
+ * @param {boolean} [removeTempFilepath=false]
+ */
async removeTask(task, removeTempFilepath = false) {
Logger.info('[AbMergeManager] Removing task ' + task.id)
diff --git a/server/objects/Stream.js b/server/objects/Stream.js
index c49372b0..2ab6f503 100644
--- a/server/objects/Stream.js
+++ b/server/objects/Stream.js
@@ -1,4 +1,3 @@
-
const EventEmitter = require('events')
const Path = require('path')
const Logger = require('../Logger')
@@ -46,7 +45,7 @@ class Stream extends EventEmitter {
}
get episode() {
if (!this.isPodcast) return null
- return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
+ return this.libraryItem.media.episodes.find((ep) => ep.id === this.episodeId)
}
get libraryItemId() {
return this.libraryItem.id
@@ -76,21 +75,10 @@ class Stream extends EventEmitter {
return this.tracks[0].codec
}
get mimeTypesToForceAAC() {
- return [
- AudioMimeType.FLAC,
- AudioMimeType.OPUS,
- AudioMimeType.WMA,
- AudioMimeType.AIFF,
- AudioMimeType.WEBM,
- AudioMimeType.WEBMA,
- AudioMimeType.AWB,
- AudioMimeType.CAF
- ]
+ return [AudioMimeType.FLAC, AudioMimeType.OPUS, AudioMimeType.WMA, AudioMimeType.AIFF, AudioMimeType.WEBM, AudioMimeType.WEBMA, AudioMimeType.AWB, AudioMimeType.CAF]
}
get codecsToForceAAC() {
- return [
- 'alac'
- ]
+ return ['alac']
}
get userToken() {
return this.user.token
@@ -109,7 +97,7 @@ class Stream extends EventEmitter {
}
get numSegments() {
var numSegs = Math.floor(this.totalDuration / this.segmentLength)
- if (this.totalDuration - (numSegs * this.segmentLength) > 0) {
+ if (this.totalDuration - numSegs * this.segmentLength > 0) {
numSegs++
}
return numSegs
@@ -135,7 +123,7 @@ class Stream extends EventEmitter {
clientPlaylistUri: this.clientPlaylistUri,
startTime: this.startTime,
segmentStartNumber: this.segmentStartNumber,
- isTranscodeComplete: this.isTranscodeComplete,
+ isTranscodeComplete: this.isTranscodeComplete
}
}
@@ -143,7 +131,7 @@ class Stream extends EventEmitter {
const segStartTime = segNum * this.segmentLength
if (this.segmentStartNumber > segNum) {
Logger.warn(`[STREAM] Segment #${segNum} Request is before starting segment number #${this.segmentStartNumber} - Reset Transcode`)
- await this.reset(segStartTime - (this.segmentLength * 5))
+ await this.reset(segStartTime - this.segmentLength * 5)
return segStartTime
} else if (this.isTranscodeComplete) {
return false
@@ -153,7 +141,7 @@ class Stream extends EventEmitter {
const distanceFromFurthestSegment = segNum - this.furthestSegmentCreated
if (distanceFromFurthestSegment > 10) {
Logger.info(`Segment #${segNum} requested is ${distanceFromFurthestSegment} segments from latest (${secondsToTimestamp(segStartTime)}) - Reset Transcode`)
- await this.reset(segStartTime - (this.segmentLength * 5))
+ await this.reset(segStartTime - this.segmentLength * 5)
return segStartTime
}
}
@@ -217,7 +205,7 @@ class Stream extends EventEmitter {
else chunks.push(`${current_chunk[0]}-${current_chunk[current_chunk.length - 1]}`)
}
- var perc = (this.segmentsCreated.size * 100 / this.numSegments).toFixed(2) + '%'
+ var perc = ((this.segmentsCreated.size * 100) / this.numSegments).toFixed(2) + '%'
Logger.info('[STREAM-CHECK] Check Files', this.segmentsCreated.size, 'of', this.numSegments, perc, `Furthest Segment: ${this.furthestSegmentCreated}`)
// Logger.debug('[STREAM-CHECK] Chunks', chunks.join(', '))
@@ -251,6 +239,7 @@ class Stream extends EventEmitter {
async start() {
Logger.info(`[STREAM] START STREAM - Num Segments: ${this.numSegments}`)
+ /** @type {import('../libs/fluentFfmpeg/index').FfmpegCommand} */
this.ffmpeg = Ffmpeg()
this.furthestSegmentCreated = 0
@@ -289,24 +278,8 @@ class Stream extends EventEmitter {
audioCodec = 'aac'
}
- this.ffmpeg.addOption([
- `-loglevel ${logLevel}`,
- '-map 0:a',
- `-c:a ${audioCodec}`
- ])
- const hlsOptions = [
- '-f hls',
- "-copyts",
- "-avoid_negative_ts make_non_negative",
- "-max_delay 5000000",
- "-max_muxing_queue_size 2048",
- `-hls_time 6`,
- `-hls_segment_type ${this.hlsSegmentType}`,
- `-start_number ${this.segmentStartNumber}`,
- "-hls_playlist_type vod",
- "-hls_list_size 0",
- "-hls_allow_cache 0"
- ]
+ this.ffmpeg.addOption([`-loglevel ${logLevel}`, '-map 0:a', `-c:a ${audioCodec}`])
+ const hlsOptions = ['-f hls', '-copyts', '-avoid_negative_ts make_non_negative', '-max_delay 5000000', '-max_muxing_queue_size 2048', `-hls_time 6`, `-hls_segment_type ${this.hlsSegmentType}`, `-start_number ${this.segmentStartNumber}`, '-hls_playlist_type vod', '-hls_list_size 0', '-hls_allow_cache 0']
if (this.hlsSegmentType === 'fmp4') {
hlsOptions.push('-strict -2')
var fmp4InitFilename = Path.join(this.streamPath, 'init.mp4')
@@ -369,7 +342,6 @@ class Stream extends EventEmitter {
Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`)
this.clientEmit('stream_open', this.toJSON())
-
}
this.isTranscodeComplete = true
this.ffmpeg = null
@@ -387,11 +359,14 @@ class Stream extends EventEmitter {
this.ffmpeg.kill('SIGKILL')
}
- await fs.remove(this.streamPath).then(() => {
- Logger.info('Deleted session data', this.streamPath)
- }).catch((err) => {
- Logger.error('Failed to delete session data', err)
- })
+ await fs
+ .remove(this.streamPath)
+ .then(() => {
+ Logger.info('Deleted session data', this.streamPath)
+ })
+ .catch((err) => {
+ Logger.error('Failed to delete session data', err)
+ })
if (errorMessage) this.clientEmit('stream_error', { id: this.id, error: (errorMessage || '').trim() })
else this.clientEmit('stream_closed', this.id)
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index b66df030..c22f24ff 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -40,6 +40,7 @@ class ApiRouter {
/** @type {import('../Auth')} */
this.auth = Server.auth
this.playbackSessionManager = Server.playbackSessionManager
+ /** @type {import('../managers/AbMergeManager')} */
this.abMergeManager = Server.abMergeManager
/** @type {import('../managers/BackupManager')} */
this.backupManager = Server.backupManager
@@ -47,6 +48,7 @@ class ApiRouter {
this.watcher = Server.watcher
/** @type {import('../managers/PodcastManager')} */
this.podcastManager = Server.podcastManager
+ /** @type {import('../managers/AudioMetadataManager')} */
this.audioMetadataManager = Server.audioMetadataManager
this.rssFeedManager = Server.rssFeedManager
this.cronManager = Server.cronManager
diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js
index f3c40bf6..60cd0f30 100644
--- a/server/utils/ffmpegHelpers.js
+++ b/server/utils/ffmpegHelpers.js
@@ -53,6 +53,7 @@ async function extractCoverArt(filepath, outputpath) {
await fs.ensureDir(dirname)
return new Promise((resolve) => {
+ /** @type {import('../libs/fluentFfmpeg/index').FfmpegCommand} */
var ffmpeg = Ffmpeg(filepath)
ffmpeg.addOption(['-map 0:v', '-frames:v 1'])
ffmpeg.output(outputpath)
@@ -76,6 +77,7 @@ module.exports.extractCoverArt = extractCoverArt
//This should convert based on the output file extension as well
async function resizeImage(filePath, outputPath, width, height) {
return new Promise((resolve) => {
+ /** @type {import('../libs/fluentFfmpeg/index').FfmpegCommand} */
var ffmpeg = Ffmpeg(filePath)
ffmpeg.addOption(['-vf', `scale=${width || -1}:${height || -1}`])
ffmpeg.addOutput(outputPath)
@@ -111,6 +113,7 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
})
if (!response) return resolve(false)
+ /** @type {import('../libs/fluentFfmpeg/index').FfmpegCommand} */
const ffmpeg = Ffmpeg(response.data)
ffmpeg.addOption('-loglevel debug') // Debug logs printed on error
ffmpeg.outputOptions('-c:a', 'copy', '-map', '0:a', '-metadata', 'podcast=1')
@@ -251,7 +254,7 @@ module.exports.writeFFMetadataFile = writeFFMetadataFile
* @param {number} track - The track number to embed in the audio file.
* @param {string} mimeType - The MIME type of the audio file.
* @param {function(number): void|null} progressCB - A callback function to report progress.
- * @param {Ffmpeg} ffmpeg - The Ffmpeg instance to use (optional). Used for dependency injection in tests.
+ * @param {import('../libs/fluentFfmpeg/index').FfmpegCommand} ffmpeg - The Ffmpeg instance to use (optional). Used for dependency injection in tests.
* @param {function(string, string): Promise} copyFunc - The function to use for copying files (optional). Used for dependency injection in tests.
* @returns {Promise} A promise that resolves if the operation is successful, rejects otherwise.
*/
@@ -392,9 +395,9 @@ module.exports.getFFMetadataObject = getFFMetadataObject
* @param {number} duration - The total duration of the audio tracks.
* @param {string} itemCachePath - The path to the item cache.
* @param {string} outputFilePath - The path to the output file.
- * @param {Object} encodingOptions - The options for encoding the audio.
+ * @param {import('../managers/AbMergeManager').AbMergeEncodeOptions} encodingOptions - The options for encoding the audio.
* @param {Function} [progressCB=null] - The callback function to track the progress of the merge.
- * @param {Object} [ffmpeg=Ffmpeg()] - The FFmpeg instance to use for merging.
+ * @param {import('../libs/fluentFfmpeg/index').FfmpegCommand} [ffmpeg=Ffmpeg()] - The FFmpeg instance to use for merging.
* @returns {Promise} A promise that resolves when the audio files are merged successfully.
*/
async function mergeAudioFiles(audioTracks, duration, itemCachePath, outputFilePath, encodingOptions, progressCB = null, ffmpeg = Ffmpeg()) {