fix: unignore modular settings and logs scripts in .gitignore and add them to repo

This commit is contained in:
Admin9705
2026-01-29 08:19:04 -05:00
parent a07de4fd9f
commit 0314921d72
14 changed files with 5761 additions and 4 deletions
+4 -4
View File
@@ -236,7 +236,7 @@ data/
*.sqlite3
huntarr.db
/config/
logs/
settings/
stateful/
tally/
/logs/
/settings/
/stateful/
/tally/
@@ -0,0 +1,248 @@
/**
* Logs Module
* Handles log streaming, searching, and filtering
*/
window.HuntarrLogs = {
autoScrollWasEnabled: false,
connectToLogs: function() {
if (window.LogsModule && typeof window.LogsModule.connectToLogs === 'function') {
window.LogsModule.connectToLogs();
}
},
clearLogs: function() {
if (window.LogsModule && typeof window.LogsModule.clearLogs === 'function') {
window.LogsModule.clearLogs();
}
},
insertLogInChronologicalOrder: function(newLogEntry) {
if (!window.huntarrUI || !window.huntarrUI.elements.logsContainer || !newLogEntry) return;
const logsContainer = window.huntarrUI.elements.logsContainer;
const newTimestamp = this.parseLogTimestamp(newLogEntry);
if (!newTimestamp) {
logsContainer.appendChild(newLogEntry);
return;
}
const existingEntries = Array.from(logsContainer.children);
if (existingEntries.length === 0) {
logsContainer.appendChild(newLogEntry);
return;
}
let insertPosition = null;
for (let i = 0; i < existingEntries.length; i++) {
const existingTimestamp = this.parseLogTimestamp(existingEntries[i]);
if (!existingTimestamp) continue;
if (newTimestamp > existingTimestamp) {
insertPosition = existingEntries[i];
break;
}
}
if (insertPosition) {
logsContainer.insertBefore(newLogEntry, insertPosition);
} else {
logsContainer.appendChild(newLogEntry);
}
},
parseLogTimestamp: function(logEntry) {
if (!logEntry) return null;
try {
const dateSpan = logEntry.querySelector('.log-timestamp .date');
const timeSpan = logEntry.querySelector('.log-timestamp .time');
if (!dateSpan || !timeSpan) return null;
const dateText = dateSpan.textContent.trim();
const timeText = timeSpan.textContent.trim();
if (!dateText || !timeText || dateText === '--' || timeText === '--:--:--') {
return null;
}
const timestampString = `${dateText} ${timeText}`;
const timestamp = new Date(timestampString);
return isNaN(timestamp.getTime()) ? null : timestamp;
} catch (error) {
console.warn('[HuntarrLogs] Error parsing log timestamp:', error);
return null;
}
},
searchLogs: function() {
if (!window.huntarrUI || !window.huntarrUI.elements.logsContainer || !window.huntarrUI.elements.logSearchInput) return;
const logsContainer = window.huntarrUI.elements.logsContainer;
const logSearchInput = window.huntarrUI.elements.logSearchInput;
const searchText = logSearchInput.value.trim().toLowerCase();
if (!searchText) {
this.clearLogSearch();
return;
}
if (window.huntarrUI.elements.clearSearchButton) {
window.huntarrUI.elements.clearSearchButton.style.display = 'block';
}
const logEntries = Array.from(logsContainer.querySelectorAll('.log-table-row'));
let matchCount = 0;
const MAX_ENTRIES_TO_PROCESS = 300;
const processedLogEntries = logEntries.slice(0, MAX_ENTRIES_TO_PROCESS);
const remainingCount = Math.max(0, logEntries.length - MAX_ENTRIES_TO_PROCESS);
processedLogEntries.forEach((entry) => {
const entryText = entry.textContent.toLowerCase();
if (entryText.includes(searchText)) {
entry.style.display = '';
matchCount++;
this.simpleHighlightMatch(entry, searchText);
} else {
entry.style.display = 'none';
}
});
if (remainingCount > 0) {
logEntries.slice(MAX_ENTRIES_TO_PROCESS).forEach(entry => {
const entryText = entry.textContent.toLowerCase();
if (entryText.includes(searchText)) {
entry.style.display = '';
matchCount++;
} else {
entry.style.display = 'none';
}
});
}
if (window.huntarrUI.elements.logSearchResults) {
window.huntarrUI.elements.logSearchResults.textContent = `Found ${matchCount} matching log entries`;
window.huntarrUI.elements.logSearchResults.style.display = 'block';
}
if (window.huntarrUI.elements.autoScrollCheckbox && window.huntarrUI.elements.autoScrollCheckbox.checked) {
this.autoScrollWasEnabled = true;
window.huntarrUI.elements.autoScrollCheckbox.checked = false;
}
},
simpleHighlightMatch: function(logEntry, searchText) {
if (searchText.length < 2) return;
if (!logEntry.hasAttribute('data-original-html')) {
logEntry.setAttribute('data-original-html', logEntry.innerHTML);
}
const html = logEntry.getAttribute('data-original-html');
const escapedSearchText = searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedSearchText})`, 'gi');
const newHtml = html.replace(regex, '<span class="search-highlight">$1</span>');
logEntry.innerHTML = newHtml;
},
clearLogSearch: function() {
if (!window.huntarrUI || !window.huntarrUI.elements.logsContainer) return;
const logsContainer = window.huntarrUI.elements.logsContainer;
if (window.huntarrUI.elements.logSearchInput) {
window.huntarrUI.elements.logSearchInput.value = '';
}
if (window.huntarrUI.elements.clearSearchButton) {
window.huntarrUI.elements.clearSearchButton.style.display = 'none';
}
if (window.huntarrUI.elements.logSearchResults) {
window.huntarrUI.elements.logSearchResults.style.display = 'none';
}
const allLogEntries = logsContainer.querySelectorAll('.log-table-row');
Array.from(allLogEntries).forEach(entry => {
entry.style.display = '';
if (entry.hasAttribute('data-original-html')) {
entry.innerHTML = entry.getAttribute('data-original-html');
}
});
if (this.autoScrollWasEnabled && window.huntarrUI.elements.autoScrollCheckbox) {
window.huntarrUI.elements.autoScrollCheckbox.checked = true;
this.autoScrollWasEnabled = false;
}
},
filterLogsByLevel: function(selectedLevel) {
if (!window.huntarrUI || !window.huntarrUI.elements.logsContainer) return;
const logsContainer = window.huntarrUI.elements.logsContainer;
const logEntries = logsContainer.querySelectorAll('.log-table-row');
let visibleCount = 0;
let totalCount = logEntries.length;
logEntries.forEach(entry => {
if (selectedLevel === 'all') {
entry.style.display = '';
entry.removeAttribute('data-hidden-by-filter');
visibleCount++;
} else {
const levelBadge = entry.querySelector('.log-level-badge');
if (levelBadge) {
const level = levelBadge.textContent.trim().toLowerCase();
if (level === selectedLevel.toLowerCase()) {
entry.style.display = '';
entry.removeAttribute('data-hidden-by-filter');
visibleCount++;
} else {
entry.style.display = 'none';
entry.setAttribute('data-hidden-by-filter', 'true');
}
} else {
entry.style.display = 'none';
entry.setAttribute('data-hidden-by-filter', 'true');
}
}
});
if (window.huntarrUI.autoScroll && window.huntarrUI.elements.autoScrollCheckbox && window.huntarrUI.elements.autoScrollCheckbox.checked && visibleCount > 0) {
setTimeout(() => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}, 100);
}
console.log(`[HuntarrLogs] Filtered logs by level '${selectedLevel}': showing ${visibleCount}/${totalCount} entries`);
},
applyFilterToSingleEntry: function(logEntry, selectedLevel) {
if (!logEntry || selectedLevel === 'all') return;
const levelBadge = logEntry.querySelector('.log-level-badge');
if (levelBadge) {
const level = levelBadge.textContent.trim().toLowerCase();
if (level !== selectedLevel.toLowerCase()) {
logEntry.style.display = 'none';
logEntry.setAttribute('data-hidden-by-filter', 'true');
}
} else {
logEntry.style.display = 'none';
logEntry.setAttribute('data-hidden-by-filter', 'true');
}
}
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,259 @@
(function() {
window.SettingsForms = window.SettingsForms || {};
window.SettingsForms.generateGeneralForm = function(container, settings = {}) {
if (!settings || typeof settings !== "object") {
settings = {};
}
container.setAttribute("data-app-type", "general");
container.innerHTML = `
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>System Settings</h3>
<div class="setting-item">
<label for="check_for_updates"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#check-for-updates" class="info-icon" title="Learn more about update checking" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Check for Updates:</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="check_for_updates" ${
settings.check_for_updates !== 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" style="margin-left: -3ch !important;">Automatically check for Huntarr updates</p>
</div>
<div class="setting-item">
<label for="show_trending"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#show-trending" class="info-icon" title="Learn more about showing trending content on home page" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Show Trending:</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="show_trending" ${
settings.show_trending !== 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" style="margin-left: -3ch !important;">Display "Trending This Week" section on the home page</p>
</div>
<div class="setting-item">
<label for="low_usage_mode"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#low-usage-mode" class="info-icon" title="Learn more about Low Usage Mode" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Low Usage Mode:</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="low_usage_mode" ${
settings.low_usage_mode === 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" style="margin-left: -3ch !important;">Disables animations to reduce CPU/GPU usage on older devices</p>
</div>
<div class="setting-item">
<label for="timezone"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#timezone" class="info-icon" title="Set your timezone for accurate time display" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Timezone:</label>
<select id="timezone" name="timezone" style="width: 300px; padding: 8px 12px; border-radius: 6px; cursor: pointer; border: 1px solid rgba(255, 255, 255, 0.1); background-color: #1f2937; color: #d1d5db;">
${(() => {
const predefinedTimezones = [
"UTC", "America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles", "Pacific/Honolulu",
"America/Toronto", "America/Vancouver", "America/Sao_Paulo", "America/Argentina/Buenos_Aires", "America/Mexico_City",
"Europe/London", "Europe/Paris", "Europe/Berlin", "Europe/Amsterdam", "Europe/Rome", "Europe/Madrid",
"Asia/Tokyo", "Asia/Seoul", "Asia/Shanghai", "Asia/Singapore", "Australia/Sydney", "Pacific/Auckland"
];
const currentTimezone = settings.timezone;
if (currentTimezone && !predefinedTimezones.includes(currentTimezone)) {
return `<option value="${currentTimezone}" selected>${currentTimezone} (Custom)</option>`;
}
return "";
})()}
<option value="UTC" ${settings.timezone === "UTC" || !settings.timezone ? "selected" : ""}>UTC</option>
<option value="America/New_York" ${settings.timezone === "America/New_York" ? "selected" : ""}>Eastern Time</option>
<option value="America/Chicago" ${settings.timezone === "America/Chicago" ? "selected" : ""}>Central Time</option>
<option value="America/Denver" ${settings.timezone === "America/Denver" ? "selected" : ""}>Mountain Time</option>
<option value="America/Los_Angeles" ${settings.timezone === "America/Los_Angeles" ? "selected" : ""}>Pacific Time</option>
<option value="Europe/London" ${settings.timezone === "Europe/London" ? "selected" : ""}>UK Time</option>
<option value="Europe/Paris" ${settings.timezone === "Europe/Paris" ? "selected" : ""}>Central Europe</option>
<option value="Asia/Tokyo" ${settings.timezone === "Asia/Tokyo" ? "selected" : ""}>Japan</option>
<option value="Australia/Sydney" ${settings.timezone === "Australia/Sydney" ? "selected" : ""}>Australia East</option>
</select>
<p class="setting-help" style="margin-left: -3ch !important;">Set your timezone for accurate time display in logs and scheduling.</p>
</div>
</div>
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Security</h3>
<div class="setting-item">
<label for="auth_mode"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#authentication-mode" class="info-icon" title="Learn more about authentication modes" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Authentication Mode:</label>
<select id="auth_mode" name="auth_mode" style="width: 300px; padding: 8px 12px; border-radius: 6px; cursor: pointer; border: 1px solid rgba(255, 255, 255, 0.1); background-color: #1f2937; color: #d1d5db;">
<option value="login" ${
settings.auth_mode === "login" ||
(!settings.auth_mode && !settings.local_access_bypass && !settings.proxy_auth_bypass)
? "selected" : ""
}>Login Mode</option>
<option value="local_bypass" ${
settings.auth_mode === "local_bypass" ||
(!settings.auth_mode && settings.local_access_bypass === true && !settings.proxy_auth_bypass)
? "selected" : ""
}>Local Bypass Mode</option>
<option value="no_login" ${
settings.auth_mode === "no_login" ||
(!settings.auth_mode && settings.proxy_auth_bypass === true)
? "selected" : ""
}>No Login Mode</option>
</select>
<p class="setting-help" style="margin-left: -3ch !important;">Login Mode: Standard login. Local Bypass: No login on local network. No Login: Completely open (use behind proxy).</p>
</div>
<div class="setting-item">
<label for="ssl_verify"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#enable-ssl-verify" class="info-icon" title="Learn more about SSL verification" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Enable SSL Verify:</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="ssl_verify" ${
settings.ssl_verify === 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" style="margin-left: -3ch !important;">Disable SSL certificate verification when using self-signed certificates.</p>
</div>
</div>
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Advanced Settings</h3>
<div class="setting-item">
<label for="api_timeout">API Timeout:</label>
<input type="number" id="api_timeout" min="10" value="${settings.api_timeout || 120}">
<p class="setting-help" style="margin-left: -3ch !important;">API request timeout in seconds</p>
</div>
<div class="setting-item">
<label for="base_url">Base URL:</label>
<input type="text" id="base_url" value="${settings.base_url || ""}" placeholder="/huntarr">
<p class="setting-help" style="margin-left: -3ch !important;">Base URL path for reverse proxy. Requires restart.</p>
</div>
</div>
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Display Settings</h3>
<div class="setting-item">
<label for="display_community_resources">Display Resources:</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="display_community_resources" ${
settings.display_community_resources !== 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" style="margin-left: -3ch !important;">Show or hide the Resources section on the home page</p>
</div>
<div class="setting-item">
<label for="display_huntarr_support">Display Huntarr Support:</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="display_huntarr_support" ${
settings.display_huntarr_support !== 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" style="margin-left: -3ch !important;">Display support section</p>
</div>
</div>
<div style="margin-top: 20px;">
<button type="button" id="settings-save-button" disabled style="
background: #6b7280;
color: #9ca3af;
border: 1px solid #4b5563;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<i class="fas fa-save"></i>
Save Changes
</button>
</div>
`;
if (window.SettingsForms.setupSettingsManualSave) {
window.SettingsForms.setupSettingsManualSave(container, settings);
}
};
window.SettingsForms.setupSettingsManualSave = function(container, originalSettings = {}) {
const saveButton = container.querySelector("#settings-save-button");
if (!saveButton) return;
saveButton.disabled = true;
saveButton.style.background = "#6b7280";
saveButton.style.cursor = "not-allowed";
let hasChanges = false;
window.settingsUnsavedChanges = false;
if (window.SettingsForms.removeUnsavedChangesWarning) {
window.SettingsForms.removeUnsavedChangesWarning();
}
const updateSaveButtonState = (changesDetected) => {
hasChanges = changesDetected;
window.settingsUnsavedChanges = changesDetected;
if (hasChanges) {
saveButton.disabled = false;
saveButton.style.background = "#dc2626";
saveButton.style.cursor = "pointer";
if (window.SettingsForms.addUnsavedChangesWarning) {
window.SettingsForms.addUnsavedChangesWarning();
}
} else {
saveButton.disabled = true;
saveButton.style.background = "#6b7280";
saveButton.style.cursor = "not-allowed";
if (window.SettingsForms.removeUnsavedChangesWarning) {
window.SettingsForms.removeUnsavedChangesWarning();
}
}
};
container.addEventListener('input', () => updateSaveButtonState(true));
container.addEventListener('change', () => updateSaveButtonState(true));
const newSaveButton = saveButton.cloneNode(true);
saveButton.parentNode.replaceChild(newSaveButton, saveButton);
newSaveButton.addEventListener("click", () => {
if (!hasChanges) return;
newSaveButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
newSaveButton.disabled = true;
// Collect data using the centralized getFormSettings
const settings = window.SettingsForms.getFormSettings(container, "general");
window.SettingsForms.saveAppSettings("general", settings);
newSaveButton.innerHTML = '<i class="fas fa-save"></i> Save Changes';
updateSaveButtonState(false);
});
};
})();
@@ -0,0 +1,138 @@
(function() {
window.SettingsForms = window.SettingsForms || {};
window.SettingsForms.generateLidarrForm = function(container, settings = {}) {
if (!settings || typeof settings !== "object") {
settings = {};
}
const wasSuppressionActive = window._appsSuppressChangeDetection;
window._appsSuppressChangeDetection = true;
container.setAttribute("data-app-type", "lidarr");
if (!settings.instances || !Array.isArray(settings.instances)) {
settings.instances = [];
}
let lidarrSaveButtonHtml = `
<div style="margin-bottom: 20px;">
<button type="button" id="lidarr-save-button" disabled style="
background: #6b7280;
color: #9ca3af;
border: 1px solid #4b5563;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<i class="fas fa-save"></i>
Save Changes
</button>
</div>
`;
let instancesHtml = `
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Lidarr Instances</h3>
<div class="instance-card-grid" id="lidarr-instances-grid">
`;
if (settings.instances && settings.instances.length > 0) {
settings.instances.forEach((instance, index) => {
instancesHtml += window.SettingsForms.renderInstanceCard('lidarr', instance, index);
});
}
instancesHtml += `
<div class="add-instance-card" data-app-type="lidarr">
<div class="add-icon"><i class="fas fa-plus-circle"></i></div>
<div class="add-text">Add Lidarr Instance</div>
</div>
`;
instancesHtml += `
</div>
</div>
`;
let searchSettingsHtml = `
<div class="settings-group">
<h3>Search Settings</h3>
<div class="setting-item">
<label for="lidarr_hunt_missing_mode"><a href="https://plexguide.github.io/Huntarr.io/apps/lidarr.html#search-settings" class="info-icon" title="Learn more about missing search modes" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Missing Search Mode:</label>
<select id="lidarr_hunt_missing_mode" name="hunt_missing_mode">
<option value="album" selected>Album</option>
</select>
<p class="setting-help">Search for individual albums (Artist mode deprecated in Huntarr 7.5.0+)</p>
</div>
<div class="setting-item">
<label for="lidarr_sleep_duration"><a href="https://plexguide.github.io/Huntarr.io/apps/lidarr.html#search-settings" class="info-icon" title="Learn more about sleep duration" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Sleep Duration (Minutes):</label>
<input type="number" id="lidarr_sleep_duration" name="sleep_duration" min="10" value="${
settings.sleep_duration !== undefined ? Math.round(settings.sleep_duration / 60) : 15
}">
<p class="setting-help">Time in minutes between processing cycles (minimum 10 minutes)</p>
</div>
<div class="setting-item">
<label for="lidarr_hourly_cap"><a href="https://plexguide.github.io/Huntarr.io/apps/lidarr.html#search-settings" class="info-icon" title="Maximum API requests per hour for this app (20 is safe)" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>API Cap - Hourly:</label>
<input type="number" id="lidarr_hourly_cap" name="hourly_cap" min="1" max="400" value="${
settings.hourly_cap !== undefined ? settings.hourly_cap : 20
}">
<p class="setting-help">Maximum API requests per hour to prevent being banned by your indexers. Keep lower for safety (20-50 recommended). Max allowed: 400.</p>
</div>
</div>
`;
container.innerHTML = lidarrSaveButtonHtml + instancesHtml + searchSettingsHtml;
const grid = container.querySelector('#lidarr-instances-grid');
if (grid) {
grid.addEventListener('click', (e) => {
const editBtn = e.target.closest('.btn-card.edit');
const deleteBtn = e.target.closest('.btn-card.delete');
const addCard = e.target.closest('.add-instance-card');
if (editBtn) {
const appType = editBtn.dataset.appType;
const index = parseInt(editBtn.dataset.instanceIndex);
window.SettingsForms.openInstanceModal(appType, index);
} else if (deleteBtn) {
const appType = deleteBtn.dataset.appType;
const index = parseInt(deleteBtn.dataset.instanceIndex);
window.SettingsForms.deleteInstance(appType, index);
} else if (addCard) {
const appType = addCard.dataset.appType;
window.SettingsForms.openInstanceModal(appType);
}
});
}
if (window.SettingsForms.setupAppManualSave) {
window.SettingsForms.setupAppManualSave(container, "lidarr", settings);
}
// Test instance connections after rendering
setTimeout(() => {
if (window.SettingsForms.testAllInstanceConnections) {
window.SettingsForms.testAllInstanceConnections("lidarr");
}
}, 100);
setTimeout(() => {
window._appsSuppressChangeDetection = wasSuppressionActive;
}, 100);
};
})();
@@ -0,0 +1,119 @@
(function() {
window.SettingsForms = window.SettingsForms || {};
window.SettingsForms.generateLogsSettingsForm = function(container, settings = {}) {
if (!settings || typeof settings !== "object") {
settings = {};
}
container.setAttribute("data-app-type", "logs");
let logsSaveButtonHtml = `
<div style="margin-bottom: 20px;">
<button type="button" id="general-save-button" disabled style="
background: #6b7280;
color: #9ca3af;
border: 1px solid #4b5563;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<i class="fas fa-save"></i>
Save Changes
</button>
</div>
`;
let logsHtml = `
<div class="settings-group">
<h3>Log Rotation</h3>
<div class="setting-item">
<label for="log_rotation_enabled">Enable Log Rotation:</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="log_rotation_enabled" name="log_rotation_enabled" ${
settings.log_rotation_enabled !== 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">Automatically rotate log files when they reach a certain size</p>
</div>
<div class="setting-item">
<label for="log_max_size_mb">Max File Size:</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="number" id="log_max_size_mb" name="log_max_size_mb" min="1" max="100" value="${
settings.log_max_size_mb || 10
}" style="width: 100px;">
<span style="color: #9ca3af;">MB</span>
</div>
<p class="setting-help">Maximum size before rotating to a new file</p>
</div>
<div class="setting-item">
<label for="log_backup_count">Backup Files to Keep:</label>
<input type="number" id="log_backup_count" name="log_backup_count" min="0" max="50" value="${
settings.log_backup_count || 5
}" style="width: 100px;">
<p class="setting-help">Number of rotated log files to retain (0-50)</p>
</div>
</div>
<div class="settings-group">
<h3>Retention & Cleanup</h3>
<div class="setting-item">
<label for="log_retention_days">Retention Days:</label>
<input type="number" id="log_retention_days" name="log_retention_days" min="0" max="365" value="${
settings.log_retention_days || 30
}" style="width: 100px;">
<p class="setting-help">Delete logs older than this many days (0 = unlimited)</p>
</div>
<div class="setting-item">
<label for="log_auto_cleanup">Auto-Cleanup on Startup:</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="log_auto_cleanup" name="log_auto_cleanup" ${
settings.log_auto_cleanup !== 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">Automatically clean up old logs when Huntarr starts</p>
</div>
</div>
<div class="settings-group">
<h3>Advanced Settings</h3>
<div class="setting-item">
<label for="log_refresh_interval_seconds">Log Refresh Interval:</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="number" id="log_refresh_interval_seconds" name="log_refresh_interval_seconds" min="5" max="300" value="${
settings.log_refresh_interval_seconds || 30
}" style="width: 100px;">
<span style="color: #9ca3af;">seconds</span>
</div>
<p class="setting-help">How often the log viewer checks for new logs</p>
</div>
</div>
`;
container.innerHTML = logsSaveButtonHtml + logsHtml;
HuntarrUtils.fetchWithTimeout('./api/logs/usage')
.then(response => response.json())
.then(data => {
if (data.success) {
const sizeEl = container.querySelector('#log-usage-size');
const filesEl = container.querySelector('#log-usage-files');
if (sizeEl) sizeEl.textContent = data.total_size_formatted;
if (filesEl) filesEl.textContent = `${data.file_count} files across log directory`;
}
})
.catch(err => console.error('Error fetching log usage:', err));
if (window.SettingsForms.setupAppManualSave) {
window.SettingsForms.setupAppManualSave(container, "general", settings);
}
};
})();
@@ -0,0 +1,229 @@
(function() {
window.SettingsForms = window.SettingsForms || {};
window.SettingsForms.generateProwlarrForm = function(container, settings = {}) {
if (!settings || typeof settings !== "object") {
settings = {};
}
container.setAttribute("data-app-type", "prowlarr");
let prowlarrSaveButtonHtml = `
<div style="margin-bottom: 20px;">
<button type="button" id="prowlarr-save-button" disabled style="
background: #6b7280;
color: #9ca3af;
border: 1px solid #4b5563;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<i class="fas fa-save"></i>
Save Changes
</button>
</div>
`;
let prowlarrHtml = `
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Prowlarr Configuration</h3>
<div class="instance-card-grid" id="prowlarr-instances-grid">
`;
const prowlarrInstance = {
name: 'Prowlarr',
api_url: settings.api_url || '',
api_key: settings.api_key || '',
enabled: settings.enabled !== false
};
prowlarrHtml += window.SettingsForms.renderInstanceCard('prowlarr', prowlarrInstance, 0);
prowlarrHtml += `
</div>
</div>
`;
container.innerHTML = prowlarrSaveButtonHtml + prowlarrHtml;
const grid = container.querySelector('#prowlarr-instances-grid');
if (grid) {
grid.addEventListener('click', (e) => {
const editBtn = e.target.closest('.btn-card.edit');
if (editBtn) {
window.SettingsForms.openProwlarrModal();
}
});
}
if (window.SettingsForms.setupAppManualSave) {
window.SettingsForms.setupAppManualSave(container, "prowlarr", settings);
}
// Test instance connections after rendering
setTimeout(() => {
if (window.SettingsForms.testAllInstanceConnections) {
window.SettingsForms.testAllInstanceConnections("prowlarr");
}
}, 100);
};
window.SettingsForms.openProwlarrModal = function() {
const settings = window.huntarrUI.originalSettings.prowlarr;
if (!settings) return;
const prowlarrInstance = {
name: 'Prowlarr',
api_url: settings.api_url || '',
api_key: settings.api_key || '',
enabled: settings.enabled !== false
};
// Use the instance editor section
const titleEl = document.getElementById('instance-editor-title');
if (titleEl) {
titleEl.textContent = `Edit Prowlarr Configuration`;
}
const contentEl = document.getElementById('instance-editor-content');
if (contentEl) {
contentEl.innerHTML = `
<div class="editor-grid">
<div class="editor-section">
<div class="editor-section-title">
Connection Details
<span id="prowlarr-connection-status" class="connection-status-badge status-unknown">
<i class="fas fa-question-circle"></i> Not Tested
</span>
</div>
<div class="setting-item" style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px;">
<label style="color: #f8fafc; font-weight: 500; margin: 0;">Enabled</label>
<label class="toggle-switch" style="margin: 0;">
<input type="checkbox" id="editor-enabled" ${prowlarrInstance.enabled ? 'checked' : ''}>
<span class="toggle-slider"></span>
</label>
</div>
<p class="setting-help" style="margin: 0 0 20px 0; color: #94a3b8; font-size: 0.85rem;">Enable or disable Prowlarr integration</p>
<div class="setting-item" style="margin-bottom: 20px;">
<label style="display: block; color: #f8fafc; font-weight: 500; margin-bottom: 8px;">Name</label>
<input type="text" id="editor-name" value="Prowlarr" readonly style="width: 100%; padding: 10px; border-radius: 8px; border: 1px solid rgba(148, 163, 184, 0.2); background: rgba(15, 23, 42, 0.3); color: #94a3b8; cursor: not-allowed;">
<p class="setting-help" style="margin-top: 5px; color: #94a3b8; font-size: 0.85rem;">A friendly name to identify this instance</p>
</div>
<div class="setting-item" style="margin-bottom: 20px;">
<label style="display: block; color: #f8fafc; font-weight: 500; margin-bottom: 8px;">URL</label>
<input type="text" id="editor-url" value="${prowlarrInstance.api_url || ''}" placeholder="http://localhost:9696" style="width: 100%; padding: 10px; border-radius: 8px; border: 1px solid rgba(148, 163, 184, 0.2); background: rgba(15, 23, 42, 0.5); color: white;">
<p class="setting-help" style="margin-top: 5px; color: #94a3b8; font-size: 0.85rem;">The full URL including port (e.g. http://localhost:9696)</p>
</div>
<div class="setting-item" style="margin-bottom: 0;">
<label style="display: block; color: #f8fafc; font-weight: 500; margin-bottom: 8px;">API Key</label>
<input type="text" id="editor-key" value="${prowlarrInstance.api_key || ''}" placeholder="Your API Key" style="width: 100%; padding: 10px; border-radius: 8px; border: 1px solid rgba(148, 163, 184, 0.2); background: rgba(15, 23, 42, 0.5); color: white;">
<p class="setting-help" style="margin-top: 5px; color: #94a3b8; font-size: 0.85rem;">Found in Settings > General in Prowlarr</p>
</div>
</div>
</div>
`;
// Setup auto-connection testing on input change
const urlInput = document.getElementById('editor-url');
const keyInput = document.getElementById('editor-key');
const testConnection = () => {
const url = urlInput.value.trim();
const key = keyInput.value.trim();
const statusBadge = document.getElementById('prowlarr-connection-status');
if (!statusBadge) return;
if (!url || !key) {
statusBadge.className = 'connection-status-badge status-error';
statusBadge.innerHTML = '<i class="fas fa-exclamation-circle"></i> Missing URL or API Key';
return;
}
// Show testing state
statusBadge.className = 'connection-status-badge status-testing';
statusBadge.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Testing Connection...';
// Test connection
fetch('./api/prowlarr/test-connection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ api_url: url, api_key: key })
})
.then(response => response.json())
.then(data => {
if (data.success) {
const version = data.version ? ` (${data.version})` : '';
statusBadge.className = 'connection-status-badge status-success';
statusBadge.innerHTML = `<i class="fas fa-check-circle"></i> Connected${version}`;
} else {
statusBadge.className = 'connection-status-badge status-error';
statusBadge.innerHTML = `<i class="fas fa-times-circle"></i> Connection Failed${data.error ? ': ' + data.error : ''}`;
}
})
.catch(error => {
statusBadge.className = 'connection-status-badge status-error';
statusBadge.innerHTML = '<i class="fas fa-times-circle"></i> Connection Test Failed';
});
};
// Test connection on input blur
if (urlInput) urlInput.addEventListener('blur', testConnection);
if (keyInput) keyInput.addEventListener('blur', testConnection);
// Auto-test if both fields have values
setTimeout(() => {
if (urlInput.value.trim() && keyInput.value.trim()) {
testConnection();
}
}, 100);
}
// Setup button listeners
const saveBtn = document.getElementById('instance-editor-save');
const cancelBtn = document.getElementById('instance-editor-cancel');
const backBtn = document.getElementById('instance-editor-back');
if (saveBtn) {
saveBtn.onclick = () => window.SettingsForms.saveProwlarrFromEditor();
}
if (cancelBtn) {
cancelBtn.onclick = () => window.huntarrUI.switchSection('prowlarr');
}
if (backBtn) {
backBtn.onclick = () => window.huntarrUI.switchSection('prowlarr');
}
// Switch to the editor section
window.huntarrUI.switchSection('instance-editor');
};
window.SettingsForms.saveProwlarrFromEditor = function() {
const settings = window.huntarrUI.originalSettings.prowlarr;
settings.enabled = document.getElementById('editor-enabled').checked;
settings.api_url = document.getElementById('editor-url').value;
settings.api_key = document.getElementById('editor-key').value;
window.SettingsForms.saveAppSettings('prowlarr', settings);
window.huntarrUI.switchSection('prowlarr');
};
})();
@@ -0,0 +1,130 @@
(function() {
window.SettingsForms = window.SettingsForms || {};
window.SettingsForms.generateRadarrForm = function(container, settings = {}) {
if (!settings || typeof settings !== "object") {
settings = {};
}
const wasSuppressionActive = window._appsSuppressChangeDetection;
window._appsSuppressChangeDetection = true;
container.setAttribute("data-app-type", "radarr");
if (!settings.instances || !Array.isArray(settings.instances)) {
settings.instances = [];
}
let radarrSaveButtonHtml = `
<div style="margin-bottom: 20px;">
<button type="button" id="radarr-save-button" disabled style="
background: #6b7280;
color: #9ca3af;
border: 1px solid #4b5563;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<i class="fas fa-save"></i>
Save Changes
</button>
</div>
`;
let instancesHtml = `
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Radarr Instances</h3>
<div class="instance-card-grid" id="radarr-instances-grid">
`;
if (settings.instances && settings.instances.length > 0) {
settings.instances.forEach((instance, index) => {
instancesHtml += window.SettingsForms.renderInstanceCard('radarr', instance, index);
});
}
instancesHtml += `
<div class="add-instance-card" data-app-type="radarr">
<div class="add-icon"><i class="fas fa-plus-circle"></i></div>
<div class="add-text">Add Radarr Instance</div>
</div>
`;
instancesHtml += `
</div>
</div>
`;
let searchSettingsHtml = `
<div class="settings-group">
<h3>Search Settings</h3>
<div class="setting-item">
<label for="radarr_sleep_duration"><a href="https://plexguide.github.io/Huntarr.io/apps/radarr.html#search-settings" class="info-icon" title="Learn more about sleep duration" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Sleep Duration (Minutes):</label>
<input type="number" id="radarr_sleep_duration" name="sleep_duration" min="10" value="${
settings.sleep_duration !== undefined ? Math.round(settings.sleep_duration / 60) : 15
}">
<p class="setting-help">Time in minutes between processing cycles (minimum 10 minutes)</p>
</div>
<div class="setting-item">
<label for="radarr_hourly_cap"><a href="https://plexguide.github.io/Huntarr.io/apps/radarr.html#search-settings" class="info-icon" title="Maximum API requests per hour for this app" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>API Cap - Hourly:</label>
<input type="number" id="radarr_hourly_cap" name="hourly_cap" min="1" max="400" value="${
settings.hourly_cap !== undefined ? settings.hourly_cap : 20
}">
<p class="setting-help">Maximum API requests per hour to prevent being banned by your indexers. Keep lower for safety (20-50 recommended). Max allowed: 400.</p>
</div>
</div>
`;
container.innerHTML = radarrSaveButtonHtml + instancesHtml + searchSettingsHtml;
const grid = container.querySelector('#radarr-instances-grid');
if (grid) {
grid.addEventListener('click', (e) => {
const editBtn = e.target.closest('.btn-card.edit');
const deleteBtn = e.target.closest('.btn-card.delete');
const addCard = e.target.closest('.add-instance-card');
if (editBtn) {
const appType = editBtn.dataset.appType;
const index = parseInt(editBtn.dataset.instanceIndex);
window.SettingsForms.openInstanceModal(appType, index);
} else if (deleteBtn) {
const appType = deleteBtn.dataset.appType;
const index = parseInt(deleteBtn.dataset.instanceIndex);
window.SettingsForms.deleteInstance(appType, index);
} else if (addCard) {
const appType = addCard.dataset.appType;
window.SettingsForms.openInstanceModal(appType);
}
});
}
if (window.SettingsForms.setupAppManualSave) {
window.SettingsForms.setupAppManualSave(container, "radarr", settings);
}
// Test instance connections after rendering
setTimeout(() => {
if (window.SettingsForms.testAllInstanceConnections) {
window.SettingsForms.testAllInstanceConnections("radarr");
}
}, 100);
setTimeout(() => {
window._appsSuppressChangeDetection = wasSuppressionActive;
}, 100);
};
})();
@@ -0,0 +1,130 @@
(function() {
window.SettingsForms = window.SettingsForms || {};
window.SettingsForms.generateReadarrForm = function(container, settings = {}) {
if (!settings || typeof settings !== "object") {
settings = {};
}
const wasSuppressionActive = window._appsSuppressChangeDetection;
window._appsSuppressChangeDetection = true;
container.setAttribute("data-app-type", "readarr");
if (!settings.instances || !Array.isArray(settings.instances)) {
settings.instances = [];
}
let readarrSaveButtonHtml = `
<div style="margin-bottom: 20px;">
<button type="button" id="readarr-save-button" disabled style="
background: #6b7280;
color: #9ca3af;
border: 1px solid #4b5563;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<i class="fas fa-save"></i>
Save Changes
</button>
</div>
`;
let instancesHtml = `
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Readarr Instances</h3>
<div class="instance-card-grid" id="readarr-instances-grid">
`;
if (settings.instances && settings.instances.length > 0) {
settings.instances.forEach((instance, index) => {
instancesHtml += window.SettingsForms.renderInstanceCard('readarr', instance, index);
});
}
instancesHtml += `
<div class="add-instance-card" data-app-type="readarr">
<div class="add-icon"><i class="fas fa-plus-circle"></i></div>
<div class="add-text">Add Readarr Instance</div>
</div>
`;
instancesHtml += `
</div>
</div>
`;
let searchSettingsHtml = `
<div class="settings-group">
<h3>Search Settings</h3>
<div class="setting-item">
<label for="readarr_sleep_duration"><a href="https://plexguide.github.io/Huntarr.io/apps/readarr.html#search-settings" class="info-icon" title="Learn more about sleep duration" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Sleep Duration (Minutes):</label>
<input type="number" id="readarr_sleep_duration" name="sleep_duration" min="10" value="${
settings.sleep_duration !== undefined ? Math.round(settings.sleep_duration / 60) : 15
}">
<p class="setting-help">Time in minutes between processing cycles (minimum 10 minutes)</p>
</div>
<div class="setting-item">
<label for="readarr_hourly_cap"><a href="https://plexguide.github.io/Huntarr.io/apps/readarr.html#search-settings" class="info-icon" title="Maximum API requests per hour for this app (20 is safe)" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>API Cap - Hourly:</label>
<input type="number" id="readarr_hourly_cap" name="hourly_cap" min="1" max="400" value="${
settings.hourly_cap !== undefined ? settings.hourly_cap : 20
}">
<p class="setting-help">Maximum API requests per hour to prevent being banned by your indexers. Keep lower for safety (20-50 recommended). Max allowed: 400.</p>
</div>
</div>
`;
container.innerHTML = readarrSaveButtonHtml + instancesHtml + searchSettingsHtml;
const grid = container.querySelector('#readarr-instances-grid');
if (grid) {
grid.addEventListener('click', (e) => {
const editBtn = e.target.closest('.btn-card.edit');
const deleteBtn = e.target.closest('.btn-card.delete');
const addCard = e.target.closest('.add-instance-card');
if (editBtn) {
const appType = editBtn.dataset.appType;
const index = parseInt(editBtn.dataset.instanceIndex);
window.SettingsForms.openInstanceModal(appType, index);
} else if (deleteBtn) {
const appType = deleteBtn.dataset.appType;
const index = parseInt(deleteBtn.dataset.instanceIndex);
window.SettingsForms.deleteInstance(appType, index);
} else if (addCard) {
const appType = addCard.dataset.appType;
window.SettingsForms.openInstanceModal(appType);
}
});
}
if (window.SettingsForms.setupAppManualSave) {
window.SettingsForms.setupAppManualSave(container, "readarr", settings);
}
// Test instance connections after rendering
setTimeout(() => {
if (window.SettingsForms.testAllInstanceConnections) {
window.SettingsForms.testAllInstanceConnections("readarr");
}
}, 100);
setTimeout(() => {
window._appsSuppressChangeDetection = wasSuppressionActive;
}, 100);
};
})();
@@ -0,0 +1,429 @@
/**
* Settings Module
* Handles loading, saving, and auto-saving of application settings
*/
window.HuntarrSettings = {
settingsCurrentlySaving: false,
loadAllSettings: function() {
if (!window.huntarrUI) return;
window.huntarrUI.updateSaveResetButtonState(false);
window.huntarrUI.settingsChanged = false;
HuntarrUtils.fetchWithTimeout('./api/settings')
.then(response => response.json())
.then(data => {
console.log('[HuntarrSettings] Loaded settings:', data);
window.huntarrUI.originalSettings = data;
try {
localStorage.setItem('huntarr-settings-cache', JSON.stringify(data));
} catch (e) {
console.warn('[HuntarrSettings] Failed to cache settings:', e);
}
const apps = ['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros', 'swaparr', 'prowlarr', 'general'];
apps.forEach(app => {
if (data[app]) {
if (app === 'swaparr') window.swaparrSettings = data.swaparr;
this.populateSettingsForm(app, data[app]);
}
});
if (typeof SettingsForms !== 'undefined') {
if (typeof SettingsForms.updateDurationDisplay === 'function') SettingsForms.updateDurationDisplay();
if (typeof SettingsForms.updateAllSwaparrInstanceVisibility === 'function') SettingsForms.updateAllSwaparrInstanceVisibility();
}
if (window.huntarrUI.loadStatefulInfo) window.huntarrUI.loadStatefulInfo();
})
.catch(error => {
console.error('[HuntarrSettings] Error loading settings:', error);
if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification('Error loading settings', 'error');
});
},
populateSettingsForm: function(app, appSettings) {
const form = document.getElementById(`${app}Settings`);
if (!form) return;
if (typeof SettingsForms !== 'undefined') {
const formFunction = SettingsForms[`generate${app.charAt(0).toUpperCase()}${app.slice(1)}Form`];
if (typeof formFunction === 'function') {
formFunction(form, appSettings);
if (typeof SettingsForms.updateDurationDisplay === 'function') {
try { SettingsForms.updateDurationDisplay(); } catch (e) {}
}
if (app === 'swaparr' && typeof SettingsForms.updateAllSwaparrInstanceVisibility === 'function') {
try { SettingsForms.updateAllSwaparrInstanceVisibility(); } catch (e) {}
}
}
}
},
saveSettings: function() {
if (!window.huntarrUI) return;
const app = window.huntarrUI.currentSettingsTab;
window.huntarrUI.settingsChanged = false;
window.huntarrUI.updateSaveResetButtonState(false);
let settings = this.getFormSettings(app);
if (!settings) {
if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification('Error collecting settings', 'error');
return;
}
const isAuthModeChanged = app === 'general' &&
window.huntarrUI.originalSettings?.general?.auth_mode !== settings.auth_mode;
const endpoint = app === 'general' ? './api/settings/general' : `./api/settings/${app}`;
HuntarrUtils.fetchWithTimeout(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
})
.then(response => {
if (!response.ok) return response.json().then(err => { throw new Error(err.error || `HTTP ${response.status}`); });
return response.json();
})
.then(savedConfig => {
if (isAuthModeChanged) {
if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification('Settings saved. Reloading...', 'success');
setTimeout(() => window.location.href = './', 1500);
return;
}
if (typeof savedConfig === 'object' && savedConfig !== null) {
window.huntarrUI.originalSettings = JSON.parse(JSON.stringify(savedConfig));
if (app === 'swaparr') {
const swaparrData = savedConfig.swaparr || (savedConfig && !savedConfig.sonarr ? savedConfig : null);
if (swaparrData) window.swaparrSettings = swaparrData;
}
if (app === 'general' && 'low_usage_mode' in settings) {
if (window.huntarrUI.applyLowUsageMode) window.huntarrUI.applyLowUsageMode(settings.low_usage_mode);
}
}
const currentAppSettings = window.huntarrUI.originalSettings[app] || {};
if (app === 'sonarr' && !currentAppSettings.instances && settings.instances) {
currentAppSettings.instances = settings.instances;
}
this.populateSettingsForm(app, currentAppSettings);
if (window.huntarrUI.checkAppConnection) window.huntarrUI.checkAppConnection(app);
if (window.huntarrUI.updateHomeConnectionStatus) window.huntarrUI.updateHomeConnectionStatus();
if (app === 'general') {
if (settings.stateful_management_hours && document.getElementById('stateful_management_hours')) {
if (window.huntarrUI.updateStatefulExpirationOnUI) window.huntarrUI.updateStatefulExpirationOnUI();
} else {
if (window.huntarrUI.loadStatefulInfo) window.huntarrUI.loadStatefulInfo();
}
window.dispatchEvent(new CustomEvent('settings-saved', { detail: { appType: app, settings: settings } }));
}
})
.catch(error => {
console.error('[HuntarrSettings] Error saving settings:', error);
if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification(`Error: ${error.message}`, 'error');
window.huntarrUI.settingsChanged = true;
window.huntarrUI.updateSaveResetButtonState(true);
});
},
setupSettingsAutoSave: function() {
const settingsContainer = document.getElementById('settingsSection');
if (!settingsContainer) return;
settingsContainer.addEventListener('input', (event) => {
if (event.target.matches('input, textarea')) this.triggerSettingsAutoSave();
});
settingsContainer.addEventListener('change', (event) => {
if (event.target.matches('input, select, textarea')) {
if (event.target.id === 'low_usage_mode' && window.huntarrUI.applyLowUsageMode) {
window.huntarrUI.applyLowUsageMode(event.target.checked);
} else if (event.target.id === 'timezone' && window.huntarrUI.applyTimezoneChange) {
window.huntarrUI.applyTimezoneChange(event.target.value);
} else if (event.target.id === 'auth_mode' && window.huntarrUI.applyAuthModeChange) {
window.huntarrUI.applyAuthModeChange(event.target.value);
} else if (event.target.id === 'check_for_updates' && window.huntarrUI.applyUpdateCheckingChange) {
window.huntarrUI.applyUpdateCheckingChange(event.target.checked);
} else if (event.target.id === 'show_trending' && window.huntarrUI.applyShowTrendingChange) {
window.huntarrUI.applyShowTrendingChange(event.target.checked);
}
this.triggerSettingsAutoSave();
}
});
},
triggerSettingsAutoSave: function() {
if (this.settingsCurrentlySaving || !window.huntarrUI) return;
const app = window.huntarrUI.currentSettingsTab;
const isGeneralSettings = window.huntarrUI.currentSection === 'settings' && !app;
if (!app && !isGeneralSettings) return;
if (isGeneralSettings) {
this.autoSaveGeneralSettings(true).catch(e => console.error(e));
} else {
this.autoSaveSettings(app);
}
},
autoSaveSettings: function(app) {
if (this.settingsCurrentlySaving || !window.huntarrUI) return;
this.settingsCurrentlySaving = true;
const originalShowNotification = window.huntarrUI.showNotification;
window.huntarrUI.showNotification = (message, type) => {
if (type === 'error' && window.HuntarrNotifications) window.HuntarrNotifications.showNotification(message, type);
};
this.saveSettings();
setTimeout(() => {
if (window.huntarrUI) window.huntarrUI.showNotification = originalShowNotification;
this.settingsCurrentlySaving = false;
}, 1000);
},
getFormSettings: function(app) {
const settings = {};
let form = document.getElementById(`${app}Settings`);
if (app === 'swaparr') {
form = document.getElementById('swaparrContainer') ||
document.querySelector('.swaparr-container') ||
document.querySelector('[data-app-type="swaparr"]');
}
if (!form) return null;
if (app === 'swaparr') {
const inputs = form.querySelectorAll('input, select, textarea');
inputs.forEach(input => {
let key = input.id.startsWith('swaparr_') ? input.id.substring(8) : input.id;
let value = input.type === 'checkbox' ? input.checked : (input.type === 'number' ? (input.value === '' ? null : parseInt(input.value, 10)) : input.value.trim());
if (key === 'malicious_detection') key = 'malicious_file_detection';
if (key && !key.includes('_tags') && !key.includes('_input')) {
if (key === 'sleep_duration' && input.type === 'number') settings[key] = value * 60;
else settings[key] = value;
}
});
const tagContainers = [
{ id: 'swaparr_malicious_extensions_tags', key: 'malicious_extensions' },
{ id: 'swaparr_suspicious_patterns_tags', key: 'suspicious_patterns' },
{ id: 'swaparr_quality_patterns_tags', key: 'blocked_quality_patterns' }
];
tagContainers.forEach(({ id, key }) => {
const container = document.getElementById(id);
settings[key] = container ? Array.from(container.querySelectorAll('.tag-text')).map(el => el.textContent) : [];
});
return settings;
}
if (app === 'general') {
const inputs = form.querySelectorAll('input, select, textarea');
const notificationsContainer = document.querySelector('#notificationsContainer');
const notificationInputs = notificationsContainer ? notificationsContainer.querySelectorAll('input, select, textarea') : [];
const allInputs = [...inputs, ...notificationInputs];
allInputs.forEach(input => {
let key = input.id;
let value = input.type === 'checkbox' ? input.checked : (input.type === 'number' ? (input.value === '' ? null : parseInt(input.value, 10)) : input.value.trim());
if (key === 'apprise_urls') {
settings.apprise_urls = value.split('\n').map(url => url.trim()).filter(url => url.length > 0);
} else if (key && !key.includes('_instance_')) {
settings[key] = value;
}
});
return settings;
}
const instanceItems = form.querySelectorAll('.instance-item');
settings.instances = [];
if (instanceItems.length > 0) {
instanceItems.forEach((item, index) => {
const id = item.dataset.instanceId;
const url = form.querySelector(`#${app}-url-${id}`);
const key = form.querySelector(`#${app}-key-${id}`);
const name = form.querySelector(`#${app}-name-${id}`);
const enabled = form.querySelector(`#${app}-enabled-${id}`);
if (url && key) {
settings.instances.push({
name: name?.value.trim() || `Instance ${index + 1}`,
api_url: window.HuntarrHelpers ? window.HuntarrHelpers.cleanUrlString(url.value) : url.value.trim(),
api_key: key.value.trim(),
enabled: enabled ? enabled.checked : true
});
}
});
} else {
const url = form.querySelector(`#${app}_api_url`);
const key = form.querySelector(`#${app}_api_key`);
const name = form.querySelector(`#${app}_instance_name`);
const enabled = form.querySelector(`#${app}_enabled`);
if (url?.value.trim() && key?.value.trim()) {
settings.instances.push({
name: name?.value.trim() || `${app} Instance 1`,
api_url: window.HuntarrHelpers ? window.HuntarrHelpers.cleanUrlString(url.value) : url.value.trim(),
api_key: key.value.trim(),
enabled: enabled ? enabled.checked : true
});
}
}
const allInputs = form.querySelectorAll('input, select');
allInputs.forEach(input => {
if (input.type === 'button' || input.id.includes('-url-') || input.id.includes('-key-') || input.id.includes('-name-') || input.id.includes('-enabled-') || input.id.includes('_api_url') || input.id.includes('_api_key') || input.id.includes('_instance_name') || input.id.includes('_enabled')) return;
let key = input.id.startsWith(`${app}_`) ? input.id.substring(app.length + 1) : input.id;
if (!key || /^\d+$/.test(key)) return;
settings[key] = input.type === 'checkbox' ? input.checked : (input.type === 'number' ? (input.value === '' ? null : parseInt(input.value, 10)) : input.value.trim());
});
return settings;
},
testNotification: function() {
const status = document.getElementById('testNotificationStatus');
const btn = document.getElementById('testNotificationBtn');
if (!status || !btn || !window.huntarrUI) return;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Auto-saving...';
status.innerHTML = '<span style="color: #fbbf24;">Auto-saving settings before testing...</span>';
this.autoSaveGeneralSettings().then(() => {
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
status.innerHTML = '<span style="color: #fbbf24;">Sending test notification...</span>';
return HuntarrUtils.fetchWithTimeout('./api/test-notification', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
})
.then(r => r.json())
.then(data => {
if (data.success) {
status.innerHTML = '<span style="color: #10b981;">✓ Test notification sent!</span>';
if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification('Test notification sent!', 'success');
} else {
status.innerHTML = '<span style="color: #ef4444;">✗ Failed to send</span>';
if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification(data.error || 'Failed to send', 'error');
}
})
.catch(e => {
status.innerHTML = '<span style="color: #ef4444;">✗ Error</span>';
if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification(e.message, 'error');
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-bell"></i> Test Notification';
setTimeout(() => { if (status) status.innerHTML = ''; }, 5000);
});
},
autoSaveGeneralSettings: function(silent = false) {
if (this.settingsCurrentlySaving) return Promise.resolve();
this.settingsCurrentlySaving = true;
const settings = this.getFormSettings('general');
return HuntarrUtils.fetchWithTimeout('./api/settings/general', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
})
.then(r => r.json())
.then(data => {
if (data.success && !silent && window.HuntarrNotifications) window.HuntarrNotifications.showNotification('General settings auto-saved', 'success');
this.settingsCurrentlySaving = false;
return data;
})
.catch(e => {
this.settingsCurrentlySaving = false;
throw e;
});
},
autoSaveSwaparrSettings: function(silent = false) {
if (this.settingsCurrentlySaving) return Promise.resolve();
this.settingsCurrentlySaving = true;
const settings = this.getFormSettings('swaparr');
return HuntarrUtils.fetchWithTimeout('./api/settings/swaparr', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
})
.then(r => r.json())
.then(data => {
if (data.success && !silent && window.HuntarrNotifications) window.HuntarrNotifications.showNotification('Swaparr settings auto-saved', 'success');
this.settingsCurrentlySaving = false;
return data;
})
.catch(e => {
this.settingsCurrentlySaving = false;
throw e;
});
},
applyTimezoneChange: function(timezone) {
console.log(`[HuntarrSettings] Applying timezone change to: ${timezone}`);
fetch('./api/settings/apply-timezone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ timezone: timezone })
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('[HuntarrSettings] Timezone applied successfully');
if (window.huntarrUI && window.huntarrUI.refreshTimeDisplays) window.huntarrUI.refreshTimeDisplays();
} else {
console.error('[HuntarrSettings] Failed to apply timezone:', data.error);
if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification(`Failed to apply timezone: ${data.error}`, 'error');
}
})
.catch(error => {
console.error('[HuntarrSettings] Error applying timezone:', error);
if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification(`Error applying timezone: ${error.message}`, 'error');
});
},
applyAuthModeChange: function(authMode) {
console.log(`[HuntarrSettings] Authentication mode changed to: ${authMode}`);
},
applyUpdateCheckingChange: function(enabled) {
console.log(`[HuntarrSettings] Update checking ${enabled ? 'enabled' : 'disabled'}`);
},
applyShowTrendingChange: function(enabled) {
console.log(`[HuntarrSettings] Show Trending ${enabled ? 'enabled' : 'disabled'}`);
if (window.HomeRequestarr) {
window.HomeRequestarr.showTrending = enabled;
if (typeof window.HomeRequestarr.applyTrendingVisibility === 'function') {
window.HomeRequestarr.applyTrendingVisibility();
}
}
}
};
@@ -0,0 +1,130 @@
(function() {
window.SettingsForms = window.SettingsForms || {};
window.SettingsForms.generateSonarrForm = function(container, settings = {}) {
if (!settings || typeof settings !== "object") {
settings = {};
}
const wasSuppressionActive = window._appsSuppressChangeDetection;
window._appsSuppressChangeDetection = true;
container.setAttribute("data-app-type", "sonarr");
if (!settings.instances || !Array.isArray(settings.instances)) {
settings.instances = [];
}
let sonarrSaveButtonHtml = `
<div style="margin-bottom: 20px;">
<button type="button" id="sonarr-save-button" disabled style="
background: #6b7280;
color: #9ca3af;
border: 1px solid #4b5563;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<i class="fas fa-save"></i>
Save Changes
</button>
</div>
`;
let instancesHtml = `
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Sonarr Instances</h3>
<div class="instance-card-grid" id="sonarr-instances-grid">
`;
if (settings.instances && settings.instances.length > 0) {
settings.instances.forEach((instance, index) => {
instancesHtml += window.SettingsForms.renderInstanceCard('sonarr', instance, index);
});
}
instancesHtml += `
<div class="add-instance-card" data-app-type="sonarr">
<div class="add-icon"><i class="fas fa-plus-circle"></i></div>
<div class="add-text">Add Sonarr Instance</div>
</div>
`;
instancesHtml += `
</div>
</div>
`;
let searchSettingsHtml = `
<div class="settings-group">
<h3>Global Settings</h3>
<div class="setting-item">
<label for="sonarr_sleep_duration"><a href="https://plexguide.github.io/Huntarr.io/apps/sonarr.html#search-settings" class="info-icon" title="Learn more about sleep duration" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Sleep Duration (Minutes):</label>
<input type="number" id="sonarr_sleep_duration" name="sleep_duration" min="10" value="${
settings.sleep_duration !== undefined ? Math.round(settings.sleep_duration / 60) : 15
}">
<p class="setting-help">Time in minutes between processing cycles (minimum 10 minutes)</p>
</div>
<div class="setting-item">
<label for="sonarr_hourly_cap"><a href="https://plexguide.github.io/Huntarr.io/apps/sonarr.html#search-settings" class="info-icon" title="Maximum API requests per hour for this app (20 is safe)" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>API Cap - Hourly:</label>
<input type="number" id="sonarr_hourly_cap" name="hourly_cap" min="1" max="400" value="${
settings.hourly_cap !== undefined ? settings.hourly_cap : 20
}">
<p class="setting-help">Maximum API requests per hour to prevent being banned by your indexers. Keep lower for safety (20-50 recommended). Max allowed: 400.</p>
</div>
</div>
`;
container.innerHTML = sonarrSaveButtonHtml + instancesHtml + searchSettingsHtml;
const grid = container.querySelector('#sonarr-instances-grid');
if (grid) {
grid.addEventListener('click', (e) => {
const editBtn = e.target.closest('.btn-card.edit');
const deleteBtn = e.target.closest('.btn-card.delete');
const addCard = e.target.closest('.add-instance-card');
if (editBtn) {
const appType = editBtn.dataset.appType;
const index = parseInt(editBtn.dataset.instanceIndex);
window.SettingsForms.openInstanceModal(appType, index);
} else if (deleteBtn) {
const appType = deleteBtn.dataset.appType;
const index = parseInt(deleteBtn.dataset.instanceIndex);
window.SettingsForms.deleteInstance(appType, index);
} else if (addCard) {
const appType = addCard.dataset.appType;
window.SettingsForms.openInstanceModal(appType);
}
});
}
if (window.SettingsForms.setupAppManualSave) {
window.SettingsForms.setupAppManualSave(container, "sonarr", settings);
}
// Test instance connections after rendering
setTimeout(() => {
if (window.SettingsForms.testAllInstanceConnections) {
window.SettingsForms.testAllInstanceConnections("sonarr");
}
}, 100);
setTimeout(() => {
window._appsSuppressChangeDetection = wasSuppressionActive;
}, 100);
};
})();
@@ -0,0 +1,751 @@
(function() {
window.SettingsForms = window.SettingsForms || {};
window.SettingsForms.generateSwaparrForm = function(container, settings = {}) {
if (!settings || typeof settings !== "object") {
settings = {};
}
container.setAttribute("data-app-type", "swaparr");
let html = `
<div style="margin-bottom: 25px;">
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
<button type="button" id="swaparr-save-button" disabled style="
background: #6b7280;
color: #9ca3af;
border: 1px solid #4b5563;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<i class="fas fa-save"></i>
Save Changes
</button>
<div style="margin-left: auto; display: flex; gap: 10px;">
<a href="https://github.com/plexguide/swaparr" target="_blank" rel="noopener" style="
background: linear-gradient(135deg, #24292e 0%, #161b22 100%);
color: #f0f6fc;
border: 1px solid #30363d;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
<i class="fab fa-github" style="font-size: 16px;"></i>
View on GitHub
</a>
<a href="https://github.com/plexguide/swaparr/stargazers" target="_blank" rel="noopener" style="
background: linear-gradient(135deg, #f1c40f 0%, #f39c12 100%);
color: #fff;
border: 1px solid #d35400;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
<i class="fas fa-star" style="margin-right: 4px;"></i>
<span id="swaparr-stars-count">Loading...</span>
</a>
</div>
</div>
<!-- Advanced Options Notice -->
<div style="
background: linear-gradient(135deg, #164e63 0%, #0e7490 50%, #0891b2 100%);
border: 1px solid #22d3ee;
border-radius: 6px;
padding: 10px;
margin: 10px 0 15px 0;
box-shadow: 0 2px 8px rgba(34, 211, 238, 0.1);
">
<p style="color: #e0f7fa; margin: 0; font-size: 0.8em; line-height: 1.4;">
<i class="fas fa-rocket" style="margin-right: 6px; color: #22d3ee;"></i>
<strong>Need Advanced Options?</strong> For enhanced control and features, we recommend
<a href="https://github.com/flmorg/cleanuperr" target="_blank" rel="noopener" style="color: #fbbf24; text-decoration: none; font-weight: 600;">
<strong>Cleanuperr</strong>
</a> which offers more comprehensive management capabilities.
</p>
</div>
</div>
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Swaparr Configuration</h3>
<p class="setting-help" style="margin-bottom: 20px; color: #9ca3af;">
Swaparr monitors your *arr applications' download queues and removes stalled downloads automatically.
</p>
<div class="setting-item">
<label for="swaparr_enabled">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#enable-swaparr" class="info-icon" title="Enable or disable Swaparr" 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 === 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">Enable automatic removal of stalled downloads</p>
</div>
<div class="setting-item">
<label for="swaparr_max_strikes">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#max-strikes" class="info-icon" title="Number of strikes before removal" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Max 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 a download gets before being removed (default: 3)</p>
</div>
<div class="setting-item">
<label for="swaparr_max_download_time">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#max-download-time" class="info-icon" title="Maximum time before considering download stalled" 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"
}" placeholder="e.g., 2h, 120m, 7200s">
<p class="setting-help">Maximum time before considering a download stalled (examples: 2h, 120m, 7200s)</p>
</div>
<div class="setting-item">
<label for="swaparr_ignore_above_size">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#ignore-above-size" class="info-icon" title="Ignore downloads larger than this size" 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"
}" placeholder="e.g., 25GB, 10GB, 5000MB">
<p class="setting-help">Ignore downloads larger than this size (examples: 25GB, 10GB, 5000MB)</p>
</div>
<div class="setting-item">
<label for="swaparr_remove_from_client">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#remove-from-client" class="info-icon" title="Remove downloads from download client" 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">Also remove downloads from the download client (recommended: enabled)</p>
</div>
<div class="setting-item">
<label for="swaparr_research_removed">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#research-removed" class="info-icon" title="Automatically blocklist and re-search removed downloads" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Re-Search Removed Download:
</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="swaparr_research_removed" ${
settings.research_removed === 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">When a download is removed, blocklist it in the *arr app and automatically search for alternatives (retry once)</p>
</div>
<div class="setting-item">
<label for="swaparr_failed_import_detection">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#failed-import-detection" class="info-icon" title="Automatically handle failed imports" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Handle Failed Imports:
</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="swaparr_failed_import_detection" ${
settings.failed_import_detection === 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">Automatically detect failed imports, blocklist them, and search for alternatives</p>
</div>
<div class="setting-item">
<label for="swaparr_dry_run">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#dry-run-mode" class="info-icon" title="Test mode - no actual removals" 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">Test mode - logs what would be removed without actually removing anything</p>
</div>
<div class="setting-item">
<label for="swaparr_sleep_duration">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#sleep-duration" class="info-icon" title="Time between Swaparr cycles" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Sleep Duration (Minutes):
</label>
<div class="input-group" style="display: flex; align-items: center; gap: 10px;">
<input type="number" id="swaparr_sleep_duration" value="${
settings.sleep_duration
? Math.round(settings.sleep_duration / 60)
: 15
}" min="10" max="1440" style="width: 120px;">
<span style="color: #9ca3af; font-size: 14px;">minutes</span>
</div>
<p class="setting-help">Time to wait between Swaparr processing cycles (minimum 10 minutes, default: 15 minutes)</p>
</div>
</div>
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Security Features</h3>
<p class="setting-help" style="margin-bottom: 20px; color: #9ca3af;">
Advanced security features to protect your system from malicious downloads and suspicious content by analyzing download names and titles. Detection is based on filename patterns, not file contents.
</p>
<div class="setting-item">
<label for="swaparr_malicious_detection">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#malicious-file-detection" class="info-icon" title="Enable malicious file detection" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Malicious File Detection:
</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="swaparr_malicious_detection" ${
settings.malicious_file_detection === 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">Automatically detect and immediately remove downloads with malicious file types</p>
</div>
<div class="setting-item">
<label for="swaparr_malicious_extensions_input">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#malicious-extensions" class="info-icon" title="File extensions to consider malicious" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Malicious File Extensions:
</label>
<div class="tag-input-container">
<div class="tag-list" id="swaparr_malicious_extensions_tags"></div>
<div class="tag-input-wrapper">
<input type="text" id="swaparr_malicious_extensions_input" placeholder="Type extension and press Enter (e.g. .lnk)" class="tag-input">
<button type="button" class="tag-add-btn" onclick="window.SettingsForms.addExtensionTag()">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<p class="setting-help">File extensions to block. Type extension and press Enter or click +. Examples: .lnk, .exe, .bat, .zipx</p>
</div>
<div class="setting-item">
<label for="swaparr_suspicious_patterns_input">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#suspicious-patterns" class="info-icon" title="Suspicious filename patterns" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Suspicious Patterns:
</label>
<div class="tag-input-container">
<div class="tag-list" id="swaparr_suspicious_patterns_tags"></div>
<div class="tag-input-wrapper">
<input type="text" id="swaparr_suspicious_patterns_input" placeholder="Type pattern and press Enter (e.g. keygen)" class="tag-input">
<button type="button" class="tag-add-btn" onclick="window.SettingsForms.addPatternTag()">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<p class="setting-help">Filename patterns to block. Type pattern and press Enter or click +. Examples: password.txt, keygen, crack</p>
</div>
</div>
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Age-Based Cleanup</h3>
<p class="setting-help" style="margin-bottom: 20px; color: #9ca3af;">
Automatically remove downloads that have been stuck for too long, regardless of strike count.
</p>
<div class="setting-item">
<label for="swaparr_age_based_removal">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#age-based-removal" class="info-icon" title="Enable age-based removal" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Enable Age-Based Removal:
</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="swaparr_age_based_removal" ${
settings.age_based_removal === 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">Remove downloads that have been stuck longer than the specified age limit</p>
</div>
<div class="setting-item">
<label for="swaparr_max_age_days">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#max-age-days" class="info-icon" title="Maximum age before removal" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Maximum Age (Days):
</label>
<input type="number" id="swaparr_max_age_days" min="1" max="30" value="${
settings.max_age_days || 7
}">
<p class="setting-help">Remove downloads older than this many days (default: 7 days)</p>
</div>
</div>
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Quality-Based Filtering</h3>
<p class="setting-help" style="margin-bottom: 20px; color: #9ca3af;">
Automatically remove downloads with poor or undesirable quality indicators in their names.
</p>
<div class="setting-item">
<label for="swaparr_quality_based_removal">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#quality-based-removal" class="info-icon" title="Enable quality-based filtering" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Enable Quality-Based Filtering:
</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="swaparr_quality_based_removal" ${
settings.quality_based_removal === 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">Automatically remove downloads with blocked quality patterns in their names</p>
</div>
<div class="setting-item">
<label for="swaparr_quality_patterns_input">
<a href="https://plexguide.github.io/Huntarr.io/apps/swaparr.html#blocked-quality-patterns" class="info-icon" title="Quality patterns to block" target="_blank" rel="noopener">
<i class="fas fa-info-circle"></i>
</a>
Blocked Quality Patterns:
</label>
<div class="tag-input-container">
<div class="tag-list" id="swaparr_quality_patterns_tags"></div>
<div class="tag-input-wrapper">
<input type="text" id="swaparr_quality_patterns_input" placeholder="Type quality pattern and press Enter (e.g. cam)" class="tag-input">
<button type="button" class="tag-add-btn" onclick="window.SettingsForms.addQualityTag()">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<p class="setting-help">Quality patterns to block. Type pattern and press Enter or click +. Examples: cam, ts, hdcam, workprint</p>
</div>
</div>
`;
container.innerHTML = html;
window.SettingsForms.loadSwaparrStarCount();
window.SettingsForms.initializeTagSystem(settings);
const swaparrEnabledToggle = container.querySelector("#swaparr_enabled");
if (swaparrEnabledToggle) {
swaparrEnabledToggle.addEventListener("change", () => {
if (window.huntarrUI && window.huntarrUI.originalSettings && window.huntarrUI.originalSettings.swaparr) {
window.huntarrUI.originalSettings.swaparr.enabled = swaparrEnabledToggle.checked;
}
try {
const cachedSettings = localStorage.getItem("huntarr-settings-cache");
if (cachedSettings) {
const settings = JSON.parse(cachedSettings);
if (!settings.swaparr) settings.swaparr = {};
settings.swaparr.enabled = swaparrEnabledToggle.checked;
localStorage.setItem("huntarr-settings-cache", JSON.stringify(settings));
}
} catch (e) {
console.warn("[SettingsForms] Failed to update cached settings:", e);
}
if (window.SettingsForms.updateSwaparrFieldsDisabledState) {
window.SettingsForms.updateSwaparrFieldsDisabledState();
}
});
setTimeout(() => {
if (window.SettingsForms.updateSwaparrFieldsDisabledState) {
window.SettingsForms.updateSwaparrFieldsDisabledState();
}
}, 100);
}
if (window.SettingsForms.setupAppManualSave) {
window.SettingsForms.setupAppManualSave(container, "swaparr", settings);
}
};
window.SettingsForms.loadSwaparrStarCount = function() {
const starsElement = document.getElementById("swaparr-stars-count");
if (!starsElement) return;
const cachedData = localStorage.getItem("swaparr-github-stars");
if (cachedData) {
try {
const parsed = JSON.parse(cachedData);
if (parsed.stars !== undefined) {
starsElement.textContent = parsed.stars.toLocaleString();
const cacheAge = Date.now() - (parsed.timestamp || 0);
if (cacheAge < 3600000) {
return;
}
}
} catch (e) {
console.warn("Invalid cached Swaparr star data, will fetch fresh");
localStorage.removeItem("swaparr-github-stars");
}
}
starsElement.textContent = "Loading...";
const apiUrl = "https://api.github.com/repos/ThijmenGThN/swaparr";
HuntarrUtils.fetchWithTimeout(apiUrl)
.then((response) => {
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
return response.json();
})
.then((data) => {
if (data && data.stargazers_count !== undefined) {
const formattedStars = data.stargazers_count.toLocaleString();
starsElement.textContent = formattedStars;
const cacheData = {
stars: data.stargazers_count,
timestamp: Date.now(),
};
localStorage.setItem("swaparr-github-stars", JSON.stringify(cacheData));
}
})
.catch((error) => {
console.warn("Failed to fetch Swaparr stars:", error);
if (starsElement.textContent === "Loading...") {
starsElement.textContent = "Unknown";
}
});
};
window.SettingsForms.initializeTagSystem = function(settings) {
const defaultExtensions = [".lnk", ".exe", ".bat", ".cmd", ".scr", ".pif", ".com", ".zipx", ".jar", ".vbs", ".js", ".jse", ".wsf", ".wsh"];
const extensions = settings.malicious_extensions || defaultExtensions;
window.SettingsForms.loadTags("swaparr_malicious_extensions_tags", extensions);
const defaultPatterns = ["password.txt", "readme.txt", "install.exe", "setup.exe", "keygen", "crack", "patch.exe", "activator"];
const patterns = settings.suspicious_patterns || defaultPatterns;
window.SettingsForms.loadTags("swaparr_suspicious_patterns_tags", patterns);
const defaultQualityPatterns = ["cam", "camrip", "hdcam", "ts", "telesync", "tc", "telecine", "r6", "dvdscr", "dvdscreener", "workprint", "wp"];
const qualityPatterns = settings.blocked_quality_patterns || defaultQualityPatterns;
window.SettingsForms.loadTags("swaparr_quality_patterns_tags", qualityPatterns);
const extensionInput = document.getElementById("swaparr_malicious_extensions_input");
const patternInput = document.getElementById("swaparr_suspicious_patterns_input");
const qualityInput = document.getElementById("swaparr_quality_patterns_input");
if (extensionInput) {
extensionInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
e.preventDefault();
window.SettingsForms.addExtensionTag();
}
});
}
if (patternInput) {
patternInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
e.preventDefault();
window.SettingsForms.addPatternTag();
}
});
}
if (qualityInput) {
qualityInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
e.preventDefault();
window.SettingsForms.addQualityTag();
}
});
}
// Expose helper functions globally if needed by inline onclicks, though we prefer window.SettingsForms
// The inline onclicks in HTML above use window.SettingsForms.add*Tag()
};
window.SettingsForms.loadTags = function(containerId, tags) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = "";
tags.forEach((tag) => {
window.SettingsForms.createTagElement(container, tag);
});
};
window.SettingsForms.createTagElement = function(container, text) {
const tagDiv = document.createElement("div");
tagDiv.className = "tag-item";
tagDiv.innerHTML = `
<span class="tag-text">${text}</span>
<button type="button" class="tag-remove" onclick="this.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
`;
container.appendChild(tagDiv);
};
window.SettingsForms.addExtensionTag = function() {
const input = document.getElementById("swaparr_malicious_extensions_input");
const container = document.getElementById("swaparr_malicious_extensions_tags");
if (!input || !container) return;
let value = input.value.trim();
if (!value) return;
if (!value.startsWith(".")) {
value = "." + value;
}
const existing = Array.from(container.querySelectorAll(".tag-text")).map((el) => el.textContent);
if (existing.includes(value)) {
input.value = "";
return;
}
window.SettingsForms.createTagElement(container, value);
input.value = "";
};
window.SettingsForms.addPatternTag = function() {
const input = document.getElementById("swaparr_suspicious_patterns_input");
const container = document.getElementById("swaparr_suspicious_patterns_tags");
if (!input || !container) return;
const value = input.value.trim();
if (!value) return;
const existing = Array.from(container.querySelectorAll(".tag-text")).map((el) => el.textContent);
if (existing.includes(value)) {
input.value = "";
return;
}
window.SettingsForms.createTagElement(container, value);
input.value = "";
};
window.SettingsForms.addQualityTag = function() {
const input = document.getElementById("swaparr_quality_patterns_input");
const container = document.getElementById("swaparr_quality_patterns_tags");
if (!input || !container) return;
const value = input.value.trim().toLowerCase();
if (!value) return;
const existing = Array.from(container.querySelectorAll(".tag-text")).map((el) => el.textContent.toLowerCase());
if (existing.includes(value)) {
input.value = "";
return;
}
window.SettingsForms.createTagElement(container, value);
input.value = "";
};
window.SettingsForms.setupSwaparrManualSave = function(container, originalSettings = {}) {
const saveButton = container.querySelector("#swaparr-save-button");
if (!saveButton) return;
saveButton.disabled = true;
saveButton.style.background = "#6b7280";
saveButton.style.color = "#9ca3af";
saveButton.style.borderColor = "#4b5563";
saveButton.style.cursor = "not-allowed";
let hasChanges = false;
window.swaparrUnsavedChanges = false;
if (window.SettingsForms.removeUnsavedChangesWarning) {
window.SettingsForms.removeUnsavedChangesWarning();
}
const updateSaveButtonState = (changesDetected) => {
hasChanges = changesDetected;
window.swaparrUnsavedChanges = changesDetected;
if (hasChanges) {
saveButton.disabled = false;
saveButton.style.background = "#dc2626";
saveButton.style.color = "#ffffff";
saveButton.style.borderColor = "#dc2626";
saveButton.style.cursor = "pointer";
if (window.SettingsForms.addUnsavedChangesWarning) {
window.SettingsForms.addUnsavedChangesWarning();
}
} else {
saveButton.disabled = true;
saveButton.style.background = "#6b7280";
saveButton.style.color = "#9ca3af";
saveButton.style.borderColor = "#4b5563";
saveButton.style.cursor = "not-allowed";
if (window.SettingsForms.removeUnsavedChangesWarning) {
window.SettingsForms.removeUnsavedChangesWarning();
}
}
};
container.addEventListener('input', () => updateSaveButtonState(true));
container.addEventListener('change', () => updateSaveButtonState(true));
const newSaveButton = saveButton.cloneNode(true);
saveButton.parentNode.replaceChild(newSaveButton, saveButton);
newSaveButton.addEventListener("click", () => {
if (!hasChanges) return;
newSaveButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
newSaveButton.disabled = true;
// Collect data
const settings = { ...originalSettings };
const enabled = document.getElementById("swaparr_enabled");
if (enabled) settings.enabled = enabled.checked;
const maxStrikes = document.getElementById("swaparr_max_strikes");
if (maxStrikes) settings.max_strikes = parseInt(maxStrikes.value);
const maxDownloadTime = document.getElementById("swaparr_max_download_time");
if (maxDownloadTime) settings.max_download_time = maxDownloadTime.value;
const ignoreAboveSize = document.getElementById("swaparr_ignore_above_size");
if (ignoreAboveSize) settings.ignore_above_size = ignoreAboveSize.value;
const removeFromClient = document.getElementById("swaparr_remove_from_client");
if (removeFromClient) settings.remove_from_client = removeFromClient.checked;
const researchRemoved = document.getElementById("swaparr_research_removed");
if (researchRemoved) settings.research_removed = researchRemoved.checked;
const failedImport = document.getElementById("swaparr_failed_import_detection");
if (failedImport) settings.failed_import_detection = failedImport.checked;
const dryRun = document.getElementById("swaparr_dry_run");
if (dryRun) settings.dry_run = dryRun.checked;
const sleepDuration = document.getElementById("swaparr_sleep_duration");
if (sleepDuration) settings.sleep_duration = parseInt(sleepDuration.value) * 60;
const malicious = document.getElementById("swaparr_malicious_detection");
if (malicious) settings.malicious_file_detection = malicious.checked;
const ageRemoval = document.getElementById("swaparr_age_based_removal");
if (ageRemoval) settings.age_based_removal = ageRemoval.checked;
const maxAge = document.getElementById("swaparr_max_age_days");
if (maxAge) settings.max_age_days = parseInt(maxAge.value);
const qualityRemoval = document.getElementById("swaparr_quality_based_removal");
if (qualityRemoval) settings.quality_based_removal = qualityRemoval.checked;
// Collect tags
const getTags = (id) => {
const container = document.getElementById(id);
if (!container) return [];
return Array.from(container.querySelectorAll(".tag-text")).map(el => el.textContent);
};
settings.malicious_extensions = getTags("swaparr_malicious_extensions_tags");
settings.suspicious_patterns = getTags("swaparr_suspicious_patterns_tags");
settings.blocked_quality_patterns = getTags("swaparr_quality_patterns_tags");
// Save
window.SettingsForms.saveAppSettings("swaparr", settings);
// Reset UI state
newSaveButton.innerHTML = '<i class="fas fa-save"></i> Save Changes';
updateSaveButtonState(false);
});
};
})();
@@ -0,0 +1,266 @@
(function() {
window.SettingsForms = window.SettingsForms || {};
window.SettingsForms.generateWhisparrForm = function(container, settings = {}) {
if (!settings || typeof settings !== "object") {
settings = {};
}
const wasSuppressionActive = window._appsSuppressChangeDetection;
window._appsSuppressChangeDetection = true;
container.setAttribute("data-app-type", "whisparr");
if (!settings.instances || !Array.isArray(settings.instances)) {
settings.instances = [];
}
let whisparrSaveButtonHtml = `
<div style="margin-bottom: 20px;">
<button type="button" id="whisparr-save-button" disabled style="
background: #6b7280;
color: #9ca3af;
border: 1px solid #4b5563;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<i class="fas fa-save"></i>
Save Changes
</button>
</div>
`;
let instancesHtml = `
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Whisparr V2 Instances</h3>
<div class="instance-card-grid" id="whisparr-instances-grid">
`;
if (settings.instances && settings.instances.length > 0) {
settings.instances.forEach((instance, index) => {
instancesHtml += window.SettingsForms.renderInstanceCard('whisparr', instance, index);
});
}
instancesHtml += `
<div class="add-instance-card" data-app-type="whisparr">
<div class="add-icon"><i class="fas fa-plus-circle"></i></div>
<div class="add-text">Add Whisparr Instance</div>
</div>
`;
instancesHtml += `
</div>
</div>
`;
let searchSettingsHtml = `
<div class="settings-group">
<h3>Search Settings</h3>
<div class="setting-item">
<label for="whisparr_sleep_duration"><a href="https://plexguide.github.io/Huntarr.io/apps/whisparr.html#search-settings" class="info-icon" title="Learn more about sleep duration" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Sleep Duration (Minutes):</label>
<input type="number" id="whisparr_sleep_duration" name="sleep_duration" min="10" value="${
settings.sleep_duration !== undefined ? Math.round(settings.sleep_duration / 60) : 15
}">
<p class="setting-help">Time in minutes between processing cycles (minimum 10 minutes)</p>
</div>
<div class="setting-item">
<label for="whisparr_hourly_cap"><a href="https://plexguide.github.io/Huntarr.io/apps/whisparr.html#search-settings" class="info-icon" title="Maximum API requests per hour for this app (20 is safe)" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>API Cap - Hourly:</label>
<input type="number" id="whisparr_hourly_cap" name="hourly_cap" min="1" max="400" value="${
settings.hourly_cap !== undefined ? settings.hourly_cap : 20
}">
<p class="setting-help">Maximum API requests per hour to prevent being banned by your indexers. Keep lower for safety (20-50 recommended). Max allowed: 400.</p>
</div>
</div>
`;
container.innerHTML = whisparrSaveButtonHtml + instancesHtml + searchSettingsHtml;
const grid = container.querySelector('#whisparr-instances-grid');
if (grid) {
grid.addEventListener('click', (e) => {
const editBtn = e.target.closest('.btn-card.edit');
const deleteBtn = e.target.closest('.btn-card.delete');
const addCard = e.target.closest('.add-instance-card');
if (editBtn) {
const appType = editBtn.dataset.appType;
const index = parseInt(editBtn.dataset.instanceIndex);
window.SettingsForms.openInstanceModal(appType, index);
} else if (deleteBtn) {
const appType = deleteBtn.dataset.appType;
const index = parseInt(deleteBtn.dataset.instanceIndex);
window.SettingsForms.deleteInstance(appType, index);
} else if (addCard) {
const appType = addCard.dataset.appType;
window.SettingsForms.openInstanceModal(appType);
}
});
}
if (window.SettingsForms.setupAppManualSave) {
window.SettingsForms.setupAppManualSave(container, "whisparr", settings);
}
// Test instance connections after rendering
setTimeout(() => {
if (window.SettingsForms.testAllInstanceConnections) {
window.SettingsForms.testAllInstanceConnections("whisparr");
}
}, 100);
setTimeout(() => {
window._appsSuppressChangeDetection = wasSuppressionActive;
}, 100);
};
window.SettingsForms.generateErosForm = function(container, settings = {}) {
if (!settings || typeof settings !== "object") {
settings = {};
}
const wasSuppressionActive = window._appsSuppressChangeDetection;
window._appsSuppressChangeDetection = true;
container.setAttribute("data-app-type", "eros");
if (!settings.instances || !Array.isArray(settings.instances)) {
settings.instances = [];
}
let erosSaveButtonHtml = `
<div style="margin-bottom: 20px;">
<button type="button" id="eros-save-button" disabled style="
background: #6b7280;
color: #9ca3af;
border: 1px solid #4b5563;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<i class="fas fa-save"></i>
Save Changes
</button>
</div>
`;
let instancesHtml = `
<div class="settings-group" style="
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border: 2px solid rgba(90, 109, 137, 0.3);
border-radius: 12px;
padding: 20px;
margin: 15px 0 25px 0;
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Whisparr V3 Instances</h3>
<div class="instance-card-grid" id="eros-instances-grid">
`;
if (settings.instances && settings.instances.length > 0) {
settings.instances.forEach((instance, index) => {
instancesHtml += window.SettingsForms.renderInstanceCard('eros', instance, index);
});
}
instancesHtml += `
<div class="add-instance-card" data-app-type="eros">
<div class="add-icon"><i class="fas fa-plus-circle"></i></div>
<div class="add-text">Add Whisparr V3 Instance</div>
</div>
`;
instancesHtml += `
</div>
</div>
`;
let searchSettingsHtml = `
<div class="settings-group">
<h3>Search Settings</h3>
<div class="setting-item">
<label for="eros_search_mode"><a href="https://plexguide.github.io/Huntarr.io/apps/eros.html#search-mode" class="info-icon" title="Learn more about search modes" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Search Mode:</label>
<select id="eros_search_mode" name="search_mode">
<option value="movie" ${settings.search_mode === "movie" || !settings.search_mode ? "selected" : ""}>Movie</option>
<option value="scene" ${settings.search_mode === "scene" ? "selected" : ""}>Scene</option>
</select>
<p class="setting-help">How to search for missing and upgradable Whisparr V3 content (Movie-based or Scene-based)</p>
</div>
<div class="setting-item">
<label for="eros_sleep_duration"><a href="https://plexguide.github.io/Huntarr.io/apps/eros.html#sleep-duration" class="info-icon" title="Learn more about sleep duration" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Sleep Duration (Minutes):</label>
<input type="number" id="eros_sleep_duration" name="sleep_duration" min="10" value="${
settings.sleep_duration !== undefined ? Math.round(settings.sleep_duration / 60) : 15
}">
<p class="setting-help">Time in minutes between processing cycles (minimum 10 minutes)</p>
</div>
<div class="setting-item">
<label for="eros_hourly_cap"><a href="https://plexguide.github.io/Huntarr.io/apps/eros.html#api-cap" class="info-icon" title="Maximum API requests per hour for this app (20 is safe)" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>API Cap - Hourly:</label>
<input type="number" id="eros_hourly_cap" name="hourly_cap" min="1" max="400" value="${
settings.hourly_cap !== undefined ? settings.hourly_cap : 20
}">
<p class="setting-help">Maximum API requests per hour to prevent being banned by your indexers. Keep lower for safety (20-50 recommended). Max allowed: 400.</p>
</div>
</div>
`;
container.innerHTML = erosSaveButtonHtml + instancesHtml + searchSettingsHtml;
const grid = container.querySelector('#eros-instances-grid');
if (grid) {
grid.addEventListener('click', (e) => {
const editBtn = e.target.closest('.btn-card.edit');
const deleteBtn = e.target.closest('.btn-card.delete');
const addCard = e.target.closest('.add-instance-card');
if (editBtn) {
const appType = editBtn.dataset.appType;
const index = parseInt(editBtn.dataset.instanceIndex);
window.SettingsForms.openInstanceModal(appType, index);
} else if (deleteBtn) {
const appType = deleteBtn.dataset.appType;
const index = parseInt(deleteBtn.dataset.instanceIndex);
window.SettingsForms.deleteInstance(appType, index);
} else if (addCard) {
const appType = addCard.dataset.appType;
window.SettingsForms.openInstanceModal(appType);
}
});
}
if (window.SettingsForms.setupAppManualSave) {
window.SettingsForms.setupAppManualSave(container, "eros", settings);
}
// Test instance connections after rendering
setTimeout(() => {
if (window.SettingsForms.testAllInstanceConnections) {
window.SettingsForms.testAllInstanceConnections("eros");
}
}, 100);
setTimeout(() => {
window._appsSuppressChangeDetection = wasSuppressionActive;
}, 100);
};
})();