feat(newsbin): Add Usenet search and download system
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>
This commit is contained in:
parent
078a3bea5f
commit
aef0284b44
27
package/secubox/luci-app-newsbin/Makefile
Normal file
27
package/secubox/luci-app-newsbin/Makefile
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-newsbin
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI Newsbin - Usenet Search & Download
|
||||||
|
LUCI_DEPENDS:=+secubox-app-sabnzbd +secubox-app-nzbhydra +luci-base
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/luci-app-newsbin/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-newsbin.json $(1)/usr/share/luci/menu.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-newsbin.json $(1)/usr/share/rpcd/acl.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.newsbin $(1)/usr/libexec/rpcd/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/newsbin
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/newsbin/*.js $(1)/www/luci-static/resources/view/newsbin/
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,luci-app-newsbin))
|
||||||
@ -0,0 +1,341 @@
|
|||||||
|
'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
|
||||||
|
});
|
||||||
@ -0,0 +1,285 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# RPCD handler for Newsbin - Usenet Search & Download
|
||||||
|
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
SAB_PORT="8085"
|
||||||
|
HYDRA_PORT="5076"
|
||||||
|
SAB_CONFIG="/srv/sabnzbd/config/sabnzbd.ini"
|
||||||
|
|
||||||
|
get_sab_api() {
|
||||||
|
grep "^api_key" "$SAB_CONFIG" 2>/dev/null | cut -d'=' -f2 | tr -d ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
echo '{"status":{},"queue":{},"history":{},"search":{"query":"string"},"add_nzb":{"url":"string","category":"string"},"pause":{},"resume":{},"remove":{"nzo_id":"string"},"config":{}}'
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
status)
|
||||||
|
json_init
|
||||||
|
|
||||||
|
# SABnzbd status
|
||||||
|
local sab_running="false"
|
||||||
|
local sab_speed="0"
|
||||||
|
local sab_queue="0"
|
||||||
|
local sab_disk="0"
|
||||||
|
|
||||||
|
if lxc-info -n sabnzbd 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
sab_running="true"
|
||||||
|
local api_key=$(get_sab_api)
|
||||||
|
if [ -n "$api_key" ]; then
|
||||||
|
local sab_data=$(curl -s "http://127.0.0.1:$SAB_PORT/api?mode=queue&output=json&apikey=$api_key" 2>/dev/null)
|
||||||
|
if [ -n "$sab_data" ]; then
|
||||||
|
sab_speed=$(echo "$sab_data" | jsonfilter -e '@.queue.speed' 2>/dev/null || echo "0")
|
||||||
|
sab_queue=$(echo "$sab_data" | jsonfilter -e '@.queue.noofslots' 2>/dev/null || echo "0")
|
||||||
|
sab_disk=$(echo "$sab_data" | jsonfilter -e '@.queue.diskspace1' 2>/dev/null || echo "0")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# NZBHydra status
|
||||||
|
local hydra_running="false"
|
||||||
|
if lxc-info -n nzbhydra 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
hydra_running="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_add_object "sabnzbd"
|
||||||
|
json_add_boolean "running" "$sab_running"
|
||||||
|
json_add_string "speed" "$sab_speed"
|
||||||
|
json_add_int "queue_size" "$sab_queue"
|
||||||
|
json_add_string "disk_free" "$sab_disk"
|
||||||
|
json_add_string "url" "http://127.0.0.1:$SAB_PORT/"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_add_object "nzbhydra"
|
||||||
|
json_add_boolean "running" "$hydra_running"
|
||||||
|
json_add_string "url" "http://127.0.0.1:$HYDRA_PORT/"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
;;
|
||||||
|
|
||||||
|
queue)
|
||||||
|
json_init
|
||||||
|
json_add_array "items"
|
||||||
|
|
||||||
|
local api_key=$(get_sab_api)
|
||||||
|
if [ -n "$api_key" ]; then
|
||||||
|
local queue_data=$(curl -s "http://127.0.0.1:$SAB_PORT/api?mode=queue&output=json&apikey=$api_key" 2>/dev/null)
|
||||||
|
if [ -n "$queue_data" ]; then
|
||||||
|
echo "$queue_data" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
for slot in data.get('queue', {}).get('slots', []):
|
||||||
|
print(json.dumps({
|
||||||
|
'nzo_id': slot.get('nzo_id', ''),
|
||||||
|
'filename': slot.get('filename', ''),
|
||||||
|
'size': slot.get('size', ''),
|
||||||
|
'percentage': slot.get('percentage', '0'),
|
||||||
|
'status': slot.get('status', ''),
|
||||||
|
'timeleft': slot.get('timeleft', '')
|
||||||
|
}))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
" 2>/dev/null | while read item; do
|
||||||
|
echo "$item" | {
|
||||||
|
read line
|
||||||
|
json_add_object ""
|
||||||
|
local nzo_id=$(echo "$line" | jsonfilter -e '@.nzo_id')
|
||||||
|
local filename=$(echo "$line" | jsonfilter -e '@.filename')
|
||||||
|
local size=$(echo "$line" | jsonfilter -e '@.size')
|
||||||
|
local percentage=$(echo "$line" | jsonfilter -e '@.percentage')
|
||||||
|
local status=$(echo "$line" | jsonfilter -e '@.status')
|
||||||
|
local timeleft=$(echo "$line" | jsonfilter -e '@.timeleft')
|
||||||
|
json_add_string "nzo_id" "$nzo_id"
|
||||||
|
json_add_string "filename" "$filename"
|
||||||
|
json_add_string "size" "$size"
|
||||||
|
json_add_string "percentage" "$percentage"
|
||||||
|
json_add_string "status" "$status"
|
||||||
|
json_add_string "timeleft" "$timeleft"
|
||||||
|
json_close_object
|
||||||
|
}
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
;;
|
||||||
|
|
||||||
|
history)
|
||||||
|
json_init
|
||||||
|
json_add_array "items"
|
||||||
|
|
||||||
|
local api_key=$(get_sab_api)
|
||||||
|
if [ -n "$api_key" ]; then
|
||||||
|
local hist_data=$(curl -s "http://127.0.0.1:$SAB_PORT/api?mode=history&limit=20&output=json&apikey=$api_key" 2>/dev/null)
|
||||||
|
if [ -n "$hist_data" ]; then
|
||||||
|
echo "$hist_data" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
for slot in data.get('history', {}).get('slots', []):
|
||||||
|
print(json.dumps({
|
||||||
|
'nzo_id': slot.get('nzo_id', ''),
|
||||||
|
'name': slot.get('name', ''),
|
||||||
|
'size': slot.get('size', ''),
|
||||||
|
'status': slot.get('status', ''),
|
||||||
|
'completed': slot.get('completed', 0)
|
||||||
|
}))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
" 2>/dev/null | while read item; do
|
||||||
|
echo "$item" | {
|
||||||
|
read line
|
||||||
|
json_add_object ""
|
||||||
|
local nzo_id=$(echo "$line" | jsonfilter -e '@.nzo_id')
|
||||||
|
local name=$(echo "$line" | jsonfilter -e '@.name')
|
||||||
|
local size=$(echo "$line" | jsonfilter -e '@.size')
|
||||||
|
local status=$(echo "$line" | jsonfilter -e '@.status')
|
||||||
|
json_add_string "nzo_id" "$nzo_id"
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "size" "$size"
|
||||||
|
json_add_string "status" "$status"
|
||||||
|
json_close_object
|
||||||
|
}
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
;;
|
||||||
|
|
||||||
|
search)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var query query
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_array "results"
|
||||||
|
|
||||||
|
if [ -n "$query" ]; then
|
||||||
|
local search_url="http://127.0.0.1:$HYDRA_PORT/api?t=search&q=$(echo "$query" | sed 's/ /%20/g')"
|
||||||
|
local result=$(curl -s "$search_url" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$result" ]; then
|
||||||
|
echo "$result" | python3 -c "
|
||||||
|
import sys, xml.etree.ElementTree as ET, json
|
||||||
|
xml = sys.stdin.read()
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(xml)
|
||||||
|
for item in root.findall('.//item')[:20]:
|
||||||
|
title = item.find('title').text if item.find('title') is not None else ''
|
||||||
|
link = item.find('link').text if item.find('link') is not None else ''
|
||||||
|
size = item.find('enclosure').get('length', '0') if item.find('enclosure') is not None else '0'
|
||||||
|
print(json.dumps({'title': title, 'link': link, 'size': int(size)}))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
" 2>/dev/null | while read item; do
|
||||||
|
echo "$item" | {
|
||||||
|
read line
|
||||||
|
json_add_object ""
|
||||||
|
local title=$(echo "$line" | jsonfilter -e '@.title')
|
||||||
|
local link=$(echo "$line" | jsonfilter -e '@.link')
|
||||||
|
local size=$(echo "$line" | jsonfilter -e '@.size')
|
||||||
|
json_add_string "title" "$title"
|
||||||
|
json_add_string "link" "$link"
|
||||||
|
json_add_int "size" "$size"
|
||||||
|
json_close_object
|
||||||
|
}
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
;;
|
||||||
|
|
||||||
|
add_nzb)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var url url
|
||||||
|
json_get_var category category
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$url" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "URL required"
|
||||||
|
else
|
||||||
|
local api_key=$(get_sab_api)
|
||||||
|
local add_url="http://127.0.0.1:$SAB_PORT/api?mode=addurl&name=$(echo "$url" | sed 's/&/%26/g')&apikey=$api_key"
|
||||||
|
[ -n "$category" ] && add_url="$add_url&cat=$category"
|
||||||
|
|
||||||
|
local result=$(curl -s "$add_url" 2>/dev/null)
|
||||||
|
if echo "$result" | grep -q "ok"; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "NZB added to queue"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Failed to add NZB"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
;;
|
||||||
|
|
||||||
|
pause)
|
||||||
|
local api_key=$(get_sab_api)
|
||||||
|
curl -s "http://127.0.0.1:$SAB_PORT/api?mode=pause&apikey=$api_key" >/dev/null 2>&1
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_dump
|
||||||
|
;;
|
||||||
|
|
||||||
|
resume)
|
||||||
|
local api_key=$(get_sab_api)
|
||||||
|
curl -s "http://127.0.0.1:$SAB_PORT/api?mode=resume&apikey=$api_key" >/dev/null 2>&1
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_dump
|
||||||
|
;;
|
||||||
|
|
||||||
|
remove)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var nzo_id nzo_id
|
||||||
|
|
||||||
|
local api_key=$(get_sab_api)
|
||||||
|
curl -s "http://127.0.0.1:$SAB_PORT/api?mode=queue&name=delete&value=$nzo_id&apikey=$api_key" >/dev/null 2>&1
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_dump
|
||||||
|
;;
|
||||||
|
|
||||||
|
config)
|
||||||
|
json_init
|
||||||
|
|
||||||
|
# NNTP servers
|
||||||
|
json_add_array "servers"
|
||||||
|
for section in $(uci show sabnzbd 2>/dev/null | grep "=nntp$" | sed 's/sabnzbd\.\(.*\)=nntp/\1/'); do
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "id" "$section"
|
||||||
|
json_add_string "name" "$(uci -q get sabnzbd.$section.name)"
|
||||||
|
json_add_string "host" "$(uci -q get sabnzbd.$section.host)"
|
||||||
|
json_add_string "port" "$(uci -q get sabnzbd.$section.port)"
|
||||||
|
json_add_boolean "ssl" "$(uci -q get sabnzbd.$section.ssl)"
|
||||||
|
json_add_string "connections" "$(uci -q get sabnzbd.$section.connections)"
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
json_close_array
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo '{"error":"Unknown method"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"admin/services/newsbin": {
|
||||||
|
"title": "Newsbin",
|
||||||
|
"order": 50,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "newsbin/overview"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-newsbin"],
|
||||||
|
"uci": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"luci-app-newsbin": {
|
||||||
|
"description": "Newsbin - Usenet Search & Download",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.newsbin": ["status", "queue", "history", "search"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.newsbin": ["add_nzb", "pause", "resume", "remove", "config"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
package/secubox/secubox-app-nzbhydra/Makefile
Normal file
40
package/secubox/secubox-app-nzbhydra/Makefile
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=secubox-app-nzbhydra
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=SecuBox <secubox@cybermind.fr>
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
define Package/secubox-app-nzbhydra
|
||||||
|
SECTION:=secubox
|
||||||
|
CATEGORY:=SecuBox
|
||||||
|
SUBMENU:=Apps
|
||||||
|
TITLE:=NZBHydra2 - Usenet Meta Search
|
||||||
|
DEPENDS:=+lxc +curl +jq
|
||||||
|
PKGARCH:=all
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-nzbhydra/description
|
||||||
|
NZBHydra2 meta search for Usenet indexers.
|
||||||
|
Aggregates multiple NZB indexers into single search.
|
||||||
|
Provides Newznab API for Sonarr/Radarr integration.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-nzbhydra/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/sbin
|
||||||
|
$(INSTALL_BIN) ./files/usr/sbin/nzbhydractl $(1)/usr/sbin/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/etc/config
|
||||||
|
$(INSTALL_CONF) ./files/etc/config/nzbhydra $(1)/etc/config/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/etc/init.d
|
||||||
|
$(INSTALL_BIN) ./files/etc/init.d/nzbhydra $(1)/etc/init.d/
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,secubox-app-nzbhydra))
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
config nzbhydra 'main'
|
||||||
|
option enabled '1'
|
||||||
|
option port '5076'
|
||||||
|
option data_dir '/srv/nzbhydra'
|
||||||
|
option memory '256M'
|
||||||
|
|
||||||
|
config downloader 'sabnzbd'
|
||||||
|
option name 'SABnzbd'
|
||||||
|
option type 'sabnzbd'
|
||||||
|
option host 'http://127.0.0.1:8085'
|
||||||
|
option api_key ''
|
||||||
|
|
||||||
|
config haproxy 'exposure'
|
||||||
|
option domain 'nzbhydra.gk2.secubox.in'
|
||||||
|
option ssl '1'
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
|
||||||
|
START=99
|
||||||
|
STOP=10
|
||||||
|
USE_PROCD=1
|
||||||
|
|
||||||
|
CONTAINER_NAME="nzbhydra"
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
local enabled
|
||||||
|
config_load nzbhydra
|
||||||
|
config_get enabled main enabled '0'
|
||||||
|
|
||||||
|
[ "$enabled" = "1" ] || return 0
|
||||||
|
|
||||||
|
if lxc-info -n "$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||||
|
lxc-start -n "$CONTAINER_NAME" -d 2>/dev/null
|
||||||
|
logger -t nzbhydra "NZBHydra container started"
|
||||||
|
else
|
||||||
|
logger -t nzbhydra "Container not installed. Run: nzbhydractl install"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
if lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
lxc-stop -n "$CONTAINER_NAME" -t 30
|
||||||
|
logger -t nzbhydra "NZBHydra container stopped"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_service() {
|
||||||
|
stop_service
|
||||||
|
start_service
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger "nzbhydra"
|
||||||
|
}
|
||||||
352
package/secubox/secubox-app-nzbhydra/files/usr/sbin/nzbhydractl
Normal file
352
package/secubox/secubox-app-nzbhydra/files/usr/sbin/nzbhydractl
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# NZBHydra2 Controller - Usenet Meta Search
|
||||||
|
# LXC container management for NZBHydra2
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
CONTAINER_NAME="nzbhydra"
|
||||||
|
CONTAINER_DIR="/srv/lxc/$CONTAINER_NAME"
|
||||||
|
DATA_DIR="/srv/nzbhydra"
|
||||||
|
DOCKER_IMAGE="linuxserver/nzbhydra2:latest"
|
||||||
|
CONFIG="nzbhydra"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log_info() { logger -t nzbhydra -p user.info "$*"; echo "[INFO] $*"; }
|
||||||
|
log_error() { logger -t nzbhydra -p user.error "$*"; echo "[ERROR] $*" >&2; }
|
||||||
|
log_ok() { echo "[OK] $*"; }
|
||||||
|
|
||||||
|
# UCI helpers
|
||||||
|
uci_get() { uci -q get "$CONFIG.$1"; }
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Install container from Docker image
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_install() {
|
||||||
|
log_info "Installing NZBHydra2 container..."
|
||||||
|
|
||||||
|
# Check for podman or docker
|
||||||
|
local runtime=""
|
||||||
|
if command -v podman >/dev/null 2>&1; then
|
||||||
|
runtime="podman"
|
||||||
|
elif command -v docker >/dev/null 2>&1; then
|
||||||
|
runtime="docker"
|
||||||
|
else
|
||||||
|
log_error "Neither podman nor docker found. Install one first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
mkdir -p "$CONTAINER_DIR/rootfs"
|
||||||
|
mkdir -p "$DATA_DIR/config"
|
||||||
|
|
||||||
|
# Pull and extract image
|
||||||
|
log_info "Pulling Docker image: $DOCKER_IMAGE"
|
||||||
|
if [ "$runtime" = "podman" ]; then
|
||||||
|
podman pull "$DOCKER_IMAGE" || { log_error "Failed to pull image"; return 1; }
|
||||||
|
local container_id=$(podman create "$DOCKER_IMAGE")
|
||||||
|
podman export "$container_id" | tar -xf - -C "$CONTAINER_DIR/rootfs"
|
||||||
|
podman rm "$container_id" >/dev/null
|
||||||
|
else
|
||||||
|
docker pull "$DOCKER_IMAGE" || { log_error "Failed to pull image"; return 1; }
|
||||||
|
local container_id=$(docker create "$DOCKER_IMAGE")
|
||||||
|
docker export "$container_id" | tar -xf - -C "$CONTAINER_DIR/rootfs"
|
||||||
|
docker rm "$container_id" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create LXC config
|
||||||
|
local memory=$(uci_get main.memory)
|
||||||
|
[ -z "$memory" ] && memory="256M"
|
||||||
|
|
||||||
|
cat > "$CONTAINER_DIR/config" <<EOF
|
||||||
|
lxc.uts.name = $CONTAINER_NAME
|
||||||
|
lxc.rootfs.path = dir:$CONTAINER_DIR/rootfs
|
||||||
|
lxc.init.cmd = /init
|
||||||
|
|
||||||
|
# Network - share host namespace
|
||||||
|
lxc.namespace.share.net = 1
|
||||||
|
|
||||||
|
# Capabilities
|
||||||
|
lxc.cap.drop = sys_admin sys_boot sys_module sys_rawio sys_time
|
||||||
|
|
||||||
|
# Memory limit
|
||||||
|
lxc.cgroup2.memory.max = $memory
|
||||||
|
|
||||||
|
# Mounts
|
||||||
|
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
||||||
|
lxc.mount.entry = $DATA_DIR/config config none bind,create=dir 0 0
|
||||||
|
lxc.mount.entry = tmpfs tmp tmpfs defaults 0 0
|
||||||
|
lxc.mount.entry = tmpfs run tmpfs defaults 0 0
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
lxc.environment = PUID=1000
|
||||||
|
lxc.environment = PGID=1000
|
||||||
|
lxc.environment = TZ=Europe/Paris
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_ok "NZBHydra2 container installed"
|
||||||
|
log_info "Run: nzbhydractl start"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Start container
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_start() {
|
||||||
|
if ! [ -d "$CONTAINER_DIR/rootfs" ]; then
|
||||||
|
log_error "Container not installed. Run: nzbhydractl install"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
log_info "NZBHydra2 already running"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Starting NZBHydra2..."
|
||||||
|
lxc-start -n "$CONTAINER_NAME" -d -f "$CONTAINER_DIR/config"
|
||||||
|
|
||||||
|
# Wait for startup
|
||||||
|
sleep 5
|
||||||
|
if lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
local port=$(uci_get main.port)
|
||||||
|
[ -z "$port" ] && port="5076"
|
||||||
|
log_ok "NZBHydra2 started on http://127.0.0.1:$port/"
|
||||||
|
else
|
||||||
|
log_error "Failed to start NZBHydra2"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Stop container
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_stop() {
|
||||||
|
if ! lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
log_info "NZBHydra2 not running"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Stopping NZBHydra2..."
|
||||||
|
lxc-stop -n "$CONTAINER_NAME" -t 30
|
||||||
|
log_ok "NZBHydra2 stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Restart container
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_restart() {
|
||||||
|
cmd_stop
|
||||||
|
sleep 2
|
||||||
|
cmd_start
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Status
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_status() {
|
||||||
|
echo "=== NZBHydra2 Status ==="
|
||||||
|
|
||||||
|
# Container state
|
||||||
|
if lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
echo "Container: RUNNING"
|
||||||
|
else
|
||||||
|
echo "Container: STOPPED"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local port=$(uci_get main.port)
|
||||||
|
[ -z "$port" ] && port="5076"
|
||||||
|
|
||||||
|
# Check if API responds
|
||||||
|
if curl -s "http://127.0.0.1:$port/api?t=caps" >/dev/null 2>&1; then
|
||||||
|
echo "API: OK"
|
||||||
|
else
|
||||||
|
echo "API: Not responding"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Web UI: http://127.0.0.1:$port/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Logs
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_logs() {
|
||||||
|
local lines="${1:-50}"
|
||||||
|
if [ -f "$DATA_DIR/config/logs/nzbhydra2.log" ]; then
|
||||||
|
tail -n "$lines" "$DATA_DIR/config/logs/nzbhydra2.log"
|
||||||
|
else
|
||||||
|
log_info "No logs yet. NZBHydra may not have run."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Shell access
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_shell() {
|
||||||
|
if ! lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
log_error "Container not running"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
lxc-attach -n "$CONTAINER_NAME" -- /bin/bash
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Link SABnzbd as downloader
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_link_sabnzbd() {
|
||||||
|
log_info "Linking SABnzbd..."
|
||||||
|
|
||||||
|
# Get SABnzbd API key
|
||||||
|
local sab_ini="/srv/sabnzbd/config/sabnzbd.ini"
|
||||||
|
if [ ! -f "$sab_ini" ]; then
|
||||||
|
log_error "SABnzbd not configured. Start SABnzbd first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local sab_api=$(grep "^api_key" "$sab_ini" 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
|
||||||
|
local sab_port=$(uci -q get sabnzbd.main.port)
|
||||||
|
[ -z "$sab_port" ] && sab_port="8085"
|
||||||
|
|
||||||
|
if [ -z "$sab_api" ]; then
|
||||||
|
log_error "SABnzbd API key not found. Configure SABnzbd first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update UCI
|
||||||
|
uci set "nzbhydra.sabnzbd.host=http://127.0.0.1:$sab_port"
|
||||||
|
uci set "nzbhydra.sabnzbd.api_key=$sab_api"
|
||||||
|
uci commit nzbhydra
|
||||||
|
|
||||||
|
log_ok "SABnzbd linked: http://127.0.0.1:$sab_port"
|
||||||
|
log_info "Restart NZBHydra to apply changes"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Search
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_search() {
|
||||||
|
local query="$1"
|
||||||
|
[ -z "$query" ] && { log_error "Usage: nzbhydractl search <query>"; return 1; }
|
||||||
|
|
||||||
|
local port=$(uci_get main.port)
|
||||||
|
[ -z "$port" ] && port="5076"
|
||||||
|
|
||||||
|
log_info "Searching: $query"
|
||||||
|
|
||||||
|
local result=$(curl -s "http://127.0.0.1:$port/api?t=search&q=$(echo "$query" | sed 's/ /%20/g')" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$result" ]; then
|
||||||
|
echo "$result" | python3 -c "
|
||||||
|
import sys, xml.etree.ElementTree as ET
|
||||||
|
xml = sys.stdin.read()
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(xml)
|
||||||
|
items = root.findall('.//item')
|
||||||
|
for i, item in enumerate(items[:10], 1):
|
||||||
|
title = item.find('title').text if item.find('title') is not None else 'N/A'
|
||||||
|
size = item.find('enclosure').get('length', '0') if item.find('enclosure') is not None else '0'
|
||||||
|
size_mb = int(size) // (1024*1024)
|
||||||
|
print(f'{i}. {title} ({size_mb} MB)')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error parsing: {e}')
|
||||||
|
" 2>/dev/null
|
||||||
|
else
|
||||||
|
log_error "Search failed or no results"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Configure HAProxy exposure
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_configure_haproxy() {
|
||||||
|
local domain=$(uci_get exposure.domain)
|
||||||
|
[ -z "$domain" ] && domain="nzbhydra.gk2.secubox.in"
|
||||||
|
|
||||||
|
local port=$(uci_get main.port)
|
||||||
|
[ -z "$port" ] && port="5076"
|
||||||
|
|
||||||
|
log_info "Configuring HAProxy for $domain"
|
||||||
|
|
||||||
|
# Create backend
|
||||||
|
uci set haproxy.nzbhydra_web=backend
|
||||||
|
uci set haproxy.nzbhydra_web.name='nzbhydra_web'
|
||||||
|
uci set haproxy.nzbhydra_web.mode='http'
|
||||||
|
uci set haproxy.nzbhydra_web.server="nzbhydra 127.0.0.1:$port weight 100 check"
|
||||||
|
|
||||||
|
# Create vhost
|
||||||
|
local vhost_id=$(echo "$domain" | tr '.' '_')
|
||||||
|
uci set "haproxy.$vhost_id=vhost"
|
||||||
|
uci set "haproxy.$vhost_id.domain=$domain"
|
||||||
|
uci set "haproxy.$vhost_id.backend=mitmproxy_inspector"
|
||||||
|
uci set "haproxy.$vhost_id.original_backend=nzbhydra_web"
|
||||||
|
uci set "haproxy.$vhost_id.ssl=1"
|
||||||
|
uci set "haproxy.$vhost_id.ssl_redirect=1"
|
||||||
|
uci set "haproxy.$vhost_id.acme=1"
|
||||||
|
uci commit haproxy
|
||||||
|
|
||||||
|
# Add mitmproxy route
|
||||||
|
if [ -f /srv/mitmproxy/haproxy-routes.json ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
with open('/srv/mitmproxy/haproxy-routes.json') as f:
|
||||||
|
routes = json.load(f)
|
||||||
|
routes['$domain'] = ['127.0.0.1', $port]
|
||||||
|
with open('/srv/mitmproxy/haproxy-routes.json', 'w') as f:
|
||||||
|
json.dump(routes, f, indent=2)
|
||||||
|
" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reload
|
||||||
|
haproxyctl reload 2>/dev/null || true
|
||||||
|
/etc/init.d/mitmproxy restart 2>/dev/null || true
|
||||||
|
|
||||||
|
log_ok "HAProxy configured: https://$domain/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Uninstall
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_uninstall() {
|
||||||
|
log_info "Uninstalling NZBHydra2..."
|
||||||
|
|
||||||
|
cmd_stop 2>/dev/null
|
||||||
|
|
||||||
|
rm -rf "$CONTAINER_DIR"
|
||||||
|
log_info "Container removed. Data preserved in $DATA_DIR"
|
||||||
|
|
||||||
|
log_ok "NZBHydra2 uninstalled"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Main
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
case "$1" in
|
||||||
|
install) cmd_install ;;
|
||||||
|
start) cmd_start ;;
|
||||||
|
stop) cmd_stop ;;
|
||||||
|
restart) cmd_restart ;;
|
||||||
|
status) cmd_status ;;
|
||||||
|
logs) shift; cmd_logs "$@" ;;
|
||||||
|
shell) cmd_shell ;;
|
||||||
|
search) shift; cmd_search "$@" ;;
|
||||||
|
link-sabnzbd) cmd_link_sabnzbd ;;
|
||||||
|
configure-haproxy) cmd_configure_haproxy ;;
|
||||||
|
uninstall) cmd_uninstall ;;
|
||||||
|
*)
|
||||||
|
echo "NZBHydra2 Controller - Usenet Meta Search"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: nzbhydractl <command>"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " install Install container from Docker image"
|
||||||
|
echo " start Start NZBHydra2"
|
||||||
|
echo " stop Stop NZBHydra2"
|
||||||
|
echo " restart Restart NZBHydra2"
|
||||||
|
echo " status Show status"
|
||||||
|
echo " logs [n] Show last n log lines (default 50)"
|
||||||
|
echo " shell Interactive shell in container"
|
||||||
|
echo " search <query> Search indexers"
|
||||||
|
echo " link-sabnzbd Configure SABnzbd as downloader"
|
||||||
|
echo " configure-haproxy Setup HAProxy reverse proxy"
|
||||||
|
echo " uninstall Remove container (keeps data)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
40
package/secubox/secubox-app-sabnzbd/Makefile
Normal file
40
package/secubox/secubox-app-sabnzbd/Makefile
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=secubox-app-sabnzbd
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=SecuBox <secubox@cybermind.fr>
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
define Package/secubox-app-sabnzbd
|
||||||
|
SECTION:=secubox
|
||||||
|
CATEGORY:=SecuBox
|
||||||
|
SUBMENU:=Apps
|
||||||
|
TITLE:=SABnzbd - Usenet NZB Downloader
|
||||||
|
DEPENDS:=+lxc +curl +jq
|
||||||
|
PKGARCH:=all
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-sabnzbd/description
|
||||||
|
SABnzbd NZB downloader for Usenet.
|
||||||
|
Runs in LXC container with Docker image extraction.
|
||||||
|
Features: par2 repair, unrar, SSL NNTP support.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-sabnzbd/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/sbin
|
||||||
|
$(INSTALL_BIN) ./files/usr/sbin/sabnzbdctl $(1)/usr/sbin/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/etc/config
|
||||||
|
$(INSTALL_CONF) ./files/etc/config/sabnzbd $(1)/etc/config/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/etc/init.d
|
||||||
|
$(INSTALL_BIN) ./files/etc/init.d/sabnzbd $(1)/etc/init.d/
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,secubox-app-sabnzbd))
|
||||||
21
package/secubox/secubox-app-sabnzbd/files/etc/config/sabnzbd
Normal file
21
package/secubox/secubox-app-sabnzbd/files/etc/config/sabnzbd
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
config sabnzbd 'main'
|
||||||
|
option enabled '1'
|
||||||
|
option port '8085'
|
||||||
|
option data_dir '/srv/sabnzbd'
|
||||||
|
option download_dir '/srv/downloads/usenet'
|
||||||
|
option incomplete_dir '/srv/downloads/usenet/incomplete'
|
||||||
|
option memory '512M'
|
||||||
|
|
||||||
|
config nntp 'eweka'
|
||||||
|
option name 'EWEKA'
|
||||||
|
option host 'news.eweka.nl'
|
||||||
|
option port '563'
|
||||||
|
option ssl '1'
|
||||||
|
option username '590143'
|
||||||
|
option password 'Gk24@EWEKA;001'
|
||||||
|
option connections '50'
|
||||||
|
option priority '0'
|
||||||
|
|
||||||
|
config haproxy 'exposure'
|
||||||
|
option domain 'sabnzbd.gk2.secubox.in'
|
||||||
|
option ssl '1'
|
||||||
39
package/secubox/secubox-app-sabnzbd/files/etc/init.d/sabnzbd
Normal file
39
package/secubox/secubox-app-sabnzbd/files/etc/init.d/sabnzbd
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
|
||||||
|
START=99
|
||||||
|
STOP=10
|
||||||
|
USE_PROCD=1
|
||||||
|
|
||||||
|
CONTAINER_NAME="sabnzbd"
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
local enabled
|
||||||
|
config_load sabnzbd
|
||||||
|
config_get enabled main enabled '0'
|
||||||
|
|
||||||
|
[ "$enabled" = "1" ] || return 0
|
||||||
|
|
||||||
|
# Start LXC container
|
||||||
|
if lxc-info -n "$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||||
|
lxc-start -n "$CONTAINER_NAME" -d 2>/dev/null
|
||||||
|
logger -t sabnzbd "SABnzbd container started"
|
||||||
|
else
|
||||||
|
logger -t sabnzbd "Container not installed. Run: sabnzbdctl install"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
if lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
lxc-stop -n "$CONTAINER_NAME" -t 30
|
||||||
|
logger -t sabnzbd "SABnzbd container stopped"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_service() {
|
||||||
|
stop_service
|
||||||
|
start_service
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger "sabnzbd"
|
||||||
|
}
|
||||||
354
package/secubox/secubox-app-sabnzbd/files/usr/sbin/sabnzbdctl
Normal file
354
package/secubox/secubox-app-sabnzbd/files/usr/sbin/sabnzbdctl
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# SABnzbd Controller - Usenet NZB Downloader
|
||||||
|
# LXC container management for SABnzbd
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
CONTAINER_NAME="sabnzbd"
|
||||||
|
CONTAINER_DIR="/srv/lxc/$CONTAINER_NAME"
|
||||||
|
DATA_DIR="/srv/sabnzbd"
|
||||||
|
DOWNLOAD_DIR="/srv/downloads/usenet"
|
||||||
|
DOCKER_IMAGE="linuxserver/sabnzbd:latest"
|
||||||
|
CONFIG="sabnzbd"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log_info() { logger -t sabnzbd -p user.info "$*"; echo "[INFO] $*"; }
|
||||||
|
log_error() { logger -t sabnzbd -p user.error "$*"; echo "[ERROR] $*" >&2; }
|
||||||
|
log_ok() { echo "[OK] $*"; }
|
||||||
|
|
||||||
|
# UCI helpers
|
||||||
|
uci_get() { uci -q get "$CONFIG.$1"; }
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Install container from Docker image
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_install() {
|
||||||
|
log_info "Installing SABnzbd container..."
|
||||||
|
|
||||||
|
# Check for podman or docker
|
||||||
|
local runtime=""
|
||||||
|
if command -v podman >/dev/null 2>&1; then
|
||||||
|
runtime="podman"
|
||||||
|
elif command -v docker >/dev/null 2>&1; then
|
||||||
|
runtime="docker"
|
||||||
|
else
|
||||||
|
log_error "Neither podman nor docker found. Install one first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
mkdir -p "$CONTAINER_DIR/rootfs"
|
||||||
|
mkdir -p "$DATA_DIR/config"
|
||||||
|
mkdir -p "$DOWNLOAD_DIR"/{complete,incomplete,nzb}
|
||||||
|
|
||||||
|
# Pull and extract image
|
||||||
|
log_info "Pulling Docker image: $DOCKER_IMAGE"
|
||||||
|
if [ "$runtime" = "podman" ]; then
|
||||||
|
podman pull "$DOCKER_IMAGE" || { log_error "Failed to pull image"; return 1; }
|
||||||
|
local container_id=$(podman create "$DOCKER_IMAGE")
|
||||||
|
podman export "$container_id" | tar -xf - -C "$CONTAINER_DIR/rootfs"
|
||||||
|
podman rm "$container_id" >/dev/null
|
||||||
|
else
|
||||||
|
docker pull "$DOCKER_IMAGE" || { log_error "Failed to pull image"; return 1; }
|
||||||
|
local container_id=$(docker create "$DOCKER_IMAGE")
|
||||||
|
docker export "$container_id" | tar -xf - -C "$CONTAINER_DIR/rootfs"
|
||||||
|
docker rm "$container_id" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create LXC config
|
||||||
|
local memory=$(uci_get main.memory)
|
||||||
|
[ -z "$memory" ] && memory="512M"
|
||||||
|
|
||||||
|
cat > "$CONTAINER_DIR/config" <<EOF
|
||||||
|
lxc.uts.name = $CONTAINER_NAME
|
||||||
|
lxc.rootfs.path = dir:$CONTAINER_DIR/rootfs
|
||||||
|
lxc.init.cmd = /init
|
||||||
|
|
||||||
|
# Network - share host namespace
|
||||||
|
lxc.namespace.share.net = 1
|
||||||
|
|
||||||
|
# Capabilities
|
||||||
|
lxc.cap.drop = sys_admin sys_boot sys_module sys_rawio sys_time
|
||||||
|
|
||||||
|
# Memory limit
|
||||||
|
lxc.cgroup2.memory.max = $memory
|
||||||
|
|
||||||
|
# Mounts
|
||||||
|
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
||||||
|
lxc.mount.entry = $DATA_DIR/config config none bind,create=dir 0 0
|
||||||
|
lxc.mount.entry = $DOWNLOAD_DIR downloads none bind,create=dir 0 0
|
||||||
|
lxc.mount.entry = tmpfs tmp tmpfs defaults 0 0
|
||||||
|
lxc.mount.entry = tmpfs run tmpfs defaults 0 0
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
lxc.environment = PUID=1000
|
||||||
|
lxc.environment = PGID=1000
|
||||||
|
lxc.environment = TZ=Europe/Paris
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create startup wrapper
|
||||||
|
cat > "$CONTAINER_DIR/rootfs/start-sabnzbd.sh" <<'STARTEOF'
|
||||||
|
#!/bin/bash
|
||||||
|
export HOME=/config
|
||||||
|
export SABNZBD_HOME=/config
|
||||||
|
cd /app/sabnzbd
|
||||||
|
exec python3 SABnzbd.py --config-file /config/sabnzbd.ini --server 0.0.0.0:8085 --browser 0
|
||||||
|
STARTEOF
|
||||||
|
chmod +x "$CONTAINER_DIR/rootfs/start-sabnzbd.sh"
|
||||||
|
|
||||||
|
log_ok "SABnzbd container installed"
|
||||||
|
log_info "Run: sabnzbdctl start"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Start container
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_start() {
|
||||||
|
if ! [ -d "$CONTAINER_DIR/rootfs" ]; then
|
||||||
|
log_error "Container not installed. Run: sabnzbdctl install"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
log_info "SABnzbd already running"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Starting SABnzbd..."
|
||||||
|
lxc-start -n "$CONTAINER_NAME" -d -f "$CONTAINER_DIR/config"
|
||||||
|
|
||||||
|
# Wait for startup
|
||||||
|
sleep 3
|
||||||
|
if lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
local port=$(uci_get main.port)
|
||||||
|
[ -z "$port" ] && port="8085"
|
||||||
|
log_ok "SABnzbd started on http://127.0.0.1:$port/"
|
||||||
|
else
|
||||||
|
log_error "Failed to start SABnzbd"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Stop container
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_stop() {
|
||||||
|
if ! lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
log_info "SABnzbd not running"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Stopping SABnzbd..."
|
||||||
|
lxc-stop -n "$CONTAINER_NAME" -t 30
|
||||||
|
log_ok "SABnzbd stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Restart container
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_restart() {
|
||||||
|
cmd_stop
|
||||||
|
sleep 2
|
||||||
|
cmd_start
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Status
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_status() {
|
||||||
|
echo "=== SABnzbd Status ==="
|
||||||
|
|
||||||
|
# Container state
|
||||||
|
if lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
echo "Container: RUNNING"
|
||||||
|
else
|
||||||
|
echo "Container: STOPPED"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# API status
|
||||||
|
local port=$(uci_get main.port)
|
||||||
|
[ -z "$port" ] && port="8085"
|
||||||
|
local api_key=$(cat "$DATA_DIR/config/sabnzbd.ini" 2>/dev/null | grep "^api_key" | cut -d'=' -f2 | tr -d ' ')
|
||||||
|
|
||||||
|
if [ -n "$api_key" ]; then
|
||||||
|
local status=$(curl -s "http://127.0.0.1:$port/api?mode=queue&output=json&apikey=$api_key" 2>/dev/null)
|
||||||
|
if [ -n "$status" ]; then
|
||||||
|
local speed=$(echo "$status" | jsonfilter -e '@.queue.speed' 2>/dev/null)
|
||||||
|
local queue_size=$(echo "$status" | jsonfilter -e '@.queue.noofslots' 2>/dev/null)
|
||||||
|
local disk_free=$(echo "$status" | jsonfilter -e '@.queue.diskspace1' 2>/dev/null)
|
||||||
|
|
||||||
|
echo "Speed: ${speed:-0}"
|
||||||
|
echo "Queue: ${queue_size:-0} items"
|
||||||
|
echo "Disk Free: ${disk_free:-?} GB"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Web UI: http://127.0.0.1:$port/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Logs
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_logs() {
|
||||||
|
local lines="${1:-50}"
|
||||||
|
if [ -f "$DATA_DIR/config/logs/sabnzbd.log" ]; then
|
||||||
|
tail -n "$lines" "$DATA_DIR/config/logs/sabnzbd.log"
|
||||||
|
else
|
||||||
|
log_info "No logs yet. SABnzbd may not have run."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Shell access
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_shell() {
|
||||||
|
if ! lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
log_error "Container not running"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
lxc-attach -n "$CONTAINER_NAME" -- /bin/bash
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Configure NNTP server from UCI
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_add_server() {
|
||||||
|
local ini_file="$DATA_DIR/config/sabnzbd.ini"
|
||||||
|
|
||||||
|
if [ ! -f "$ini_file" ]; then
|
||||||
|
log_error "SABnzbd config not found. Start SABnzbd first to create initial config."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read NNTP config from UCI
|
||||||
|
local name=$(uci_get eweka.name)
|
||||||
|
local host=$(uci_get eweka.host)
|
||||||
|
local port=$(uci_get eweka.port)
|
||||||
|
local ssl=$(uci_get eweka.ssl)
|
||||||
|
local username=$(uci_get eweka.username)
|
||||||
|
local password=$(uci_get eweka.password)
|
||||||
|
local connections=$(uci_get eweka.connections)
|
||||||
|
|
||||||
|
[ -z "$host" ] && { log_error "No NNTP server configured in UCI"; return 1; }
|
||||||
|
|
||||||
|
log_info "Adding NNTP server: $name ($host)"
|
||||||
|
|
||||||
|
# SABnzbd uses INI format with [servers] section
|
||||||
|
# We'll add via API if running, otherwise manual config
|
||||||
|
local api_port=$(uci_get main.port)
|
||||||
|
[ -z "$api_port" ] && api_port="8085"
|
||||||
|
local api_key=$(grep "^api_key" "$ini_file" 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
|
||||||
|
|
||||||
|
if [ -n "$api_key" ] && curl -s "http://127.0.0.1:$api_port/api?mode=version&apikey=$api_key" >/dev/null 2>&1; then
|
||||||
|
# Use API to add server
|
||||||
|
curl -s "http://127.0.0.1:$api_port/api?mode=set_config§ion=servers&keyword=eweka&apikey=$api_key" \
|
||||||
|
-d "name=$name" \
|
||||||
|
-d "host=$host" \
|
||||||
|
-d "port=$port" \
|
||||||
|
-d "ssl=$ssl" \
|
||||||
|
-d "username=$username" \
|
||||||
|
-d "password=$password" \
|
||||||
|
-d "connections=$connections" \
|
||||||
|
-d "enable=1" >/dev/null
|
||||||
|
|
||||||
|
log_ok "NNTP server added via API"
|
||||||
|
else
|
||||||
|
log_info "SABnzbd not running. Server will be configured on first start."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Configure HAProxy exposure
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_configure_haproxy() {
|
||||||
|
local domain=$(uci_get exposure.domain)
|
||||||
|
[ -z "$domain" ] && domain="sabnzbd.gk2.secubox.in"
|
||||||
|
|
||||||
|
local port=$(uci_get main.port)
|
||||||
|
[ -z "$port" ] && port="8085"
|
||||||
|
|
||||||
|
log_info "Configuring HAProxy for $domain"
|
||||||
|
|
||||||
|
# Create backend
|
||||||
|
uci set haproxy.sabnzbd_web=backend
|
||||||
|
uci set haproxy.sabnzbd_web.name='sabnzbd_web'
|
||||||
|
uci set haproxy.sabnzbd_web.mode='http'
|
||||||
|
uci set haproxy.sabnzbd_web.server="sabnzbd 127.0.0.1:$port weight 100 check"
|
||||||
|
|
||||||
|
# Create vhost
|
||||||
|
local vhost_id=$(echo "$domain" | tr '.' '_')
|
||||||
|
uci set "haproxy.$vhost_id=vhost"
|
||||||
|
uci set "haproxy.$vhost_id.domain=$domain"
|
||||||
|
uci set "haproxy.$vhost_id.backend=mitmproxy_inspector"
|
||||||
|
uci set "haproxy.$vhost_id.original_backend=sabnzbd_web"
|
||||||
|
uci set "haproxy.$vhost_id.ssl=1"
|
||||||
|
uci set "haproxy.$vhost_id.ssl_redirect=1"
|
||||||
|
uci set "haproxy.$vhost_id.acme=1"
|
||||||
|
uci commit haproxy
|
||||||
|
|
||||||
|
# Add mitmproxy route
|
||||||
|
if [ -f /srv/mitmproxy/haproxy-routes.json ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
with open('/srv/mitmproxy/haproxy-routes.json') as f:
|
||||||
|
routes = json.load(f)
|
||||||
|
routes['$domain'] = ['127.0.0.1', $port]
|
||||||
|
with open('/srv/mitmproxy/haproxy-routes.json', 'w') as f:
|
||||||
|
json.dump(routes, f, indent=2)
|
||||||
|
" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reload
|
||||||
|
haproxyctl reload 2>/dev/null || true
|
||||||
|
/etc/init.d/mitmproxy restart 2>/dev/null || true
|
||||||
|
|
||||||
|
log_ok "HAProxy configured: https://$domain/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Uninstall
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_uninstall() {
|
||||||
|
log_info "Uninstalling SABnzbd..."
|
||||||
|
|
||||||
|
cmd_stop 2>/dev/null
|
||||||
|
|
||||||
|
rm -rf "$CONTAINER_DIR"
|
||||||
|
log_info "Container removed. Data preserved in $DATA_DIR"
|
||||||
|
|
||||||
|
log_ok "SABnzbd uninstalled"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Main
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
case "$1" in
|
||||||
|
install) cmd_install ;;
|
||||||
|
start) cmd_start ;;
|
||||||
|
stop) cmd_stop ;;
|
||||||
|
restart) cmd_restart ;;
|
||||||
|
status) cmd_status ;;
|
||||||
|
logs) shift; cmd_logs "$@" ;;
|
||||||
|
shell) cmd_shell ;;
|
||||||
|
add-server) cmd_add_server ;;
|
||||||
|
configure-haproxy) cmd_configure_haproxy ;;
|
||||||
|
uninstall) cmd_uninstall ;;
|
||||||
|
*)
|
||||||
|
echo "SABnzbd Controller - Usenet NZB Downloader"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: sabnzbdctl <command>"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " install Install container from Docker image"
|
||||||
|
echo " start Start SABnzbd"
|
||||||
|
echo " stop Stop SABnzbd"
|
||||||
|
echo " restart Restart SABnzbd"
|
||||||
|
echo " status Show status and queue info"
|
||||||
|
echo " logs [n] Show last n log lines (default 50)"
|
||||||
|
echo " shell Interactive shell in container"
|
||||||
|
echo " add-server Configure NNTP server from UCI"
|
||||||
|
echo " configure-haproxy Setup HAProxy reverse proxy"
|
||||||
|
echo " uninstall Remove container (keeps data)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Loading…
Reference in New Issue
Block a user