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"] },