diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md
index ae82d41e..b418669d 100644
--- a/.claude/HISTORY.md
+++ b/.claude/HISTORY.md
@@ -2686,3 +2686,23 @@ git checkout HEAD -- index.html
- Added missing ipset existence check before trying to list IPs
- Version bumped to 0.5.2-r2
- Files modified: `luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub`
+
+31. **PeerTube Auto-Upload Import (2026-02-21)**
+ - Enhanced video import to automatically upload to PeerTube after yt-dlp download
+ - Flow: Download → Extract metadata → OAuth authentication → API upload → Cleanup
+ - New features:
+ - OAuth token acquisition from UCI-stored admin credentials
+ - Video upload via PeerTube REST API (POST /api/v1/videos/upload)
+ - Real-time job status polling with `import_job_status` method
+ - Progress indicator in LuCI UI (downloading → uploading → completed)
+ - Automatic cleanup of temp files after successful upload
+ - RPCD methods:
+ - `import_video`: Now includes auto-upload (replaces download-only)
+ - `import_job_status`: Poll import job progress by job_id
+ - Prerequisites: Admin password stored in UCI (`uci set peertube.admin.password`)
+ - Version bumped to 1.1.0
+ - Files modified:
+ - `luci-app-peertube/root/usr/libexec/rpcd/luci.peertube`
+ - `luci-app-peertube/htdocs/luci-static/resources/view/peertube/overview.js`
+ - `luci-app-peertube/htdocs/luci-static/resources/peertube/api.js`
+ - `luci-app-peertube/root/usr/share/rpcd/acl.d/luci-app-peertube.json`
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 063e4e91..fd8a4be2 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -379,7 +379,8 @@
"Bash(/home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/c3box-vm-builder.sh:*)",
"Bash(__NEW_LINE_ba6f66f0b013f58d__ echo \"\")",
"WebFetch(domain:cf.gk2.secubox.in)",
- "WebFetch(domain:streamlit.gk2.secubox.in)"
+ "WebFetch(domain:streamlit.gk2.secubox.in)",
+ "Bash(# Use SDK''s package tools cd /home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/sdk # Copy the manually created IPK to SDK''s output cp /home/reepost/CyberMindStudio/secubox-openwrt/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-ipblocklist_1.0.0-r1_all.ipk bin/packages/aarch64_cortex-a72/secubox/ # Regenerate index for that feed cd bin/packages/aarch64_cortex-a72/secubox ../../../../scripts/ipkg-make-index.sh . gzip -k -f Packages # Now rebuild the bonus package which will include everything cd /home/reepost/CyberMindStudio/secubox-openwrt ./secubox-tools/local-build.sh build secubox-app-bonus 2>&1)"
]
}
}
diff --git a/package/secubox/luci-app-peertube/Makefile b/package/secubox/luci-app-peertube/Makefile
index 68514e9b..e3d49035 100644
--- a/package/secubox/luci-app-peertube/Makefile
+++ b/package/secubox/luci-app-peertube/Makefile
@@ -1,5 +1,9 @@
include $(TOPDIR)/rules.mk
+PKG_NAME:=luci-app-peertube
+PKG_VERSION:=1.1.0
+PKG_RELEASE:=1
+
LUCI_TITLE:=LuCI PeerTube Video Platform
LUCI_DEPENDS:=+luci-base +secubox-app-peertube
LUCI_PKGARCH:=all
diff --git a/package/secubox/luci-app-peertube/htdocs/luci-static/resources/peertube/api.js b/package/secubox/luci-app-peertube/htdocs/luci-static/resources/peertube/api.js
index 5da5dec6..3f93d58a 100644
--- a/package/secubox/luci-app-peertube/htdocs/luci-static/resources/peertube/api.js
+++ b/package/secubox/luci-app-peertube/htdocs/luci-static/resources/peertube/api.js
@@ -81,5 +81,12 @@ return L.Class.extend({
object: 'luci.peertube',
method: 'import_status',
expect: { }
+ }),
+
+ importJobStatus: rpc.declare({
+ object: 'luci.peertube',
+ method: 'import_job_status',
+ params: ['job_id'],
+ expect: { }
})
});
diff --git a/package/secubox/luci-app-peertube/htdocs/luci-static/resources/view/peertube/overview.js b/package/secubox/luci-app-peertube/htdocs/luci-static/resources/view/peertube/overview.js
index 1f3a52d5..8ab91e48 100644
--- a/package/secubox/luci-app-peertube/htdocs/luci-static/resources/view/peertube/overview.js
+++ b/package/secubox/luci-app-peertube/htdocs/luci-static/resources/view/peertube/overview.js
@@ -60,7 +60,7 @@ return view.extend({
ui.addNotification(null, E('p', _('Video URL is required')), 'error');
return;
}
- promise = api.importVideo(url);
+ promise = api.importVideo(url).then(function(res) { if (res && res.success && res.job_id) { self.pollImportJob(res.job_id); } return res; });
break;
default:
ui.hideModal();
@@ -83,6 +83,66 @@ return view.extend({
});
},
+ pollImportJob: function(jobId) {
+ var self = this;
+ var statusDiv = document.getElementById('import-status');
+ var pollCount = 0;
+ var maxPolls = 120; // 10 minutes max (5s intervals)
+
+ var updateStatus = function(status, message, isError) {
+ if (statusDiv) {
+ statusDiv.style.display = 'block';
+ statusDiv.style.background = isError ? '#ffebee' : (status === 'completed' ? '#e8f5e9' : '#e3f2fd');
+ statusDiv.innerHTML = '' + message + '';
+ }
+ };
+
+ var poll = function() {
+ api.importJobStatus(jobId).then(function(res) {
+ pollCount++;
+
+ switch(res.status) {
+ case 'downloading':
+ updateStatus('downloading', _('⬇️ Downloading video...'));
+ break;
+ case 'uploading':
+ updateStatus('uploading', _('⬆️ Uploading to PeerTube...'));
+ break;
+ case 'completed':
+ var videoUrl = res.video_uuid ?
+ 'https://' + (document.getElementById('emancipate-domain').value || 'tube.gk2.secubox.in') + '/w/' + res.video_uuid :
+ '';
+ updateStatus('completed', _('✅ Import complete! ') + (videoUrl ? '' + _('View video') + '' : ''));
+ ui.addNotification(null, E('p', _('Video imported successfully!')), 'success');
+ return;
+ case 'download_failed':
+ updateStatus('error', _('❌ Download failed'), true);
+ return;
+ case 'upload_failed':
+ updateStatus('error', _('❌ Upload failed'), true);
+ return;
+ case 'file_not_found':
+ updateStatus('error', _('❌ Downloaded file not found'), true);
+ return;
+ default:
+ if (pollCount >= maxPolls) {
+ updateStatus('error', _('❌ Timeout waiting for import'), true);
+ return;
+ }
+ }
+
+ // Continue polling
+ setTimeout(poll, 5000);
+ }).catch(function(e) {
+ updateStatus('error', _('❌ Error: ') + e.message, true);
+ });
+ };
+
+ // Start polling
+ updateStatus('starting', _('🚀 Starting import...'));
+ setTimeout(poll, 2000);
+ },
+
load: function() {
return Promise.all([
api.status(),
@@ -275,8 +335,9 @@ return view.extend({
E('hr'),
- E('h4', {}, _('Import Video (yt-dlp)')),
- E('p', {}, _('Download and import videos from YouTube, Vimeo, and 1000+ other sites using yt-dlp.')),
+ E('h4', {}, _('Import Video (Auto-Upload)')),
+ E('p', {}, _('Download videos from YouTube, Vimeo, and 1000+ sites. Videos are automatically uploaded to PeerTube.')),
+ E('div', { 'id': 'import-status', 'style': 'padding: 10px; margin-bottom: 10px; border-radius: 4px; background: #f5f5f5; display: none;' }),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Video URL')),
E('div', { 'class': 'cbi-value-field' }, [
@@ -296,7 +357,7 @@ return view.extend({
var urlInput = document.getElementById('import-video-url');
self.handleAction('import_video', urlInput.value);
}
- }, _('Download Video')),
+ }, _('Import & Upload')),
' ',
E('button', {
'class': 'btn cbi-button',
diff --git a/package/secubox/luci-app-peertube/root/usr/libexec/rpcd/luci.peertube b/package/secubox/luci-app-peertube/root/usr/libexec/rpcd/luci.peertube
index d093433c..38b83b06 100644
--- a/package/secubox/luci-app-peertube/root/usr/libexec/rpcd/luci.peertube
+++ b/package/secubox/luci-app-peertube/root/usr/libexec/rpcd/luci.peertube
@@ -254,11 +254,131 @@ method_configure_haproxy() {
json_dump
}
-# Method: import_video (yt-dlp)
+# Helper: Get PeerTube OAuth token
+get_peertube_token() {
+ local hostname port client_id client_secret username password
+ hostname=$(uci_get server hostname "peertube.local")
+ port=$(uci_get server port "9001")
+ username=$(uci_get admin username "root")
+ password=$(uci_get admin password "")
+
+ # Get OAuth client credentials
+ local oauth_response
+ oauth_response=$(curl -s -H "Host: $hostname" "http://127.0.0.1:${port}/api/v1/oauth-clients/local" 2>/dev/null)
+ client_id=$(echo "$oauth_response" | jsonfilter -e '@.client_id' 2>/dev/null)
+ client_secret=$(echo "$oauth_response" | jsonfilter -e '@.client_secret' 2>/dev/null)
+
+ if [ -z "$client_id" ] || [ -z "$client_secret" ]; then
+ echo ""
+ return 1
+ fi
+
+ # Get access token
+ local token_response
+ token_response=$(curl -s -H "Host: $hostname" \
+ -X POST "http://127.0.0.1:${port}/api/v1/users/token" \
+ -d "client_id=$client_id" \
+ -d "client_secret=$client_secret" \
+ -d "grant_type=password" \
+ -d "username=$username" \
+ -d "password=$password" 2>/dev/null)
+
+ local access_token
+ access_token=$(echo "$token_response" | jsonfilter -e '@.access_token' 2>/dev/null)
+ echo "$access_token"
+}
+
+# Helper: Upload video to PeerTube via API (from host via HAProxy)
+upload_to_peertube() {
+ local video_file="$1"
+ local title="$2"
+ local description="$3"
+
+ local hostname port username password
+ hostname=$(uci_get server hostname "peertube.local")
+ port=$(uci_get server port "9001")
+ username=$(uci_get admin username "root")
+ password=$(uci_get admin password "")
+
+ if [ -z "$password" ]; then
+ echo "Admin password not set in UCI config"
+ return 1
+ fi
+
+ # Convert container path to host path via LXC rootfs
+ local host_video_file="/srv/lxc/peertube/rootfs${video_file}"
+ if [ ! -f "$host_video_file" ]; then
+ echo "Video file not found at $host_video_file"
+ return 1
+ fi
+
+ # Get OAuth client credentials (via HAProxy on port 9001)
+ local oauth_response client_id client_secret
+ oauth_response=$(curl -s -H "Host: $hostname" "http://127.0.0.1:${port}/api/v1/oauth-clients/local" 2>/dev/null)
+ client_id=$(echo "$oauth_response" | jsonfilter -e '@.client_id' 2>/dev/null)
+ client_secret=$(echo "$oauth_response" | jsonfilter -e '@.client_secret' 2>/dev/null)
+
+ if [ -z "$client_id" ] || [ -z "$client_secret" ]; then
+ echo "Could not get OAuth client credentials"
+ return 1
+ fi
+
+ # Get access token
+ local token_response access_token
+ token_response=$(curl -s -H "Host: $hostname" \
+ -X POST "http://127.0.0.1:${port}/api/v1/users/token" \
+ -d "client_id=$client_id" \
+ -d "client_secret=$client_secret" \
+ -d "grant_type=password" \
+ -d "username=$username" \
+ -d "password=$password" 2>/dev/null)
+
+ access_token=$(echo "$token_response" | jsonfilter -e '@.access_token' 2>/dev/null)
+
+ if [ -z "$access_token" ]; then
+ echo "Could not get OAuth token: $token_response"
+ return 1
+ fi
+
+ # Get first video channel
+ local channels_response channel_id
+ channels_response=$(curl -s -H "Host: $hostname" -H "Authorization: Bearer $access_token" \
+ "http://127.0.0.1:${port}/api/v1/video-channels" 2>/dev/null)
+ channel_id=$(echo "$channels_response" | jsonfilter -e '@.data[0].id' 2>/dev/null)
+ [ -z "$channel_id" ] && channel_id="1"
+
+ # Truncate title and description
+ local safe_title safe_desc
+ safe_title=$(echo "$title" | head -c 120 | tr -d '"')
+ safe_desc=$(echo "$description" | head -c 500 | tr -d '"')
+
+ # Upload video via API
+ local upload_response video_uuid
+ upload_response=$(curl -s -H "Host: $hostname" -H "Authorization: Bearer $access_token" \
+ -X POST "http://127.0.0.1:${port}/api/v1/videos/upload" \
+ -F "videofile=@$host_video_file" \
+ -F "channelId=$channel_id" \
+ -F "name=$safe_title" \
+ -F "privacy=1" \
+ -F "waitTranscoding=false" 2>/dev/null)
+
+ video_uuid=$(echo "$upload_response" | jsonfilter -e '@.video.uuid' 2>/dev/null)
+
+ if [ -n "$video_uuid" ]; then
+ echo "$video_uuid"
+ return 0
+ else
+ echo "Upload failed: $upload_response"
+ return 1
+ fi
+}
+
+# Method: import_video (yt-dlp + auto-upload)
method_import_video() {
read -r input
json_load "$input"
json_get_var url url
+ json_get_var auto_upload auto_upload
if [ -z "$url" ]; then
json_init
@@ -277,35 +397,136 @@ method_import_video() {
return
fi
- # Get videos path from UCI or use default
- local videos_path
- videos_path=$(uci_get main videos_path "/srv/peertube/videos")
-
# Create import directory inside container
local import_dir="/var/lib/peertube/storage/tmp/import"
lxc-attach -n peertube -- mkdir -p "$import_dir" 2>/dev/null
- # Run yt-dlp in background, save to import directory
- local logfile="/tmp/ytdlp-import-$$.log"
- lxc-attach -n peertube -- /usr/local/bin/yt-dlp \
- --no-playlist \
- --format "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" \
- --merge-output-format mp4 \
- --output "$import_dir/%(title)s.%(ext)s" \
- --write-info-json \
- --write-thumbnail \
- "$url" > "$logfile" 2>&1 &
+ # Generate unique job ID
+ local job_id="import_$(date +%s)"
+ local logfile="/tmp/peertube-${job_id}.log"
+ local statusfile="/tmp/peertube-${job_id}.status"
+
+ # Write initial status
+ echo "downloading" > "$statusfile"
+
+ # Run import script in background
+ (
+ # Download with yt-dlp
+ echo "[$(date)] Starting download: $url" >> "$logfile"
+
+ local output_template="$import_dir/${job_id}_%(title)s.%(ext)s"
+ lxc-attach -n peertube -- /usr/local/bin/yt-dlp \
+ --no-playlist \
+ --format "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]/best" \
+ --merge-output-format mp4 \
+ --output "$output_template" \
+ --write-info-json \
+ --print-to-file filename "/tmp/peertube-${job_id}.filename" \
+ "$url" >> "$logfile" 2>&1
+
+ local dl_rc=$?
+
+ if [ $dl_rc -ne 0 ]; then
+ echo "download_failed" > "$statusfile"
+ echo "[$(date)] Download failed with code $dl_rc" >> "$logfile"
+ exit 1
+ fi
+
+ echo "[$(date)] Download completed" >> "$logfile"
+
+ # Find the downloaded file
+ local video_file
+ video_file=$(lxc-attach -n peertube -- find "$import_dir" -name "${job_id}_*.mp4" -type f 2>/dev/null | head -1)
+
+ if [ -z "$video_file" ]; then
+ echo "file_not_found" > "$statusfile"
+ echo "[$(date)] Downloaded file not found" >> "$logfile"
+ exit 1
+ fi
+
+ echo "[$(date)] Found video: $video_file" >> "$logfile"
+
+ # Convert to host path via LXC rootfs
+ local host_video_file="/srv/lxc/peertube/rootfs${video_file}"
+ local host_info_file="${host_video_file%.mp4}.info.json"
+
+ # Extract title from info.json (using host path)
+ local title description
+ if [ -f "$host_info_file" ]; then
+ title=$(jsonfilter -i "$host_info_file" -e '@.title' 2>/dev/null)
+ description=$(jsonfilter -i "$host_info_file" -e '@.description' 2>/dev/null | head -c 1000)
+ fi
+
+ [ -z "$title" ] && title="Imported Video $(date +%Y%m%d-%H%M%S)"
+ [ -z "$description" ] && description="Imported from: $url"
+
+ echo "uploading" > "$statusfile"
+ echo "[$(date)] Starting upload: $title" >> "$logfile"
+
+ # Upload to PeerTube (pass container path, function converts to host path)
+ local result
+ result=$(upload_to_peertube "$video_file" "$title" "$description")
+
+ if echo "$result" | grep -q "^[a-f0-9-]\{36\}$"; then
+ echo "completed:$result" > "$statusfile"
+ echo "[$(date)] Upload successful! Video UUID: $result" >> "$logfile"
+
+ # Cleanup temp files (using host paths)
+ rm -f "$host_video_file" "${host_video_file%.mp4}.info.json" "${host_video_file%.mp4}.webp" 2>/dev/null
+ echo "[$(date)] Cleaned up temporary files" >> "$logfile"
+ else
+ echo "upload_failed" > "$statusfile"
+ echo "[$(date)] Upload failed: $result" >> "$logfile"
+ fi
+ ) &
local pid=$!
- # Wait up to 5 seconds for initial response
- sleep 2
-
json_init
json_add_boolean "success" 1
- json_add_string "message" "Download started in background (PID: $pid)"
- json_add_string "log_file" "$logfile"
- json_add_string "import_dir" "$import_dir"
+ json_add_string "message" "Import started (auto-upload enabled)"
+ json_add_string "job_id" "$job_id"
+ json_add_int "pid" "$pid"
+ json_dump
+}
+
+# Method: import_job_status - Check specific import job
+method_import_job_status() {
+ read -r input
+ json_load "$input"
+ json_get_var job_id job_id
+
+ local statusfile="/tmp/peertube-${job_id}.status"
+ local logfile="/tmp/peertube-${job_id}.log"
+
+ local status="unknown"
+ local video_uuid=""
+ local logs=""
+
+ if [ -f "$statusfile" ]; then
+ local raw_status
+ raw_status=$(cat "$statusfile")
+
+ case "$raw_status" in
+ completed:*)
+ status="completed"
+ video_uuid="${raw_status#completed:}"
+ ;;
+ *)
+ status="$raw_status"
+ ;;
+ esac
+ fi
+
+ if [ -f "$logfile" ]; then
+ logs=$(tail -20 "$logfile")
+ fi
+
+ json_init
+ json_add_string "job_id" "$job_id"
+ json_add_string "status" "$status"
+ json_add_string "video_uuid" "$video_uuid"
+ json_add_string "logs" "$logs"
json_dump
}
@@ -365,6 +586,9 @@ list_methods() {
json_close_object
json_add_object "import_status"
json_close_object
+ json_add_object "import_job_status"
+ json_add_string "job_id" ""
+ json_close_object
json_dump
}
@@ -417,6 +641,9 @@ case "$1" in
import_status)
method_import_status
;;
+ import_job_status)
+ method_import_job_status
+ ;;
*)
echo '{"error":"Method not found"}'
;;
diff --git a/package/secubox/luci-app-peertube/root/usr/share/rpcd/acl.d/luci-app-peertube.json b/package/secubox/luci-app-peertube/root/usr/share/rpcd/acl.d/luci-app-peertube.json
index 26c1e313..7993bed1 100644
--- a/package/secubox/luci-app-peertube/root/usr/share/rpcd/acl.d/luci-app-peertube.json
+++ b/package/secubox/luci-app-peertube/root/usr/share/rpcd/acl.d/luci-app-peertube.json
@@ -3,7 +3,7 @@
"description": "Grant access to PeerTube management",
"read": {
"ubus": {
- "luci.peertube": ["status", "logs", "import_status"]
+ "luci.peertube": ["status", "logs", "import_status", "import_job_status"]
},
"uci": ["peertube"]
},