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
425 lines (381 loc) • 13.5 kB
JavaScript
import { exec } from "child_process";
import { promisify } from "util";
import os from "os";
import fs from "fs/promises";
import { Logger } from "./logger.js";
import { NativeOpticalDrive } from "./native-optical-drive.js";
const execAsync = promisify(exec);
/**
* Cross-platform optical drive utility for ejecting and loading CD/DVD/Blu-ray drives
* Supports Windows, macOS, and Linux
*
* Active Windows Support: Windows 8/Server 2012 and later (including Windows 10, 11, Server 2016+)
* Uses MCI (Media Control Interface) which is available across all supported Windows versions
* Note: May work on older Windows versions, but not tested or officially supported
*/
export class OpticalDriveUtil {
static #platform = os.platform();
/**
* Get all optical drives on the system
* @returns {Promise<Array<Object>>} Array of optical drive objects
*/
static async getOpticalDrives() {
switch (this.#platform) {
case "win32":
return await this.#getWindowsOpticalDrives();
case "darwin":
return await this.#getMacOpticalDrives();
case "linux":
return await this.#getLinuxOpticalDrives();
default:
throw new Error(`Unsupported platform: ${this.#platform}`);
}
}
/**
* Eject all optical drives
* @returns {Promise<{total: number, successful: number, failed: number}>}
*/
static async ejectAllDrives() {
const drives = await this.getOpticalDrives();
const ejectPromises = drives.map((drive) => this.ejectDrive(drive));
const results = await Promise.all(ejectPromises);
const successful = results.filter((result) => result === true).length;
const failed = results.filter((result) => result === false).length;
return {
total: drives.length,
successful,
failed,
};
}
/**
* Load/close all optical drives
* @returns {Promise<{total: number, successful: number, failed: number}>}
*/
static async loadAllDrives() {
const drives = await this.getOpticalDrives();
const loadPromises = drives.map((drive) => this.loadDrive(drive));
const results = await Promise.all(loadPromises);
const successful = results.filter((result) => result === true).length;
const failed = results.filter((result) => result === false).length;
return {
total: drives.length,
successful,
failed,
};
}
/**
* Eject a specific optical drive
* @param {Object} drive - Drive object from getOpticalDrives()
* @returns {Promise<boolean>} Success status
*/
static async ejectDrive(drive) {
try {
switch (this.#platform) {
case "win32":
await this.#windowsEjectDrive(drive);
break;
case "darwin":
await this.#macEjectDrive(drive);
break;
case "linux":
await this.#linuxEjectDrive(drive);
break;
}
Logger.info(`Ejected drive: ${drive.description}`);
return true;
} catch (error) {
Logger.error(
`Failed to eject drive: "${drive.description}" (${error.message})`
);
return false;
}
}
/**
* Load/close a specific optical drive
* @param {Object} drive - Drive object from getOpticalDrives()
* @returns {Promise<boolean>} Success status
*/
static async loadDrive(drive) {
try {
switch (this.#platform) {
case "win32":
await this.#windowsLoadDrive(drive);
break;
case "darwin":
await this.#macLoadDrive(drive);
break;
case "linux":
await this.#linuxLoadDrive(drive);
break;
}
Logger.info(`Loaded drive: ${drive.description}`);
return true;
} catch (error) {
Logger.warning(
`Failed to load drive ${drive.description}: ${error.message}`
);
return false;
}
}
// Windows implementation - Hybrid approach: WMI detection + native operations
// Uses PowerShell WMI for reliable drive detection, native C++ for eject/load operations
static async #getWindowsOpticalDrives() {
try {
// Use PowerShell WMI query for reliable optical drive detection
const { stdout } = await execAsync(
'powershell -Command "Get-WmiObject Win32_CDROMDrive | Select-Object Drive, Caption | ConvertTo-Json"'
);
let drives = [];
if (stdout.trim()) {
const wmiData = JSON.parse(stdout);
const driveArray = Array.isArray(wmiData) ? wmiData : [wmiData];
drives = driveArray
.filter((drive) => drive.Drive) // Only include drives with valid drive letters
.map((drive) => ({
id: drive.Drive,
path: drive.Drive,
description: drive.Caption || "Optical Drive",
mediaType: "Optical",
platform: "win32",
}));
}
return drives;
} catch (error) {
Logger.error(`Windows optical drive detection failed: ${error.message}`);
return [];
}
}
static async #windowsEjectDrive(drive) {
try {
// Use native C++ addon for reliable Windows optical drive control
if (NativeOpticalDrive.isNativeAvailable) {
const success = await NativeOpticalDrive.ejectDrive(drive.id);
if (success) {
return;
}
throw new Error(`Native eject failed for drive ${drive.id}`);
} else {
throw new Error("Native optical drive addon is not available");
}
} catch (error) {
throw error;
}
}
static async #windowsLoadDrive(drive) {
try {
// Use native C++ addon for reliable Windows optical drive control
if (NativeOpticalDrive.isNativeAvailable) {
const success = await NativeOpticalDrive.loadDrive(drive.id);
if (success) {
return;
}
throw new Error(`Native load failed for drive ${drive.id}`);
} else {
throw new Error("Native optical drive addon is not available");
}
} catch (error) {
throw error;
}
}
// macOS implementation - Proper optical drive detection
static async #getMacOpticalDrives() {
try {
// Use system_profiler to get disc burning devices (optical drives)
const { stdout } = await execAsync(
"system_profiler SPDiscBurningDataType -json"
);
const data = JSON.parse(stdout);
const drives = [];
if (data.SPDiscBurningDataType && data.SPDiscBurningDataType.length > 0) {
data.SPDiscBurningDataType.forEach((drive, index) => {
if (drive && drive._name) {
drives.push({
id: `optical${index}`,
path: `/dev/rdisk${index + 1}`, // Use raw disk device
description: drive._name,
mediaType: "Optical",
platform: "darwin",
});
}
});
}
// If no drives found through system_profiler, try diskutil
if (drives.length === 0) {
try {
const { stdout: diskutilOut } = await execAsync("diskutil list");
const lines = diskutilOut.split("\n");
for (const line of lines) {
// Look for optical disk entries
if (
line.includes("(optical)") ||
line.includes("CD_ROM") ||
line.includes("DVD")
) {
const diskMatch = line.match(/\/dev\/(disk\d+)/);
if (diskMatch) {
drives.push({
id: diskMatch[1],
path: `/dev/r${diskMatch[1]}`,
description: "Optical Drive",
mediaType: "Optical",
platform: "darwin",
});
}
}
}
} catch (diskutilError) {
Logger.warning("Could not detect optical drives via diskutil");
}
}
return drives;
} catch (error) {
Logger.error(`macOS optical drive detection failed: ${error.message}`);
return [];
}
}
static async #macEjectDrive(drive) {
// Try drutil first (works for most optical drives)
try {
await execAsync("drutil tray open");
} catch (error) {
// Fallback to diskutil eject for the specific drive
try {
await execAsync(`diskutil eject ${drive.path}`);
} catch (fallbackError) {
// Final fallback - try drutil eject
await execAsync("drutil eject");
}
}
}
static async #macLoadDrive(drive) {
try {
await execAsync("drutil tray close");
} catch (error) {
Logger.warning("Drive may not support automatic closing");
}
}
// Linux implementation - Proper optical drive detection using /proc and sysfs
static async #getLinuxOpticalDrives() {
try {
const drives = [];
// Method 1: Check /proc/sys/dev/cdrom/info for optical drives
try {
const cdromInfo = await fs.readFile("/proc/sys/dev/cdrom/info", "utf8");
const lines = cdromInfo.split("\n");
const driveNameLine = lines.find((line) =>
line.startsWith("drive name:")
);
if (driveNameLine) {
const driveNames = driveNameLine.split(":")[1].trim().split(/\s+/);
for (const driveName of driveNames) {
if (driveName && driveName.startsWith("sr")) {
const devicePath = `/dev/${driveName}`;
try {
// Verify the device exists and get additional info
await fs.access(devicePath);
let description = "Optical Drive";
try {
const { stdout } = await execAsync(
`udevadm info --query=property --name=${devicePath} 2>/dev/null`
);
const modelMatch = stdout.match(/ID_MODEL=(.+)/);
if (modelMatch) {
description = modelMatch[1].replace(/_/g, " ");
}
} catch {
// If udevadm fails, keep default description
}
drives.push({
id: driveName,
path: devicePath,
description: description,
mediaType: "Optical",
platform: "linux",
});
} catch {
// Device doesn't exist or isn't accessible
continue;
}
}
}
}
} catch (procError) {
Logger.warning("Could not read /proc/sys/dev/cdrom/info");
}
// Method 2: If no drives found, scan /sys/block for optical devices
if (drives.length === 0) {
try {
const blockDevices = await fs.readdir("/sys/block");
for (const device of blockDevices) {
// Check if it's a CD/DVD drive by examining the device type
try {
const devicePath = `/sys/block/${device}`;
const removableFile = `${devicePath}/removable`;
const capabilityFile = `${devicePath}/capability`;
// Check if device is removable
const removable = await fs.readFile(removableFile, "utf8");
if (removable.trim() !== "1") continue;
// Check capabilities for optical drive indicators
const capability = await fs.readFile(capabilityFile, "utf8");
const capNum = parseInt(capability.trim(), 16);
// Check for optical drive capabilities (bits for CD-ROM, etc.)
// Bit 3 (0x8) = CD-ROM, Bit 4 (0x10) = DVD
if (capNum & 0x8 || capNum & 0x10) {
const deviceFile = `/dev/${device}`;
try {
await fs.access(deviceFile);
let description = "Optical Drive";
try {
const { stdout } = await execAsync(
`udevadm info --query=property --name=${deviceFile} 2>/dev/null`
);
const modelMatch = stdout.match(/ID_MODEL=(.+)/);
if (modelMatch) {
description = modelMatch[1].replace(/_/g, " ");
}
} catch {
// Keep default description
}
drives.push({
id: device,
path: deviceFile,
description: description,
mediaType: "Optical",
platform: "linux",
});
} catch {
continue;
}
}
} catch {
continue;
}
}
} catch (sysError) {
Logger.warning("Could not scan /sys/block for optical drives");
}
}
return drives;
} catch (error) {
Logger.error(`Linux optical drive detection failed: ${error.message}`);
return [];
}
}
static async #linuxEjectDrive(drive) {
try {
await execAsync(`udisksctl unmount -b ${drive.path}`);
await execAsync(`udisksctl eject -b ${drive.path}`);
} catch (error) {
// Fallback to traditional eject if udisksctl fails
await execAsync(`eject ${drive.path}`);
}
}
static async #linuxLoadDrive(drive) {
try {
// No direct udisksctl load/close, so fallback to eject -t
await execAsync(`eject -t ${drive.path}`);
} catch (error) {
Logger.warning(`Drive ${drive.path} may not support automatic loading`);
}
}
}