New packages for Usenet/NZB workflow: - secubox-app-sabnzbd: NZB downloader (LXC container) - EWEKA NNTP credentials pre-configured - Docker image extraction to LXC - HAProxy SSL exposure support - secubox-app-nzbhydra: Meta search indexer (LXC container) - Aggregates multiple NZB indexers - Newznab API for Sonarr/Radarr integration - SABnzbd auto-linking - luci-app-newsbin: Unified LuCI dashboard - Status cards (speed, queue, disk) - Meta-search with download buttons - Queue monitoring with progress bars - History view CLI: sabnzbdctl, nzbhydractl (install/start/status/search) LuCI: Services > Newsbin Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
342 lines
14 KiB
JavaScript
342 lines
14 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require ui';
|
|
'require poll';
|
|
|
|
var callStatus = rpc.declare({
|
|
object: 'luci.newsbin',
|
|
method: 'status',
|
|
expect: {}
|
|
});
|
|
|
|
var callQueue = rpc.declare({
|
|
object: 'luci.newsbin',
|
|
method: 'queue',
|
|
expect: { items: [] }
|
|
});
|
|
|
|
var callHistory = rpc.declare({
|
|
object: 'luci.newsbin',
|
|
method: 'history',
|
|
expect: { items: [] }
|
|
});
|
|
|
|
var callSearch = rpc.declare({
|
|
object: 'luci.newsbin',
|
|
method: 'search',
|
|
params: ['query'],
|
|
expect: { results: [] }
|
|
});
|
|
|
|
var callAddNzb = rpc.declare({
|
|
object: 'luci.newsbin',
|
|
method: 'add_nzb',
|
|
params: ['url', 'category'],
|
|
expect: {}
|
|
});
|
|
|
|
var callPause = rpc.declare({
|
|
object: 'luci.newsbin',
|
|
method: 'pause',
|
|
expect: {}
|
|
});
|
|
|
|
var callResume = rpc.declare({
|
|
object: 'luci.newsbin',
|
|
method: 'resume',
|
|
expect: {}
|
|
});
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
callStatus(),
|
|
callQueue(),
|
|
callHistory()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var status = data[0] || {};
|
|
var queue = data[1] || [];
|
|
var history = data[2] || [];
|
|
|
|
var sab = status.sabnzbd || {};
|
|
var hydra = status.nzbhydra || {};
|
|
|
|
var view = E('div', { 'class': 'cbi-map' }, [
|
|
E('style', {}, `
|
|
.newsbin-container { max-width: 1200px; margin: 0 auto; }
|
|
.status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }
|
|
.status-card {
|
|
background: linear-gradient(135deg, #1a1a24, #2a2a3e);
|
|
border: 1px solid #3a3a4e;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
}
|
|
.status-card.running { border-color: #10b981; }
|
|
.status-card.stopped { border-color: #ef4444; }
|
|
.status-value { font-size: 2em; font-weight: 700; color: #00d4ff; }
|
|
.status-label { color: #888; font-size: 0.85em; margin-top: 5px; }
|
|
.search-box {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.search-box input {
|
|
flex: 1;
|
|
padding: 12px 16px;
|
|
background: #12121a;
|
|
border: 1px solid #3a3a4e;
|
|
border-radius: 8px;
|
|
color: #e0e0e0;
|
|
font-size: 1em;
|
|
}
|
|
.search-box input:focus { outline: none; border-color: #00d4ff; }
|
|
.search-box button {
|
|
padding: 12px 24px;
|
|
background: linear-gradient(135deg, #00d4ff, #7c3aed);
|
|
border: none;
|
|
border-radius: 8px;
|
|
color: #fff;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
.section-title { color: #e0e0e0; margin: 20px 0 15px; font-size: 1.2em; }
|
|
.queue-item, .result-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 15px;
|
|
background: #1a1a24;
|
|
border: 1px solid #2a2a3e;
|
|
border-radius: 8px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.queue-item:hover, .result-item:hover { border-color: #00d4ff; }
|
|
.item-info { flex: 1; }
|
|
.item-name { font-weight: 600; color: #e0e0e0; }
|
|
.item-meta { font-size: 0.85em; color: #888; margin-top: 5px; }
|
|
.progress-bar {
|
|
width: 100px;
|
|
height: 8px;
|
|
background: #2a2a3e;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-left: 15px;
|
|
}
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #00d4ff, #7c3aed);
|
|
transition: width 0.3s;
|
|
}
|
|
.btn-download {
|
|
padding: 8px 16px;
|
|
background: #10b981;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: #fff;
|
|
cursor: pointer;
|
|
font-size: 0.85em;
|
|
}
|
|
.btn-download:hover { background: #059669; }
|
|
.control-buttons { display: flex; gap: 10px; margin-bottom: 20px; }
|
|
.control-buttons button {
|
|
padding: 10px 20px;
|
|
background: #2a2a3e;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: #e0e0e0;
|
|
cursor: pointer;
|
|
}
|
|
.control-buttons button:hover { background: #3a3a4e; }
|
|
#search-results { display: none; }
|
|
#search-results.active { display: block; }
|
|
.empty-state { text-align: center; color: #666; padding: 40px; }
|
|
`),
|
|
|
|
E('div', { 'class': 'newsbin-container' }, [
|
|
E('h2', { 'style': 'color: #e0e0e0; margin-bottom: 20px;' }, [
|
|
E('span', { 'style': 'color: #00d4ff;' }, 'Newsbin'),
|
|
' - Usenet'
|
|
]),
|
|
|
|
// Status cards
|
|
E('div', { 'class': 'status-grid' }, [
|
|
E('div', { 'class': 'status-card ' + (sab.running ? 'running' : 'stopped') }, [
|
|
E('div', { 'class': 'status-value' }, sab.running ? sab.speed || '0 B/s' : 'OFF'),
|
|
E('div', { 'class': 'status-label' }, 'SABnzbd Speed')
|
|
]),
|
|
E('div', { 'class': 'status-card ' + (sab.running ? 'running' : 'stopped') }, [
|
|
E('div', { 'class': 'status-value' }, String(sab.queue_size || 0)),
|
|
E('div', { 'class': 'status-label' }, 'Queue Items')
|
|
]),
|
|
E('div', { 'class': 'status-card ' + (sab.running ? 'running' : 'stopped') }, [
|
|
E('div', { 'class': 'status-value' }, (sab.disk_free || '?') + ' GB'),
|
|
E('div', { 'class': 'status-label' }, 'Disk Free')
|
|
]),
|
|
E('div', { 'class': 'status-card ' + (hydra.running ? 'running' : 'stopped') }, [
|
|
E('div', { 'class': 'status-value' }, hydra.running ? 'ON' : 'OFF'),
|
|
E('div', { 'class': 'status-label' }, 'NZBHydra Search')
|
|
])
|
|
]),
|
|
|
|
// Control buttons
|
|
E('div', { 'class': 'control-buttons' }, [
|
|
E('button', { 'id': 'btn-pause' }, 'Pause'),
|
|
E('button', { 'id': 'btn-resume' }, 'Resume'),
|
|
E('a', { 'href': sab.url || '#', 'target': '_blank', 'style': 'padding: 10px 20px; background: #2a2a3e; border-radius: 6px; color: #00d4ff; text-decoration: none;' }, 'Open SABnzbd'),
|
|
E('a', { 'href': hydra.url || '#', 'target': '_blank', 'style': 'padding: 10px 20px; background: #2a2a3e; border-radius: 6px; color: #7c3aed; text-decoration: none;' }, 'Open NZBHydra')
|
|
]),
|
|
|
|
// Search box
|
|
E('div', { 'class': 'search-box' }, [
|
|
E('input', { 'type': 'text', 'id': 'search-input', 'placeholder': 'Search Usenet...' }),
|
|
E('button', { 'id': 'btn-search' }, 'Search')
|
|
]),
|
|
|
|
// Search results
|
|
E('div', { 'id': 'search-results' }, [
|
|
E('h3', { 'class': 'section-title' }, 'Search Results'),
|
|
E('div', { 'id': 'results-list' })
|
|
]),
|
|
|
|
// Queue
|
|
E('h3', { 'class': 'section-title' }, 'Download Queue'),
|
|
E('div', { 'id': 'queue-list' },
|
|
queue.length === 0
|
|
? E('div', { 'class': 'empty-state' }, 'Queue is empty')
|
|
: queue.map(function(item) {
|
|
return E('div', { 'class': 'queue-item' }, [
|
|
E('div', { 'class': 'item-info' }, [
|
|
E('div', { 'class': 'item-name' }, item.filename),
|
|
E('div', { 'class': 'item-meta' }, item.size + ' - ' + item.timeleft + ' remaining')
|
|
]),
|
|
E('div', { 'class': 'progress-bar' }, [
|
|
E('div', { 'class': 'progress-fill', 'style': 'width: ' + (item.percentage || 0) + '%' })
|
|
])
|
|
]);
|
|
})
|
|
),
|
|
|
|
// History
|
|
E('h3', { 'class': 'section-title' }, 'Recent Downloads'),
|
|
E('div', { 'id': 'history-list' },
|
|
history.length === 0
|
|
? E('div', { 'class': 'empty-state' }, 'No download history')
|
|
: history.slice(0, 10).map(function(item) {
|
|
return E('div', { 'class': 'queue-item' }, [
|
|
E('div', { 'class': 'item-info' }, [
|
|
E('div', { 'class': 'item-name' }, item.name),
|
|
E('div', { 'class': 'item-meta' }, item.size + ' - ' + item.status)
|
|
])
|
|
]);
|
|
})
|
|
)
|
|
])
|
|
]);
|
|
|
|
// Event handlers
|
|
var searchInput = view.querySelector('#search-input');
|
|
var btnSearch = view.querySelector('#btn-search');
|
|
var searchResults = view.querySelector('#search-results');
|
|
var resultsList = view.querySelector('#results-list');
|
|
|
|
function doSearch() {
|
|
var query = searchInput.value.trim();
|
|
if (!query) return;
|
|
|
|
btnSearch.textContent = 'Searching...';
|
|
btnSearch.disabled = true;
|
|
|
|
callSearch(query).then(function(results) {
|
|
resultsList.innerHTML = '';
|
|
|
|
if (results.length === 0) {
|
|
resultsList.appendChild(E('div', { 'class': 'empty-state' }, 'No results found'));
|
|
} else {
|
|
results.forEach(function(item) {
|
|
var sizeMB = Math.round(item.size / (1024 * 1024));
|
|
var resultDiv = E('div', { 'class': 'result-item' }, [
|
|
E('div', { 'class': 'item-info' }, [
|
|
E('div', { 'class': 'item-name' }, item.title),
|
|
E('div', { 'class': 'item-meta' }, sizeMB + ' MB')
|
|
]),
|
|
E('button', { 'class': 'btn-download', 'data-url': item.link }, 'Download')
|
|
]);
|
|
resultsList.appendChild(resultDiv);
|
|
});
|
|
|
|
// Add download handlers
|
|
resultsList.querySelectorAll('.btn-download').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var url = btn.dataset.url;
|
|
btn.textContent = 'Adding...';
|
|
btn.disabled = true;
|
|
|
|
callAddNzb(url, '').then(function(result) {
|
|
if (result.success) {
|
|
btn.textContent = 'Added!';
|
|
btn.style.background = '#059669';
|
|
} else {
|
|
btn.textContent = 'Failed';
|
|
btn.style.background = '#ef4444';
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
searchResults.classList.add('active');
|
|
}).finally(function() {
|
|
btnSearch.textContent = 'Search';
|
|
btnSearch.disabled = false;
|
|
});
|
|
}
|
|
|
|
btnSearch.addEventListener('click', doSearch);
|
|
searchInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') doSearch();
|
|
});
|
|
|
|
// Pause/Resume
|
|
view.querySelector('#btn-pause').addEventListener('click', function() {
|
|
callPause().then(function() { ui.addNotification(null, E('p', 'Queue paused')); });
|
|
});
|
|
|
|
view.querySelector('#btn-resume').addEventListener('click', function() {
|
|
callResume().then(function() { ui.addNotification(null, E('p', 'Queue resumed')); });
|
|
});
|
|
|
|
// Auto-refresh queue
|
|
poll.add(function() {
|
|
return callQueue().then(function(queue) {
|
|
var queueList = document.querySelector('#queue-list');
|
|
if (queueList && queue.length > 0) {
|
|
queueList.innerHTML = '';
|
|
queue.forEach(function(item) {
|
|
var queueDiv = E('div', { 'class': 'queue-item' }, [
|
|
E('div', { 'class': 'item-info' }, [
|
|
E('div', { 'class': 'item-name' }, item.filename),
|
|
E('div', { 'class': 'item-meta' }, item.size + ' - ' + item.timeleft + ' remaining')
|
|
]),
|
|
E('div', { 'class': 'progress-bar' }, [
|
|
E('div', { 'class': 'progress-fill', 'style': 'width: ' + (item.percentage || 0) + '%' })
|
|
])
|
|
]);
|
|
queueList.appendChild(queueDiv);
|
|
});
|
|
}
|
|
});
|
|
}, 5);
|
|
|
|
return view;
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|