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

586 lines (502 loc) 16.4 kB
/** * MakeMKV Auto Rip - Configuration Editor JavaScript * Handles the configuration form functionality */ class ConfigEditor { constructor() { this.originalConfig = null; this.originalFormData = null; this.hasUnsavedChanges = false; this.init(); } /** * Initialize the configuration editor */ init() { this.setupEventListeners(); this.loadConfiguration(); } /** * Setup event listeners for form elements */ setupEventListeners() { // Form submission document.getElementById("configForm").addEventListener("submit", (e) => { e.preventDefault(); if (this.validateForm()) { this.saveConfiguration(); } }); // Reset form button document.getElementById("resetForm").addEventListener("click", () => { this.resetForm(); }); // Navigation setup this.setupNavigation(); // Conditional field visibility this.setupConditionalFields(); // Change detection this.setupChangeDetection(); } /** * Setup navigation between configuration sections */ setupNavigation() { const navButtons = document.querySelectorAll(".nav-button"); const sections = document.querySelectorAll(".config-section"); navButtons.forEach((button) => { button.addEventListener("click", () => { const targetSection = button.dataset.section; // Update active nav button navButtons.forEach((btn) => btn.classList.remove("active")); button.classList.add("active"); // Update active section sections.forEach((section) => { section.classList.remove("active"); if (section.dataset.section === targetSection) { section.classList.add("active"); } }); }); }); } /** * Setup conditional field visibility based on form values */ setupConditionalFields() { // Show/hide logging directory based on logging enabled status const loggingEnabledRadios = document.querySelectorAll( 'input[name="paths.logging.enabled"]' ); const loggingDirGroup = document.getElementById("logging_dir_group"); loggingEnabledRadios.forEach((radio) => { radio.addEventListener("change", () => { const isEnabled = document.querySelector('input[name="paths.logging.enabled"]:checked') ?.value === "true"; if (isEnabled) { loggingDirGroup.classList.remove("hidden"); document.getElementById("logging_dir").required = true; } else { loggingDirGroup.classList.add("hidden"); document.getElementById("logging_dir").required = false; } }); }); // Handle MakeMKV directory override checkbox const overrideMakemkvCheckbox = document.getElementById( "override_makemkv_dir" ); const makemkvDirField = document.getElementById("makemkv_dir_field"); if (overrideMakemkvCheckbox && makemkvDirField) { overrideMakemkvCheckbox.addEventListener("change", () => { if (overrideMakemkvCheckbox.checked) { makemkvDirField.classList.add("enabled"); } else { makemkvDirField.classList.remove("enabled"); document.getElementById("makemkv_dir").value = ""; } }); } // Handle fake date fields const fakeDateDateInput = document.getElementById("fake_date_date"); const fakeDateTimeInput = document.getElementById("fake_date_time"); const clearFakeDateButton = document.getElementById("clear_fake_date"); if (fakeDateDateInput && fakeDateTimeInput) { fakeDateDateInput.addEventListener("change", () => { this.updateFakeDateValue(); }); fakeDateTimeInput.addEventListener("change", () => { this.updateFakeDateValue(); }); } if (clearFakeDateButton) { clearFakeDateButton.addEventListener("click", () => { if (fakeDateDateInput) fakeDateDateInput.value = ""; if (fakeDateTimeInput) fakeDateTimeInput.value = ""; this.updateFakeDateValue(); this.checkForChanges(); }); } } /** * Update the hidden fake_date field based on date and time inputs */ updateFakeDateValue() { const fakeDateDateInput = document.getElementById("fake_date_date"); const fakeDateTimeInput = document.getElementById("fake_date_time"); const fakeDateHidden = document.getElementById("fake_date"); if (!fakeDateHidden) return; const dateValue = fakeDateDateInput?.value || ""; const timeValue = fakeDateTimeInput?.value || ""; if (dateValue && timeValue) { fakeDateHidden.value = `${dateValue} ${timeValue}:00`; } else if (dateValue) { fakeDateHidden.value = dateValue; } else { fakeDateHidden.value = ""; } } /** * Setup change detection for form fields */ setupChangeDetection() { const form = document.getElementById("configForm"); // Listen for changes on all form inputs form.addEventListener("input", () => { this.checkForChanges(); }); form.addEventListener("change", () => { this.checkForChanges(); }); } /** * Check if form has unsaved changes */ checkForChanges() { if (!this.originalFormData) return; const currentFormData = this.getFormDataString(); const hasChanges = currentFormData !== this.originalFormData; if (hasChanges !== this.hasUnsavedChanges) { this.hasUnsavedChanges = hasChanges; this.updateSaveBanner(); } } /** * Get form data as a string for comparison */ getFormDataString() { const form = document.getElementById("configForm"); const data = {}; // Get all form elements const formElements = form.querySelectorAll("input, select, textarea"); formElements.forEach((element) => { // Skip elements without names if (!element.name) return; if (element.type === "radio") { if (element.checked) { data[element.name] = element.value; } } else if (element.type === "checkbox") { data[element.name] = element.checked; } else { data[element.name] = element.value; } }); return JSON.stringify(data, Object.keys(data).sort()); } /** * Update save banner visibility */ updateSaveBanner() { const saveBanner = document.getElementById("saveBanner"); if (!saveBanner) { console.error("Save banner element not found!"); return; } if (this.hasUnsavedChanges) { saveBanner.classList.add("show"); } else { saveBanner.classList.remove("show"); } } /** * Load current configuration from server */ async loadConfiguration() { try { this.showLoading(true); const response = await fetch("/api/config/structured"); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); this.originalConfig = data.config; this.populateForm(data.config); this.showMessage("Configuration loaded successfully", "success"); } catch (error) { console.error("Failed to load configuration:", error); this.showMessage( `Failed to load configuration: ${error.message}`, "error" ); } finally { this.showLoading(false); } } /** * Populate form fields with configuration data */ populateForm(config) { // Paths section const makemkvDir = config.paths?.makemkv_dir || ""; const overrideMakemkvCheckbox = document.getElementById( "override_makemkv_dir" ); const makemkvDirField = document.getElementById("makemkv_dir_field"); if (makemkvDir) { overrideMakemkvCheckbox.checked = true; makemkvDirField.classList.add("enabled"); this.setFieldValue("paths.makemkv_dir", makemkvDir); } else { overrideMakemkvCheckbox.checked = false; makemkvDirField.classList.remove("enabled"); this.setFieldValue("paths.makemkv_dir", ""); } this.setFieldValue( "paths.movie_rips_dir", config.paths?.movie_rips_dir || "" ); this.setRadioValue( "paths.logging.enabled", config.paths?.logging?.enabled ?? true ); this.setFieldValue("paths.logging.dir", config.paths?.logging?.dir || ""); this.setFieldValue( "paths.logging.time_format", config.paths?.logging?.time_format || "12hr" ); // Drives section this.setRadioValue("drives.auto_load", config.drives?.auto_load ?? true); this.setRadioValue("drives.auto_eject", config.drives?.auto_eject ?? true); this.setFieldValue("drives.load_delay", config.drives?.load_delay ?? 0); // Mount detection section this.setFieldValue( "mount_detection.wait_timeout", config.mount_detection?.wait_timeout ?? 10 ); this.setFieldValue( "mount_detection.poll_interval", config.mount_detection?.poll_interval ?? 1 ); // Ripping section this.setRadioValue( "ripping.rip_all_titles", config.ripping?.rip_all_titles ?? false ); this.setFieldValue("ripping.mode", config.ripping?.mode || "async"); // Interface section this.setRadioValue( "interface.repeat_mode", config.interface?.repeat_mode ?? true ); // MakeMKV section const fakeDate = config.makemkv?.fake_date || ""; const fakeDateDateInput = document.getElementById("fake_date_date"); const fakeDateTimeInput = document.getElementById("fake_date_time"); this.setFieldValue("makemkv.fake_date", fakeDate); if (fakeDate && fakeDateDateInput && fakeDateTimeInput) { // Parse the fake date to populate date and time inputs const dateTimeMatch = fakeDate.match( /^(\d{4}-\d{2}-\d{2})(?:\s+(\d{2}:\d{2}))?/ ); if (dateTimeMatch) { fakeDateDateInput.value = dateTimeMatch[1]; if (dateTimeMatch[2]) { fakeDateTimeInput.value = dateTimeMatch[2]; } } } else if (fakeDateDateInput && fakeDateTimeInput) { fakeDateDateInput.value = ""; fakeDateTimeInput.value = ""; } // Trigger conditional field visibility const loggingEnabledEvent = new Event("change"); const loggingEnabledRadio = document.querySelector( 'input[name="paths.logging.enabled"]:checked' ); if (loggingEnabledRadio) { loggingEnabledRadio.dispatchEvent(loggingEnabledEvent); } // Capture original form data for change detection setTimeout(() => { this.originalFormData = this.getFormDataString(); this.hasUnsavedChanges = false; this.updateSaveBanner(); }, 100); } /** * Set a field value by name */ setFieldValue(name, value) { const field = document.querySelector(`[name="${name}"]`); if (field) { field.value = value; } } /** * Set a radio button value by name */ setRadioValue(name, value) { const radio = document.querySelector( `input[name="${name}"][value="${value}"]` ); if (radio) { radio.checked = true; } } /** * Get form data as structured object */ getFormData() { const formData = new FormData(document.getElementById("configForm")); const config = {}; // Helper function to set nested property const setNestedProperty = (obj, path, value) => { const keys = path.split("."); const lastKey = keys.pop(); const target = keys.reduce((o, key) => { if (!o[key]) o[key] = {}; return o[key]; }, obj); // Convert string values to appropriate types if (value === "true") value = true; else if (value === "false") value = false; else if (!isNaN(value) && value !== "") value = Number(value); else if (value === "" && path !== "makemkv.fake_date") value = undefined; // Remove empty strings except for fake_date if (value !== undefined) { target[lastKey] = value; } }; // Process all form fields for (const [name, value] of formData.entries()) { setNestedProperty(config, name, value); } // Handle conditional fields const overrideMakemkvCheckbox = document.getElementById( "override_makemkv_dir" ); if (!overrideMakemkvCheckbox.checked) { // Remove makemkv_dir if override is not checked if (config.paths?.makemkv_dir !== undefined) { delete config.paths.makemkv_dir; } } return config; } /** * Save configuration to server */ async saveConfiguration() { try { this.showLoading(true); const config = this.getFormData(); const response = await fetch("/api/config/structured", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ config }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error( errorData.error || `HTTP ${response.status}: ${response.statusText}` ); } const data = await response.json(); // Show appropriate message based on whether a process was killed if (data.processKilled) { this.showMessage( "Configuration saved successfully! Running rip process was stopped to apply changes.", "success" ); } else { this.showMessage("Configuration saved successfully!", "success"); } this.originalConfig = config; // Update original form data and hide save banner this.originalFormData = this.getFormDataString(); this.hasUnsavedChanges = false; this.updateSaveBanner(); } catch (error) { console.error("Failed to save configuration:", error); this.showMessage( `Failed to save configuration: ${error.message}`, "error" ); } finally { this.showLoading(false); } } /** * Reset form to original configuration */ resetForm() { if (this.originalConfig) { this.populateForm(this.originalConfig); this.showMessage("Form reset to current configuration", "warning"); // Reset change tracking setTimeout(() => { this.originalFormData = this.getFormDataString(); this.hasUnsavedChanges = false; this.updateSaveBanner(); }, 100); } } /** * Show loading overlay */ showLoading(show) { const overlay = document.getElementById("loadingOverlay"); overlay.style.display = show ? "flex" : "none"; } /** * Show message to user */ showMessage(message, type = "info") { // Remove existing messages const existingMessages = document.querySelectorAll(".message"); existingMessages.forEach((msg) => msg.remove()); // Create new message const messageDiv = document.createElement("div"); messageDiv.className = `message ${type}`; messageDiv.textContent = message; // Insert at top of form const form = document.getElementById("configForm"); form.insertBefore(messageDiv, form.firstChild); // Auto-remove success/warning messages after delay if (type === "success" || type === "warning") { setTimeout(() => { if (messageDiv.parentNode) { messageDiv.remove(); } }, 5000); } // Scroll to top to show message window.scrollTo({ top: 0, behavior: "smooth" }); } /** * Validate form before submission */ validateForm() { const form = document.getElementById("configForm"); const isValid = form.checkValidity(); if (!isValid) { form.reportValidity(); return false; } // Additional custom validation const movieRipsDir = document.getElementById("movie_rips_dir").value.trim(); if (!movieRipsDir) { this.showMessage("Movie rips directory is required", "error"); return false; } const loggingEnabled = document.querySelector('input[name="paths.logging.enabled"]:checked') ?.value === "true"; const loggingDir = document.getElementById("logging_dir").value.trim(); if (loggingEnabled && !loggingDir) { this.showMessage( "Log directory is required when logging is enabled", "error" ); return false; } return true; } } // Initialize the configuration editor when the page loads document.addEventListener("DOMContentLoaded", () => { new ConfigEditor(); });