This commit is contained in:
Admin9705
2025-04-30 08:47:21 -04:00
parent 11280a84bb
commit 7f245e9519
7 changed files with 578 additions and 24 deletions

View File

@@ -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);
}

View File

@@ -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 = `
<div class="swaparr-config">
<h3>Swaparr${this.logData.config.platform ? ' — ' + this.logData.config.platform : ''}</h3>
<div class="swaparr-config-content">
<span>Max strikes: ${this.logData.config.maxStrikes}</span>
<span>Scan interval: ${this.logData.config.scanInterval}</span>
<span>Max download time: ${this.logData.config.maxDownloadTime}</span>
<span>Ignore above size: ${this.logData.config.ignoreAboveSize}</span>
</div>
</div>
`;
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 = `
<table>
<thead>
<tr>
<th>Strikes</th>
<th>Status</th>
<th>Name</th>
<th>Size</th>
<th>ETA</th>
</tr>
</thead>
<tbody>
`;
// 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 += `
<tr class="swaparr-status-${statusClass}">
<td>${download.strikes}</td>
<td>${download.status}</td>
<td>${download.name}</td>
<td>${download.size}</td>
<td>${download.eta}</td>
</tr>
`;
});
tableHTML += `
</tbody>
</table>
`;
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 = `
<div class="swaparr-config">
<h3>Swaparr Logs</h3>
<p>Waiting for structured Swaparr data. Showing raw logs below:</p>
</div>
`;
logsContainer.appendChild(noDataMessage);
// Add raw logs
for (const logLine of this.logData.rawLogs) {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
logEntry.innerHTML = `<span class="log-message">${logLine}</span>`;
// 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

View File

@@ -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 = `
<span class="log-timestamp" title="${timestamp}">${timestamp.split(' ')[1]}</span>
${appName ? `<span class="log-app" title="Source: ${appName}">[${appName}]</span>` : ''}
<span class="log-level log-level-${level.toLowerCase()}" title="Level: ${level}">${level}</span>
<span class="log-logger" title="Logger: ${loggerName}">(${loggerName.replace('huntarr.', '')})</span>
<span class="log-message">${message}</span>
`;
logEntry.classList.add(`log-${level.toLowerCase()}`);
} else {
// Fallback for lines that don't match the expected format
logEntry.innerHTML = `<span class="log-message">${logString}</span>`;
// 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 = `
<span class="log-timestamp" title="${timestamp}">${timestamp.split(' ')[1]}</span>
${appName ? `<span class="log-app" title="Source: ${appName}">[${appName}]</span>` : ''}
<span class="log-level log-level-${level.toLowerCase()}" title="Level: ${level}">${level}</span>
<span class="log-logger" title="Logger: ${loggerName}">(${loggerName.replace('huntarr.', '')})</span>
<span class="log-message">${message}</span>
`; // End template literal with backtick
logEntry.classList.add(`log-${level.toLowerCase()}`);
} else {
// Fallback for lines that don't match the expected format
logEntry.innerHTML = `<span class="log-message">${logString}</span>`;
// 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 = `
<span class="log-timestamp" title="${timestamp}">${timestamp.split(' ')[1]}</span>
${appName ? `<span class="log-app" title="Source: ${appName}">[${appName}]</span>` : ''}
<span class="log-level log-level-${level.toLowerCase()}" title="Level: ${level}">${level}</span>
<span class="log-logger" title="Logger: ${loggerName}">(${loggerName.replace('huntarr.', '')})</span>
<span class="log-message">${message}</span>
`;
logEntry.classList.add(`log-${level.toLowerCase()}`);
} else {
// Fallback for lines that don't match the expected format
logEntry.innerHTML = `<span class="log-message">${logString}</span>`;
// 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) {

View File

@@ -21,6 +21,7 @@
<script src="/static/js/apps/radarr.js"></script>
<script src="/static/js/apps/lidarr.js"></script>
<script src="/static/js/apps/readarr.js"></script>
<script src="/static/js/apps/swaparr.js"></script>
<!-- ...existing code... -->
</body>
</html>

View File

@@ -8,6 +8,7 @@
<button class="log-tab" data-app="lidarr">Lidarr</button>
<button class="log-tab" data-app="readarr">Readarr</button>
<button class="log-tab" data-app="whisparr">Whisparr</button>
<button class="log-tab" data-app="swaparr">Swaparr</button>
<button class="log-tab" data-app="system">System</button>
</div>
<div class="log-controls">

View File

@@ -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

View File

@@ -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