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:
parent
b2ec879814
commit
42218a4b78
@ -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`
|
||||
|
||||
@ -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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: { }
|
||||
})
|
||||
});
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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"}'
|
||||
;;
|
||||
|
||||
@ -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"]
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user