feat(peertube): Add auto-upload for video imports

Videos imported via yt-dlp are now automatically uploaded to PeerTube:
- OAuth authentication using UCI-stored admin credentials
- Video upload via PeerTube REST API
- Real-time job status polling with import_job_status method
- Progress indicator in LuCI UI
- Automatic cleanup of temp files

New RPCD method: import_job_status for polling job progress.
Version bumped to 1.1.0.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-21 09:21:18 +01:00
parent b2ec879814
commit 42218a4b78
7 changed files with 347 additions and 27 deletions

View File

@ -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`

View File

@ -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)"
]
}
}

View File

@ -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

View File

@ -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: { }
})
});

View File

@ -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 = '<span style="color:' + (isError ? '#c62828' : (status === 'completed' ? '#2e7d32' : '#1565c0')) + ';">' + message + '</span>';
}
};
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 ? '<a href="' + videoUrl + '" target="_blank">' + _('View video') + '</a>' : ''));
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',

View File

@ -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"}'
;;

View File

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