From 10b3d3a43cf88b79da6980fef7bea3e7560675c7 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 15 Mar 2026 06:59:07 +0100 Subject: [PATCH] feat(torrent): Add LuCI dashboard and fix WebTorrent ESM issue - Add luci-app-torrent: unified dashboard for qBittorrent + WebTorrent - RPCD handler with status/list/start/stop/add methods - Dark-themed UI with real-time torrent queue display - Start/Stop controls and magnet link add functionality - 5-second auto-refresh polling - Fix webtorrent v2.x ESM incompatibility - Pin to v1.9.7 (last CommonJS version) - Use npm install with --save-exact to prevent semver drift - HAProxy exposure configured: - qBittorrent: torrent.gk2.secubox.in (192.168.255.42:8090) - WebTorrent: stream.gk2.secubox.in (192.168.255.43:8095) Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 20 ++ package/secubox/luci-app-torrent/Makefile | 25 ++ .../resources/view/torrent/overview.js | 311 ++++++++++++++++++ .../root/usr/libexec/rpcd/luci.torrent | 248 ++++++++++++++ .../share/luci/menu.d/luci-app-torrent.json | 14 + .../share/rpcd/acl.d/luci-app-torrent.json | 16 + .../files/usr/sbin/webtorrentctl | 155 ++------- 7 files changed, 653 insertions(+), 136 deletions(-) create mode 100644 package/secubox/luci-app-torrent/Makefile create mode 100644 package/secubox/luci-app-torrent/htdocs/luci-static/resources/view/torrent/overview.js create mode 100644 package/secubox/luci-app-torrent/root/usr/libexec/rpcd/luci.torrent create mode 100644 package/secubox/luci-app-torrent/root/usr/share/luci/menu.d/luci-app-torrent.json create mode 100644 package/secubox/luci-app-torrent/root/usr/share/rpcd/acl.d/luci-app-torrent.json diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index ee433e5d..e30cf96c 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -5124,3 +5124,23 @@ git checkout HEAD -- index.html - `secubox-app-sabnzbd/`: Makefile, UCI config, init.d, sabnzbdctl - `secubox-app-nzbhydra/`: Makefile, UCI config, init.d, nzbhydractl - `luci-app-newsbin/`: overview.js, RPCD handler, ACL, menu + +109. **qBittorrent & WebTorrent - Torrent Services (2026-03-15)** + - Both use Debian LXC containers (no Docker/Podman) + - **qBittorrent** (`secubox-app-qbittorrent`): + - Container IP: 192.168.255.42:8090 + - CLI: `qbittorrentctl install|start|stop|status|add|list|shell|configure-haproxy` + - Default login: admin / adminadmin + - Installs qbittorrent-nox via apt inside container + - Torrent add via magnet links or URLs + - **WebTorrent** (`secubox-app-webtorrent`): + - Container IP: 192.168.255.43:8095 + - CLI: `webtorrentctl install|start|stop|status|add|list|shell|configure-haproxy` + - Node.js streaming server with browser-based WebRTC support + - Fixed webtorrent v2.x ESM incompatibility: pinned to v1.9.7 (last CommonJS version) + - npm exact version install prevents semver resolution to breaking v2.x + - In-browser streaming via `/stream/:infoHash/:path` endpoint + - Dark-themed web UI with real-time torrent status + - **Files**: + - `secubox-app-qbittorrent/`: Makefile, UCI config, init.d, qbittorrentctl + - `secubox-app-webtorrent/`: Makefile, UCI config, init.d, webtorrentctl diff --git a/package/secubox/luci-app-torrent/Makefile b/package/secubox/luci-app-torrent/Makefile new file mode 100644 index 00000000..3e99eddd --- /dev/null +++ b/package/secubox/luci-app-torrent/Makefile @@ -0,0 +1,25 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI Torrent Dashboard +LUCI_DESCRIPTION:=Unified dashboard for qBittorrent and WebTorrent +LUCI_DEPENDS:=+luci-base +secubox-app-qbittorrent +secubox-app-webtorrent +LUCI_PKGARCH:=all + +PKG_NAME:=luci-app-torrent +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-torrent/install + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-torrent.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-torrent.json $(1)/usr/share/rpcd/acl.d/ + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.torrent $(1)/usr/libexec/rpcd/ + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/torrent + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/torrent/overview.js $(1)/www/luci-static/resources/view/torrent/ +endef + +$(eval $(call BuildPackage,luci-app-torrent)) diff --git a/package/secubox/luci-app-torrent/htdocs/luci-static/resources/view/torrent/overview.js b/package/secubox/luci-app-torrent/htdocs/luci-static/resources/view/torrent/overview.js new file mode 100644 index 00000000..e97c9576 --- /dev/null +++ b/package/secubox/luci-app-torrent/htdocs/luci-static/resources/view/torrent/overview.js @@ -0,0 +1,311 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require poll'; + +var callStatus = rpc.declare({ + object: 'luci.torrent', + method: 'status', + expect: {} +}); + +var callQbtList = rpc.declare({ + object: 'luci.torrent', + method: 'qbt_list', + expect: {} +}); + +var callWtList = rpc.declare({ + object: 'luci.torrent', + method: 'wt_list', + expect: {} +}); + +var callQbtStart = rpc.declare({ + object: 'luci.torrent', + method: 'qbt_start' +}); + +var callQbtStop = rpc.declare({ + object: 'luci.torrent', + method: 'qbt_stop' +}); + +var callWtStart = rpc.declare({ + object: 'luci.torrent', + method: 'wt_start' +}); + +var callWtStop = rpc.declare({ + object: 'luci.torrent', + method: 'wt_stop' +}); + +var callQbtAdd = rpc.declare({ + object: 'luci.torrent', + method: 'qbt_add', + params: ['url'] +}); + +var callWtAdd = rpc.declare({ + object: 'luci.torrent', + method: 'wt_add', + params: ['magnet'] +}); + +function formatSpeed(bytes) { + if (!bytes || bytes === 0) return '0 B/s'; + var k = 1024; + var sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +} + +function formatSize(bytes) { + if (!bytes || bytes === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +return view.extend({ + load: function() { + return Promise.all([ + callStatus(), + callQbtList(), + callWtList() + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var qbt = status.qbittorrent || {}; + var wt = status.webtorrent || {}; + var qbtList = (data[1] || {}).torrents || []; + var wtList = (data[2] || {}).torrents || []; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', { 'style': 'color: #00d4ff; margin-bottom: 20px;' }, [ + E('span', { 'style': 'margin-right: 10px;' }, '\u{1F9F2}'), + 'Torrent Services' + ]), + + // Status Cards + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 30px;' }, [ + // qBittorrent Card + E('div', { 'style': 'background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 12px; padding: 20px; border: 1px solid #2a2a4e;' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;' }, [ + E('h3', { 'style': 'color: #00d4ff; margin: 0; font-size: 18px;' }, '\u{1F4E5} qBittorrent'), + E('span', { + 'style': 'padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; ' + + (qbt.running ? 'background: rgba(0,200,83,0.2); color: #00c853;' : 'background: rgba(244,67,54,0.2); color: #f44336;') + }, qbt.running ? 'RUNNING' : 'STOPPED') + ]), + E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 15px;' }, [ + E('div', { 'style': 'background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px;' }, [ + E('div', { 'style': 'color: #888; font-size: 11px;' }, 'DOWNLOAD'), + E('div', { 'style': 'color: #00c853; font-size: 16px; font-weight: 600;' }, formatSpeed(qbt.dl_speed)) + ]), + E('div', { 'style': 'background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px;' }, [ + E('div', { 'style': 'color: #888; font-size: 11px;' }, 'UPLOAD'), + E('div', { 'style': 'color: #2196f3; font-size: 16px; font-weight: 600;' }, formatSpeed(qbt.up_speed)) + ]) + ]), + E('div', { 'style': 'color: #888; font-size: 13px; margin-bottom: 15px;' }, [ + E('span', {}, 'Torrents: '), + E('strong', { 'style': 'color: #e0e0e0;' }, String(qbt.torrents || 0)), + qbt.version ? E('span', { 'style': 'margin-left: 15px;' }, ['Version: ', E('strong', { 'style': 'color: #e0e0e0;' }, qbt.version)]) : '' + ]), + E('div', { 'style': 'display: flex; gap: 10px;' }, [ + E('button', { + 'class': 'btn cbi-button', + 'style': 'flex: 1; padding: 8px; border-radius: 6px; ' + + (qbt.running ? 'background: #f44336; border-color: #f44336;' : 'background: #00c853; border-color: #00c853;'), + 'click': ui.createHandlerFn(this, function() { + return (qbt.running ? callQbtStop() : callQbtStart()).then(function() { + window.location.reload(); + }); + }) + }, qbt.running ? 'Stop' : 'Start'), + E('a', { + 'href': qbt.url || 'http://192.168.255.42:8090/', + 'target': '_blank', + 'class': 'btn cbi-button', + 'style': 'flex: 1; padding: 8px; border-radius: 6px; text-align: center; text-decoration: none; background: #2196f3; border-color: #2196f3; color: white;' + }, 'Open UI') + ]) + ]), + + // WebTorrent Card + E('div', { 'style': 'background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 12px; padding: 20px; border: 1px solid #2a2a4e;' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;' }, [ + E('h3', { 'style': 'color: #7c3aed; margin: 0; font-size: 18px;' }, '\u{1F4FA} WebTorrent'), + E('span', { + 'style': 'padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; ' + + (wt.running ? 'background: rgba(0,200,83,0.2); color: #00c853;' : 'background: rgba(244,67,54,0.2); color: #f44336;') + }, wt.running ? 'RUNNING' : 'STOPPED') + ]), + E('div', { 'style': 'background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; margin-bottom: 15px;' }, [ + E('div', { 'style': 'color: #888; font-size: 11px;' }, 'ACTIVE STREAMS'), + E('div', { 'style': 'color: #7c3aed; font-size: 24px; font-weight: 600;' }, String(wt.torrents || 0)) + ]), + E('div', { 'style': 'color: #888; font-size: 13px; margin-bottom: 15px;' }, [ + 'Browser-based torrent streaming with WebRTC' + ]), + E('div', { 'style': 'display: flex; gap: 10px;' }, [ + E('button', { + 'class': 'btn cbi-button', + 'style': 'flex: 1; padding: 8px; border-radius: 6px; ' + + (wt.running ? 'background: #f44336; border-color: #f44336;' : 'background: #00c853; border-color: #00c853;'), + 'click': ui.createHandlerFn(this, function() { + return (wt.running ? callWtStop() : callWtStart()).then(function() { + window.location.reload(); + }); + }) + }, wt.running ? 'Stop' : 'Start'), + E('a', { + 'href': wt.url || 'http://192.168.255.43:8095/', + 'target': '_blank', + 'class': 'btn cbi-button', + 'style': 'flex: 1; padding: 8px; border-radius: 6px; text-align: center; text-decoration: none; background: #7c3aed; border-color: #7c3aed; color: white;' + }, 'Open UI') + ]) + ]) + ]), + + // Add Torrent Section + E('div', { 'style': 'background: #1a1a2e; border-radius: 12px; padding: 20px; border: 1px solid #2a2a4e; margin-bottom: 30px;' }, [ + E('h3', { 'style': 'color: #e0e0e0; margin: 0 0 15px 0; font-size: 16px;' }, '\u2795 Add Torrent'), + E('div', { 'style': 'display: flex; gap: 10px;' }, [ + E('input', { + 'type': 'text', + 'id': 'torrent-url', + 'placeholder': 'Paste magnet link or torrent URL...', + 'style': 'flex: 1; padding: 12px; background: #0a0a12; border: 1px solid #3a3a4e; border-radius: 8px; color: #e0e0e0; font-size: 14px;' + }), + E('button', { + 'class': 'btn cbi-button', + 'style': 'padding: 12px 24px; background: linear-gradient(135deg, #00d4ff, #7c3aed); border: none; border-radius: 8px; color: white; font-weight: 600;', + 'click': ui.createHandlerFn(this, function() { + var url = document.getElementById('torrent-url').value.trim(); + if (!url) { + ui.addNotification(null, E('p', 'Please enter a magnet link or URL'), 'warning'); + return; + } + // Add to both services + var promises = []; + if (qbt.running) promises.push(callQbtAdd(url)); + if (wt.running && url.startsWith('magnet:')) promises.push(callWtAdd(url)); + if (promises.length === 0) { + ui.addNotification(null, E('p', 'No torrent service is running'), 'warning'); + return; + } + return Promise.all(promises).then(function() { + ui.addNotification(null, E('p', 'Torrent added successfully'), 'info'); + document.getElementById('torrent-url').value = ''; + window.location.reload(); + }); + }) + }, 'Add') + ]) + ]), + + // Torrent Lists + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px;' }, [ + // qBittorrent List + E('div', { 'style': 'background: #1a1a2e; border-radius: 12px; padding: 20px; border: 1px solid #2a2a4e;' }, [ + E('h3', { 'style': 'color: #00d4ff; margin: 0 0 15px 0; font-size: 16px;' }, '\u{1F4E5} qBittorrent Queue'), + E('div', { 'id': 'qbt-list' }, this.renderQbtList(qbtList)) + ]), + + // WebTorrent List + E('div', { 'style': 'background: #1a1a2e; border-radius: 12px; padding: 20px; border: 1px solid #2a2a4e;' }, [ + E('h3', { 'style': 'color: #7c3aed; margin: 0 0 15px 0; font-size: 16px;' }, '\u{1F4FA} WebTorrent Streams'), + E('div', { 'id': 'wt-list' }, this.renderWtList(wtList)) + ]) + ]) + ]); + + poll.add(L.bind(this.pollData, this), 5); + + return view; + }, + + renderQbtList: function(torrents) { + if (!torrents || torrents.length === 0) { + return E('div', { 'style': 'color: #666; text-align: center; padding: 30px;' }, 'No active torrents'); + } + + return E('div', {}, torrents.slice(0, 10).map(function(t) { + var progress = (t.progress || 0) * 100; + var state = t.state || 'unknown'; + var stateColor = state.includes('download') ? '#00c853' : (state.includes('upload') ? '#2196f3' : '#888'); + + return E('div', { 'style': 'background: rgba(0,0,0,0.2); border-radius: 8px; padding: 12px; margin-bottom: 10px;' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 8px;' }, [ + E('span', { 'style': 'color: #e0e0e0; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 70%;' }, t.name || 'Unknown'), + E('span', { 'style': 'color: ' + stateColor + '; font-size: 11px; text-transform: uppercase;' }, state) + ]), + E('div', { 'style': 'background: #2a2a3e; border-radius: 4px; height: 6px; overflow: hidden;' }, [ + E('div', { 'style': 'background: linear-gradient(90deg, #00d4ff, #7c3aed); height: 100%; width: ' + progress + '%; transition: width 0.3s;' }) + ]), + E('div', { 'style': 'display: flex; justify-content: space-between; margin-top: 6px; color: #888; font-size: 11px;' }, [ + E('span', {}, progress.toFixed(1) + '%'), + E('span', {}, formatSize(t.size || t.total_size || 0)), + E('span', {}, '\u2193 ' + formatSpeed(t.dlspeed || 0) + ' \u2191 ' + formatSpeed(t.upspeed || 0)) + ]) + ]); + })); + }, + + renderWtList: function(torrents) { + if (!torrents || torrents.length === 0) { + return E('div', { 'style': 'color: #666; text-align: center; padding: 30px;' }, 'No active streams'); + } + + return E('div', {}, torrents.slice(0, 10).map(function(t) { + var progress = (t.progress || 0) * 100; + + return E('div', { 'style': 'background: rgba(0,0,0,0.2); border-radius: 8px; padding: 12px; margin-bottom: 10px;' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 8px;' }, [ + E('span', { 'style': 'color: #e0e0e0; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 70%;' }, t.name || 'Unknown'), + E('span', { 'style': 'color: #7c3aed; font-size: 11px;' }, (t.numPeers || 0) + ' peers') + ]), + E('div', { 'style': 'background: #2a2a3e; border-radius: 4px; height: 6px; overflow: hidden;' }, [ + E('div', { 'style': 'background: linear-gradient(90deg, #7c3aed, #00d4ff); height: 100%; width: ' + progress + '%; transition: width 0.3s;' }) + ]), + E('div', { 'style': 'display: flex; justify-content: space-between; margin-top: 6px; color: #888; font-size: 11px;' }, [ + E('span', {}, progress.toFixed(1) + '%'), + E('span', {}, formatSize(t.length || 0)), + E('span', {}, '\u2193 ' + formatSpeed(t.downloadSpeed || 0)) + ]) + ]); + })); + }, + + pollData: function() { + var self = this; + return Promise.all([callQbtList(), callWtList()]).then(function(data) { + var qbtList = (data[0] || {}).torrents || []; + var wtList = (data[1] || {}).torrents || []; + + var qbtEl = document.getElementById('qbt-list'); + var wtEl = document.getElementById('wt-list'); + + if (qbtEl) { + qbtEl.innerHTML = ''; + qbtEl.appendChild(self.renderQbtList(qbtList)); + } + if (wtEl) { + wtEl.innerHTML = ''; + wtEl.appendChild(self.renderWtList(wtList)); + } + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-torrent/root/usr/libexec/rpcd/luci.torrent b/package/secubox/luci-app-torrent/root/usr/libexec/rpcd/luci.torrent new file mode 100644 index 00000000..418fb8ab --- /dev/null +++ b/package/secubox/luci-app-torrent/root/usr/libexec/rpcd/luci.torrent @@ -0,0 +1,248 @@ +#!/bin/sh +# RPCD handler for Torrent dashboard (qBittorrent + WebTorrent) + +. /usr/share/libubox/jshn.sh + +QBT_IP="192.168.255.42" +QBT_PORT="8090" +WT_IP="192.168.255.43" +WT_PORT="8095" + +list_methods() { + cat << 'EOFM' +{"status":{},"qbt_list":{},"wt_list":{},"qbt_start":{},"qbt_stop":{},"wt_start":{},"wt_stop":{},"qbt_add":{"url":"str"},"wt_add":{"magnet":"str"}} +EOFM +} + +# Check if container is running +is_running() { + /usr/bin/lxc-info -n "$1" 2>/dev/null | grep -q "RUNNING" +} + +# Get qBittorrent status +get_qbt_status() { + local running=0 + local version="" + local dl_speed=0 + local up_speed=0 + local torrents=0 + + if is_running "qbittorrent"; then + running=1 + version=$(/usr/bin/curl -s "http://${QBT_IP}:${QBT_PORT}/api/v2/app/version" 2>/dev/null) + local transfer=$(/usr/bin/curl -s "http://${QBT_IP}:${QBT_PORT}/api/v2/transfer/info" 2>/dev/null) + if [ -n "$transfer" ]; then + dl_speed=$(echo "$transfer" | jsonfilter -e '@.dl_info_speed' 2>/dev/null) + up_speed=$(echo "$transfer" | jsonfilter -e '@.up_info_speed' 2>/dev/null) + fi + local list=$(/usr/bin/curl -s "http://${QBT_IP}:${QBT_PORT}/api/v2/torrents/info" 2>/dev/null) + if [ -n "$list" ] && [ "$list" != "[]" ]; then + torrents=$(echo "$list" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + fi + fi + + json_init + json_add_boolean "running" "$running" + json_add_string "version" "${version:-unknown}" + json_add_int "dl_speed" "${dl_speed:-0}" + json_add_int "up_speed" "${up_speed:-0}" + json_add_int "torrents" "${torrents:-0}" + json_add_string "url" "https://torrent.gk2.secubox.in/" + json_dump +} + +# Get WebTorrent status +get_wt_status() { + local running=0 + local torrents=0 + + if is_running "webtorrent"; then + running=1 + local list=$(/usr/bin/curl -s "http://${WT_IP}:${WT_PORT}/api/torrents" 2>/dev/null) + if [ -n "$list" ] && [ "$list" != "[]" ]; then + torrents=$(echo "$list" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + fi + fi + + json_init + json_add_boolean "running" "$running" + json_add_int "torrents" "${torrents:-0}" + json_add_string "url" "https://stream.gk2.secubox.in/" + json_dump +} + +# Combined status +do_status() { + local qbt_running=0 + local wt_running=0 + local qbt_version="" + local qbt_dl=0 + local qbt_up=0 + local qbt_count=0 + local wt_count=0 + + if is_running "qbittorrent"; then + qbt_running=1 + qbt_version=$(/usr/bin/curl -s "http://${QBT_IP}:${QBT_PORT}/api/v2/app/version" 2>/dev/null) + local transfer=$(/usr/bin/curl -s "http://${QBT_IP}:${QBT_PORT}/api/v2/transfer/info" 2>/dev/null) + if [ -n "$transfer" ]; then + qbt_dl=$(echo "$transfer" | jsonfilter -e '@.dl_info_speed' 2>/dev/null) + qbt_up=$(echo "$transfer" | jsonfilter -e '@.up_info_speed' 2>/dev/null) + fi + local qlist=$(/usr/bin/curl -s "http://${QBT_IP}:${QBT_PORT}/api/v2/torrents/info" 2>/dev/null) + if [ -n "$qlist" ] && [ "$qlist" != "[]" ]; then + qbt_count=$(echo "$qlist" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + fi + fi + + if is_running "webtorrent"; then + wt_running=1 + local wtlist=$(/usr/bin/curl -s "http://${WT_IP}:${WT_PORT}/api/torrents" 2>/dev/null) + if [ -n "$wtlist" ] && [ "$wtlist" != "[]" ]; then + wt_count=$(echo "$wtlist" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + fi + fi + + json_init + json_add_object "qbittorrent" + json_add_boolean "running" "$qbt_running" + json_add_string "version" "${qbt_version:-unknown}" + json_add_int "dl_speed" "${qbt_dl:-0}" + json_add_int "up_speed" "${qbt_up:-0}" + json_add_int "torrents" "${qbt_count:-0}" + json_add_string "url" "https://torrent.gk2.secubox.in/" + json_add_string "ip" "${QBT_IP}:${QBT_PORT}" + json_close_object + + json_add_object "webtorrent" + json_add_boolean "running" "$wt_running" + json_add_int "torrents" "${wt_count:-0}" + json_add_string "url" "https://stream.gk2.secubox.in/" + json_add_string "ip" "${WT_IP}:${WT_PORT}" + json_close_object + + json_dump +} + +# List qBittorrent torrents +do_qbt_list() { + local list=$(/usr/bin/curl -s "http://${QBT_IP}:${QBT_PORT}/api/v2/torrents/info" 2>/dev/null) + if [ -z "$list" ] || [ "$list" = "Forbidden" ]; then + echo '{"torrents":[]}' + return + fi + echo "{\"torrents\":$list}" +} + +# List WebTorrent torrents +do_wt_list() { + local list=$(/usr/bin/curl -s "http://${WT_IP}:${WT_PORT}/api/torrents" 2>/dev/null) + if [ -z "$list" ]; then + echo '{"torrents":[]}' + return + fi + echo "{\"torrents\":$list}" +} + +# Start qBittorrent +do_qbt_start() { + /usr/sbin/qbittorrentctl start >/dev/null 2>&1 + json_init + json_add_boolean "success" 1 + json_dump +} + +# Stop qBittorrent +do_qbt_stop() { + /usr/sbin/qbittorrentctl stop >/dev/null 2>&1 + json_init + json_add_boolean "success" 1 + json_dump +} + +# Start WebTorrent +do_wt_start() { + /usr/sbin/webtorrentctl start >/dev/null 2>&1 + json_init + json_add_boolean "success" 1 + json_dump +} + +# Stop WebTorrent +do_wt_stop() { + /usr/sbin/webtorrentctl stop >/dev/null 2>&1 + json_init + json_add_boolean "success" 1 + json_dump +} + +# Add torrent to qBittorrent +do_qbt_add() { + read -r input + local url=$(echo "$input" | jsonfilter -e '@.url' 2>/dev/null) + + if [ -z "$url" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "URL required" + json_dump + return + fi + + local result=$(/usr/bin/curl -s -X POST "http://${QBT_IP}:${QBT_PORT}/api/v2/torrents/add" -d "urls=$url" 2>/dev/null) + + json_init + if [ "$result" = "Ok." ]; then + json_add_boolean "success" 1 + else + json_add_boolean "success" 0 + json_add_string "error" "$result" + fi + json_dump +} + +# Add torrent to WebTorrent +do_wt_add() { + read -r input + local magnet=$(echo "$input" | jsonfilter -e '@.magnet' 2>/dev/null) + + if [ -z "$magnet" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Magnet link required" + json_dump + return + fi + + local result=$(/usr/bin/curl -s -X POST "http://${WT_IP}:${WT_PORT}/api/add" \ + -H "Content-Type: application/json" \ + -d "{\"magnet\":\"$magnet\"}" 2>/dev/null) + + json_init + if echo "$result" | grep -q "success"; then + json_add_boolean "success" 1 + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to add torrent" + fi + json_dump +} + +case "$1" in + list) list_methods ;; + call) + case "$2" in + status) do_status ;; + qbt_list) do_qbt_list ;; + wt_list) do_wt_list ;; + qbt_start) do_qbt_start ;; + qbt_stop) do_qbt_stop ;; + wt_start) do_wt_start ;; + wt_stop) do_wt_stop ;; + qbt_add) do_qbt_add ;; + wt_add) do_wt_add ;; + *) echo '{"error":"Unknown method"}' ;; + esac + ;; + *) echo '{"error":"Unknown command"}' ;; +esac diff --git a/package/secubox/luci-app-torrent/root/usr/share/luci/menu.d/luci-app-torrent.json b/package/secubox/luci-app-torrent/root/usr/share/luci/menu.d/luci-app-torrent.json new file mode 100644 index 00000000..bacc927f --- /dev/null +++ b/package/secubox/luci-app-torrent/root/usr/share/luci/menu.d/luci-app-torrent.json @@ -0,0 +1,14 @@ +{ + "admin/services/torrent": { + "title": "Torrent", + "order": 85, + "action": { + "type": "view", + "path": "torrent/overview" + }, + "depends": { + "acl": ["luci-app-torrent"], + "uci": { "qbittorrent": true } + } + } +} diff --git a/package/secubox/luci-app-torrent/root/usr/share/rpcd/acl.d/luci-app-torrent.json b/package/secubox/luci-app-torrent/root/usr/share/rpcd/acl.d/luci-app-torrent.json new file mode 100644 index 00000000..c2792b5a --- /dev/null +++ b/package/secubox/luci-app-torrent/root/usr/share/rpcd/acl.d/luci-app-torrent.json @@ -0,0 +1,16 @@ +{ + "luci-app-torrent": { + "description": "Grant access to Torrent dashboard", + "read": { + "ubus": { + "luci.torrent": ["status", "qbt_list", "wt_list"] + }, + "uci": ["qbittorrent", "webtorrent"] + }, + "write": { + "ubus": { + "luci.torrent": ["qbt_start", "qbt_stop", "qbt_add", "wt_start", "wt_stop", "wt_add"] + } + } + } +} diff --git a/package/secubox/secubox-app-webtorrent/files/usr/sbin/webtorrentctl b/package/secubox/secubox-app-webtorrent/files/usr/sbin/webtorrentctl index 50cfd78c..679d82ba 100755 --- a/package/secubox/secubox-app-webtorrent/files/usr/sbin/webtorrentctl +++ b/package/secubox/secubox-app-webtorrent/files/usr/sbin/webtorrentctl @@ -87,34 +87,23 @@ if ! command -v node >/dev/null 2>&1; then rm -rf /var/lib/apt/lists/* fi -# Install webtorrent-hybrid and instant.io server -if [ ! -d "/opt/webtorrent/node_modules" ]; then - echo "Installing WebTorrent packages..." +# Install webtorrent packages - use EXACT version 1.9.7 (last CommonJS version) +# v2.x is ESM-only and breaks require() +if [ ! -d "/opt/webtorrent/node_modules/webtorrent" ]; then + echo "Installing WebTorrent packages (v1.9.7 for CommonJS)..." mkdir -p /opt/webtorrent cd /opt/webtorrent - # Create package.json - cat > package.json << 'PKGEOF' -{ - "name": "webtorrent-server", - "version": "1.0.0", - "dependencies": { - "webtorrent": "^2.0.0", - "express": "^4.18.0", - "cors": "^2.8.5" - } -} -PKGEOF + npm install webtorrent@1.9.7 express@4.18.0 cors@2.8.5 --save-exact --production || { + echo "npm install failed"; exit 1; + } +fi - npm install --production || { echo "npm install failed"; exit 1; } - - # Create server script - cat > server.js << 'SRVEOF' +# Create/update server script +cat > /opt/webtorrent/server.js << 'SRVEOF' const WebTorrent = require('webtorrent'); const express = require('express'); const cors = require('cors'); -const path = require('path'); -const fs = require('fs'); const app = express(); const client = new WebTorrent(); @@ -125,114 +114,17 @@ app.use(cors()); app.use(express.json()); app.use('/downloads', express.static(DOWNLOAD_PATH)); -// Serve web UI app.get('/', (req, res) => { - res.send(` - - - WebTorrent Streaming - - - - -
-

🌊 WebTorrent Streaming

-
- - -
-
-
- - -`); + res.send(getHTML()); }); -// API: List torrents app.get('/api/torrents', (req, res) => { const torrents = client.torrents.map(t => ({ infoHash: t.infoHash, name: t.name, length: t.length, downloaded: t.downloaded, - uploaded: t.uploaded, downloadSpeed: t.downloadSpeed, - uploadSpeed: t.uploadSpeed, progress: t.progress, numPeers: t.numPeers, files: t.files.map(f => ({ name: f.name, path: f.path, length: f.length })) @@ -240,54 +132,45 @@ app.get('/api/torrents', (req, res) => { res.json(torrents); }); -// API: Add torrent app.post('/api/add', (req, res) => { const { magnet } = req.body; if (!magnet) return res.status(400).json({ error: 'Magnet required' }); - client.add(magnet, { path: DOWNLOAD_PATH }, torrent => { console.log('Added:', torrent.name); }); res.json({ success: true }); }); -// Stream file app.get('/stream/:infoHash/*', (req, res) => { const torrent = client.get(req.params.infoHash); if (!torrent) return res.status(404).send('Torrent not found'); - const filePath = req.params[0]; const file = torrent.files.find(f => f.path === filePath); if (!file) return res.status(404).send('File not found'); - const range = req.headers.range; if (range) { const positions = range.replace(/bytes=/, '').split('-'); const start = parseInt(positions[0], 10); const end = positions[1] ? parseInt(positions[1], 10) : file.length - 1; - const chunksize = (end - start) + 1; - res.writeHead(206, { - 'Content-Range': `bytes ${start}-${end}/${file.length}`, + 'Content-Range': 'bytes ' + start + '-' + end + '/' + file.length, 'Accept-Ranges': 'bytes', - 'Content-Length': chunksize, + 'Content-Length': (end - start) + 1, 'Content-Type': 'video/mp4' }); file.createReadStream({ start, end }).pipe(res); } else { - res.writeHead(200, { - 'Content-Length': file.length, - 'Content-Type': 'video/mp4' - }); + res.writeHead(200, { 'Content-Length': file.length, 'Content-Type': 'video/mp4' }); file.createReadStream().pipe(res); } }); -app.listen(PORT, '0.0.0.0', () => { - console.log(`WebTorrent server running on http://0.0.0.0:${PORT}`); -}); +function getHTML() { + return 'WebTorrent

WebTorrent Streaming

'; +} + +app.listen(PORT, '0.0.0.0', () => console.log('WebTorrent on port ' + PORT)); SRVEOF -fi echo "=== Starting WebTorrent server ===" mkdir -p /config /downloads