From aef0284b446e91ffe10f9f34caa81046fd16d9eb Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 14 Mar 2026 15:31:46 +0100 Subject: [PATCH] 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 --- package/secubox/luci-app-newsbin/Makefile | 27 ++ .../resources/view/newsbin/overview.js | 341 +++++++++++++++++ .../root/usr/libexec/rpcd/luci.newsbin | 285 ++++++++++++++ .../share/luci/menu.d/luci-app-newsbin.json | 14 + .../share/rpcd/acl.d/luci-app-newsbin.json | 15 + package/secubox/secubox-app-nzbhydra/Makefile | 40 ++ .../files/etc/config/nzbhydra | 15 + .../files/etc/init.d/nzbhydra | 38 ++ .../files/usr/sbin/nzbhydractl | 352 +++++++++++++++++ package/secubox/secubox-app-sabnzbd/Makefile | 40 ++ .../files/etc/config/sabnzbd | 21 ++ .../files/etc/init.d/sabnzbd | 39 ++ .../files/usr/sbin/sabnzbdctl | 354 ++++++++++++++++++ 13 files changed, 1581 insertions(+) create mode 100644 package/secubox/luci-app-newsbin/Makefile create mode 100644 package/secubox/luci-app-newsbin/htdocs/luci-static/resources/view/newsbin/overview.js create mode 100644 package/secubox/luci-app-newsbin/root/usr/libexec/rpcd/luci.newsbin create mode 100644 package/secubox/luci-app-newsbin/root/usr/share/luci/menu.d/luci-app-newsbin.json create mode 100644 package/secubox/luci-app-newsbin/root/usr/share/rpcd/acl.d/luci-app-newsbin.json create mode 100644 package/secubox/secubox-app-nzbhydra/Makefile create mode 100644 package/secubox/secubox-app-nzbhydra/files/etc/config/nzbhydra create mode 100644 package/secubox/secubox-app-nzbhydra/files/etc/init.d/nzbhydra create mode 100644 package/secubox/secubox-app-nzbhydra/files/usr/sbin/nzbhydractl create mode 100644 package/secubox/secubox-app-sabnzbd/Makefile create mode 100644 package/secubox/secubox-app-sabnzbd/files/etc/config/sabnzbd create mode 100644 package/secubox/secubox-app-sabnzbd/files/etc/init.d/sabnzbd create mode 100644 package/secubox/secubox-app-sabnzbd/files/usr/sbin/sabnzbdctl diff --git a/package/secubox/luci-app-newsbin/Makefile b/package/secubox/luci-app-newsbin/Makefile new file mode 100644 index 00000000..5bc43f05 --- /dev/null +++ b/package/secubox/luci-app-newsbin/Makefile @@ -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)) diff --git a/package/secubox/luci-app-newsbin/htdocs/luci-static/resources/view/newsbin/overview.js b/package/secubox/luci-app-newsbin/htdocs/luci-static/resources/view/newsbin/overview.js new file mode 100644 index 00000000..7cd95e7d --- /dev/null +++ b/package/secubox/luci-app-newsbin/htdocs/luci-static/resources/view/newsbin/overview.js @@ -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 +}); diff --git a/package/secubox/luci-app-newsbin/root/usr/libexec/rpcd/luci.newsbin b/package/secubox/luci-app-newsbin/root/usr/libexec/rpcd/luci.newsbin new file mode 100644 index 00000000..27d3b2e2 --- /dev/null +++ b/package/secubox/luci-app-newsbin/root/usr/libexec/rpcd/luci.newsbin @@ -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 diff --git a/package/secubox/luci-app-newsbin/root/usr/share/luci/menu.d/luci-app-newsbin.json b/package/secubox/luci-app-newsbin/root/usr/share/luci/menu.d/luci-app-newsbin.json new file mode 100644 index 00000000..9bb105ba --- /dev/null +++ b/package/secubox/luci-app-newsbin/root/usr/share/luci/menu.d/luci-app-newsbin.json @@ -0,0 +1,14 @@ +{ + "admin/services/newsbin": { + "title": "Newsbin", + "order": 50, + "action": { + "type": "view", + "path": "newsbin/overview" + }, + "depends": { + "acl": ["luci-app-newsbin"], + "uci": {} + } + } +} diff --git a/package/secubox/luci-app-newsbin/root/usr/share/rpcd/acl.d/luci-app-newsbin.json b/package/secubox/luci-app-newsbin/root/usr/share/rpcd/acl.d/luci-app-newsbin.json new file mode 100644 index 00000000..7e10e3da --- /dev/null +++ b/package/secubox/luci-app-newsbin/root/usr/share/rpcd/acl.d/luci-app-newsbin.json @@ -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"] + } + } + } +} diff --git a/package/secubox/secubox-app-nzbhydra/Makefile b/package/secubox/secubox-app-nzbhydra/Makefile new file mode 100644 index 00000000..a84ac888 --- /dev/null +++ b/package/secubox/secubox-app-nzbhydra/Makefile @@ -0,0 +1,40 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-nzbhydra +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox + +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)) diff --git a/package/secubox/secubox-app-nzbhydra/files/etc/config/nzbhydra b/package/secubox/secubox-app-nzbhydra/files/etc/config/nzbhydra new file mode 100644 index 00000000..fdd78a2f --- /dev/null +++ b/package/secubox/secubox-app-nzbhydra/files/etc/config/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' diff --git a/package/secubox/secubox-app-nzbhydra/files/etc/init.d/nzbhydra b/package/secubox/secubox-app-nzbhydra/files/etc/init.d/nzbhydra new file mode 100644 index 00000000..8e47098f --- /dev/null +++ b/package/secubox/secubox-app-nzbhydra/files/etc/init.d/nzbhydra @@ -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" +} diff --git a/package/secubox/secubox-app-nzbhydra/files/usr/sbin/nzbhydractl b/package/secubox/secubox-app-nzbhydra/files/usr/sbin/nzbhydractl new file mode 100644 index 00000000..fcc9b1a4 --- /dev/null +++ b/package/secubox/secubox-app-nzbhydra/files/usr/sbin/nzbhydractl @@ -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" </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 "; 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 " + 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 Search indexers" + echo " link-sabnzbd Configure SABnzbd as downloader" + echo " configure-haproxy Setup HAProxy reverse proxy" + echo " uninstall Remove container (keeps data)" + ;; +esac diff --git a/package/secubox/secubox-app-sabnzbd/Makefile b/package/secubox/secubox-app-sabnzbd/Makefile new file mode 100644 index 00000000..7ff1d781 --- /dev/null +++ b/package/secubox/secubox-app-sabnzbd/Makefile @@ -0,0 +1,40 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-sabnzbd +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox + +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)) diff --git a/package/secubox/secubox-app-sabnzbd/files/etc/config/sabnzbd b/package/secubox/secubox-app-sabnzbd/files/etc/config/sabnzbd new file mode 100644 index 00000000..e4916a45 --- /dev/null +++ b/package/secubox/secubox-app-sabnzbd/files/etc/config/sabnzbd @@ -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' diff --git a/package/secubox/secubox-app-sabnzbd/files/etc/init.d/sabnzbd b/package/secubox/secubox-app-sabnzbd/files/etc/init.d/sabnzbd new file mode 100644 index 00000000..a8097b6f --- /dev/null +++ b/package/secubox/secubox-app-sabnzbd/files/etc/init.d/sabnzbd @@ -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" +} diff --git a/package/secubox/secubox-app-sabnzbd/files/usr/sbin/sabnzbdctl b/package/secubox/secubox-app-sabnzbd/files/usr/sbin/sabnzbdctl new file mode 100644 index 00000000..3f69fe02 --- /dev/null +++ b/package/secubox/secubox-app-sabnzbd/files/usr/sbin/sabnzbdctl @@ -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" < "$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 " + 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