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
409 lines (347 loc) • 10.7 kB
JavaScript
/**
* MakeMKV Auto Rip - Web UI JavaScript
* Handles the frontend functionality and WebSocket communication
*/
class MakeMKVWebUI {
constructor() {
this.ws = null;
this.reconnectInterval = null;
this.isConnected = false;
this.currentStatus = "idle";
this.currentOperation = null;
this.init();
}
/**
* Initialize the web UI
*/
init() {
this.setupWebSocket();
this.setupEventListeners();
this.updateConnectionStatus("connecting");
this.addLog("info", "Initializing Web UI...");
}
/**
* Setup WebSocket connection
*/
setupWebSocket() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}`;
try {
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
this.isConnected = true;
this.updateConnectionStatus("connected");
this.addLog("success", "Connected to server");
// Subscribe to status updates
this.sendWebSocketMessage({ type: "subscribe" });
// Start status polling
this.startStatusPolling();
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
}
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleWebSocketMessage(data);
} catch (error) {
console.error("Failed to parse WebSocket message:", error);
}
};
this.ws.onclose = () => {
this.isConnected = false;
this.updateConnectionStatus("disconnected");
this.addLog("warn", "Connection lost. Attempting to reconnect...");
this.startReconnecting();
};
this.ws.onerror = (error) => {
console.error("WebSocket error:", error);
this.addLog("error", "Connection error occurred");
};
} catch (error) {
console.error("Failed to create WebSocket connection:", error);
this.updateConnectionStatus("disconnected");
this.startReconnecting();
}
}
/**
* Handle incoming WebSocket messages
*/
handleWebSocketMessage(data) {
switch (data.type) {
case "connected":
this.addLog("info", data.message);
break;
case "status_update":
this.updateStatus(data.status, data.operation, data.canStop);
break;
case "log":
this.addLog(data.level, data.message);
break;
case "subscribed":
this.addLog("info", "Subscribed to status updates");
break;
default:
console.log("Unknown message type:", data.type);
}
}
/**
* Send message through WebSocket
*/
sendWebSocketMessage(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
/**
* Start reconnecting to WebSocket
*/
startReconnecting() {
if (this.reconnectInterval) {
return;
}
this.reconnectInterval = setInterval(() => {
if (!this.isConnected) {
this.addLog("info", "Attempting to reconnect...");
this.setupWebSocket();
}
}, 5000);
}
/**
* Update connection status in UI
*/
updateConnectionStatus(status) {
const indicator = document.getElementById("statusIndicator");
const text = document.getElementById("statusText");
indicator.className = `status-indicator ${status}`;
switch (status) {
case "connected":
text.textContent = "Connected";
break;
case "connecting":
text.textContent = "Connecting...";
break;
case "disconnected":
text.textContent = "Disconnected";
break;
}
}
/**
* Setup event listeners for UI elements
*/
setupEventListeners() {
// Drive operation buttons
document.getElementById("loadDrivesBtn").addEventListener("click", () => {
this.performDriveOperation("load");
});
document.getElementById("ejectDrivesBtn").addEventListener("click", () => {
this.performDriveOperation("eject");
});
// Ripping operations
document.getElementById("startRippingBtn").addEventListener("click", () => {
this.startRipping();
});
// Logs
document.getElementById("clearLogsBtn").addEventListener("click", () => {
this.clearLogs();
});
}
/**
* Start status polling
*/
startStatusPolling() {
// Poll status every 2 seconds
setInterval(() => {
if (this.isConnected) {
this.fetchStatus();
}
}, 2000);
// Initial status fetch
this.fetchStatus();
}
/**
* Fetch current status from API
*/
async fetchStatus() {
try {
const response = await fetch("/api/status");
const data = await response.json();
this.updateStatus(data.status, data.operation, data.canStop);
} catch (error) {
console.error("Failed to fetch status:", error);
}
}
/**
* Update status display
*/
updateStatus(status, operation, canStop = false) {
this.currentStatus = status;
this.currentOperation = operation;
this.canStop = canStop;
const statusElement = document.getElementById("currentStatus");
const operationElement = document.getElementById("currentOperation");
statusElement.textContent =
status.charAt(0).toUpperCase() + status.slice(1);
statusElement.className = `status-value ${status}`;
operationElement.textContent = operation || "None";
// Update button states
this.updateButtonStates(status, canStop);
}
/**
* Update button states based on current status
*/
updateButtonStates(status, canStop = false) {
const isOperationInProgress = status !== "idle";
// Get button elements
const loadBtn = document.getElementById("loadDrivesBtn");
const ejectBtn = document.getElementById("ejectDrivesBtn");
const ripBtn = document.getElementById("startRippingBtn");
if (isOperationInProgress && canStop) {
// Show stop buttons when operations are running
if (status === "loading") {
loadBtn.innerHTML = "⏹️ Stop Loading";
loadBtn.className = "btn btn-warning";
loadBtn.disabled = false;
ejectBtn.disabled = true;
ripBtn.disabled = true;
} else if (status === "ejecting") {
ejectBtn.innerHTML = "⏹️ Stop Ejecting";
ejectBtn.className = "btn btn-warning";
ejectBtn.disabled = false;
loadBtn.disabled = true;
ripBtn.disabled = true;
} else if (status === "ripping") {
ripBtn.innerHTML = "⏹️ Stop Ripping";
ripBtn.className = "btn btn-warning";
ripBtn.disabled = false;
loadBtn.disabled = true;
ejectBtn.disabled = true;
}
} else {
// Show normal buttons when idle
loadBtn.innerHTML = "📥 Load All Drives";
loadBtn.className = "btn btn-primary";
loadBtn.disabled = isOperationInProgress;
ejectBtn.innerHTML = "📤 Eject All Drives";
ejectBtn.className = "btn btn-secondary";
ejectBtn.disabled = isOperationInProgress;
ripBtn.innerHTML = "▶️ Start Ripping";
ripBtn.className = "btn btn-success";
ripBtn.disabled = isOperationInProgress;
}
}
/**
* Perform drive operation (load/eject) or stop current operation
*/
async performDriveOperation(operation) {
try {
// Check if this is a stop operation
if (this.canStop && this.currentStatus !== "idle") {
await this.stopCurrentOperation();
return;
}
this.addLog(
"info",
`${operation === "load" ? "Loading" : "Ejecting"} drives...`
);
const response = await fetch(`/api/drives/${operation}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data.success) {
this.addLog("success", data.message);
} else {
this.addLog("error", data.error || "Operation failed");
}
} catch (error) {
this.addLog("error", `Failed to ${operation} drives: ${error.message}`);
}
}
/**
* Start the ripping process or stop current operation
*/
async startRipping() {
try {
// Check if this is a stop operation
if (this.canStop && this.currentStatus === "ripping") {
await this.stopCurrentOperation();
return;
}
this.addLog("info", "Starting ripping process...");
const response = await fetch("/api/rip/start", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data.success) {
this.addLog("success", data.message);
} else {
this.addLog("error", data.error || "Failed to start ripping");
}
} catch (error) {
this.addLog("error", `Failed to start ripping: ${error.message}`);
}
}
/**
* Stop the current operation
*/
async stopCurrentOperation() {
try {
this.addLog("warn", "Stopping current operation...");
const response = await fetch("/api/stop", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data.success) {
this.addLog("warn", data.message);
} else {
this.addLog("error", data.error || "Failed to stop operation");
}
} catch (error) {
this.addLog("error", `Failed to stop operation: ${error.message}`);
}
}
/**
* Add log entry to the UI
*/
addLog(level, message) {
const logsContent = document.getElementById("logsContent");
const logEntry = document.createElement("div");
logEntry.className = `log-entry ${level}`;
const timestamp = new Date().toLocaleTimeString();
logEntry.innerHTML = `
<span class="log-time">${timestamp}</span>
<span class="log-message">${message}</span>
`;
logsContent.appendChild(logEntry);
// Auto-scroll to bottom
const logsContainer = logsContent.parentElement;
logsContainer.scrollTop = logsContainer.scrollHeight;
// Keep only last 100 log entries
const entries = logsContent.children;
if (entries.length > 100) {
logsContent.removeChild(entries[0]);
}
}
/**
* Clear all logs
*/
clearLogs() {
const logsContent = document.getElementById("logsContent");
logsContent.innerHTML = "";
this.addLog("info", "Logs cleared");
}
}
// Initialize the web UI when the page loads
document.addEventListener("DOMContentLoaded", () => {
new MakeMKVWebUI();
});