makemkv-auto-rip
Version:
Automatically rips DVDs & Blu-rays using the MakeMKV console, then saves them to unique folders. It can be used from the command line or via a web interface, and is cross-platform. It is also containerized, so it can be run on any system with Docker insta
255 lines (228 loc) • 7.66 kB
JavaScript
import { exec } from "child_process";
import { AppConfig } from "../config/index.js";
import { Logger } from "../utils/logger.js";
import { FileSystemUtils } from "../utils/filesystem.js";
import { ValidationUtils } from "../utils/validation.js";
import { DiscService } from "./disc.service.js";
import { DriveService } from "./drive.service.js";
import { safeExit, withSystemDate } from "../utils/process.js";
import { MakeMKVMessages } from "../utils/makemkv-messages.js";
/**
* Service for handling DVD/Blu-ray ripping operations
*/
export class RipService {
constructor() {
this.goodVideoArray = [];
this.badVideoArray = [];
}
/**
* Start the ripping process for all available discs
* @returns {Promise<void>}
*/
async startRipping() {
try {
// Load drives first if loading is enabled
if (AppConfig.isLoadDrivesEnabled) {
Logger.info("Loading drives before ripping...");
await DriveService.loadDrivesWithWait();
}
// Get fake date from config and execute entire ripping operation with temporary system date
const fakeDate = AppConfig.makeMKVFakeDate;
await withSystemDate(fakeDate, async () => {
Logger.info("Beginning AutoRip... Please Wait.");
const commandDataItems = await DiscService.getAvailableDiscs();
// Check if any discs were found
if (commandDataItems.length === 0) {
Logger.info(
"No discs found to rip. No ripping operations will be performed."
);
Logger.separator();
await this.handlePostRipActions();
return;
}
Logger.info(
`Found ${commandDataItems.length} disc(s) ready for ripping.`
);
await this.processRippingQueue(commandDataItems);
this.displayResults();
await this.handlePostRipActions();
});
} catch (error) {
Logger.error("Critical error during ripping process", error);
await this.ejectDiscs();
safeExit(1, "Critical error during ripping process");
}
}
/**
* Process the queue of discs to rip
* @param {Array} commandDataItems - Array of disc information objects
* @returns {Promise<void>}
*/
async processRippingQueue(commandDataItems) {
if (AppConfig.rippingMode === "sync") {
// Process discs one at a time (synchronously)
Logger.info("Ripping discs synchronously (one at a time)...");
for (const item of commandDataItems) {
try {
await this.ripSingleDisc(item, AppConfig.movieRipsDir);
} catch (error) {
Logger.error(`Error ripping ${item.title}`, error);
this.badVideoArray.push(item.title);
}
}
} else {
// Process discs in parallel (asynchronously) - default behavior
Logger.info("Ripping discs asynchronously (parallel processing)...");
const promises = [];
for (const item of commandDataItems) {
const promise = this.ripSingleDisc(item, AppConfig.movieRipsDir)
.then((result) => result)
.catch((error) => {
Logger.error(`Error ripping ${item.title}`, error);
this.badVideoArray.push(item.title);
});
promises.push(promise);
}
try {
await Promise.all(promises);
} catch (error) {
Logger.error("Uncorrectable Error Ripping One or More DVDs.", error);
throw error;
}
}
}
/**
* Rip a single disc
* @param {Object} commandDataItem - Disc information object
* @param {string} outputPath - Output directory path
* @returns {Promise<string>} - Title of the ripped disc
*/
async ripSingleDisc(commandDataItem, outputPath) {
return new Promise(async (resolve, reject) => {
const dir = FileSystemUtils.createUniqueFolder(
outputPath,
commandDataItem.title
);
Logger.info(`Ripping Title ${commandDataItem.title} to ${dir}...`);
// Get MakeMKV executable path with cross-platform detection
const makeMKVExecutable = await AppConfig.getMakeMKVExecutable();
if (!makeMKVExecutable) {
reject(
new Error(
"MakeMKV executable not found. Please ensure MakeMKV is installed."
)
);
return;
}
const makeMKVCommand = `${makeMKVExecutable} -r mkv disc:${commandDataItem.driveNumber} ${commandDataItem.fileNumber} "${dir}"`;
exec(makeMKVCommand, async (err, stdout, stderr) => {
// Check for critical MakeMKV messages (not first call, so only check for errors)
const shouldContinue = MakeMKVMessages.checkOutput(
stdout + (stderr || ""),
false
);
if (!shouldContinue) {
Logger.error(
"MakeMKV version is too old, please update to the latest version"
);
reject(
new Error(
"MakeMKV version is too old, please update to the latest version"
)
);
return;
}
if (err || stderr) {
Logger.error(
`Critical Error Ripping ${commandDataItem.title}`,
err || stderr
);
reject(err || stderr);
return;
}
try {
await this.handleRipCompletion(stdout, commandDataItem);
resolve(commandDataItem.title);
} catch (error) {
reject(error);
}
});
});
}
/**
* Handle post-rip completion tasks (logging, validation)
* @param {string} stdout - MakeMKV output
* @param {Object} commandDataItem - Disc information object
* @returns {Promise<void>}
*/
async handleRipCompletion(stdout, commandDataItem) {
if (AppConfig.isFileLogEnabled) {
const fileName = FileSystemUtils.createUniqueLogFile(
AppConfig.logDir,
commandDataItem.title
);
try {
await FileSystemUtils.writeLogFile(
fileName,
stdout,
commandDataItem.title
);
} catch (error) {
Logger.error("Error writing log file", error);
}
}
this.checkCopyCompletion(stdout, commandDataItem);
Logger.separator();
}
/**
* Check if the copy completed successfully and update results arrays
* @param {string} data - MakeMKV output
* @param {Object} commandDataItem - Disc information object
*/
checkCopyCompletion(data, commandDataItem) {
const titleName = commandDataItem.title;
if (ValidationUtils.isCopyComplete(data)) {
Logger.info(`Done Ripping ${titleName}`);
this.goodVideoArray.push(titleName);
} else {
Logger.info(`Unable to rip ${titleName}. Try ripping with MakeMKV GUI.`);
this.badVideoArray.push(titleName);
}
}
/**
* Display the results of the ripping process
*/
displayResults() {
if (this.goodVideoArray.length > 0) {
Logger.info(
"The following DVD/Blu-ray titles have been successfully ripped: ",
this.goodVideoArray.join(", ")
);
}
if (this.badVideoArray.length > 0) {
Logger.info(
"The following DVD/Blu-ray titles failed to rip: ",
this.badVideoArray.join(", ")
);
}
// Reset arrays for next run
this.goodVideoArray = [];
this.badVideoArray = [];
}
/**
* Handle post-ripping actions (ejection, etc.)
* @returns {Promise<void>}
*/
async handlePostRipActions() {
await this.ejectDiscs();
}
/**
* Eject all DVDs if configured to do so
* @returns {Promise<void>}
*/
async ejectDiscs() {
if (AppConfig.isEjectDrivesEnabled) {
await DriveService.ejectAllDrives();
}
}
}