diff --git a/frontend/static/css/new-style.css b/frontend/static/css/new-style.css index 759c3893..bac5e53d 100644 --- a/frontend/static/css/new-style.css +++ b/frontend/static/css/new-style.css @@ -1065,3 +1065,101 @@ input:checked + .slider:before { .connection-status.testing { color: var(--accent-color); } + +/* Swaparr specific styles */ +.swaparr-panel { + margin-bottom: 15px; + border-radius: 8px; + background-color: var(--bg-secondary); + border-left: 4px solid var(--accent-color); +} + +.swaparr-config { + padding: 12px; +} + +.swaparr-config h3 { + margin-top: 0; + margin-bottom: 10px; + color: var(--accent-color); +} + +.swaparr-config-content { + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.swaparr-config-content span { + background-color: var(--bg-tertiary); + padding: 5px 10px; + border-radius: 4px; + font-family: monospace; +} + +.swaparr-table { + width: 100%; + overflow-x: auto; + margin-bottom: 15px; +} + +.swaparr-table table { + width: 100%; + border-collapse: collapse; + background-color: var(--bg-secondary); + border-radius: 8px; + overflow: hidden; +} + +.swaparr-table th { + background-color: var(--bg-tertiary); + padding: 10px; + text-align: left; + color: var(--text-secondary); +} + +.swaparr-table td { + padding: 10px; + border-bottom: 1px solid var(--bg-tertiary); +} + +.swaparr-status-striked { + background-color: rgba(255, 193, 7, 0.1); +} + +.swaparr-status-pending { + background-color: rgba(13, 110, 253, 0.1); +} + +.swaparr-status-ignored { + background-color: rgba(108, 117, 125, 0.1); +} + +.swaparr-status-normal { + background-color: rgba(25, 135, 84, 0.1); +} + +.swaparr-status-removed { + background-color: rgba(220, 53, 69, 0.1); +} + +/* When in dark mode */ +.dark-theme .swaparr-status-striked { + background-color: rgba(255, 193, 7, 0.2); +} + +.dark-theme .swaparr-status-pending { + background-color: rgba(13, 110, 253, 0.2); +} + +.dark-theme .swaparr-status-ignored { + background-color: rgba(108, 117, 125, 0.2); +} + +.dark-theme .swaparr-status-normal { + background-color: rgba(25, 135, 84, 0.2); +} + +.dark-theme .swaparr-status-removed { + background-color: rgba(220, 53, 69, 0.2); +} diff --git a/frontend/static/js/apps/swaparr.js b/frontend/static/js/apps/swaparr.js new file mode 100644 index 00000000..9a0f10ad --- /dev/null +++ b/frontend/static/js/apps/swaparr.js @@ -0,0 +1,381 @@ +// Swaparr-specific functionality + +(function(app) { + if (!app) { + console.error("Huntarr App core is not loaded!"); + return; + } + + const swaparrModule = { + elements: {}, + isTableView: true, // Default to table view for Swaparr logs + hasRenderedAnyContent: false, // Track if we've rendered any content + + // Store data for display + logData: { + config: { + platform: '', + maxStrikes: 3, + scanInterval: '10m', + maxDownloadTime: '2h', + ignoreAboveSize: '25 GB' + }, + downloads: [], // Will store download status records + rawLogs: [] // Store raw logs for backup display + }, + + init: function() { + console.log('[Swaparr Module] Initializing...'); + this.setupLogProcessor(); + + // Add a listener for when the log tab changes to Swaparr + const swaparrTab = document.querySelector('.log-tab[data-app="swaparr"]'); + if (swaparrTab) { + swaparrTab.addEventListener('click', () => { + console.log('[Swaparr Module] Swaparr tab clicked'); + // Small delay to ensure everything is ready + setTimeout(() => { + this.ensureContentRendered(); + }, 200); + }); + } + }, + + setupLogProcessor: function() { + // Setup a listener for custom event from huntarrUI's log processing + document.addEventListener('swaparrLogReceived', (event) => { + console.log('[Swaparr Module] Received log event:', event.detail.logData.substring(0, 100) + '...'); + this.processLogLine(event.detail.logData); + }); + }, + + processLogLine: function(logLine) { + // Always store raw logs for backup display + this.logData.rawLogs.push(logLine); + + // Limit raw logs storage to prevent memory issues + if (this.logData.rawLogs.length > 500) { + this.logData.rawLogs.shift(); + } + + // Process log lines specific to Swaparr + if (!logLine) return; + + // Check if this looks like a Swaparr config line and extract information + if (logLine.includes('Platform:') && logLine.includes('Max strikes:')) { + this.extractConfigInfo(logLine); + this.renderConfigPanel(); + return; + } + + // Look for strike-related logs from system + if (logLine.includes('Added strike') || + logLine.includes('Max strikes reached') || + logLine.includes('removing download') || + logLine.includes('Would have removed')) { + + this.processStrikeLog(logLine); + return; + } + + // Check if this is a table header/separator line + if (logLine.includes('strikes') && logLine.includes('status') && logLine.includes('name') && logLine.includes('size') && logLine.includes('eta')) { + // This is the header line, we can ignore it or use it to confirm table format + return; + } + + // Try to match download info line + // Format: [strikes/max] status name size eta + // Example: 2/3 Striked MyDownload.mkv 1.5 GB 2h 15m + const downloadLinePattern = /(\d+\/\d+)\s+(\w+)\s+(.+?)\s+(\d+(?:\.\d+)?)\s*(\w+)\s+([\ddhms\s]+|Infinite)/; + const match = logLine.match(downloadLinePattern); + + if (match) { + // Extract download information + const downloadInfo = { + strikes: match[1], + status: match[2], + name: match[3], + size: match[4] + ' ' + match[5], + eta: match[6] + }; + + // Update or add to our list of downloads + this.updateDownloadsList(downloadInfo); + this.renderTableView(); + } + + // If we're viewing the Swaparr tab, always ensure content is rendered + if (app.currentLogApp === 'swaparr') { + this.ensureContentRendered(); + } + }, + + // Process strike-related logs from system logs + processStrikeLog: function(logLine) { + // Try to extract download name and strike info + let downloadName = ''; + let strikes = '1/3'; // Default value + let status = 'Striked'; + + // Extract download name + if (logLine.includes('Added strike')) { + const match = logLine.match(/Added strike \((\d+)\/(\d+)\) to (.+?) - Reason:/); + if (match) { + strikes = `${match[1]}/${match[2]}`; + downloadName = match[3]; + status = 'Striked'; + } + } else if (logLine.includes('Max strikes reached')) { + const match = logLine.match(/Max strikes reached for (.+?), removing download/); + if (match) { + downloadName = match[1]; + status = 'Removed'; + } + } else if (logLine.includes('Would have removed')) { + const match = logLine.match(/Would have removed (.+?) after (\d+) strikes/); + if (match) { + downloadName = match[1]; + status = 'Pending Removal'; + strikes = `${match[2]}/3`; + } + } + + if (downloadName) { + // Create a download info object with partial information + const downloadInfo = { + strikes: strikes, + status: status, + name: downloadName, + size: 'Unknown', + eta: 'Unknown' + }; + + // Update downloads list + this.updateDownloadsList(downloadInfo); + this.renderTableView(); + } + }, + + extractConfigInfo: function(logLine) { + // Extract the config data from the log line + const platformMatch = logLine.match(/Platform:\s+(\w+)/); + const maxStrikesMatch = logLine.match(/Max strikes:\s+(\d+)/); + const scanIntervalMatch = logLine.match(/Scan interval:\s+(\d+\w+)/); + const maxDownloadTimeMatch = logLine.match(/Max download time:\s+(\d+\w+)/); + const ignoreSizeMatch = logLine.match(/Ignore above size:\s+(\d+\s*\w+)/); + + if (platformMatch) this.logData.config.platform = platformMatch[1]; + if (maxStrikesMatch) this.logData.config.maxStrikes = maxStrikesMatch[1]; + if (scanIntervalMatch) this.logData.config.scanInterval = scanIntervalMatch[1]; + if (maxDownloadTimeMatch) this.logData.config.maxDownloadTime = maxDownloadTimeMatch[1]; + if (ignoreSizeMatch) this.logData.config.ignoreAboveSize = ignoreSizeMatch[1]; + }, + + updateDownloadsList: function(downloadInfo) { + // Find if this download already exists in our list + const existingIndex = this.logData.downloads.findIndex(item => + item.name.trim() === downloadInfo.name.trim() + ); + + if (existingIndex >= 0) { + // Update existing entry + this.logData.downloads[existingIndex] = downloadInfo; + } else { + // Add new entry + this.logData.downloads.push(downloadInfo); + } + }, + + renderConfigPanel: function() { + // Find the logs container + const logsContainer = document.getElementById('logsContainer'); + if (!logsContainer) return; + + // If the user has selected swaparr logs, show the config panel at the top + if (app.currentLogApp === 'swaparr') { + // Check if config panel already exists + let configPanel = document.getElementById('swaparr-config-panel'); + if (!configPanel) { + // Create the panel + configPanel = document.createElement('div'); + configPanel.id = 'swaparr-config-panel'; + configPanel.classList.add('swaparr-panel'); + logsContainer.appendChild(configPanel); + } + + // Update the panel content + configPanel.innerHTML = ` +
+

Swaparr${this.logData.config.platform ? ' — ' + this.logData.config.platform : ''}

+
+ Max strikes: ${this.logData.config.maxStrikes} + Scan interval: ${this.logData.config.scanInterval} + Max download time: ${this.logData.config.maxDownloadTime} + Ignore above size: ${this.logData.config.ignoreAboveSize} +
+
+ `; + + this.hasRenderedAnyContent = true; + } + }, + + renderTableView: function() { + // Find the logs container + const logsContainer = document.getElementById('logsContainer'); + if (!logsContainer || app.currentLogApp !== 'swaparr') return; + + // Check if table already exists + let tableView = document.getElementById('swaparr-table-view'); + if (!tableView) { + // Create the table + tableView = document.createElement('div'); + tableView.id = 'swaparr-table-view'; + tableView.classList.add('swaparr-table'); + logsContainer.appendChild(tableView); + } + + // Only render table if we have downloads to show + if (this.logData.downloads.length > 0) { + // Generate table HTML + let tableHTML = ` + + + + + + + + + + + + `; + + // Add each download as a row + this.logData.downloads.forEach(download => { + // Apply status-specific CSS class + let statusClass = download.status.toLowerCase(); + + // Normalize some status values + if (statusClass === 'pending removal') statusClass = 'pending'; + if (statusClass === 'removed') statusClass = 'removed'; + if (statusClass === 'striked') statusClass = 'striked'; + if (statusClass === 'normal') statusClass = 'normal'; + if (statusClass === 'ignored') statusClass = 'ignored'; + + tableHTML += ` + + + + + + + + `; + }); + + tableHTML += ` + +
StrikesStatusNameSizeETA
${download.strikes}${download.status}${download.name}${download.size}${download.eta}
+ `; + + tableView.innerHTML = tableHTML; + this.hasRenderedAnyContent = true; + } + }, + + // Render raw logs if we don't have structured content + renderRawLogs: function() { + // Only show raw logs if we have no other content + if (this.hasRenderedAnyContent) return; + + const logsContainer = document.getElementById('logsContainer'); + if (!logsContainer || app.currentLogApp !== 'swaparr') return; + + // Start with a message + const noDataMessage = document.createElement('div'); + noDataMessage.classList.add('swaparr-panel'); + noDataMessage.innerHTML = ` +
+

Swaparr Logs

+

Waiting for structured Swaparr data. Showing raw logs below:

+
+ `; + logsContainer.appendChild(noDataMessage); + + // Add raw logs + for (const logLine of this.logData.rawLogs) { + const logEntry = document.createElement('div'); + logEntry.className = 'log-entry'; + logEntry.innerHTML = `${logLine}`; + + // Basic level detection + if (logLine.includes('ERROR')) logEntry.classList.add('log-error'); + else if (logLine.includes('WARN') || logLine.includes('WARNING')) logEntry.classList.add('log-warning'); + else if (logLine.includes('DEBUG')) logEntry.classList.add('log-debug'); + else logEntry.classList.add('log-info'); + + logsContainer.appendChild(logEntry); + } + + this.hasRenderedAnyContent = true; + }, + + // Make sure we display something in the Swaparr tab + ensureContentRendered: function() { + console.log('[Swaparr Module] Ensuring content is rendered, has content:', this.hasRenderedAnyContent); + + // Reset rendered flag + this.hasRenderedAnyContent = false; + + // Check if we're viewing Swaparr tab + if (app.currentLogApp !== 'swaparr') return; + + // First try to render structured content + this.renderConfigPanel(); + this.renderTableView(); + + // If no structured content, show raw logs + if (!this.hasRenderedAnyContent) { + this.renderRawLogs(); + } + }, + + // Clear the data when switching log views + clearData: function() { + this.logData.downloads = []; + // Keep raw logs for now + this.hasRenderedAnyContent = false; + } + }; + + // Initialize the module + document.addEventListener('DOMContentLoaded', () => { + swaparrModule.init(); + + if (app) { + app.swaparrModule = swaparrModule; + + // Setup a handler for when log tabs are changed + document.querySelectorAll('.log-tab').forEach(tab => { + tab.addEventListener('click', (e) => { + // If switching to swaparr tab, make sure we render the view + if (e.target.getAttribute('data-app') === 'swaparr') { + console.log('[Swaparr Module] Swaparr tab clicked via delegation'); + // Small delay to allow logs to load + setTimeout(() => { + swaparrModule.ensureContentRendered(); + }, 200); + } + // If switching away from swaparr tab, clear the data + else if (app.currentLogApp === 'swaparr') { + swaparrModule.clearData(); + } + }); + }); + } + }); + +})(window.huntarrUI); // Pass the global UI object \ No newline at end of file diff --git a/frontend/static/js/new-main.js b/frontend/static/js/new-main.js index 9ce86d82..b9259cd2 100644 --- a/frontend/static/js/new-main.js +++ b/frontend/static/js/new-main.js @@ -487,34 +487,105 @@ let huntarrUI = { const logRegex = /^(?:\\[(\\w+)\\]\\s)?([\\d\\-]+\\s[\\d:]+)\\s-\\s([\\w\\.]+)\\s-\\s(\\w+)\\s-\\s(.*)$/; const match = logString.match(logRegex); + // First determine the app type for this log message + let logAppType = 'system'; // Default to system + + if (match && match[1]) { + // If we have a match with app tag like [SONARR], use that + logAppType = match[1].toLowerCase(); + } else if (match && match[3]) { + // Otherwise try to determine from the logger name (e.g., huntarr.sonarr) + const loggerParts = match[3].split('.'); + if (loggerParts.length > 1) { + const possibleApp = loggerParts[1].toLowerCase(); + if (['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'swaparr'].includes(possibleApp)) { + logAppType = possibleApp; + } + } + } + + // Special case for Swaparr-related system logs (added strikes, etc.) + if (logAppType === 'system' && + (logString.includes('Added strike') || + logString.includes('Max strikes reached') || + logString.includes('Would have removed') || + logString.includes('strikes, removing download') || + logString.includes('processing stalled downloads'))) { + logAppType = 'swaparr'; + } + + // Determine if this log should be displayed based on the selected app tab + const shouldDisplay = + this.currentLogApp === 'all' || + this.currentLogApp === logAppType; + + if (!shouldDisplay) return; + const logEntry = document.createElement('div'); logEntry.className = 'log-entry'; - if (match) { - const [, appName, timestamp, loggerName, level, message] = match; + // Special handling for Swaparr logs to enable table view + if (logAppType === 'swaparr' && this.currentLogApp === 'swaparr') { + if (match) { + const [, appName, timestamp, loggerName, level, message] = match; + + logEntry.innerHTML = ` + ${timestamp.split(' ')[1]} + ${appName ? `[${appName}]` : ''} + ${level} + (${loggerName.replace('huntarr.', '')}) + ${message} + `; + logEntry.classList.add(`log-${level.toLowerCase()}`); + } else { + // Fallback for lines that don't match the expected format + logEntry.innerHTML = `${logString}`; + + // Basic level detection for fallback + if (logString.includes('ERROR')) logEntry.classList.add('log-error'); + else if (logString.includes('WARN') || logString.includes('WARNING')) logEntry.classList.add('log-warning'); + else if (logString.includes('DEBUG')) logEntry.classList.add('log-debug'); + else logEntry.classList.add('log-info'); + } - // Use backticks for template literal - logEntry.innerHTML = ` - ${timestamp.split(' ')[1]} - ${appName ? `[${appName}]` : ''} - ${level} - (${loggerName.replace('huntarr.', '')}) - ${message} - `; // End template literal with backtick - logEntry.classList.add(`log-${level.toLowerCase()}`); - - } else { - // Fallback for lines that don't match the expected format - logEntry.innerHTML = `${logString}`; - // Basic level detection for fallback - if (logString.includes('ERROR')) logEntry.classList.add('log-error'); - else if (logString.includes('WARN') || logString.includes('WARNING')) logEntry.classList.add('log-warning'); // Added WARN check - else if (logString.includes('DEBUG')) logEntry.classList.add('log-debug'); - else logEntry.classList.add('log-info'); + // Add to logs container + this.elements.logsContainer.appendChild(logEntry); + + // Dispatch a custom event for swaparr.js to process + const swaparrEvent = new CustomEvent('swaparrLogReceived', { + detail: { + logData: match && match[5] ? match[5] : logString + } + }); + document.dispatchEvent(swaparrEvent); + } + // Standard log handling for other apps or all logs + else { + if (match) { + const [, appName, timestamp, loggerName, level, message] = match; + + logEntry.innerHTML = ` + ${timestamp.split(' ')[1]} + ${appName ? `[${appName}]` : ''} + ${level} + (${loggerName.replace('huntarr.', '')}) + ${message} + `; + logEntry.classList.add(`log-${level.toLowerCase()}`); + } else { + // Fallback for lines that don't match the expected format + logEntry.innerHTML = `${logString}`; + + // Basic level detection for fallback + if (logString.includes('ERROR')) logEntry.classList.add('log-error'); + else if (logString.includes('WARN') || logString.includes('WARNING')) logEntry.classList.add('log-warning'); + else if (logString.includes('DEBUG')) logEntry.classList.add('log-debug'); + else logEntry.classList.add('log-info'); + } + + // Add to logs container + this.elements.logsContainer.appendChild(logEntry); } - - // Add to logs container - this.elements.logsContainer.appendChild(logEntry); // Auto-scroll to bottom if enabled if (this.autoScroll) { diff --git a/frontend/templates/base.html b/frontend/templates/base.html index 506bc602..c971f1aa 100644 --- a/frontend/templates/base.html +++ b/frontend/templates/base.html @@ -21,6 +21,7 @@ + \ No newline at end of file diff --git a/frontend/templates/components/logs_section.html b/frontend/templates/components/logs_section.html index cad17909..179b7792 100644 --- a/frontend/templates/components/logs_section.html +++ b/frontend/templates/components/logs_section.html @@ -8,6 +8,7 @@ +
diff --git a/src/primary/utils/logger.py b/src/primary/utils/logger.py index bdb17e82..9c4697c6 100644 --- a/src/primary/utils/logger.py +++ b/src/primary/utils/logger.py @@ -23,7 +23,8 @@ APP_LOG_FILES = { "radarr": LOG_DIR / "radarr.log", # Updated filename "lidarr": LOG_DIR / "lidarr.log", # Updated filename "readarr": LOG_DIR / "readarr.log", # Updated filename - "whisparr": LOG_DIR / "whisparr.log" # Added Whisparr + "whisparr": LOG_DIR / "whisparr.log", # Added Whisparr + "swaparr": LOG_DIR / "swaparr.log" # Added Swaparr } # Global logger instances diff --git a/src/primary/web_server.py b/src/primary/web_server.py index 9601353f..9a9045dc 100644 --- a/src/primary/web_server.py +++ b/src/primary/web_server.py @@ -74,6 +74,7 @@ KNOWN_LOG_FILES = { "lidarr": APP_LOG_FILES.get("lidarr"), "readarr": APP_LOG_FILES.get("readarr"), "whisparr": APP_LOG_FILES.get("whisparr"), + "swaparr": APP_LOG_FILES.get("swaparr"), # Added Swaparr to known log files "system": MAIN_LOG_FILE, # Map 'system' to the main huntarr log } # Filter out None values if an app log file doesn't exist