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
201 lines (176 loc) • 6.71 kB
JavaScript
import { OpticalDriveUtil } from "../utils/optical-drive.js";
import { Logger } from "../utils/logger.js";
import { VALIDATION_CONSTANTS } from "../constants/index.js";
import { MakeMKVMessages } from "../utils/makemkv-messages.js";
/**
* Service for handling drive operations (loading and ejecting)
* Now supports Windows, macOS, and Linux optical drives
*/
export class DriveService {
constructor() {
throw new Error(
"DriveService is a static class and cannot be instantiated"
);
}
/**
* Load/close all DVD/Blu-ray drives
* @returns {Promise<void>}
*/
static async loadAllDrives() {
try {
const results = await OpticalDriveUtil.loadAllDrives();
if (results.failed === 0) {
Logger.info("All optical drives have been loaded/closed.");
} else if (results.successful === 0) {
Logger.warning("No optical drives could be loaded.");
} else {
Logger.info(
`${results.successful} of ${results.total} optical drives loaded successfully.`
);
}
} catch (error) {
Logger.error(`Failed to load drives: ${error.message}`);
throw error;
}
}
/**
* Eject all DVD/Blu-ray drives
* @returns {Promise<void>}
*/
static async ejectAllDrives() {
try {
const results = await OpticalDriveUtil.ejectAllDrives();
if (results.failed === 0) {
Logger.info("All optical drives have been ejected.");
} else if (results.successful === 0) {
Logger.warning("No optical drives could be ejected.");
} else {
Logger.info(
`${results.successful} of ${results.total} optical drives ejected successfully.`
);
}
} catch (error) {
Logger.error(`Failed to eject drives: ${error.message}`);
throw error;
}
}
/**
* Load drives and wait with user instruction
* @returns {Promise<void>}
*/
static async loadDrivesWithWait() {
await this.loadAllDrives();
const { AppConfig } = await import("../config/index.js");
const delaySeconds = AppConfig.driveLoadDelay;
if (delaySeconds > 0) {
Logger.separator();
Logger.warning(`Waiting ${delaySeconds} seconds...`);
Logger.warning(
"Please manually close any drives that were not automatically closed."
);
Logger.separator();
await this.wait(delaySeconds * 1000);
}
Logger.info("Drive loading complete. Ready to proceed.");
}
/**
* Get information about available optical drives
* @returns {Promise<Array<Object>>} Array of optical drive objects
*/
static async getOpticalDrives() {
try {
return await OpticalDriveUtil.getOpticalDrives();
} catch (error) {
Logger.error(`Failed to get optical drives: ${error.message}`);
return [];
}
}
/**
* Get count of unmounted optical drives using MakeMKV detection
* @returns {Promise<{total: number, mounted: number, unmounted: number}>}
*/
static async getDriveMountStatus() {
try {
const { AppConfig } = await import("../config/index.js");
const { exec } = await import("child_process");
// Get MakeMKV executable path
const makeMKVExecutable = await AppConfig.getMakeMKVExecutable();
if (!makeMKVExecutable) {
return { total: 0, mounted: 0, unmounted: 0 };
}
const command = `${makeMKVExecutable} -r info disc:index`;
return new Promise((resolve) => {
exec(command, (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"
);
resolve({ total: 0, mounted: 0, unmounted: 0 });
return;
}
// Don't treat stderr as fatal error - MakeMKV often writes warnings there
// Only fail if we have no stdout data or a critical exec error
if (!stdout || stdout.trim() === "") {
Logger.error("No output from MakeMKV command");
resolve({ total: 0, mounted: 0, unmounted: 0 });
return;
}
try {
const lines = stdout.split("\n");
// Filter for actual optical drives (exclude virtual slots with state 256)
const realDriveLines = lines.filter((line) => {
const lineArray = line.split(",");
if (!lineArray[0].startsWith(VALIDATION_CONSTANTS.DRIVE_FILTER))
return false;
const driveState = parseInt(lineArray[1]);
// State 0 = Physical drive present, closed, no media/empty
// State 1 = Physical drive present but open/ejected
// State 2 = Physical drive with media present and mounted
// State 3 = Physical drive loaded but still processing (OS mounting)
// State 256 = No physical drive (virtual slot)
return driveState !== 256;
});
// Count drives with mounted media (state 2 AND has media title)
const mountedDriveLines = realDriveLines.filter((line) => {
const lineArray = line.split(",");
const driveState = parseInt(lineArray[1]);
const mediaTitle = lineArray[5] || "";
// Drive is considered "mounted" if state is 2 AND has a media title
return (
driveState === VALIDATION_CONSTANTS.MEDIA_PRESENT &&
mediaTitle.trim() !== ""
);
});
const total = realDriveLines.length;
const mounted = mountedDriveLines.length;
const unmounted = total - mounted;
Logger.info(
`Drive status: ${total} drives, ${mounted} with mounted media, ${unmounted} available for mounting`
);
Logger.separator();
resolve({ total, mounted, unmounted });
} catch (error) {
Logger.error(`Error parsing drive mount status: ${error.message}`);
resolve({ total: 0, mounted: 0, unmounted: 0 });
}
});
});
} catch (error) {
Logger.error(`Failed to get drive mount status: ${error.message}`);
return { total: 0, mounted: 0, unmounted: 0 };
}
}
/**
* Wait for a specified amount of time
* @param {number} ms - Milliseconds to wait
* @returns {Promise<void>}
*/
static async wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}