mirror of
https://github.com/plexguide/Huntarr.io.git
synced 2026-04-20 16:06:52 -04:00
394 lines
23 KiB
JavaScript
394 lines
23 KiB
JavaScript
/**
|
|
* Client Editor (Movie Hunt) - full-page editor for adding/editing a download client.
|
|
* Separate from Client Management (clients.js). Attaches to window.SettingsForms.
|
|
* Load after settings/core.js and instance-editor.js.
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
if (typeof window.SettingsForms === 'undefined') return;
|
|
|
|
const Forms = window.SettingsForms;
|
|
|
|
var CLIENT_TYPES = [
|
|
{ value: 'nzbhunt', label: 'NZB Hunt (Built-in)' },
|
|
{ value: 'nzbget', label: 'NZBGet' },
|
|
{ value: 'sabnzbd', label: 'SABnzbd' }
|
|
];
|
|
|
|
var PRIORITY_OPTIONS = [
|
|
{ value: 'last', label: 'Last' },
|
|
{ value: 'first', label: 'First' },
|
|
{ value: 'default', label: 'Default' },
|
|
{ value: 'high', label: 'High' },
|
|
{ value: 'low', label: 'Low' }
|
|
];
|
|
|
|
Forms.openClientEditor = function(isAdd, index, instance) {
|
|
const inst = instance || {};
|
|
this._currentEditing = { appType: 'client', index: index, isAdd: isAdd, originalInstance: JSON.parse(JSON.stringify(inst)) };
|
|
|
|
const typeRaw = (inst.type || 'nzbget').toLowerCase().trim();
|
|
const typeVal = CLIENT_TYPES.some(function(o) { return o.value === typeRaw; }) ? typeRaw : 'nzbget';
|
|
const clientDisplayName = (CLIENT_TYPES.find(function(o) { return o.value === typeVal; }) || { label: typeVal }).label;
|
|
|
|
const titleEl = document.getElementById('instance-editor-title');
|
|
if (titleEl) {
|
|
titleEl.innerHTML = '<span class="client-editor-title-app">' + String(clientDisplayName).replace(/</g, '<').replace(/>/g, '>') + '</span> Connection Settings';
|
|
}
|
|
|
|
const contentEl = document.getElementById('instance-editor-content');
|
|
if (contentEl) contentEl.innerHTML = this.generateClientEditorHtml(inst);
|
|
|
|
const saveBtn = document.getElementById('instance-editor-save');
|
|
const backBtn = document.getElementById('instance-editor-back');
|
|
if (saveBtn) {
|
|
saveBtn.onclick = () => this.saveClientFromEditor();
|
|
saveBtn.disabled = false;
|
|
saveBtn.classList.add('enabled');
|
|
}
|
|
if (backBtn) backBtn.onclick = () => this.cancelInstanceEditor();
|
|
|
|
const enabledSelect = document.getElementById('editor-client-enabled');
|
|
const enableIcon = document.getElementById('client-enable-status-icon');
|
|
if (enabledSelect && enableIcon) {
|
|
enabledSelect.addEventListener('change', function() {
|
|
const isEnabled = enabledSelect.value === 'true';
|
|
enableIcon.className = isEnabled ? 'fas fa-check-circle' : 'fas fa-minus-circle';
|
|
enableIcon.style.color = isEnabled ? '#10b981' : '#ef4444';
|
|
});
|
|
}
|
|
|
|
// Add event listeners for real-time connection status checking
|
|
// For NZB Hunt, only check on initial load (no host/port fields)
|
|
if (typeVal !== 'nzbhunt') {
|
|
const hostEl = document.getElementById('editor-client-host');
|
|
const portEl = document.getElementById('editor-client-port');
|
|
const apiKeyEl = document.getElementById('editor-client-apikey');
|
|
const usernameEl = document.getElementById('editor-client-username');
|
|
const passwordEl = document.getElementById('editor-client-password');
|
|
|
|
if (hostEl) hostEl.addEventListener('input', () => this.checkClientConnection());
|
|
if (portEl) portEl.addEventListener('input', () => this.checkClientConnection());
|
|
if (apiKeyEl) apiKeyEl.addEventListener('input', () => this.checkClientConnection());
|
|
if (usernameEl) usernameEl.addEventListener('input', () => this.checkClientConnection());
|
|
if (passwordEl) passwordEl.addEventListener('input', () => this.checkClientConnection());
|
|
}
|
|
|
|
// Initial connection check (skip for NZB Hunt - built-in, no status needed)
|
|
if (typeVal !== 'nzbhunt') {
|
|
this.checkClientConnection();
|
|
}
|
|
|
|
if (window.huntarrUI && window.huntarrUI.switchSection) {
|
|
window.huntarrUI.switchSection('instance-editor');
|
|
}
|
|
};
|
|
|
|
Forms.generateClientEditorHtml = function(instance) {
|
|
const name = (instance.name || '').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
const typeRaw = (instance.type || 'nzbget').toLowerCase().trim();
|
|
const typeVal = CLIENT_TYPES.some(function(o) { return o.value === typeRaw; }) ? typeRaw : 'nzbget';
|
|
const host = (instance.host || '').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
const defaultPort = typeVal === 'nzbget' ? '6789' : '8080';
|
|
const port = instance.port !== undefined && instance.port !== '' ? String(instance.port) : defaultPort;
|
|
const username = (instance.username || '').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
const enabled = instance.enabled !== false;
|
|
const isEdit = !!(instance.name && instance.name.trim());
|
|
|
|
const apiKeyPlaceholder = isEdit && (instance.api_key_last4 || '')
|
|
? ('Enter new key or leave blank to keep existing (••••' + (instance.api_key_last4 || '') + ')')
|
|
: 'Enter API key';
|
|
const pwdPlaceholder = isEdit && (instance.password_last4 || '')
|
|
? ('Enter new password or leave blank to keep existing (••••' + (instance.password_last4 || '') + ')')
|
|
: 'Password (if required)';
|
|
const isTvContext = !!(window._mediaHuntInstanceEditorMode === 'tv');
|
|
const catDefault = isTvContext ? 'tv' : 'movies';
|
|
const category = (instance.category || catDefault).replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
const recentPriority = (instance.recent_priority || 'default').toLowerCase();
|
|
const olderPriority = (instance.older_priority || 'default').toLowerCase();
|
|
let clientPriority = parseInt(instance.client_priority, 10);
|
|
if (isNaN(clientPriority) || clientPriority < 1 || clientPriority > 99) clientPriority = 50;
|
|
|
|
const recentOptionsHtml = PRIORITY_OPTIONS.map(function(o) {
|
|
return '<option value="' + o.value + '"' + (recentPriority === o.value ? ' selected' : '') + '>' + o.label + '</option>';
|
|
}).join('');
|
|
const olderOptionsHtml = PRIORITY_OPTIONS.map(function(o) {
|
|
return '<option value="' + o.value + '"' + (olderPriority === o.value ? ' selected' : '') + '>' + o.label + '</option>';
|
|
}).join('');
|
|
|
|
const isNzbHunt = typeVal === 'nzbhunt';
|
|
const hideForNzbHunt = isNzbHunt ? ' style="display: none;"' : '';
|
|
const nzbHuntGridClass = isNzbHunt ? ' editor-grid-nzbhunt' : '';
|
|
|
|
return `
|
|
<div class="editor-grid${nzbHuntGridClass}">
|
|
<div class="editor-section${isNzbHunt ? ' editor-section-single' : ''}">
|
|
<div class="editor-section-title" style="display: flex; align-items: center; justify-content: space-between;">
|
|
<span>${isNzbHunt ? 'NZB Hunt (Built-in)' : 'Connection Settings'}</span>
|
|
<div id="client-connection-status-container" style="display: ${isNzbHunt ? 'none' : 'flex'}; justify-content: flex-end; flex: 1;"></div>
|
|
</div>
|
|
${isNzbHunt ? `
|
|
<div class="editor-field-group">
|
|
<div style="background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 8px; padding: 16px; margin-bottom: 12px;">
|
|
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
|
<i class="fas fa-bolt" style="color: #10b981; font-size: 1.2rem;"></i>
|
|
<strong style="color: #10b981;">Built-in Download Client</strong>
|
|
</div>
|
|
<p style="color: #94a3b8; margin: 0; font-size: 0.9rem; line-height: 1.5;">
|
|
NZB Hunt is Huntarr's integrated usenet download client. No external host, port, or API keys needed —
|
|
it uses the usenet servers configured in <strong>NZB Hunt → Settings → Servers</strong>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
<div class="editor-field-group">
|
|
<div class="editor-setting-item">
|
|
<label style="display: flex; align-items: center;">
|
|
<span>Enable Status</span>
|
|
<i id="client-enable-status-icon" class="fas ${enabled ? 'fa-check-circle' : 'fa-minus-circle'}" style="color: ${enabled ? '#10b981' : '#ef4444'}; font-size: 1.1rem; margin-left: 8px;"></i>
|
|
</label>
|
|
<select id="editor-client-enabled">
|
|
<option value="true" ${enabled ? 'selected' : ''}>Enabled</option>
|
|
<option value="false" ${!enabled ? 'selected' : ''}>Disabled</option>
|
|
</select>
|
|
</div>
|
|
<p class="editor-help-text">Enable or disable this download client</p>
|
|
</div>
|
|
${!isNzbHunt ? `
|
|
<div class="editor-field-group">
|
|
<label for="editor-client-name">Name</label>
|
|
<input type="text" id="editor-client-name" value="${name}" placeholder="${typeVal === 'sabnzbd' ? 'e.g. My SABnzbd' : 'e.g. My NZBGet'}" />
|
|
<p class="editor-help-text">A friendly name to identify this client</p>
|
|
</div>
|
|
` : ''}
|
|
<div class="editor-field-group"${hideForNzbHunt}>
|
|
<label for="editor-client-host">Host</label>
|
|
<input type="text" id="editor-client-host" value="${host}" placeholder="localhost or 192.168.1.10" />
|
|
<p class="editor-help-text">Hostname or IP address of your download client</p>
|
|
</div>
|
|
<div class="editor-field-group"${hideForNzbHunt}>
|
|
<label for="editor-client-port">Port</label>
|
|
<input type="number" id="editor-client-port" value="${port}" placeholder="${defaultPort}" min="1" max="65535" />
|
|
<p class="editor-help-text">Port number for your download client (SABnzbd default: 8080, NZBGet default: 6789)</p>
|
|
</div>
|
|
<div class="editor-field-group"${hideForNzbHunt}>
|
|
<label for="editor-client-apikey">API Key</label>
|
|
<input type="password" id="editor-client-apikey" placeholder="${apiKeyPlaceholder.replace(/"/g, '"')}" autocomplete="off" />
|
|
<p class="editor-help-text">API key from your download client settings. ${isEdit ? 'Leave blank to keep existing.' : ''}</p>
|
|
</div>
|
|
<div class="editor-field-group"${hideForNzbHunt}>
|
|
<label for="editor-client-username">Username</label>
|
|
<input type="text" id="editor-client-username" value="${username}" placeholder="Username (if required)" autocomplete="off" />
|
|
<p class="editor-help-text">Username for basic authentication (NZBGet typically requires this)</p>
|
|
</div>
|
|
<div class="editor-field-group"${hideForNzbHunt}>
|
|
<label for="editor-client-password">Password</label>
|
|
<input type="password" id="editor-client-password" placeholder="${pwdPlaceholder.replace(/"/g, '"')}" autocomplete="off" />
|
|
<p class="editor-help-text">${isEdit ? 'Leave blank to keep existing password' : 'Password for authentication (if required)'}</p>
|
|
</div>
|
|
${!isNzbHunt ? `
|
|
</div>
|
|
<div class="editor-section">
|
|
<div class="editor-section-title">Additional Configurations</div>
|
|
<div class="editor-field-group">
|
|
<label for="editor-client-category">Category</label>
|
|
<input type="text" id="editor-client-category" value="${category}" placeholder="${catDefault}" />
|
|
<p class="editor-help-text">Adding a category specific to ${isTvContext ? 'TV' : 'Movie'} Hunt avoids conflicts with unrelated downloads. Using a category is optional, but strongly recommended.</p>
|
|
</div>
|
|
` : ''}
|
|
<div class="editor-field-group">
|
|
<label for="editor-client-recent-priority">Recent Priority</label>
|
|
<select id="editor-client-recent-priority">${recentOptionsHtml}</select>
|
|
<p class="editor-help-text">Priority to use when grabbing movies that aired within the last 21 days.</p>
|
|
</div>
|
|
<div class="editor-field-group">
|
|
<label for="editor-client-older-priority">Older Priority</label>
|
|
<select id="editor-client-older-priority">${olderOptionsHtml}</select>
|
|
<p class="editor-help-text">Priority to use when grabbing movies that aired over 21 days ago.</p>
|
|
</div>
|
|
<div class="editor-field-group">
|
|
<label for="editor-client-priority">Client Priority</label>
|
|
<input type="number" id="editor-client-priority" value="${clientPriority}" min="1" max="99" placeholder="50" />
|
|
<p class="editor-help-text">Download Client Priority from 1 (Highest) to 99 (Lowest). Default: 50. Round-Robin is used for clients with the same priority.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
Forms.saveClientFromEditor = function() {
|
|
if (!this._currentEditing || this._currentEditing.appType !== 'client') return;
|
|
const nameEl = document.getElementById('editor-client-name');
|
|
const hostEl = document.getElementById('editor-client-host');
|
|
const portEl = document.getElementById('editor-client-port');
|
|
const enabledEl = document.getElementById('editor-client-enabled');
|
|
const apiKeyEl = document.getElementById('editor-client-apikey');
|
|
const usernameEl = document.getElementById('editor-client-username');
|
|
const passwordEl = document.getElementById('editor-client-password');
|
|
const categoryEl = document.getElementById('editor-client-category');
|
|
const recentPriorityEl = document.getElementById('editor-client-recent-priority');
|
|
const olderPriorityEl = document.getElementById('editor-client-older-priority');
|
|
const clientPriorityEl = document.getElementById('editor-client-priority');
|
|
|
|
const type = (this._currentEditing && this._currentEditing.originalInstance && this._currentEditing.originalInstance.type)
|
|
? String(this._currentEditing.originalInstance.type).trim().toLowerCase()
|
|
: 'nzbget';
|
|
const isNzbHuntType = (type === 'nzbhunt' || type === 'nzb_hunt');
|
|
const name = isNzbHuntType ? 'NZB Hunt' : (nameEl ? nameEl.value.trim() : '');
|
|
const host = hostEl ? hostEl.value.trim() : '';
|
|
const portDefault = type === 'nzbget' ? 6789 : 8080;
|
|
let port = portDefault;
|
|
if (portEl && portEl.value.trim() !== '') {
|
|
const p = parseInt(portEl.value, 10);
|
|
if (!isNaN(p)) port = p;
|
|
}
|
|
const enabled = enabledEl ? enabledEl.value === 'true' : true;
|
|
const apiKey = apiKeyEl ? apiKeyEl.value.trim() : '';
|
|
const username = usernameEl ? usernameEl.value.trim() : '';
|
|
const password = passwordEl ? passwordEl.value.trim() : '';
|
|
const isTvMode = !!(window._mediaHuntInstanceEditorMode === 'tv');
|
|
const defaultCategory = isTvMode ? 'tv' : 'movies';
|
|
let category = (categoryEl && !isNzbHuntType) ? categoryEl.value.trim() : defaultCategory;
|
|
if (isNzbHuntType) {
|
|
const orig = this._currentEditing && this._currentEditing.originalInstance;
|
|
category = (orig && orig.category) ? String(orig.category).trim() : '';
|
|
}
|
|
const recentPriority = recentPriorityEl ? (recentPriorityEl.value || 'default').toLowerCase() : 'default';
|
|
const olderPriority = olderPriorityEl ? (olderPriorityEl.value || 'default').toLowerCase() : 'default';
|
|
let clientPriority = 50;
|
|
if (clientPriorityEl && clientPriorityEl.value.trim() !== '') {
|
|
const p = parseInt(clientPriorityEl.value, 10);
|
|
if (!isNaN(p) && p >= 1 && p <= 99) clientPriority = p;
|
|
}
|
|
|
|
const body = {
|
|
name: isNzbHuntType ? 'NZB Hunt' : (name || 'Unnamed'),
|
|
type: type,
|
|
host: isNzbHuntType ? 'internal' : host,
|
|
port: isNzbHuntType ? 0 : port,
|
|
enabled: enabled,
|
|
category: category || defaultCategory,
|
|
recent_priority: recentPriority,
|
|
older_priority: olderPriority,
|
|
client_priority: clientPriority
|
|
};
|
|
if (!isNzbHuntType) {
|
|
if (apiKey) body.api_key = apiKey;
|
|
if (username) body.username = username;
|
|
if (password) body.password = password;
|
|
}
|
|
|
|
const isAdd = this._currentEditing.isAdd;
|
|
const index = this._currentEditing.index;
|
|
const url = isAdd ? './api/clients' : './api/clients/' + index;
|
|
const method = isAdd ? 'POST' : 'PUT';
|
|
|
|
fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
.then(function(r) {
|
|
return r.json().then(function(data) { return { ok: r.ok, data: data }; });
|
|
})
|
|
.then(function(result) {
|
|
if (!result.ok) {
|
|
var msg = (result.data && result.data.error) ? result.data.error : 'Save failed';
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) {
|
|
window.huntarrUI.showNotification(msg, 'error');
|
|
}
|
|
return;
|
|
}
|
|
var data = result.data;
|
|
if (window.SettingsForms && window.SettingsForms.refreshClientsList) {
|
|
window.SettingsForms.refreshClientsList();
|
|
}
|
|
// Refresh NZB Hunt sidebar group visibility (may have added/removed NZB Hunt client)
|
|
if (window.huntarrUI && typeof window.huntarrUI._refreshNzbHuntSidebarGroup === 'function') {
|
|
window.huntarrUI._refreshNzbHuntSidebarGroup();
|
|
}
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) {
|
|
window.huntarrUI.showNotification(isAdd ? 'Client added.' : 'Client updated.', 'success');
|
|
}
|
|
// Don't auto-navigate back to collection after saving a client.
|
|
// The user may want to add more clients. The wizard banner on the
|
|
// clients page handles the "continue" flow when the user is ready.
|
|
if (window.SettingsForms && window.SettingsForms._currentEditing) {
|
|
window.SettingsForms._currentEditing.isAdd = false;
|
|
if (data && data.index !== undefined) {
|
|
window.SettingsForms._currentEditing.index = data.index;
|
|
} else if (!isAdd) {
|
|
window.SettingsForms._currentEditing.index = index;
|
|
}
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) {
|
|
window.huntarrUI.showNotification(err.message || 'Failed to save client', 'error');
|
|
}
|
|
});
|
|
};
|
|
|
|
Forms.checkClientConnection = function() {
|
|
const container = document.getElementById('client-connection-status-container');
|
|
const hostEl = document.getElementById('editor-client-host');
|
|
const portEl = document.getElementById('editor-client-port');
|
|
const apiKeyEl = document.getElementById('editor-client-apikey');
|
|
const usernameEl = document.getElementById('editor-client-username');
|
|
const passwordEl = document.getElementById('editor-client-password');
|
|
|
|
if (!container) return;
|
|
|
|
container.style.display = 'flex';
|
|
container.style.justifyContent = 'flex-end';
|
|
|
|
// Get client type
|
|
const type = (this._currentEditing && this._currentEditing.originalInstance && this._currentEditing.originalInstance.type)
|
|
? String(this._currentEditing.originalInstance.type).trim().toLowerCase()
|
|
: 'nzbget';
|
|
|
|
// NZB Hunt (built-in) - no connection status; it's built-in and managed in NZB Hunt Settings
|
|
if (type === 'nzbhunt' || type === 'nzb_hunt') {
|
|
if (container) container.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const host = hostEl ? hostEl.value.trim() : '';
|
|
const port = portEl ? portEl.value.trim() : '';
|
|
const apiKey = apiKeyEl ? apiKeyEl.value.trim() : '';
|
|
const username = usernameEl ? usernameEl.value.trim() : '';
|
|
const password = passwordEl ? passwordEl.value.trim() : '';
|
|
|
|
// Check if minimum requirements are met
|
|
if (!host || !port) {
|
|
container.innerHTML = '<span class="connection-status" style="background: rgba(251, 191, 36, 0.1); color: #fbbf24; border: 1px solid rgba(251, 191, 36, 0.2);"><i class="fas fa-exclamation-triangle"></i><span>Enter host and port</span></span>';
|
|
return;
|
|
}
|
|
|
|
// Show checking status
|
|
container.innerHTML = '<span class="connection-status checking"><i class="fas fa-spinner fa-spin"></i><span>Checking...</span></span>';
|
|
|
|
// Test connection
|
|
fetch('./api/clients/test-connection', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: type,
|
|
host: host,
|
|
port: parseInt(port, 10) || 8080,
|
|
api_key: apiKey,
|
|
username: username,
|
|
password: password
|
|
})
|
|
})
|
|
.then(function(r) { return r.json().then(function(data) { return { ok: r.ok, data: data }; }); })
|
|
.then(function(result) {
|
|
const data = result.data || {};
|
|
if (data.success === true) {
|
|
container.innerHTML = '<span class="connection-status success"><i class="fas fa-check-circle"></i><span>Connected</span></span>';
|
|
} else {
|
|
container.innerHTML = '<span class="connection-status error"><i class="fas fa-times-circle"></i><span>' + (data.message || data.error || 'Connection failed') + '</span></span>';
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
container.innerHTML = '<span class="connection-status error"><i class="fas fa-times-circle"></i><span>' + (err.message || 'Connection failed') + '</span></span>';
|
|
});
|
|
};
|
|
})();
|