mirror of
https://github.com/plexguide/Huntarr-Sonarr.git
synced 2025-12-16 20:04:16 -06:00
refactor: remove Swaparr app and related functionality
This commit is contained in:
@@ -111,7 +111,7 @@ var
|
||||
i: Integer;
|
||||
begin
|
||||
// Define all required configuration directories
|
||||
SetArrayLength(ConfigDirs, 14);
|
||||
SetArrayLength(ConfigDirs, 13);
|
||||
ConfigDirs[0] := '\config';
|
||||
ConfigDirs[1] := '\config\logs';
|
||||
ConfigDirs[2] := '\config\stateful';
|
||||
@@ -121,11 +121,10 @@ begin
|
||||
ConfigDirs[6] := '\config\scheduler';
|
||||
ConfigDirs[7] := '\config\reset';
|
||||
ConfigDirs[8] := '\config\tally';
|
||||
ConfigDirs[9] := '\config\swaparr';
|
||||
ConfigDirs[10] := '\config\eros';
|
||||
ConfigDirs[11] := '\logs';
|
||||
ConfigDirs[12] := '\frontend\templates';
|
||||
ConfigDirs[13] := '\frontend\static';
|
||||
ConfigDirs[9] := '\config\eros';
|
||||
ConfigDirs[10] := '\logs';
|
||||
ConfigDirs[11] := '\frontend\templates';
|
||||
ConfigDirs[12] := '\frontend\static';
|
||||
|
||||
// Create all necessary configuration directories with explicit permissions
|
||||
for i := 0 to GetArrayLength(ConfigDirs) - 1 do
|
||||
@@ -302,7 +301,7 @@ begin
|
||||
begin
|
||||
Log('Non-admin installation - attempting to ensure directories are writable...');
|
||||
|
||||
SetArrayLength(Permissions, 14);
|
||||
SetArrayLength(Permissions, 13);
|
||||
Permissions[0] := '\config';
|
||||
Permissions[1] := '\config\logs';
|
||||
Permissions[2] := '\config\stateful';
|
||||
@@ -312,11 +311,10 @@ begin
|
||||
Permissions[6] := '\config\scheduler';
|
||||
Permissions[7] := '\config\reset';
|
||||
Permissions[8] := '\config\tally';
|
||||
Permissions[9] := '\config\swaparr';
|
||||
Permissions[10] := '\config\eros';
|
||||
Permissions[11] := '\logs';
|
||||
Permissions[12] := '\frontend\templates';
|
||||
Permissions[13] := '\frontend\static';
|
||||
Permissions[9] := '\config\eros';
|
||||
Permissions[10] := '\logs';
|
||||
Permissions[11] := '\frontend\templates';
|
||||
Permissions[12] := '\frontend\static';
|
||||
|
||||
for i := 0 to GetArrayLength(Permissions) - 1 do
|
||||
begin
|
||||
|
||||
@@ -107,7 +107,6 @@ Section "Huntarr Application (required)" SecCore
|
||||
CreateDirectory "$INSTDIR\config\scheduler"
|
||||
CreateDirectory "$INSTDIR\config\reset"
|
||||
CreateDirectory "$INSTDIR\config\tally"
|
||||
CreateDirectory "$INSTDIR\config\swaparr"
|
||||
CreateDirectory "$INSTDIR\config\eros"
|
||||
CreateDirectory "$INSTDIR\logs"
|
||||
CreateDirectory "$INSTDIR\frontend\templates"
|
||||
|
||||
@@ -81,12 +81,11 @@ HISTORY_DIR = CONFIG_PATH / "history"
|
||||
SCHEDULER_DIR = CONFIG_PATH / "scheduler"
|
||||
RESET_DIR = CONFIG_PATH / "reset" # Add reset directory
|
||||
TALLY_DIR = CONFIG_PATH / "tally" # Add tally directory for stats
|
||||
SWAPARR_DIR = CONFIG_PATH / "swaparr" # Add Swaparr directory
|
||||
EROS_DIR = CONFIG_PATH / "eros" # Add Eros directory
|
||||
|
||||
# Create all directories with enhanced error reporting
|
||||
for dir_path in [LOG_DIR, SETTINGS_DIR, USER_DIR, STATEFUL_DIR, HISTORY_DIR,
|
||||
SCHEDULER_DIR, RESET_DIR, TALLY_DIR, SWAPARR_DIR, EROS_DIR]:
|
||||
SCHEDULER_DIR, RESET_DIR, TALLY_DIR, EROS_DIR]:
|
||||
try:
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -120,10 +119,6 @@ def get_reset_path(app_type):
|
||||
"""Get the path to an app's reset file"""
|
||||
return RESET_DIR / f"{app_type}.reset"
|
||||
|
||||
def get_swaparr_state_path():
|
||||
"""Get the Swaparr state directory"""
|
||||
return SWAPARR_DIR
|
||||
|
||||
def get_eros_config_path():
|
||||
"""Get the Eros config file path"""
|
||||
return CONFIG_PATH / "eros.json"
|
||||
|
||||
@@ -52,7 +52,6 @@ def setup_config_directories(base_dir=None):
|
||||
config_path / "scheduler",
|
||||
config_path / "reset",
|
||||
config_path / "tally",
|
||||
config_path / "swaparr",
|
||||
config_path / "eros"
|
||||
]
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ def setup_environment():
|
||||
os.path.join(config_dir, "scheduler"),
|
||||
os.path.join(config_dir, "reset"),
|
||||
os.path.join(config_dir, "tally"),
|
||||
os.path.join(config_dir, "swaparr"),
|
||||
os.path.join(config_dir, "eros")
|
||||
]
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ html, body {
|
||||
.app-content-panel,
|
||||
#appsContainer,
|
||||
#sonarrApps, #radarrApps, #lidarrApps, #readarrApps,
|
||||
#whisparrApps, #erosApps, #swaparrApps, #cleanuperrApps,
|
||||
#whisparrApps, #erosApps, #cleanuperrApps,
|
||||
.additional-options-section,
|
||||
.additional-options,
|
||||
.skip-series-refresh,
|
||||
@@ -64,7 +64,7 @@ table, tr, td, tbody, thead {
|
||||
}
|
||||
|
||||
#sonarrApps, #radarrApps, #lidarrApps, #readarrApps,
|
||||
#whisparrApps, #erosApps, #swaparrApps, #cleanuperrApps {
|
||||
#whisparrApps, #erosApps, #cleanuperrApps {
|
||||
padding-bottom: 30px !important;
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
/* Reduce excessive padding in various containers */
|
||||
#sonarrApps, #radarrApps, #lidarrApps, #readarrApps,
|
||||
#whisparrApps, #erosApps, #swaparrApps, #cleanuperrApps {
|
||||
#whisparrApps, #erosApps, #cleanuperrApps {
|
||||
padding-bottom: 50px !important;
|
||||
margin-bottom: 20px !important;
|
||||
overflow: visible !important;
|
||||
@@ -81,7 +81,6 @@ body #lidarrApps,
|
||||
body #readarrApps,
|
||||
body #whisparrApps,
|
||||
body #erosApps,
|
||||
body #swaparrApps,
|
||||
body #cleanuperrApps {
|
||||
overflow: visible !important;
|
||||
max-height: none !important;
|
||||
@@ -105,7 +104,7 @@ body #cleanuperrApps {
|
||||
|
||||
/* Reduce bottom spacing on mobile */
|
||||
#sonarrApps, #radarrApps, #lidarrApps, #readarrApps,
|
||||
#whisparrApps, #erosApps, #swaparrApps, #cleanuperrApps,
|
||||
#whisparrApps, #erosApps, #cleanuperrApps,
|
||||
.additional-options, .skip-series-refresh {
|
||||
margin-bottom: 50px !important;
|
||||
padding-bottom: 30px !important;
|
||||
|
||||
@@ -1750,104 +1750,6 @@ input:checked + .toggle-slider:before {
|
||||
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);
|
||||
}
|
||||
|
||||
/* Log Dropdown Styles */
|
||||
.log-dropdown-container {
|
||||
position: relative;
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
// 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
|
||||
@@ -351,16 +351,6 @@ window.LogsModule = {
|
||||
this.applyFilterToSingleEntry(logEntry, currentLogLevel);
|
||||
}
|
||||
|
||||
// Special event dispatching for Swaparr logs
|
||||
if (logAppType === 'swaparr' && this.currentLogApp === 'swaparr') {
|
||||
const swaparrEvent = new CustomEvent('swaparrLogReceived', {
|
||||
detail: {
|
||||
logData: match && match[5] ? match[5] : logString
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(swaparrEvent);
|
||||
}
|
||||
|
||||
// Auto-scroll to top
|
||||
if (this.autoScroll) {
|
||||
window.scrollTo({
|
||||
|
||||
@@ -1001,7 +1001,6 @@ let huntarrUI = {
|
||||
if (data.readarr) this.populateSettingsForm('readarr', data.readarr);
|
||||
if (data.whisparr) this.populateSettingsForm('whisparr', data.whisparr);
|
||||
if (data.eros) this.populateSettingsForm('eros', data.eros);
|
||||
if (data.swaparr) this.populateSettingsForm('swaparr', data.swaparr);
|
||||
if (data.general) this.populateSettingsForm('general', data.general);
|
||||
|
||||
// Update duration displays (like sleep durations)
|
||||
@@ -2023,7 +2022,7 @@ let huntarrUI = {
|
||||
|
||||
updateStatsDisplay: function(stats) {
|
||||
// Update each app's statistics
|
||||
const apps = ['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros', 'swaparr'];
|
||||
const apps = ['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros'];
|
||||
const statTypes = ['hunted', 'upgraded'];
|
||||
|
||||
// More robust low usage mode detection - check multiple sources
|
||||
@@ -2164,8 +2163,7 @@ let huntarrUI = {
|
||||
'lidarr': {'hunted': 0, 'upgraded': 0},
|
||||
'readarr': {'hunted': 0, 'upgraded': 0},
|
||||
'whisparr': {'hunted': 0, 'upgraded': 0},
|
||||
'eros': {'hunted': 0, 'upgraded': 0},
|
||||
'swaparr': {'hunted': 0, 'upgraded': 0}
|
||||
'eros': {'hunted': 0, 'upgraded': 0}
|
||||
};
|
||||
|
||||
// Immediately update UI before even showing the confirmation
|
||||
@@ -3194,7 +3192,7 @@ let huntarrUI = {
|
||||
/^\s*\[?\s*$/, // Just opening bracket
|
||||
/^\s*\]?,?\s*$/, // Just closing bracket
|
||||
/^,?\s*$/, // Just comma or whitespace
|
||||
/^[^"]*':\s*[^,]*,.*'.*':/, // Mid-object fragments like "g_items': 1, 'hunt_upgrade_items': 0"
|
||||
/^[^"]*':\s*[^,]*,.*':/, // Mid-object fragments like "g_items': 1, 'hunt_upgrade_items': 0"
|
||||
/^[a-zA-Z_][a-zA-Z0-9_]*':\s*\d+,/, // Property names starting without quotes
|
||||
/^[a-zA-Z_][a-zA-Z0-9_]*':\s*True|False,/, // Boolean properties without opening quotes
|
||||
/^[a-zA-Z_][a-zA-Z0-9_]*':\s*'[^']*',/, // String properties without opening quotes
|
||||
|
||||
@@ -909,187 +909,6 @@ const SettingsForms = {
|
||||
this.updateDurationDisplay();
|
||||
},
|
||||
|
||||
// Generate Swaparr settings form
|
||||
generateSwaparrForm: function(container, settings = {}) {
|
||||
// Add data-app-type attribute to container
|
||||
container.setAttribute('data-app-type', 'swaparr');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="settings-group">
|
||||
<h3>Swaparr (Beta) - Only For Torrent Users</h3>
|
||||
<div class="setting-item">
|
||||
<p>Swaparr addresses the issue of stalled downloads and I rewrote it to support Huntarr. Visit Swaparr's <a href="https://github.com/ThijmenGThN/swaparr" target="_blank">GitHub</a> for more information and support the developer!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3>Swaparr Settings</h3>
|
||||
<div class="setting-item">
|
||||
<label for="swaparr_enabled"><a href="/Huntarr.io/docs/#/guides/swaparr?id=overview" class="info-icon" title="Learn more about Swaparr's functionality" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Enable Swaparr:</label>
|
||||
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
|
||||
<input type="checkbox" id="swaparr_enabled" ${settings.enabled ? 'checked' : ''}>
|
||||
<span class="toggle-slider" style="position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background-color:#3d4353; border-radius:20px; transition:0.4s;"></span>
|
||||
</label>
|
||||
<p class="setting-help">Enable automatic handling of stalled downloads</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="swaparr_max_strikes"><a href="/Huntarr.io/docs/#/guides/swaparr?id=strike-system" class="info-icon" title="Learn more about strike system for stalled downloads" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Maximum Strikes:</label>
|
||||
<input type="number" id="swaparr_max_strikes" min="1" max="10" value="${settings.max_strikes || 3}">
|
||||
<p class="setting-help">Number of strikes before removing a stalled download</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="swaparr_max_download_time"><a href="/Huntarr.io/docs/#/guides/swaparr?id=time-thresholds" class="info-icon" title="Learn more about maximum download time setting" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Max Download Time:</label>
|
||||
<input type="text" id="swaparr_max_download_time" value="${settings.max_download_time || '2h'}">
|
||||
<p class="setting-help">Maximum time a download can be stalled (e.g., 30m, 2h, 1d)</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="swaparr_ignore_above_size"><a href="https://huntarr.io" class="info-icon" title="Learn more about size threshold settings" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Ignore Above Size:</label>
|
||||
<input type="text" id="swaparr_ignore_above_size" value="${settings.ignore_above_size || '25GB'}">
|
||||
<p class="setting-help">Ignore files larger than this size (e.g., 1GB, 25GB, 1TB)</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="swaparr_remove_from_client"><a href="https://huntarr.io" class="info-icon" title="Learn more about client removal options" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Remove From Client:</label>
|
||||
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
|
||||
<input type="checkbox" id="swaparr_remove_from_client" ${settings.remove_from_client !== false ? 'checked' : ''}>
|
||||
<span class="toggle-slider" style="position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background-color:#3d4353; border-radius:20px; transition:0.4s;"></span>
|
||||
</label>
|
||||
<p class="setting-help">Remove the download from the torrent/usenet client when removed</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="swaparr_dry_run"><a href="https://huntarr.io" class="info-icon" title="Learn more about dry run testing mode" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Dry Run Mode:</label>
|
||||
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
|
||||
<input type="checkbox" id="swaparr_dry_run" ${settings.dry_run === true ? 'checked' : ''}>
|
||||
<span class="toggle-slider" style="position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background-color:#3d4353; border-radius:20px; transition:0.4s;"></span>
|
||||
</label>
|
||||
<p class="setting-help">Log actions but don't actually remove downloads. Useful for testing the first time!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3>Swaparr Status</h3>
|
||||
<div id="swaparr_status_container">
|
||||
<div class="button-container" style="display: flex; justify-content: flex-end; margin-bottom: 15px;">
|
||||
<button type="button" id="reset_swaparr_strikes" style="background-color: #e74c3c; color: white; border: none; padding: 5px 10px; border-radius: 4px; font-size: 0.9em; cursor: pointer;">
|
||||
<i class="fas fa-trash"></i> Reset
|
||||
</button>
|
||||
</div>
|
||||
<div id="swaparr_status" class="status-display">
|
||||
<p>Loading Swaparr status...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Load Swaparr status automatically
|
||||
const resetStrikesBtn = container.querySelector('#reset_swaparr_strikes');
|
||||
const statusContainer = container.querySelector('#swaparr_status');
|
||||
|
||||
HuntarrUtils.fetchWithTimeout('/api/swaparr/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let statusHTML = '';
|
||||
|
||||
// Add stats for each app if available
|
||||
if (data.statistics && Object.keys(data.statistics).length > 0) {
|
||||
statusHTML += '<ul>';
|
||||
|
||||
for (const [app, stats] of Object.entries(data.statistics)) {
|
||||
statusHTML += `<li><strong>${app.toUpperCase()}</strong>: `;
|
||||
if (stats.error) {
|
||||
statusHTML += `Error: ${stats.error}</li>`;
|
||||
} else {
|
||||
statusHTML += `${stats.currently_striked} currently striked, ${stats.removed} removed (${stats.total_tracked} total tracked)</li>`;
|
||||
}
|
||||
}
|
||||
|
||||
statusHTML += '</ul>';
|
||||
} else {
|
||||
statusHTML += '<p>No statistics available yet.</p>';
|
||||
}
|
||||
|
||||
statusContainer.innerHTML = statusHTML;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading Swaparr status:', error);
|
||||
statusContainer.innerHTML = `<p>Error fetching status: ${error.message}</p>`;
|
||||
});
|
||||
|
||||
// Add event listener for the Reset Strikes button
|
||||
if (resetStrikesBtn) {
|
||||
resetStrikesBtn.addEventListener('click', function() {
|
||||
if (confirm('Are you sure you want to reset all Swaparr strikes? This will clear the strike history for all apps.')) {
|
||||
statusContainer.innerHTML = '<p>Resetting strikes...</p>';
|
||||
|
||||
HuntarrUtils.fetchWithTimeout('/api/swaparr/reset', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
statusContainer.innerHTML = `<p>Success: ${data.message}</p>`;
|
||||
// Reload status after a short delay
|
||||
setTimeout(() => {
|
||||
HuntarrUtils.fetchWithTimeout('/api/swaparr/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let statusHTML = '';
|
||||
if (data.statistics && Object.keys(data.statistics).length > 0) {
|
||||
statusHTML += '<ul>';
|
||||
for (const [app, stats] of Object.entries(data.statistics)) {
|
||||
statusHTML += `<li><strong>${app.toUpperCase()}</strong>: `;
|
||||
if (stats.error) {
|
||||
statusHTML += `Error: ${stats.error}</li>`;
|
||||
} else {
|
||||
statusHTML += `${stats.currently_striked} currently striked, ${stats.removed} removed (${stats.total_tracked} total tracked)</li>`;
|
||||
}
|
||||
}
|
||||
statusHTML += '</ul>';
|
||||
} else {
|
||||
statusHTML += '<p>No statistics available yet.</p>';
|
||||
}
|
||||
statusContainer.innerHTML = statusHTML;
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
statusContainer.innerHTML = `<p>Error: ${data.message}</p>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusContainer.innerHTML = `<p>Error resetting strikes: ${error.message}</p>`;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (!resetStrikesBtn) {
|
||||
console.warn('Could not find #reset_swaparr_strikes to attach listener.');
|
||||
} else {
|
||||
console.warn('huntarrUI or huntarrUI.resetStatefulManagement is not available.');
|
||||
}
|
||||
|
||||
// Add confirmation dialog for local access bypass toggle
|
||||
const localAccessBypassCheckbox = container.querySelector('#local_access_bypass');
|
||||
if (localAccessBypassCheckbox) {
|
||||
// Store original state
|
||||
const originalState = localAccessBypassCheckbox.checked;
|
||||
|
||||
localAccessBypassCheckbox.addEventListener('change', function() {
|
||||
const newState = this.checked;
|
||||
|
||||
// Preview the UI changes immediately, but they'll be reverted if user doesn't save
|
||||
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.updateUIForLocalAccessBypass === 'function') {
|
||||
huntarrUI.updateUIForLocalAccessBypass(newState);
|
||||
}
|
||||
// Also ensure the main app knows settings have changed if the preview runs
|
||||
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.markSettingsAsChanged === 'function') {
|
||||
huntarrUI.markSettingsAsChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Format date nicely for display
|
||||
formatDate: function(date) {
|
||||
if (!date) return 'Never';
|
||||
@@ -1310,14 +1129,6 @@ const SettingsForms = {
|
||||
settings.sleep_duration = getInputValue('#eros_sleep_duration', 900);
|
||||
settings.hourly_cap = getInputValue('#eros_hourly_cap', 20);
|
||||
}
|
||||
else if (appType === 'swaparr') {
|
||||
settings.enabled = getInputValue('#swaparr_enabled', false);
|
||||
settings.max_strikes = getInputValue('#swaparr_max_strikes', 3);
|
||||
settings.max_download_time = getInputValue('#swaparr_max_download_time', '2h');
|
||||
settings.ignore_above_size = getInputValue('#swaparr_ignore_above_size', '25GB');
|
||||
settings.remove_from_client = getInputValue('#swaparr_remove_from_client', true);
|
||||
settings.dry_run = getInputValue('#swaparr_dry_run', false);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Collected settings for', appType, settings);
|
||||
|
||||
@@ -88,7 +88,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
*/
|
||||
function initStatsTooltips() {
|
||||
// Add event listeners to statistics numbers
|
||||
const apps = ['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros', 'swaparr'];
|
||||
const apps = ['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros'];
|
||||
const statTypes = ['hunted', 'upgraded'];
|
||||
|
||||
apps.forEach(app => {
|
||||
@@ -136,8 +136,7 @@ function showStatsTooltip(e) {
|
||||
'lidarr': { name: 'Lidarr', color: '#2ecc71', description: type === 'hunted' ? 'Album searches triggered' : 'Albums upgraded to better quality' },
|
||||
'readarr': { name: 'Readarr', color: '#e74c3c', description: type === 'hunted' ? 'Book searches triggered' : 'Books upgraded to better quality' },
|
||||
'whisparr': { name: 'Whisparr', color: '#9b59b6', description: type === 'hunted' ? 'Adult video searches triggered' : 'Adult videos upgraded to better quality' },
|
||||
'eros': { name: 'Eros', color: '#1abc9c', description: type === 'hunted' ? 'Audio searches triggered' : 'Audio files upgraded to better quality' },
|
||||
'swaparr': { name: 'Swaparr', color: '#e67e22', description: type === 'hunted' ? 'Content swap operations' : 'Content swap upgrades' }
|
||||
'eros': { name: 'Eros', color: '#1abc9c', description: type === 'hunted' ? 'Audio searches triggered' : 'Audio files upgraded to better quality' }
|
||||
};
|
||||
|
||||
const detail = appDetails[app] || { name: app, color: '#95a5a6', description: type === 'hunted' ? 'Searches triggered' : 'Content upgraded' };
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
<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>
|
||||
<script src="./static/js/apps/whisparr.js"></script>
|
||||
<script src="./static/js/apps/eros.js"></script>
|
||||
<!-- ...existing code... -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -121,9 +121,9 @@
|
||||
<option value="lidarr">Lidarr</option>
|
||||
<option value="readarr">Readarr</option>
|
||||
<option value="whisparr">Whisparr V2</option>
|
||||
<option value="eros">Whisparr V3</option>
|
||||
<option value="swaparr">Swaparr</option>
|
||||
<option value="cleanuperr">Cleanuperr</option>
|
||||
<option value="whisparr">Whisparr V3</option>
|
||||
<option value="eros">Eros</option>
|
||||
<option value="hunting">Hunt Manager</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -148,7 +148,6 @@
|
||||
<div id="readarrApps" class="app-apps-panel app-content-panel"></div>
|
||||
<div id="whisparrApps" class="app-apps-panel app-content-panel"></div>
|
||||
<div id="erosApps" class="app-apps-panel app-content-panel"></div>
|
||||
<div id="swaparrApps" class="app-apps-panel app-content-panel"></div>
|
||||
<div id="cleanuperrApps" class="app-apps-panel app-content-panel"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,7 +205,7 @@
|
||||
}
|
||||
|
||||
/* Ensure Additional Options section is fully visible */
|
||||
#sonarrApps, #radarrApps, #lidarrApps, #readarrApps, #whisparrApps, #erosApps, #swaparrApps {
|
||||
#sonarrApps, #radarrApps, #lidarrApps, #readarrApps, #whisparrApps, #erosApps {
|
||||
padding-bottom: 150px; /* Extra padding to ensure bottom content is visible */
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
@@ -264,7 +263,6 @@
|
||||
#readarrApps::-webkit-scrollbar,
|
||||
#whisparrApps::-webkit-scrollbar,
|
||||
#erosApps::-webkit-scrollbar,
|
||||
#swaparrApps::-webkit-scrollbar,
|
||||
table::-webkit-scrollbar,
|
||||
tr::-webkit-scrollbar,
|
||||
td::-webkit-scrollbar {
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
<option value="radarr">Radarr</option>
|
||||
<option value="lidarr">Lidarr</option>
|
||||
<option value="readarr">Readarr</option>
|
||||
<option value="whisparr">Whisparr V2</option>
|
||||
<option value="eros">Whisparr V3</option>
|
||||
<option value="swaparr">Swaparr</option>
|
||||
<option value="whisparr">Whisparr</option>
|
||||
<option value="eros">Eros</option>
|
||||
<option value="hunting">Hunt Manager</option>
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
|
||||
@@ -10,7 +10,6 @@ from src.primary.apps.radarr_routes import radarr_bp
|
||||
from src.primary.apps.lidarr_routes import lidarr_bp
|
||||
from src.primary.apps.readarr_routes import readarr_bp
|
||||
from src.primary.apps.whisparr_routes import whisparr_bp
|
||||
from src.primary.apps.swaparr_routes import swaparr_bp
|
||||
from src.primary.apps.eros_routes import eros_bp
|
||||
|
||||
__all__ = [
|
||||
@@ -19,6 +18,5 @@ __all__ = [
|
||||
"lidarr_bp",
|
||||
"readarr_bp",
|
||||
"whisparr_bp",
|
||||
"swaparr_bp",
|
||||
"eros_bp"
|
||||
]
|
||||
@@ -1,185 +0,0 @@
|
||||
"""
|
||||
Swaparr module for Huntarr
|
||||
Handles stalled downloads in Starr apps based on the original Swaparr application
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
import os
|
||||
import json
|
||||
from src.primary.utils.logger import get_logger
|
||||
from src.primary.settings_manager import load_settings, save_settings
|
||||
from src.primary.apps.swaparr.handler import process_stalled_downloads
|
||||
from src.primary.apps.radarr import get_configured_instances as get_radarr_instances
|
||||
from src.primary.apps.sonarr import get_configured_instances as get_sonarr_instances
|
||||
from src.primary.apps.lidarr import get_configured_instances as get_lidarr_instances
|
||||
from src.primary.apps.readarr import get_configured_instances as get_readarr_instances
|
||||
|
||||
def get_configured_instances():
|
||||
"""Get all configured Starr app instances from their respective settings"""
|
||||
try:
|
||||
from src.primary.apps.whisparr import get_configured_instances as get_whisparr_instances
|
||||
whisparr_instances = get_whisparr_instances()
|
||||
except ImportError:
|
||||
whisparr_instances = []
|
||||
|
||||
try:
|
||||
from src.primary.apps.eros import get_configured_instances as get_eros_instances
|
||||
eros_instances = get_eros_instances()
|
||||
except ImportError:
|
||||
eros_instances = []
|
||||
|
||||
instances = {
|
||||
"radarr": get_radarr_instances(),
|
||||
"sonarr": get_sonarr_instances(),
|
||||
"lidarr": get_lidarr_instances(),
|
||||
"readarr": get_readarr_instances(),
|
||||
"whisparr": whisparr_instances,
|
||||
"eros": eros_instances
|
||||
}
|
||||
|
||||
logger = get_logger("swaparr")
|
||||
logger.info(f"Found {sum(len(v) for v in instances.values())} configured Starr app instances")
|
||||
return instances
|
||||
|
||||
swaparr_bp = Blueprint('swaparr', __name__)
|
||||
swaparr_logger = get_logger("swaparr")
|
||||
|
||||
@swaparr_bp.route('/status', methods=['GET'])
|
||||
def get_status():
|
||||
"""Get Swaparr status and statistics"""
|
||||
settings = load_settings("swaparr")
|
||||
enabled = settings.get("enabled", False)
|
||||
|
||||
# Get strike statistics from all app state directories
|
||||
statistics = {}
|
||||
# Use the cross-platform path from handler module
|
||||
from src.primary.apps.swaparr.handler import SWAPARR_STATE_DIR
|
||||
state_dir = SWAPARR_STATE_DIR
|
||||
|
||||
if os.path.exists(state_dir):
|
||||
for app_name in os.listdir(state_dir):
|
||||
app_dir = os.path.join(state_dir, app_name)
|
||||
if os.path.isdir(app_dir):
|
||||
strike_file = os.path.join(app_dir, "strikes.json")
|
||||
if os.path.exists(strike_file):
|
||||
try:
|
||||
with open(strike_file, 'r') as f:
|
||||
strike_data = json.load(f)
|
||||
|
||||
total_items = len(strike_data)
|
||||
removed_items = sum(1 for item in strike_data.values() if item.get("removed", False))
|
||||
striked_items = sum(1 for item in strike_data.values()
|
||||
if item.get("strikes", 0) > 0 and not item.get("removed", False))
|
||||
|
||||
statistics[app_name] = {
|
||||
"total_tracked": total_items,
|
||||
"currently_striked": striked_items,
|
||||
"removed": removed_items
|
||||
}
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
swaparr_logger.error(f"Error reading strike data for {app_name}: {str(e)}")
|
||||
statistics[app_name] = {"error": str(e)}
|
||||
|
||||
return jsonify({
|
||||
"enabled": enabled,
|
||||
"settings": {
|
||||
"max_strikes": settings.get("max_strikes", 3),
|
||||
"max_download_time": settings.get("max_download_time", "2h"),
|
||||
"ignore_above_size": settings.get("ignore_above_size", "25GB"),
|
||||
"remove_from_client": settings.get("remove_from_client", True),
|
||||
"dry_run": settings.get("dry_run", False)
|
||||
},
|
||||
"statistics": statistics
|
||||
})
|
||||
|
||||
@swaparr_bp.route('/settings', methods=['GET'])
|
||||
def get_settings():
|
||||
"""Get Swaparr settings"""
|
||||
settings = load_settings("swaparr")
|
||||
return jsonify(settings)
|
||||
|
||||
@swaparr_bp.route('/settings', methods=['POST'])
|
||||
def update_settings():
|
||||
"""Update Swaparr settings"""
|
||||
data = request.json
|
||||
|
||||
if not data:
|
||||
return jsonify({"success": False, "message": "No data provided"}), 400
|
||||
|
||||
# Load current settings
|
||||
settings = load_settings("swaparr")
|
||||
|
||||
# Update settings with provided data
|
||||
for key, value in data.items():
|
||||
settings[key] = value
|
||||
|
||||
# Save updated settings
|
||||
success = save_settings("swaparr", settings)
|
||||
|
||||
if success:
|
||||
return jsonify({"success": True, "message": "Settings updated successfully"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "Failed to save settings"}), 500
|
||||
|
||||
@swaparr_bp.route('/reset', methods=['POST'])
|
||||
def reset_strikes():
|
||||
"""Reset all strikes for all apps or a specific app"""
|
||||
data = request.json
|
||||
app_name = data.get('app_name') if data else None
|
||||
|
||||
# Use the cross-platform path from handler module
|
||||
from src.primary.apps.swaparr.handler import SWAPARR_STATE_DIR
|
||||
state_dir = SWAPARR_STATE_DIR
|
||||
|
||||
if not os.path.exists(state_dir):
|
||||
return jsonify({"success": True, "message": "No strike data to reset"})
|
||||
|
||||
if app_name:
|
||||
# Reset strikes for a specific app
|
||||
app_dir = os.path.join(state_dir, app_name)
|
||||
if os.path.exists(app_dir):
|
||||
strike_file = os.path.join(app_dir, "strikes.json")
|
||||
if os.path.exists(strike_file):
|
||||
try:
|
||||
os.remove(strike_file)
|
||||
swaparr_logger.info(f"Reset strikes for {app_name}")
|
||||
return jsonify({"success": True, "message": f"Strikes reset for {app_name}"})
|
||||
except IOError as e:
|
||||
swaparr_logger.error(f"Error resetting strikes for {app_name}: {str(e)}")
|
||||
return jsonify({"success": False, "message": f"Failed to reset strikes for {app_name}: {str(e)}"}), 500
|
||||
return jsonify({"success": False, "message": f"No strike data found for {app_name}"}), 404
|
||||
else:
|
||||
# Reset strikes for all apps
|
||||
try:
|
||||
for app_name in os.listdir(state_dir):
|
||||
app_dir = os.path.join(state_dir, app_name)
|
||||
if os.path.isdir(app_dir):
|
||||
strike_file = os.path.join(app_dir, "strikes.json")
|
||||
if os.path.exists(strike_file):
|
||||
os.remove(strike_file)
|
||||
|
||||
swaparr_logger.info("Reset all strikes")
|
||||
return jsonify({"success": True, "message": "All strikes reset"})
|
||||
except IOError as e:
|
||||
swaparr_logger.error(f"Error resetting all strikes: {str(e)}")
|
||||
return jsonify({"success": False, "message": f"Failed to reset all strikes: {str(e)}"}), 500
|
||||
|
||||
def is_configured():
|
||||
"""Check if Swaparr has any configured Starr app instances"""
|
||||
instances = get_configured_instances()
|
||||
return any(len(app_instances) > 0 for app_instances in instances.values())
|
||||
|
||||
def run_swaparr():
|
||||
"""Run Swaparr cycle to check for stalled downloads in all configured Starr app instances"""
|
||||
settings = load_settings("swaparr")
|
||||
|
||||
if not settings or not settings.get("enabled", False):
|
||||
swaparr_logger.debug("Swaparr is disabled, skipping run")
|
||||
return
|
||||
|
||||
instances = get_configured_instances()
|
||||
|
||||
# Process stalled downloads for each app type and instance
|
||||
for app_name, app_instances in instances.items():
|
||||
for app_settings in app_instances:
|
||||
process_stalled_downloads(app_name, app_settings, settings)
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
Swaparr app module for Huntarr
|
||||
Contains functionality for handling stalled downloads in Starr apps
|
||||
"""
|
||||
|
||||
# Add necessary imports for get_configured_instances
|
||||
from src.primary.settings_manager import load_settings
|
||||
from src.primary.utils.logger import get_logger
|
||||
|
||||
swaparr_logger = get_logger("swaparr") # Get the logger instance
|
||||
|
||||
# We don't need the get_configured_instances function here anymore as it's defined in swaparr.py
|
||||
# to avoid circular imports
|
||||
|
||||
# Export just the swaparr_logger for now
|
||||
__all__ = ["swaparr_logger"]
|
||||
@@ -1,470 +0,0 @@
|
||||
"""
|
||||
Implementation of the swaparr functionality to detect and remove stalled downloads in Starr apps.
|
||||
Based on the functionality provided by https://github.com/ThijmenGThN/swaparr
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
import requests
|
||||
|
||||
from src.primary.utils.logger import get_logger
|
||||
from src.primary.settings_manager import load_settings
|
||||
from src.primary.state import get_state_file_path
|
||||
|
||||
# Create logger
|
||||
swaparr_logger = get_logger("swaparr")
|
||||
|
||||
# Use the centralized path configuration
|
||||
from src.primary.utils.config_paths import SWAPARR_DIR
|
||||
|
||||
# Use cross-platform path for state directory
|
||||
SWAPARR_STATE_DIR = str(SWAPARR_DIR) # Convert to string for compatibility with os.path
|
||||
|
||||
def ensure_state_directory(app_name):
|
||||
"""Ensure the state directory exists for tracking strikes for a specific app"""
|
||||
app_state_dir = os.path.join(SWAPARR_STATE_DIR, app_name)
|
||||
if not os.path.exists(app_state_dir):
|
||||
os.makedirs(app_state_dir, exist_ok=True)
|
||||
swaparr_logger.info(f"Created swaparr state directory for {app_name}: {app_state_dir}")
|
||||
return app_state_dir
|
||||
|
||||
def load_strike_data(app_name):
|
||||
"""Load strike data for a specific app"""
|
||||
app_state_dir = ensure_state_directory(app_name)
|
||||
strike_file = os.path.join(app_state_dir, "strikes.json")
|
||||
|
||||
if not os.path.exists(strike_file):
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(strike_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
swaparr_logger.error(f"Error loading strike data for {app_name}: {str(e)}")
|
||||
return {}
|
||||
|
||||
def save_strike_data(app_name, strike_data):
|
||||
"""Save strike data for a specific app"""
|
||||
app_state_dir = ensure_state_directory(app_name)
|
||||
strike_file = os.path.join(app_state_dir, "strikes.json")
|
||||
|
||||
try:
|
||||
with open(strike_file, 'w') as f:
|
||||
json.dump(strike_data, f, indent=2)
|
||||
except IOError as e:
|
||||
swaparr_logger.error(f"Error saving strike data for {app_name}: {str(e)}")
|
||||
|
||||
def load_removed_items(app_name):
|
||||
"""Load list of permanently removed items"""
|
||||
app_state_dir = ensure_state_directory(app_name)
|
||||
removed_file = os.path.join(app_state_dir, "removed_items.json")
|
||||
|
||||
if not os.path.exists(removed_file):
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(removed_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
swaparr_logger.error(f"Error loading removed items for {app_name}: {str(e)}")
|
||||
return {}
|
||||
|
||||
def save_removed_items(app_name, removed_items):
|
||||
"""Save list of permanently removed items"""
|
||||
app_state_dir = ensure_state_directory(app_name)
|
||||
removed_file = os.path.join(app_state_dir, "removed_items.json")
|
||||
|
||||
try:
|
||||
with open(removed_file, 'w') as f:
|
||||
json.dump(removed_items, f, indent=2)
|
||||
except IOError as e:
|
||||
swaparr_logger.error(f"Error saving removed items for {app_name}: {str(e)}")
|
||||
|
||||
def generate_item_hash(item):
|
||||
"""Generate a unique hash for an item based on its name and size.
|
||||
This helps track items across restarts even if their queue ID changes."""
|
||||
hash_input = f"{item['name']}_{item['size']}"
|
||||
return hashlib.md5(hash_input.encode('utf-8')).hexdigest()
|
||||
|
||||
def parse_time_string_to_seconds(time_string):
|
||||
"""Parse a time string like '2h', '30m', '1d' to seconds"""
|
||||
if not time_string:
|
||||
return 7200 # Default 2 hours
|
||||
|
||||
unit = time_string[-1].lower()
|
||||
try:
|
||||
value = int(time_string[:-1])
|
||||
except ValueError:
|
||||
swaparr_logger.error(f"Invalid time string: {time_string}, using default 2 hours")
|
||||
return 7200
|
||||
|
||||
if unit == 'd':
|
||||
return value * 86400 # Days to seconds
|
||||
elif unit == 'h':
|
||||
return value * 3600 # Hours to seconds
|
||||
elif unit == 'm':
|
||||
return value * 60 # Minutes to seconds
|
||||
else:
|
||||
swaparr_logger.error(f"Unknown time unit in: {time_string}, using default 2 hours")
|
||||
return 7200
|
||||
|
||||
def parse_size_string_to_bytes(size_string):
|
||||
"""Parse a size string like '25GB', '1TB' to bytes"""
|
||||
if not size_string:
|
||||
return 25 * 1024 * 1024 * 1024 # Default 25GB
|
||||
|
||||
# Extract the numeric part and unit
|
||||
unit = ""
|
||||
for i in range(len(size_string) - 1, -1, -1):
|
||||
if not size_string[i].isalpha():
|
||||
value = float(size_string[:i+1])
|
||||
unit = size_string[i+1:].upper()
|
||||
break
|
||||
else:
|
||||
swaparr_logger.error(f"Invalid size string: {size_string}, using default 25GB")
|
||||
return 25 * 1024 * 1024 * 1024
|
||||
|
||||
# Convert to bytes based on unit
|
||||
if unit == 'B':
|
||||
return int(value)
|
||||
elif unit == 'KB':
|
||||
return int(value * 1024)
|
||||
elif unit == 'MB':
|
||||
return int(value * 1024 * 1024)
|
||||
elif unit == 'GB':
|
||||
return int(value * 1024 * 1024 * 1024)
|
||||
elif unit == 'TB':
|
||||
return int(value * 1024 * 1024 * 1024 * 1024)
|
||||
else:
|
||||
swaparr_logger.error(f"Unknown size unit in: {size_string}, using default 25GB")
|
||||
return 25 * 1024 * 1024 * 1024
|
||||
|
||||
def get_queue_items(app_name, api_url, api_key, api_timeout=120):
|
||||
"""Get download queue items from a Starr app API with pagination support"""
|
||||
api_version_map = {
|
||||
"radarr": "v3",
|
||||
"sonarr": "v3",
|
||||
"lidarr": "v1",
|
||||
"readarr": "v1",
|
||||
"whisparr": "v3"
|
||||
}
|
||||
|
||||
api_version = api_version_map.get(app_name, "v3")
|
||||
|
||||
# Initialize an empty list to store all records
|
||||
all_records = []
|
||||
|
||||
# Start with page 1
|
||||
page = 1
|
||||
page_size = 100 # Request a large page size to reduce API calls
|
||||
|
||||
while True:
|
||||
# Add pagination parameters
|
||||
queue_url = f"{api_url.rstrip('/')}/api/{api_version}/queue?page={page}&pageSize={page_size}"
|
||||
headers = {'X-Api-Key': api_key}
|
||||
|
||||
try:
|
||||
response = requests.get(queue_url, headers=headers, timeout=api_timeout)
|
||||
response.raise_for_status()
|
||||
queue_data = response.json()
|
||||
|
||||
if api_version in ["v3"]: # Radarr, Sonarr, Whisparr use v3
|
||||
records = queue_data.get("records", [])
|
||||
total_records = queue_data.get("totalRecords", 0)
|
||||
else: # Lidarr, Readarr use v1
|
||||
records = queue_data
|
||||
total_records = len(records)
|
||||
|
||||
# Add this page's records to our collection
|
||||
all_records.extend(records)
|
||||
|
||||
# If we've fetched all records or there are no more, break the loop
|
||||
if len(all_records) >= total_records or len(records) == 0:
|
||||
break
|
||||
|
||||
# Otherwise, move to the next page
|
||||
page += 1
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
swaparr_logger.error(f"Error fetching queue for {app_name} (page {page}): {str(e)}")
|
||||
break
|
||||
|
||||
swaparr_logger.info(f"Fetched {len(all_records)} queue items for {app_name}")
|
||||
|
||||
# Normalize the response based on app type
|
||||
if app_name in ["radarr", "whisparr", "eros"]:
|
||||
return parse_queue_items(all_records, "movie", app_name)
|
||||
elif app_name == "sonarr":
|
||||
return parse_queue_items(all_records, "series", app_name)
|
||||
elif app_name == "lidarr":
|
||||
return parse_queue_items(all_records, "album", app_name)
|
||||
elif app_name == "readarr":
|
||||
return parse_queue_items(all_records, "book", app_name)
|
||||
else:
|
||||
swaparr_logger.error(f"Unknown app type: {app_name}")
|
||||
return []
|
||||
|
||||
def parse_queue_items(records, item_type, app_name):
|
||||
"""Parse queue items from API response into a standardized format"""
|
||||
queue_items = []
|
||||
|
||||
for record in records:
|
||||
# Skip non-dictionary records
|
||||
if not isinstance(record, dict):
|
||||
swaparr_logger.warning(f"Skipping non-dictionary record in {app_name} queue: {record}")
|
||||
continue
|
||||
|
||||
# Extract the name based on the item type
|
||||
name = None
|
||||
if item_type == "movie" and record.get("movie"):
|
||||
name = record["movie"].get("title", "Unknown Movie")
|
||||
elif item_type == "series" and record.get("series"):
|
||||
name = record["series"].get("title", "Unknown Series")
|
||||
elif item_type == "album" and record.get("album"):
|
||||
name = record["album"].get("title", "Unknown Album")
|
||||
elif item_type == "book" and record.get("book"):
|
||||
name = record["book"].get("title", "Unknown Book")
|
||||
|
||||
# If no name was found, try to use the download title
|
||||
if not name and record.get("title"):
|
||||
name = record.get("title", "Unknown Download")
|
||||
|
||||
# Parse ETA if available
|
||||
eta_seconds = 0
|
||||
if record.get("timeleft"):
|
||||
eta = record.get("timeleft", "")
|
||||
# Basic parsing of timeleft format like "00:30:00" (30 minutes)
|
||||
try:
|
||||
eta_parts = eta.split(':')
|
||||
if len(eta_parts) == 3:
|
||||
eta_seconds = int(eta_parts[0]) * 3600 + int(eta_parts[1]) * 60 + int(eta_parts[2])
|
||||
except (ValueError, IndexError):
|
||||
eta_seconds = 0
|
||||
|
||||
queue_items.append({
|
||||
"id": record.get("id"),
|
||||
"name": name,
|
||||
"size": record.get("size", 0),
|
||||
"status": record.get("status", "unknown").lower(),
|
||||
"eta": eta_seconds,
|
||||
"error_message": record.get("errorMessage", "")
|
||||
})
|
||||
|
||||
return queue_items
|
||||
|
||||
def delete_download(app_name, api_url, api_key, download_id, remove_from_client=True, api_timeout=120):
|
||||
"""Delete a download from a Starr app"""
|
||||
api_version_map = {
|
||||
"radarr": "v3",
|
||||
"sonarr": "v3",
|
||||
"lidarr": "v1",
|
||||
"readarr": "v1",
|
||||
"whisparr": "v3"
|
||||
}
|
||||
|
||||
api_version = api_version_map.get(app_name, "v3")
|
||||
delete_url = f"{api_url.rstrip('/')}/api/{api_version}/queue/{download_id}?removeFromClient={str(remove_from_client).lower()}&blocklist=true"
|
||||
headers = {'X-Api-Key': api_key}
|
||||
|
||||
try:
|
||||
response = requests.delete(delete_url, headers=headers, timeout=api_timeout)
|
||||
response.raise_for_status()
|
||||
swaparr_logger.info(f"Successfully removed download {download_id} from {app_name}")
|
||||
return True
|
||||
except requests.exceptions.RequestException as e:
|
||||
swaparr_logger.error(f"Error removing download {download_id} from {app_name}: {str(e)}")
|
||||
return False
|
||||
|
||||
def process_stalled_downloads(app_name, app_settings, swaparr_settings=None):
|
||||
"""Process stalled downloads for a specific app instance"""
|
||||
if not swaparr_settings:
|
||||
swaparr_settings = load_settings("swaparr")
|
||||
|
||||
if not swaparr_settings or not swaparr_settings.get("enabled", False):
|
||||
swaparr_logger.debug(f"Swaparr is disabled, skipping {app_name} instance: {app_settings.get('instance_name', 'Unknown')}")
|
||||
return
|
||||
|
||||
swaparr_logger.info(f"Processing stalled downloads for {app_name} instance: {app_settings.get('instance_name', 'Unknown')}")
|
||||
|
||||
# Get settings
|
||||
max_strikes = swaparr_settings.get("max_strikes", 3)
|
||||
max_download_time = parse_time_string_to_seconds(swaparr_settings.get("max_download_time", "2h"))
|
||||
ignore_above_size = parse_size_string_to_bytes(swaparr_settings.get("ignore_above_size", "25GB"))
|
||||
remove_from_client = swaparr_settings.get("remove_from_client", True)
|
||||
dry_run = swaparr_settings.get("dry_run", False)
|
||||
|
||||
api_url = app_settings.get("api_url")
|
||||
api_key = app_settings.get("api_key")
|
||||
api_timeout = app_settings.get("api_timeout", 120)
|
||||
|
||||
if not api_url or not api_key:
|
||||
swaparr_logger.error(f"Missing API URL or API Key for {app_name} instance: {app_settings.get('instance_name', 'Unknown')}")
|
||||
return
|
||||
|
||||
# Load existing strike data
|
||||
strike_data = load_strike_data(app_name)
|
||||
|
||||
# Load list of permanently removed items
|
||||
removed_items = load_removed_items(app_name)
|
||||
|
||||
# Clean up expired removed items (older than 30 days)
|
||||
now = datetime.utcnow()
|
||||
for item_hash in list(removed_items.keys()):
|
||||
removed_date = datetime.fromisoformat(removed_items[item_hash]["removed_time"].replace('Z', '+00:00'))
|
||||
if (now - removed_date) > timedelta(days=30):
|
||||
swaparr_logger.debug(f"Removing expired entry from removed items list: {removed_items[item_hash]['name']}")
|
||||
del removed_items[item_hash]
|
||||
|
||||
# Get current queue items
|
||||
queue_items = get_queue_items(app_name, api_url, api_key, api_timeout)
|
||||
|
||||
if not queue_items:
|
||||
swaparr_logger.info(f"No queue items found for {app_name} instance: {app_settings.get('instance_name', 'Unknown')}")
|
||||
return
|
||||
|
||||
# Keep track of items still in queue for cleanup
|
||||
current_item_ids = set(item["id"] for item in queue_items)
|
||||
|
||||
# Clean up items that are no longer in the queue
|
||||
for item_id in list(strike_data.keys()):
|
||||
if int(item_id) not in current_item_ids:
|
||||
swaparr_logger.debug(f"Removing item {item_id} from strike list as it's no longer in the queue")
|
||||
del strike_data[item_id]
|
||||
|
||||
# Process each queue item
|
||||
for item in queue_items:
|
||||
item_id = str(item["id"])
|
||||
item_state = "Normal"
|
||||
item_hash = generate_item_hash(item)
|
||||
|
||||
# Check if this item has been previously removed
|
||||
if item_hash in removed_items:
|
||||
last_removed_date = datetime.fromisoformat(removed_items[item_hash]["removed_time"].replace('Z', '+00:00'))
|
||||
days_since_removal = (now - last_removed_date).days
|
||||
|
||||
# Re-remove it automatically if it's been less than 7 days since last removal
|
||||
if days_since_removal < 7:
|
||||
swaparr_logger.warning(f"Found previously removed download that reappeared: {item['name']} (removed {days_since_removal} days ago)")
|
||||
|
||||
if not dry_run:
|
||||
if delete_download(app_name, api_url, api_key, item["id"], remove_from_client, api_timeout):
|
||||
swaparr_logger.info(f"Re-removed previously removed download: {item['name']}")
|
||||
# Update the removal time
|
||||
removed_items[item_hash]["removed_time"] = datetime.utcnow().isoformat()
|
||||
else:
|
||||
swaparr_logger.info(f"DRY RUN: Would have re-removed previously removed download: {item['name']}")
|
||||
|
||||
item_state = "Re-removed" if not dry_run else "Would Re-remove (Dry Run)"
|
||||
continue
|
||||
|
||||
# Skip large files if configured
|
||||
if item["size"] >= ignore_above_size:
|
||||
swaparr_logger.debug(f"Ignoring large download: {item['name']} ({item['size']} bytes > {ignore_above_size} bytes)")
|
||||
item_state = "Ignored (Size)"
|
||||
continue
|
||||
|
||||
# Handle delayed items - we'll skip these
|
||||
if item["status"] == "delay":
|
||||
swaparr_logger.debug(f"Ignoring delayed download: {item['name']}")
|
||||
item_state = "Ignored (Delayed)"
|
||||
continue
|
||||
|
||||
# Special handling for "queued" status
|
||||
# We only skip truly queued items, not those with metadata issues
|
||||
metadata_issue = "metadata" in item["status"].lower() or "metadata" in item["error_message"].lower()
|
||||
|
||||
if item["status"] == "queued" and not metadata_issue:
|
||||
# For regular queued items, check how long they've been in strike data
|
||||
if item_id in strike_data and "first_strike_time" in strike_data[item_id]:
|
||||
first_strike = datetime.fromisoformat(strike_data[item_id]["first_strike_time"].replace('Z', '+00:00'))
|
||||
if (now - first_strike) < timedelta(hours=1):
|
||||
# Skip if it's been less than 1 hour since first seeing it
|
||||
swaparr_logger.debug(f"Ignoring recently queued download: {item['name']}")
|
||||
item_state = "Ignored (Recently Queued)"
|
||||
continue
|
||||
else:
|
||||
# Initialize with first strike time for queued items
|
||||
if item_id not in strike_data:
|
||||
strike_data[item_id] = {
|
||||
"strikes": 0,
|
||||
"name": item["name"],
|
||||
"first_strike_time": datetime.utcnow().isoformat(),
|
||||
"last_strike_time": None
|
||||
}
|
||||
swaparr_logger.debug(f"Monitoring new queued download: {item['name']}")
|
||||
item_state = "Monitoring (Queued)"
|
||||
continue
|
||||
|
||||
# Initialize strike count if not already in strike data
|
||||
if item_id not in strike_data:
|
||||
strike_data[item_id] = {
|
||||
"strikes": 0,
|
||||
"name": item["name"],
|
||||
"first_strike_time": datetime.utcnow().isoformat(),
|
||||
"last_strike_time": None
|
||||
}
|
||||
|
||||
# Check if download should be striked
|
||||
should_strike = False
|
||||
strike_reason = ""
|
||||
|
||||
# Strike if metadata issue, eta too long, or no progress (eta = 0 and not queued)
|
||||
if metadata_issue:
|
||||
should_strike = True
|
||||
strike_reason = "Metadata"
|
||||
elif item["eta"] >= max_download_time:
|
||||
should_strike = True
|
||||
strike_reason = "ETA too long"
|
||||
elif item["eta"] == 0 and item["status"] not in ["queued", "delay"]:
|
||||
should_strike = True
|
||||
strike_reason = "No progress"
|
||||
|
||||
# If we should strike this item, add a strike
|
||||
if should_strike:
|
||||
strike_data[item_id]["strikes"] += 1
|
||||
strike_data[item_id]["last_strike_time"] = datetime.utcnow().isoformat()
|
||||
|
||||
if strike_data[item_id]["first_strike_time"] is None:
|
||||
strike_data[item_id]["first_strike_time"] = datetime.utcnow().isoformat()
|
||||
|
||||
current_strikes = strike_data[item_id]["strikes"]
|
||||
swaparr_logger.info(f"Added strike ({current_strikes}/{max_strikes}) to {item['name']} - Reason: {strike_reason}")
|
||||
|
||||
# If max strikes reached, remove the download
|
||||
if current_strikes >= max_strikes:
|
||||
swaparr_logger.warning(f"Max strikes reached for {item['name']}, removing download")
|
||||
|
||||
if not dry_run:
|
||||
if delete_download(app_name, api_url, api_key, item["id"], remove_from_client, api_timeout):
|
||||
swaparr_logger.info(f"Successfully removed {item['name']} after {max_strikes} strikes")
|
||||
|
||||
# Keep the item in strike data for reference but mark as removed
|
||||
strike_data[item_id]["removed"] = True
|
||||
strike_data[item_id]["removed_time"] = datetime.utcnow().isoformat()
|
||||
|
||||
# Add to removed items list for persistent tracking
|
||||
removed_items[item_hash] = {
|
||||
"name": item["name"],
|
||||
"size": item["size"],
|
||||
"removed_time": datetime.utcnow().isoformat(),
|
||||
"reason": strike_reason
|
||||
}
|
||||
else:
|
||||
swaparr_logger.info(f"DRY RUN: Would have removed {item['name']} after {max_strikes} strikes")
|
||||
|
||||
item_state = "Removed" if not dry_run else "Would Remove (Dry Run)"
|
||||
else:
|
||||
item_state = f"Striked ({current_strikes}/{max_strikes})"
|
||||
|
||||
swaparr_logger.debug(f"Processed download: {item['name']} - State: {item_state}")
|
||||
|
||||
# Save updated strike data
|
||||
save_strike_data(app_name, strike_data)
|
||||
|
||||
# Save updated removed items list
|
||||
save_removed_items(app_name, removed_items)
|
||||
|
||||
swaparr_logger.info(f"Finished processing stalled downloads for {app_name} instance: {app_settings.get('instance_name', 'Unknown')}")
|
||||
@@ -1,136 +0,0 @@
|
||||
"""
|
||||
Route definitions for Swaparr API endpoints.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
import os
|
||||
import json
|
||||
from src.primary.utils.logger import get_logger
|
||||
from src.primary.settings_manager import load_settings, save_settings
|
||||
from src.primary.apps.swaparr.handler import process_stalled_downloads
|
||||
# Import centralized path configuration
|
||||
from src.primary.utils.config_paths import CONFIG_PATH, SWAPARR_STATE_DIR
|
||||
|
||||
# Create the blueprint directly in this file
|
||||
swaparr_bp = Blueprint('swaparr', __name__)
|
||||
swaparr_logger = get_logger("swaparr")
|
||||
|
||||
@swaparr_bp.route('/status', methods=['GET'])
|
||||
def get_status():
|
||||
"""Get Swaparr status and statistics"""
|
||||
settings = load_settings("swaparr")
|
||||
enabled = settings.get("enabled", False)
|
||||
|
||||
# Get strike statistics from all app state directories
|
||||
statistics = {}
|
||||
state_dir = SWAPARR_STATE_DIR
|
||||
|
||||
if os.path.exists(state_dir):
|
||||
for app_name in os.listdir(state_dir):
|
||||
app_dir = os.path.join(state_dir, app_name)
|
||||
if os.path.isdir(app_dir):
|
||||
strike_file = os.path.join(app_dir, "strikes.json")
|
||||
if os.path.exists(strike_file):
|
||||
try:
|
||||
with open(strike_file, 'r') as f:
|
||||
strike_data = json.load(f)
|
||||
|
||||
total_items = len(strike_data)
|
||||
removed_items = sum(1 for item in strike_data.values() if item.get("removed", False))
|
||||
striked_items = sum(1 for item in strike_data.values()
|
||||
if item.get("strikes", 0) > 0 and not item.get("removed", False))
|
||||
|
||||
statistics[app_name] = {
|
||||
"total_tracked": total_items,
|
||||
"currently_striked": striked_items,
|
||||
"removed": removed_items
|
||||
}
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
swaparr_logger.error(f"Error reading strike data for {app_name}: {str(e)}")
|
||||
statistics[app_name] = {"error": str(e)}
|
||||
|
||||
return jsonify({
|
||||
"enabled": enabled,
|
||||
"settings": {
|
||||
"max_strikes": settings.get("max_strikes", 3),
|
||||
"max_download_time": settings.get("max_download_time", "2h"),
|
||||
"ignore_above_size": settings.get("ignore_above_size", "25GB"),
|
||||
"remove_from_client": settings.get("remove_from_client", True),
|
||||
"dry_run": settings.get("dry_run", False)
|
||||
},
|
||||
"statistics": statistics
|
||||
})
|
||||
|
||||
@swaparr_bp.route('/settings', methods=['GET'])
|
||||
def get_settings():
|
||||
"""Get Swaparr settings"""
|
||||
settings = load_settings("swaparr")
|
||||
return jsonify(settings)
|
||||
|
||||
@swaparr_bp.route('/settings', methods=['POST'])
|
||||
def update_settings():
|
||||
"""Update Swaparr settings"""
|
||||
data = request.json
|
||||
|
||||
if not data:
|
||||
return jsonify({"success": False, "message": "No data provided"}), 400
|
||||
|
||||
# Load current settings
|
||||
settings = load_settings("swaparr")
|
||||
|
||||
# Update settings with provided data
|
||||
for key, value in data.items():
|
||||
settings[key] = value
|
||||
|
||||
# Save updated settings
|
||||
success = save_settings("swaparr", settings)
|
||||
|
||||
if success:
|
||||
return jsonify({"success": True, "message": "Settings updated successfully"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "Failed to save settings"}), 500
|
||||
|
||||
@swaparr_bp.route('/reset', methods=['POST'])
|
||||
def reset_strikes():
|
||||
"""Reset all strikes for all apps or a specific app"""
|
||||
data = request.json
|
||||
app_name = data.get('app_name') if data else None
|
||||
|
||||
state_dir = SWAPARR_STATE_DIR
|
||||
|
||||
if not os.path.exists(state_dir):
|
||||
return jsonify({"success": True, "message": "No strike data to reset"})
|
||||
|
||||
if app_name:
|
||||
# Reset strikes for a specific app
|
||||
app_dir = os.path.join(state_dir, app_name)
|
||||
if os.path.exists(app_dir):
|
||||
strike_file = os.path.join(app_dir, "strikes.json")
|
||||
if os.path.exists(strike_file):
|
||||
try:
|
||||
os.remove(strike_file)
|
||||
swaparr_logger.info(f"Reset strikes for {app_name}")
|
||||
return jsonify({"success": True, "message": f"Strikes reset for {app_name}"})
|
||||
except IOError as e:
|
||||
swaparr_logger.error(f"Error resetting strikes for {app_name}: {str(e)}")
|
||||
return jsonify({"success": False, "message": f"Failed to reset strikes for {app_name}: {str(e)}"}), 500
|
||||
return jsonify({"success": False, "message": f"No strike data found for {app_name}"}), 404
|
||||
else:
|
||||
# Reset strikes for all apps
|
||||
try:
|
||||
for app_name in os.listdir(state_dir):
|
||||
app_dir = os.path.join(state_dir, app_name)
|
||||
if os.path.isdir(app_dir):
|
||||
strike_file = os.path.join(app_dir, "strikes.json")
|
||||
if os.path.exists(strike_file):
|
||||
os.remove(strike_file)
|
||||
|
||||
swaparr_logger.info("Reset all strikes")
|
||||
return jsonify({"success": True, "message": "All strikes reset"})
|
||||
except IOError as e:
|
||||
swaparr_logger.error(f"Error resetting all strikes: {str(e)}")
|
||||
return jsonify({"success": False, "message": f"Failed to reset all strikes: {str(e)}"}), 500
|
||||
|
||||
def register_routes(app):
|
||||
"""Register Swaparr routes with the Flask app."""
|
||||
app.register_blueprint(swaparr_bp, url_prefix='/api/swaparr')
|
||||
@@ -393,28 +393,6 @@ def app_specific_loop(app_type: str) -> None:
|
||||
if not stop_event.is_set():
|
||||
time.sleep(1) # Short pause
|
||||
|
||||
# --- Process Swaparr (stalled downloads) --- #
|
||||
try:
|
||||
# Try to import Swaparr module
|
||||
if not 'process_stalled_downloads' in locals():
|
||||
try:
|
||||
# Import directly from handler module to avoid circular imports
|
||||
from src.primary.apps.swaparr.handler import process_stalled_downloads
|
||||
swaparr_logger = get_logger("swaparr")
|
||||
swaparr_logger.debug(f"Successfully imported Swaparr module")
|
||||
except (ImportError, AttributeError) as e:
|
||||
app_logger.debug(f"Swaparr module not available or missing functions: {e}")
|
||||
process_stalled_downloads = None
|
||||
|
||||
# Check if Swaparr is enabled
|
||||
swaparr_settings = settings_manager.load_settings("swaparr")
|
||||
if swaparr_settings and swaparr_settings.get("enabled", False) and process_stalled_downloads:
|
||||
app_logger.info(f"Running Swaparr on {app_type} instance: {instance_name}")
|
||||
process_stalled_downloads(app_type, combined_settings, swaparr_settings)
|
||||
app_logger.info(f"Completed Swaparr processing for {app_type} instance: {instance_name}")
|
||||
except Exception as e:
|
||||
app_logger.error(f"Error during Swaparr processing for {instance_name}: {e}", exc_info=True)
|
||||
|
||||
# --- Cycle End & Sleep --- #
|
||||
calculate_reset_time(app_type) # Pass app_type here if needed by the function
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"enabled": false,
|
||||
"max_strikes": 3,
|
||||
"max_download_time": "2h",
|
||||
"ignore_above_size": "25GB",
|
||||
"remove_from_client": true,
|
||||
"dry_run": false
|
||||
}
|
||||
@@ -23,8 +23,7 @@ history_locks = {
|
||||
"lidarr": threading.Lock(),
|
||||
"readarr": threading.Lock(),
|
||||
"whisparr": threading.Lock(),
|
||||
"eros": threading.Lock(),
|
||||
"swaparr": threading.Lock()
|
||||
"eros": threading.Lock()
|
||||
}
|
||||
|
||||
def ensure_history_dir():
|
||||
|
||||
@@ -33,11 +33,11 @@ def migrate_json_configs():
|
||||
"eros.json",
|
||||
"lidarr.json",
|
||||
"readarr.json",
|
||||
"swaparr.json",
|
||||
"general.json",
|
||||
"radarr.json",
|
||||
"sonarr.json",
|
||||
"whisparr.json"
|
||||
"whisparr.json",
|
||||
"eros.json"
|
||||
]
|
||||
|
||||
# Flag to track if any files were migrated
|
||||
|
||||
@@ -20,7 +20,7 @@ def get_app_history(app_type):
|
||||
page_size = 20
|
||||
|
||||
# Validate app_type
|
||||
valid_app_types = ["all", "sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros", "swaparr"]
|
||||
valid_app_types = ["all", "sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros"]
|
||||
if app_type not in valid_app_types:
|
||||
return jsonify({"error": f"Invalid app type: {app_type}"}), 400
|
||||
|
||||
@@ -36,7 +36,7 @@ def clear_app_history(app_type):
|
||||
"""Clear history for a specific app or all apps"""
|
||||
try:
|
||||
# Validate app_type
|
||||
valid_app_types = ["all", "sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros", "swaparr"]
|
||||
valid_app_types = ["all", "sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros"]
|
||||
if app_type not in valid_app_types:
|
||||
return jsonify({"error": f"Invalid app type: {app_type}"}), 400
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ from src.primary.utils.config_paths import SETTINGS_DIR
|
||||
DEFAULT_CONFIGS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), 'default_configs'))
|
||||
|
||||
# Update or add this as a class attribute or constant
|
||||
KNOWN_APP_TYPES = ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros", "general", "swaparr"]
|
||||
KNOWN_APP_TYPES = ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros", "general"]
|
||||
|
||||
# Add a settings cache with timestamps to avoid excessive disk reads
|
||||
settings_cache = {} # Format: {app_name: {'timestamp': timestamp, 'data': settings_dict}}
|
||||
|
||||
@@ -33,7 +33,7 @@ def get_state_file_path(app_type, state_name):
|
||||
The path to the state file
|
||||
"""
|
||||
# Define known app types
|
||||
known_app_types = ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros", "swaparr"]
|
||||
known_app_types = ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros"]
|
||||
|
||||
# If app_type is not in known types, log a warning but don't fail
|
||||
if app_type not in known_app_types and app_type != "general":
|
||||
|
||||
@@ -127,8 +127,7 @@ def get_default_stats() -> Dict[str, Dict[str, int]]:
|
||||
"lidarr": {"hunted": 0, "upgraded": 0},
|
||||
"readarr": {"hunted": 0, "upgraded": 0},
|
||||
"whisparr": {"hunted": 0, "upgraded": 0},
|
||||
"eros": {"hunted": 0, "upgraded": 0},
|
||||
"swaparr": {"hunted": 0, "upgraded": 0}
|
||||
"eros": {"hunted": 0, "upgraded": 0}
|
||||
}
|
||||
|
||||
def get_default_hourly_caps() -> Dict[str, Dict[str, int]]:
|
||||
@@ -391,7 +390,7 @@ def increment_stat(app_type: str, stat_type: str, count: int = 1) -> bool:
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if app_type not in ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros", "swaparr"]:
|
||||
if app_type not in ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros"]:
|
||||
logger.error(f"Invalid app_type: {app_type}")
|
||||
return False
|
||||
|
||||
@@ -399,8 +398,7 @@ def increment_stat(app_type: str, stat_type: str, count: int = 1) -> bool:
|
||||
logger.error(f"Invalid stat_type: {stat_type}")
|
||||
return False
|
||||
|
||||
# Also increment the hourly API cap for this app, unless it's swaparr which doesn't have an API
|
||||
if app_type != "swaparr":
|
||||
# Also increment the hourly API cap for this app
|
||||
increment_hourly_cap(app_type, count)
|
||||
|
||||
with stats_lock:
|
||||
@@ -439,7 +437,7 @@ def increment_stat_only(app_type: str, stat_type: str, count: int = 1) -> bool:
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if app_type not in ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros", "swaparr"]:
|
||||
if app_type not in ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros"]:
|
||||
logger.error(f"Invalid app_type: {app_type}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ CLEAN_LOG_FILES = {
|
||||
"readarr": LOG_DIR / "clean_readarr.log",
|
||||
"whisparr": LOG_DIR / "clean_whisparr.log",
|
||||
"eros": LOG_DIR / "clean_eros.log",
|
||||
"swaparr": LOG_DIR / "clean_swaparr.log",
|
||||
"hunting": LOG_DIR / "clean_hunting.log"
|
||||
}
|
||||
|
||||
@@ -154,7 +153,6 @@ class CleanLogFormatter(logging.Formatter):
|
||||
'readarr': 'readarr',
|
||||
'whisparr': 'whisparr',
|
||||
'eros': 'eros',
|
||||
'swaparr': 'swaparr',
|
||||
'hunting': 'hunting',
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ try:
|
||||
STATEFUL_DIR = CONFIG_PATH / "stateful"
|
||||
RESET_DIR = CONFIG_PATH / "reset"
|
||||
SCHEDULER_DIR = CONFIG_PATH / "scheduler"
|
||||
SWAPARR_STATE_DIR = CONFIG_PATH / "swaparr"
|
||||
|
||||
# Create essential directories
|
||||
USER_DIR.mkdir(exist_ok=True)
|
||||
@@ -53,7 +52,6 @@ try:
|
||||
STATEFUL_DIR.mkdir(exist_ok=True)
|
||||
RESET_DIR.mkdir(exist_ok=True)
|
||||
SCHEDULER_DIR.mkdir(exist_ok=True)
|
||||
SWAPARR_STATE_DIR.mkdir(exist_ok=True)
|
||||
print(f"Using configuration directory: {CONFIG_DIR}")
|
||||
# Check write permissions with a test file
|
||||
test_file = CONFIG_PATH / f"write_test_{int(time.time())}.tmp"
|
||||
@@ -88,12 +86,11 @@ HISTORY_DIR = CONFIG_PATH / "history"
|
||||
SCHEDULER_DIR = CONFIG_PATH / "scheduler"
|
||||
RESET_DIR = CONFIG_PATH / "reset" # Add reset directory
|
||||
TALLY_DIR = CONFIG_PATH / "tally" # Add tally directory for stats
|
||||
SWAPARR_DIR = CONFIG_PATH / "swaparr" # Add Swaparr directory
|
||||
EROS_DIR = CONFIG_PATH / "eros" # Add Eros directory
|
||||
|
||||
# Create all directories
|
||||
for dir_path in [LOG_DIR, SETTINGS_DIR, USER_DIR, STATEFUL_DIR, HISTORY_DIR,
|
||||
SCHEDULER_DIR, RESET_DIR, TALLY_DIR, SWAPARR_DIR, EROS_DIR]:
|
||||
SCHEDULER_DIR, RESET_DIR, TALLY_DIR, EROS_DIR]:
|
||||
try:
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
@@ -117,10 +114,6 @@ def get_reset_path(app_type):
|
||||
"""Get the path to an app's reset file"""
|
||||
return RESET_DIR / f"{app_type}.reset"
|
||||
|
||||
def get_swaparr_state_path():
|
||||
"""Get the Swaparr state directory"""
|
||||
return SWAPARR_DIR
|
||||
|
||||
def get_eros_config_path():
|
||||
"""Get the Eros config file path"""
|
||||
return CONFIG_PATH / "eros.json"
|
||||
|
||||
@@ -28,7 +28,6 @@ APP_LOG_FILES = {
|
||||
"readarr": LOG_DIR / "readarr.log", # Updated filename
|
||||
"whisparr": LOG_DIR / "whisparr.log", # Added Whisparr
|
||||
"eros": LOG_DIR / "eros.log", # Added Eros for Whisparr V3
|
||||
"swaparr": LOG_DIR / "swaparr.log", # Added Swaparr
|
||||
"hunting": LOG_DIR / "hunting.log" # Added Hunt Manager - fixed key
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ from src.primary.auth import (
|
||||
from src.primary.routes.common import common_bp
|
||||
|
||||
# Import blueprints for each app from the centralized blueprints module
|
||||
from src.primary.apps.blueprints import sonarr_bp, radarr_bp, lidarr_bp, readarr_bp, whisparr_bp, swaparr_bp, eros_bp
|
||||
from src.primary.apps.blueprints import sonarr_bp, radarr_bp, lidarr_bp, readarr_bp, whisparr_bp, eros_bp
|
||||
|
||||
# Import stateful blueprint
|
||||
from src.primary.stateful_routes import stateful_api
|
||||
@@ -262,7 +262,6 @@ app.register_blueprint(lidarr_bp, url_prefix='/api/lidarr')
|
||||
app.register_blueprint(readarr_bp, url_prefix='/api/readarr')
|
||||
app.register_blueprint(whisparr_bp, url_prefix='/api/whisparr')
|
||||
app.register_blueprint(eros_bp, url_prefix='/api/eros')
|
||||
app.register_blueprint(swaparr_bp, url_prefix='/api/swaparr')
|
||||
app.register_blueprint(stateful_api, url_prefix='/api/stateful')
|
||||
app.register_blueprint(history_blueprint, url_prefix='/api/history')
|
||||
app.register_blueprint(scheduler_api)
|
||||
@@ -289,7 +288,6 @@ KNOWN_LOG_FILES = {
|
||||
"readarr": CLEAN_LOG_FILES.get("readarr"),
|
||||
"whisparr": CLEAN_LOG_FILES.get("whisparr"),
|
||||
"eros": CLEAN_LOG_FILES.get("eros"), # Added Eros to known log files
|
||||
"swaparr": CLEAN_LOG_FILES.get("swaparr"), # Added Swaparr to known log files
|
||||
"hunting": CLEAN_LOG_FILES.get("hunting"), # Added Hunt Manager to known log files - fixed key
|
||||
"system": CLEAN_LOG_FILES.get("system"), # Map 'system' to the clean huntarr log
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user