UNPKG

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

356 lines (309 loc) 10.9 kB
/** * System date management utilities for cross-platform date manipulation */ import { exec } from "child_process"; import { promisify } from "util"; import { Logger } from "./logger.js"; const execAsync = promisify(exec); /** * System date manager for temporarily changing system date across platforms */ export class SystemDateManager { constructor() { this.originalDate = null; this.isDateChanged = false; } /** * Determine if the current process has root privileges * @returns {boolean} */ isRunningAsRoot() { try { return typeof process.getuid === "function" && process.getuid() === 0; } catch { return false; } } /** * Prefix a command with sudo when not running as root * Allows interactive password prompt if required * @param {string} command - Command to run * @returns {string} */ withSudo(command) { if (this.isRunningAsRoot()) return command; return `sudo ${command}`; } /** * Format a date as local time string acceptable by timedatectl/date * Example: 2025-07-17 00:00:00 * @param {Date} targetDate * @returns {string} */ formatLocalDateTime(targetDate) { const year = targetDate.getFullYear(); const month = String(targetDate.getMonth() + 1).padStart(2, "0"); const day = String(targetDate.getDate()).padStart(2, "0"); const hour = String(targetDate.getHours()).padStart(2, "0"); const minute = String(targetDate.getMinutes()).padStart(2, "0"); const second = String(targetDate.getSeconds()).padStart(2, "0"); return `${year}-${month}-${day} ${hour}:${minute}:${second}`; } /** * Get platform-specific commands for setting system date * @param {Date} targetDate - The date to set * @returns {Object} Commands for setting and restoring date */ getPlatformCommands(targetDate) { const platform = process.platform; switch (platform) { case "win32": return this.getWindowsCommands(targetDate); case "darwin": return this.getMacOSCommands(targetDate); case "linux": return this.getLinuxCommands(targetDate); default: throw new Error(`Unsupported platform: ${platform}`); } } /** * Get Windows date/time commands * @param {Date} targetDate - The date to set * @returns {Object} Windows commands */ getWindowsCommands(targetDate) { const dateStr = targetDate.toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric", }); const timeStr = targetDate.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", }); return { setDate: `date ${dateStr} && time ${timeStr}`, restoreDate: 'w32tm /config /manualpeerlist:"time.windows.com,0x1" /syncfromflags:manual /reliable:YES /update && w32tm /resync', requiresAdmin: true, }; } /** * Get macOS date/time commands * @param {Date} targetDate - The date to set * @returns {Object} macOS commands */ getMacOSCommands(targetDate) { // Format: MMDDHHmmYYYY (month, day, hour, minute, year) const month = String(targetDate.getMonth() + 1).padStart(2, "0"); const day = String(targetDate.getDate()).padStart(2, "0"); const hour = String(targetDate.getHours()).padStart(2, "0"); const minute = String(targetDate.getMinutes()).padStart(2, "0"); const year = targetDate.getFullYear(); const dateStr = `${month}${day}${hour}${minute}${year}`; return { setDate: `sudo date -u ${dateStr}`, restoreDate: "sudo sntp -sS time.apple.com", requiresAdmin: true, }; } /** * Get Linux date/time commands * @param {Date} targetDate - The date to set * @returns {Object} Linux commands */ getLinuxCommands(targetDate) { // Use local time for Linux commands. timedatectl expects local time by default. const localDateStr = this.formatLocalDateTime(targetDate); // Prefer systemd's timedatectl when available. Fallback to date -s. // We also explicitly disable NTP before setting time to prevent immediate resync. const setUsingTimedatectl = `${this.withSudo( "timedatectl" )} set-ntp false && ${this.withSudo( "timedatectl" )} set-time "${localDateStr}"`; const setUsingDate = `${this.withSudo("date")} -s "${localDateStr}"`; const setDate = `if command -v timedatectl >/dev/null 2>&1; then ${setUsingTimedatectl}; else ${setUsingDate}; fi`; const restoreUsingTimedatectl = `${this.withSudo( "timedatectl" )} set-ntp true`; const restoreFallback = `${this.withSudo( "ntpdate" )} -s time.nist.gov || ${this.withSudo("hwclock")} -s`; const restoreDate = `if command -v timedatectl >/dev/null 2>&1; then ${restoreUsingTimedatectl}; else ${restoreFallback}; fi`; return { setDate, restoreDate, requiresAdmin: true, }; } /** * Set system date to specified date * @param {Date} targetDate - The date to set * @returns {Promise<void>} */ async setSystemDate(targetDate) { // Check if running in Docker container if (process.env.DOCKER_CONTAINER === "true") { Logger.warning( "System date management is not supported in Docker containers. " + "The fake_date feature will be ignored. To use a different date with MakeMKV, " + "please change the host system date manually before starting the container." ); return; } if (this.isDateChanged) { Logger.warning("System date is already changed. Skipping date change."); return; } try { // Store original date this.originalDate = new Date(); const commands = this.getPlatformCommands(targetDate); Logger.info(`Changing system date to: ${targetDate.toISOString()}`); const { stdout, stderr } = await execAsync(commands.setDate); if (stderr && stderr.trim().length > 0) { Logger.warning(`Date change stderr: ${stderr}`); } // Validate that the system time actually changed (allow small drift) const currentAfter = new Date(); const deltaMs = Math.abs(currentAfter.getTime() - targetDate.getTime()); if (deltaMs > 5000) { throw new Error( `Verification failed: system time (${currentAfter.toISOString()}) does not match target (${targetDate.toISOString()}).` ); } this.isDateChanged = true; Logger.info( `System date successfully changed to: ${targetDate.toISOString()}` ); } catch (error) { Logger.error(`Failed to change system date: ${error.message}`); throw new Error(`Unable to change system date: ${error.message}`); } } /** * Restore system date to network time * @returns {Promise<void>} */ async restoreSystemDate() { // Check if running in Docker container if (process.env.DOCKER_CONTAINER === "true") { return; } if (!this.isDateChanged) { Logger.info("System date was not changed. No restoration needed."); return; } try { Logger.info("Restoring system date to network time..."); if (process.platform === "win32") { // Try multiple Windows restoration methods await this.restoreWindowsDate(); } else { // Use standard restore for Linux/macOS const commands = this.getPlatformCommands(new Date()); const { stdout, stderr } = await execAsync(commands.restoreDate); if (stderr && !stderr.includes("successfully")) { Logger.warning(`Date restore stderr: ${stderr}`); } } this.isDateChanged = false; this.originalDate = null; Logger.info("System date successfully restored to network time"); } catch (error) { Logger.error(`Failed to restore system date: ${error.message}`); Logger.warning( `You may need to manually restore your system date to network time. ` + `Original date was approximately: ${this.originalDate?.toISOString()}` ); throw new Error(`Unable to restore system date: ${error.message}`); } } /** * Windows-specific date restoration with fallback methods * @returns {Promise<void>} */ async restoreWindowsDate() { const methods = [ // Method 1: Configure and sync with Windows time server 'w32tm /config /manualpeerlist:"time.windows.com,0x1" /syncfromflags:manual /reliable:YES /update && w32tm /resync', // Method 2: Simple resync (original method) "w32tm /resync /force", // Method 3: Manual restore to original date as fallback this.originalDate ? this.getManualRestoreCommand() : null, ].filter(Boolean); for (let i = 0; i < methods.length; i++) { try { Logger.info(`Attempting Windows date restoration method ${i + 1}...`); const { stdout, stderr } = await execAsync(methods[i]); if ( stderr && !stderr.includes("successfully") && !stderr.includes("completed") ) { Logger.warning(`Date restore method ${i + 1} stderr: ${stderr}`); } else { Logger.info(`Windows date restoration method ${i + 1} succeeded`); return; // Success, exit the function } } catch (error) { Logger.warning( `Windows date restoration method ${i + 1} failed: ${error.message}` ); // If this is the last method, throw the error if (i === methods.length - 1) { throw error; } } } } /** * Get manual restore command for Windows fallback * @returns {string} Manual date restore command */ getManualRestoreCommand() { if (!this.originalDate) return null; const dateStr = this.originalDate.toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric", }); const timeStr = this.originalDate.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", }); return `date ${dateStr} && time ${timeStr}`; } /** * Execute a function with temporary system date * @param {Date} targetDate - The date to set temporarily * @param {Function} operation - The async operation to execute * @returns {Promise<any>} Result of the operation */ async withTemporaryDate(targetDate, operation) { try { await this.setSystemDate(targetDate); const result = await operation(); return result; } finally { await this.restoreSystemDate(); } } /** * Check if system date is currently changed * @returns {boolean} */ isSystemDateChanged() { return this.isDateChanged; } } /** * Global instance for system date management */ export const systemDateManager = new SystemDateManager();