diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md
index 6ab61d38..65a40e72 100644
--- a/.claude/HISTORY.md
+++ b/.claude/HISTORY.md
@@ -3045,3 +3045,46 @@ git checkout HEAD -- index.html
- YouTube videos blocked by PO token requirement for subtitle access
- PeerTube videos on tube.gk2 have no captions uploaded
- Metadata extraction works; transcript step fails without subtitles/Whisper
+
+31. **PeerTube Video Import with Multi-Track Subtitles (2026-02-21)**
+ - New `peertube-import` CLI tool for importing videos from YouTube, Vimeo, and 1000+ sites.
+ - **Features:**
+ - Download video via yt-dlp (best quality MP4)
+ - Extract metadata (title, description, tags)
+ - Download subtitles in multiple languages (configurable)
+ - Upload video to PeerTube via API
+ - Upload each subtitle track via `/api/v1/videos/{id}/captions/{lang}`
+ - **CLI Interface:**
+ ```bash
+ peertube-import --lang fr,en,de,es https://youtube.com/watch?v=xxx
+ peertube-import --privacy 2 --channel 1 https://vimeo.com/xxx
+ ```
+ - **Portal Integration:**
+ - New "Video Import" card in Intelligence & Analyse section
+ - Modal dialog with URL input, language selection, privacy options
+ - Progress bar with live status updates
+ - Direct link to imported video on completion
+ - **CGI Endpoints:**
+ - `POST /cgi-bin/peertube-import` — Start import job
+ - `GET /cgi-bin/peertube-import-status?job_id=xxx` — Poll status
+ - **Authentication:**
+ - Supports PEERTUBE_TOKEN env var
+ - UCI config: `peertube.api.username` / `peertube.api.password`
+ - OAuth client credential flow for token acquisition
+ - Package version bumped to 1.2.0
+ - **Files:**
+ - `secubox-app-peertube/files/usr/sbin/peertube-import` (new)
+ - `secubox-app-peertube/files/www/cgi-bin/peertube-import` (new)
+ - `secubox-app-peertube/files/www/cgi-bin/peertube-import-status` (new)
+ - `luci-app-secubox-portal/root/www/gk2-hub/portal.html` (updated)
+ - `secubox-app-peertube/Makefile` (updated)
+
+31. **PeerTube Import Fixes (2026-02-21)**
+ - Fixed stdout/stderr separation in `peertube-import` script
+ - Changed UCI config path from `peertube.api.*` to `peertube.admin.*`
+ - Fixed yt-dlp output redirection to prevent mixing with function return values
+ - Fixed curl response handling in upload functions (use temp file, not 2>&1)
+ - Upgraded yt-dlp to 2026.2.4 for YouTube compatibility
+ - Installed Node.js (20.20.0) for yt-dlp JavaScript runtime support
+ - Verified end-to-end import flow: YouTube → download → subtitles → PeerTube upload
+
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 2c3effaf..12aba016 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -389,7 +389,12 @@
"Bash(__NEW_LINE_02bd2dd51e90cbf8__ echo \"\")",
"Bash(__NEW_LINE_70eb6f3ae1c26753__ echo \"\")",
"WebFetch(domain:radio.gk2.secubox.in)",
- "WebFetch(domain:nextcloud-talk.readthedocs.io)"
+ "WebFetch(domain:nextcloud-talk.readthedocs.io)",
+ "Bash(__NEW_LINE_0334b7e65952251f__ rm -f \"$COOKIES\")",
+ "Bash(__NEW_LINE_d0f84baac9f3813d__ rm -f \"$COOKIES\")",
+ "Bash(__NEW_LINE_722c25da6bf58fe1__ rm -f \"$COOKIES\" /tmp/login.html)",
+ "WebFetch(domain:portal.nextcloud.com)",
+ "WebFetch(domain:arnowelzel.de)"
]
}
}
diff --git a/package/secubox/luci-app-secubox-portal/root/www/gk2-hub/portal.html b/package/secubox/luci-app-secubox-portal/root/www/gk2-hub/portal.html
index acf58b97..252d7295 100644
--- a/package/secubox/luci-app-secubox-portal/root/www/gk2-hub/portal.html
+++ b/package/secubox/luci-app-secubox-portal/root/www/gk2-hub/portal.html
@@ -108,6 +108,7 @@
text-decoration: none;
color: inherit;
transition: all 0.2s;
+ cursor: pointer;
}
.service-card:hover {
border-color: var(--accent);
@@ -126,6 +127,101 @@
font-size: 0.85rem;
}
.footer a { color: var(--accent); text-decoration: none; }
+
+ /* Modal styles */
+ .modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ }
+ .modal-box {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ width: 90%;
+ max-width: 500px;
+ box-shadow: 0 20px 60px rgba(0,95,158,0.3);
+ }
+ .modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px;
+ border-bottom: 1px solid var(--border);
+ }
+ .modal-header h3 { margin: 0; color: #fff; }
+ .modal-close {
+ background: none;
+ border: none;
+ color: var(--muted);
+ font-size: 1.5rem;
+ cursor: pointer;
+ }
+ .modal-close:hover { color: #fff; }
+ .modal-body { padding: 20px; }
+ .modal-footer {
+ padding: 15px 20px;
+ border-top: 1px solid var(--border);
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ }
+ .form-group { margin-bottom: 15px; }
+ .form-group label { display: block; margin-bottom: 5px; color: var(--text); font-size: 0.9rem; }
+ .modal-input {
+ width: 100%;
+ padding: 10px 12px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--text);
+ font-size: 0.95rem;
+ }
+ .modal-input:focus { outline: none; border-color: var(--accent); }
+ .checkbox-group { display: flex; flex-wrap: wrap; gap: 15px; }
+ .checkbox-label { display: flex; align-items: center; gap: 5px; cursor: pointer; }
+ .checkbox-label input { accent-color: var(--accent); }
+ .btn-primary {
+ background: var(--accent);
+ border: none;
+ color: #fff;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-weight: 600;
+ }
+ .btn-primary:hover { background: #007acc; }
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
+ .btn-secondary {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text);
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ }
+ .btn-secondary:hover { border-color: var(--accent); }
+ .progress-bar {
+ background: var(--bg);
+ border-radius: 4px;
+ height: 8px;
+ overflow: hidden;
+ margin-bottom: 10px;
+ }
+ .progress-fill {
+ background: var(--accent);
+ height: 100%;
+ width: 0%;
+ transition: width 0.3s;
+ }
+ .progress-status { font-size: 0.85rem; color: var(--muted); }
+ .result-success { color: #2ecc71; padding: 15px; background: rgba(46,204,113,0.1); border-radius: 6px; }
+ .result-success a { color: var(--accent); }
+ .result-error { color: #e74c3c; padding: 15px; background: rgba(231,76,60,0.1); border-radius: 6px; }
@@ -203,15 +299,25 @@
Intelligence & Analyse
Administration
@@ -243,6 +349,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package/secubox/secubox-app-peertube/Makefile b/package/secubox/secubox-app-peertube/Makefile
index 10318985..403404c0 100644
--- a/package/secubox/secubox-app-peertube/Makefile
+++ b/package/secubox/secubox-app-peertube/Makefile
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-peertube
PKG_RELEASE:=1
-PKG_VERSION:=1.1.0
+PKG_VERSION:=1.2.0
PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio
PKG_LICENSE:=AGPL-3.0
@@ -23,6 +23,7 @@ PeerTube federated video streaming platform.
Runs in an LXC Debian container with PostgreSQL, Redis, and Node.js.
Supports video hosting, live streaming, and ActivityPub federation.
Includes peertube-analyse: transcript extraction and Claude AI analysis.
+Includes peertube-import: auto-import from YouTube/Vimeo with multi-track subtitles.
endef
define Package/secubox-app-peertube/conffiles
@@ -42,6 +43,7 @@ define Package/secubox-app-peertube/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/peertubectl $(1)/usr/sbin/peertubectl
$(INSTALL_BIN) ./files/usr/sbin/peertube-analyse $(1)/usr/sbin/peertube-analyse
+ $(INSTALL_BIN) ./files/usr/sbin/peertube-import $(1)/usr/sbin/peertube-import
$(INSTALL_DIR) $(1)/www/peertube-analyse
$(INSTALL_DATA) ./files/www/peertube-analyse/index.html $(1)/www/peertube-analyse/
@@ -49,6 +51,8 @@ define Package/secubox-app-peertube/install
$(INSTALL_DIR) $(1)/www/cgi-bin
$(INSTALL_BIN) ./files/www/cgi-bin/peertube-analyse $(1)/www/cgi-bin/
$(INSTALL_BIN) ./files/www/cgi-bin/peertube-analyse-status $(1)/www/cgi-bin/
+ $(INSTALL_BIN) ./files/www/cgi-bin/peertube-import $(1)/www/cgi-bin/
+ $(INSTALL_BIN) ./files/www/cgi-bin/peertube-import-status $(1)/www/cgi-bin/
endef
$(eval $(call BuildPackage,secubox-app-peertube))
diff --git a/package/secubox/secubox-app-peertube/README.md b/package/secubox/secubox-app-peertube/README.md
new file mode 100644
index 00000000..47521efc
--- /dev/null
+++ b/package/secubox/secubox-app-peertube/README.md
@@ -0,0 +1,132 @@
+# SecuBox PeerTube
+
+Federated video streaming platform running in an LXC Debian container.
+
+## Features
+
+- **PeerTube Instance**: Self-hosted video platform with ActivityPub federation
+- **Video Import**: Import videos from YouTube, Vimeo, and 1000+ sites via yt-dlp
+- **Multi-Track Subtitles**: Automatic subtitle download and sync in multiple languages
+- **Video Analysis**: Transcript extraction and Claude AI analysis (peertube-analyse)
+- **Live Streaming**: RTMP ingest with HLS output
+
+## Components
+
+| Component | Description |
+|-----------|-------------|
+| `peertubectl` | Main control script for container management |
+| `peertube-import` | Video import with subtitle sync |
+| `peertube-analyse` | Transcript extraction and AI analysis |
+
+## Video Import
+
+Import videos from external platforms with automatic subtitle synchronization.
+
+### CLI Usage
+
+```bash
+# Basic import
+peertube-import https://youtube.com/watch?v=xxx
+
+# Import with multiple subtitle languages
+peertube-import --lang fr,en,de,es https://youtube.com/watch?v=xxx
+
+# Import as unlisted video
+peertube-import --privacy 2 https://youtube.com/watch?v=xxx
+
+# Import to specific channel
+peertube-import --channel 2 https://vimeo.com/xxx
+```
+
+### Options
+
+| Option | Description | Default |
+|--------|-------------|---------|
+| `--lang ` | Subtitle languages (comma-separated) | `fr,en` |
+| `--channel ` | PeerTube channel ID | `1` |
+| `--privacy ` | 1=public, 2=unlisted, 3=private | `1` |
+| `--output ` | Temp directory for downloads | `/tmp/peertube-import` |
+| `--peertube ` | PeerTube instance URL | from UCI config |
+
+### Portal Integration
+
+Access via SecuBox Portal → Intelligence & Analyse → Video Import
+
+The portal provides:
+- URL input for video source
+- Language selection checkboxes
+- Privacy level selector
+- Real-time progress tracking
+- Direct link to imported video
+
+### CGI Endpoints
+
+```bash
+# Start import job
+curl -X POST http://192.168.255.1/cgi-bin/peertube-import \
+ -H "Content-Type: application/json" \
+ -d '{"url":"https://youtube.com/watch?v=xxx","languages":"fr,en"}'
+
+# Response: {"success": true, "job_id": "import_xxx"}
+
+# Check status
+curl "http://192.168.255.1/cgi-bin/peertube-import-status?job_id=import_xxx"
+
+# Response (in progress):
+# {"status": "downloading", "progress": 45, "job_id": "import_xxx"}
+
+# Response (completed):
+# {"success": true, "video_url": "https://tube.example.com/w/uuid"}
+```
+
+## Configuration
+
+UCI config file: `/etc/config/peertube`
+
+```
+config peertube 'main'
+ option enabled '1'
+ option data_path '/srv/peertube'
+
+config peertube 'server'
+ option hostname 'tube.example.com'
+ option port '9001'
+ option https '1'
+
+config peertube 'admin'
+ option username 'root'
+ option password 'changeme'
+
+config peertube 'transcoding'
+ option enabled '1'
+ option threads '2'
+ list resolutions '480p'
+ list resolutions '720p'
+```
+
+## Dependencies
+
+- `lxc`, `lxc-common` - Container runtime
+- `wget-ssl` - HTTPS downloads
+- `tar`, `jsonfilter` - Archive and JSON handling
+- `yt-dlp` - Video download (pip install)
+- `node` - JavaScript runtime for yt-dlp (opkg install)
+
+## Supported Import Sources
+
+yt-dlp supports 1000+ sites including:
+- YouTube, YouTube Music
+- Vimeo
+- Dailymotion
+- Twitch (VODs)
+- Twitter/X
+- TikTok
+- And many more...
+
+See: https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md
+
+## Version
+
+- Package: 1.2.0
+- yt-dlp: 2026.2.4 (recommended)
+- Node.js: 20.20.0 (for YouTube JS runtime)
diff --git a/package/secubox/secubox-app-peertube/files/usr/sbin/peertube-import b/package/secubox/secubox-app-peertube/files/usr/sbin/peertube-import
new file mode 100644
index 00000000..12c1d788
--- /dev/null
+++ b/package/secubox/secubox-app-peertube/files/usr/sbin/peertube-import
@@ -0,0 +1,586 @@
+#!/bin/sh
+# PeerTube Video Import with Multi-Track Subtitle Sync
+# SecuBox Intelligence Module
+# Compatible: OpenWrt
+
+set -e
+
+#=============================================================================
+# CONFIGURATION
+#=============================================================================
+
+SCRIPT_VERSION="1.0.0"
+PEERTUBE_URL="${PEERTUBE_URL:-https://tube.gk2.secubox.in}"
+PEERTUBE_TOKEN="${PEERTUBE_TOKEN:-}"
+OUTPUT_BASE="${OUTPUT_BASE:-/tmp/peertube-import}"
+DEFAULT_CHANNEL_ID=1
+DEFAULT_PRIVACY=1 # 1=public, 2=unlisted, 3=private
+
+#=============================================================================
+# LOGGING
+#=============================================================================
+
+log_info() { echo >&2 "[INFO] $*"; }
+log_ok() { echo >&2 "[OK] $*"; }
+log_warn() { echo >&2 "[WARN] $*"; }
+log_error() { echo >&2 "[ERROR] $*"; }
+log_step() { echo >&2 ""; echo >&2 "==> $*"; }
+log_progress() { echo >&2 "[PROGRESS] $*"; }
+
+#=============================================================================
+# UTILITY FUNCTIONS
+#=============================================================================
+
+# Generate slug from title
+generate_slug() {
+ echo "$1" | tr '[:upper:]' '[:lower:]' | \
+ sed -E 's/[àáâãäå]/a/g; s/[èéêë]/e/g; s/[ìíîï]/i/g; s/[òóôõö]/o/g; s/[ùúûü]/u/g; s/[ç]/c/g' | \
+ sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g' | \
+ cut -c1-50
+}
+
+# Get PeerTube authentication token
+get_peertube_token() {
+ local username="$1"
+ local password="$2"
+
+ if [ -n "$PEERTUBE_TOKEN" ]; then
+ echo "$PEERTUBE_TOKEN"
+ return 0
+ fi
+
+ # Read credentials from UCI config (admin section)
+ if [ -z "$username" ]; then
+ username=$(uci -q get peertube.admin.username)
+ password=$(uci -q get peertube.admin.password)
+ fi
+
+ # Try to get OAuth client credentials
+ local client_id client_secret
+ local oauth_clients
+ oauth_clients=$(curl -s "${PEERTUBE_URL}/api/v1/oauth-clients/local")
+
+ if command -v jq >/dev/null 2>&1; then
+ client_id=$(echo "$oauth_clients" | jq -r '.client_id // empty')
+ client_secret=$(echo "$oauth_clients" | jq -r '.client_secret // empty')
+ else
+ client_id=$(echo "$oauth_clients" | jsonfilter -e '@.client_id' 2>/dev/null)
+ client_secret=$(echo "$oauth_clients" | jsonfilter -e '@.client_secret' 2>/dev/null)
+ fi
+
+ if [ -z "$client_id" ] || [ -z "$username" ]; then
+ log_error "Cannot get PeerTube token. Set PEERTUBE_TOKEN or configure api credentials."
+ return 1
+ fi
+
+ # Get access token
+ local token_response
+ token_response=$(curl -s -X POST "${PEERTUBE_URL}/api/v1/users/token" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "client_id=${client_id}" \
+ -d "client_secret=${client_secret}" \
+ -d "grant_type=password" \
+ -d "response_type=code" \
+ -d "username=${username}" \
+ -d "password=${password}")
+
+ local token
+ if command -v jq >/dev/null 2>&1; then
+ token=$(echo "$token_response" | jq -r '.access_token // empty')
+ else
+ token=$(echo "$token_response" | jsonfilter -e '@.access_token' 2>/dev/null)
+ fi
+
+ if [ -z "$token" ]; then
+ log_error "Failed to get access token"
+ return 1
+ fi
+
+ echo "$token"
+}
+
+#=============================================================================
+# VIDEO DOWNLOAD
+#=============================================================================
+
+download_video() {
+ local url="$1"
+ local output_dir="$2"
+ local slug="$3"
+
+ log_step "Downloading video"
+
+ local video_file="$output_dir/${slug}.%(ext)s"
+
+ # Download best quality video - redirect output to stderr to keep stdout clean
+ if yt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" \
+ --merge-output-format mp4 \
+ -o "$output_dir/${slug}.%(ext)s" \
+ --no-playlist \
+ "$url" >&2 2>&1; then
+
+ # Find downloaded file
+ local found_video
+ found_video=$(find "$output_dir" -name "${slug}.*" -type f | grep -E '\.(mp4|webm|mkv)$' | head -1)
+
+ if [ -n "$found_video" ] && [ -f "$found_video" ]; then
+ log_ok "Video downloaded: $found_video"
+ echo "$found_video"
+ return 0
+ fi
+ fi
+
+ log_error "Failed to download video"
+ return 1
+}
+
+#=============================================================================
+# METADATA EXTRACTION
+#=============================================================================
+
+extract_metadata() {
+ local url="$1"
+ local output_dir="$2"
+ local slug="$3"
+
+ log_step "Extracting metadata"
+
+ local meta_file="$output_dir/${slug}.meta.json"
+
+ if yt-dlp --dump-json --no-warnings "$url" 2>/dev/null > "$meta_file.tmp"; then
+ if command -v jq >/dev/null 2>&1; then
+ jq '{
+ id: .id,
+ title: .title,
+ description: .description,
+ duration: .duration,
+ upload_date: .upload_date,
+ uploader: .uploader,
+ channel: .channel,
+ tags: .tags,
+ webpage_url: .webpage_url,
+ thumbnail: .thumbnail,
+ subtitles: ((.subtitles // {}) | keys),
+ automatic_captions: ((.automatic_captions // {}) | keys)
+ }' "$meta_file.tmp" > "$meta_file"
+ else
+ mv "$meta_file.tmp" "$meta_file"
+ fi
+ rm -f "$meta_file.tmp"
+ log_ok "Metadata saved: $meta_file"
+ echo "$meta_file"
+ return 0
+ fi
+
+ log_error "Failed to extract metadata"
+ return 1
+}
+
+#=============================================================================
+# SUBTITLE DOWNLOAD
+#=============================================================================
+
+download_subtitles() {
+ local url="$1"
+ local output_dir="$2"
+ local slug="$3"
+ local languages="$4" # Comma-separated: fr,en,de
+
+ log_step "Downloading subtitles"
+
+ [ -z "$languages" ] && languages="fr,en"
+
+ log_info "Requested languages: $languages"
+
+ # Download subtitles with yt-dlp
+ yt-dlp --write-sub --write-auto-sub \
+ --sub-lang "$languages" \
+ --sub-format vtt \
+ --convert-subs vtt \
+ --skip-download \
+ -o "$output_dir/${slug}" \
+ "$url" 2>&1 || true
+
+ # List downloaded subtitle files
+ local count=0
+ for vtt in "$output_dir"/${slug}*.vtt; do
+ [ -f "$vtt" ] || continue
+ count=$((count + 1))
+ log_ok "Downloaded: $(basename "$vtt")"
+ done
+
+ if [ "$count" -eq 0 ]; then
+ log_warn "No subtitles found for requested languages"
+ return 1
+ fi
+
+ log_ok "Downloaded $count subtitle file(s)"
+ return 0
+}
+
+#=============================================================================
+# PEERTUBE UPLOAD
+#=============================================================================
+
+upload_video_to_peertube() {
+ local video_file="$1"
+ local meta_file="$2"
+ local channel_id="$3"
+ local privacy="$4"
+ local token="$5"
+
+ log_step "Uploading video to PeerTube"
+
+ [ -z "$channel_id" ] && channel_id=$DEFAULT_CHANNEL_ID
+ [ -z "$privacy" ] && privacy=$DEFAULT_PRIVACY
+
+ # Get title and description from metadata
+ local title description
+ if command -v jq >/dev/null 2>&1 && [ -f "$meta_file" ]; then
+ title=$(jq -r '.title // "Imported Video"' "$meta_file")
+ description=$(jq -r '.description // ""' "$meta_file" | head -c 10000)
+ else
+ title="Imported Video"
+ description=""
+ fi
+
+ log_info "Title: $title"
+ log_info "Channel ID: $channel_id"
+ log_info "Privacy: $privacy"
+
+ # Sanitize description (escape newlines and quotes)
+ description=$(echo "$description" | tr '\n' ' ' | sed 's/"/\\"/g' | head -c 5000)
+
+ # Upload video - use temp file to avoid mixing stderr with response
+ local tmpfile="/tmp/peertube_upload_$$.json"
+ local http_code
+
+ http_code=$(curl -s -w "%{http_code}" -o "$tmpfile" -X POST "${PEERTUBE_URL}/api/v1/videos/upload" \
+ -H "Authorization: Bearer ${token}" \
+ -F "videofile=@${video_file}" \
+ -F "channelId=${channel_id}" \
+ -F "name=${title}" \
+ -F "privacy=${privacy}" \
+ -F "description=${description}" \
+ 2>/dev/null)
+
+ local body=""
+ [ -f "$tmpfile" ] && body=$(cat "$tmpfile")
+ rm -f "$tmpfile"
+
+ if [ "$http_code" = "200" ] || [ "$http_code" = "201" ]; then
+ local video_id video_uuid
+ if command -v jq >/dev/null 2>&1; then
+ video_id=$(echo "$body" | jq -r '.video.id // empty')
+ video_uuid=$(echo "$body" | jq -r '.video.uuid // empty')
+ else
+ video_id=$(echo "$body" | jsonfilter -e '@.video.id' 2>/dev/null)
+ video_uuid=$(echo "$body" | jsonfilter -e '@.video.uuid' 2>/dev/null)
+ fi
+
+ if [ -n "$video_id" ]; then
+ log_ok "Video uploaded: ID=$video_id, UUID=$video_uuid"
+ echo "${video_id}|${video_uuid}"
+ return 0
+ fi
+ fi
+
+ log_error "Upload failed (HTTP $http_code)"
+ log_error "$body"
+ return 1
+}
+
+upload_subtitles_to_peertube() {
+ local video_id="$1"
+ local output_dir="$2"
+ local slug="$3"
+ local token="$4"
+
+ log_step "Uploading subtitles to PeerTube"
+
+ local uploaded=0
+
+ for vtt in "$output_dir"/${slug}*.vtt; do
+ [ -f "$vtt" ] || continue
+
+ # Extract language code from filename
+ # Format: slug.fr.vtt or slug.en-US.vtt
+ local filename
+ filename=$(basename "$vtt")
+ local lang
+ lang=$(echo "$filename" | sed -E "s/${slug}\.([a-zA-Z-]+)\.vtt/\1/")
+
+ # Normalize language code (en-US -> en)
+ lang=$(echo "$lang" | cut -d'-' -f1)
+
+ log_info "Uploading subtitle: $lang ($filename)"
+
+ local http_code
+ http_code=$(curl -s -w "%{http_code}" -o /dev/null -X PUT \
+ "${PEERTUBE_URL}/api/v1/videos/${video_id}/captions/${lang}" \
+ -H "Authorization: Bearer ${token}" \
+ -F "captionfile=@${vtt}" \
+ 2>/dev/null)
+
+ if [ "$http_code" = "200" ] || [ "$http_code" = "204" ] || [ "$http_code" = "201" ]; then
+ log_ok "Uploaded: $lang"
+ uploaded=$((uploaded + 1))
+ else
+ log_warn "Failed to upload $lang (HTTP $http_code)"
+ fi
+ done
+
+ log_ok "Uploaded $uploaded subtitle(s)"
+ return 0
+}
+
+#=============================================================================
+# MAIN IMPORT FUNCTION
+#=============================================================================
+
+import_video() {
+ local url="$1"
+ local languages="$2"
+ local channel_id="$3"
+ local privacy="$4"
+ local username="$5"
+ local password="$6"
+
+ log_step "Starting video import"
+ log_info "URL: $url"
+
+ # Create output directory
+ # If OUTPUT_BASE already contains import_ (called from CGI), use it as-is
+ local output_dir
+ if echo "$OUTPUT_BASE" | grep -q "/import_"; then
+ output_dir="$OUTPUT_BASE"
+ else
+ output_dir="$OUTPUT_BASE/import_$(date +%s)_$$"
+ fi
+ mkdir -p "$output_dir"
+
+ # Extract metadata first to get title
+ local meta_file
+ meta_file=$(extract_metadata "$url" "$output_dir" "video") || {
+ log_error "Metadata extraction failed"
+ return 1
+ }
+
+ # Generate slug from title
+ local title slug
+ if command -v jq >/dev/null 2>&1; then
+ title=$(jq -r '.title // "video"' "$meta_file")
+ else
+ title=$(jsonfilter -i "$meta_file" -e '@.title' 2>/dev/null || echo "video")
+ fi
+ slug=$(generate_slug "$title")
+ [ -z "$slug" ] && slug="video_$$"
+
+ # Rename metadata file
+ mv "$meta_file" "$output_dir/${slug}.meta.json"
+ meta_file="$output_dir/${slug}.meta.json"
+
+ log_progress "downloading"
+
+ # Download video
+ local video_file
+ video_file=$(download_video "$url" "$output_dir" "$slug") || {
+ log_error "Video download failed"
+ return 1
+ }
+
+ # Download subtitles
+ download_subtitles "$url" "$output_dir" "$slug" "$languages" || {
+ log_warn "Subtitle download failed (continuing without subtitles)"
+ }
+
+ log_progress "uploading"
+
+ # Get PeerTube token
+ local token
+ token=$(get_peertube_token "$username" "$password") || {
+ log_error "Authentication failed"
+ return 1
+ }
+
+ # Upload video
+ local upload_result
+ upload_result=$(upload_video_to_peertube "$video_file" "$meta_file" "$channel_id" "$privacy" "$token") || {
+ log_error "Video upload failed"
+ return 1
+ }
+
+ local video_id video_uuid
+ video_id=$(echo "$upload_result" | cut -d'|' -f1)
+ video_uuid=$(echo "$upload_result" | cut -d'|' -f2)
+
+ # Upload subtitles
+ upload_subtitles_to_peertube "$video_id" "$output_dir" "$slug" "$token" || {
+ log_warn "Subtitle upload failed"
+ }
+
+ log_progress "completed"
+
+ # Output result
+ log_step "Import complete"
+ log_ok "Video ID: $video_id"
+ log_ok "Video UUID: $video_uuid"
+ log_ok "URL: ${PEERTUBE_URL}/w/${video_uuid}"
+
+ # Output JSON result
+ cat << EOF
+{
+ "success": true,
+ "video_id": $video_id,
+ "video_uuid": "$video_uuid",
+ "video_url": "${PEERTUBE_URL}/w/${video_uuid}",
+ "title": $(echo "$title" | jq -Rs . 2>/dev/null || echo "\"$title\""),
+ "output_dir": "$output_dir"
+}
+EOF
+
+ return 0
+}
+
+#=============================================================================
+# CLI PARSING
+#=============================================================================
+
+show_help() {
+ cat << EOF
+PeerTube Video Import with Subtitle Sync
+SecuBox Intelligence Module v${SCRIPT_VERSION}
+
+Usage:
+ $(basename "$0") [OPTIONS]
+
+Options:
+ --url Video URL (YouTube, Vimeo, etc.)
+ --lang Subtitle languages (comma-separated: fr,en,de)
+ Default: fr,en
+ --channel PeerTube channel ID (default: 1)
+ --privacy 1=public, 2=unlisted, 3=private (default: 1)
+ --username PeerTube username (or set via UCI)
+ --password PeerTube password (or set via UCI)
+ --output Output directory (default: /tmp/peertube-import)
+ --peertube PeerTube instance URL
+ -h, --help Show this help message
+
+Environment Variables:
+ PEERTUBE_URL PeerTube instance URL
+ PEERTUBE_TOKEN OAuth access token (skip authentication)
+
+Examples:
+ # Basic import
+ $(basename "$0") https://youtube.com/watch?v=xxx
+
+ # Import with multiple subtitle languages
+ $(basename "$0") --lang fr,en,de,es https://youtube.com/watch?v=xxx
+
+ # Import as unlisted video
+ $(basename "$0") --privacy 2 https://youtube.com/watch?v=xxx
+
+EOF
+}
+
+parse_args() {
+ VIDEO_URL=""
+ LANGUAGES="fr,en"
+ CHANNEL_ID=""
+ PRIVACY=""
+ USERNAME=""
+ PASSWORD=""
+
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --url)
+ VIDEO_URL="$2"
+ shift 2
+ ;;
+ --lang|--languages)
+ LANGUAGES="$2"
+ shift 2
+ ;;
+ --channel)
+ CHANNEL_ID="$2"
+ shift 2
+ ;;
+ --privacy)
+ PRIVACY="$2"
+ shift 2
+ ;;
+ --username)
+ USERNAME="$2"
+ shift 2
+ ;;
+ --password)
+ PASSWORD="$2"
+ shift 2
+ ;;
+ --output)
+ OUTPUT_BASE="$2"
+ shift 2
+ ;;
+ --peertube)
+ PEERTUBE_URL="$2"
+ shift 2
+ ;;
+ -h|--help)
+ show_help
+ exit 0
+ ;;
+ -*)
+ log_error "Unknown option: $1"
+ show_help
+ exit 1
+ ;;
+ *)
+ # Positional argument = URL
+ if [ -z "$VIDEO_URL" ]; then
+ VIDEO_URL="$1"
+ else
+ log_error "Multiple URLs not supported"
+ exit 1
+ fi
+ shift
+ ;;
+ esac
+ done
+
+ if [ -z "$VIDEO_URL" ]; then
+ log_error "No video URL provided"
+ show_help
+ exit 1
+ fi
+}
+
+#=============================================================================
+# ENTRY POINT
+#=============================================================================
+
+main() {
+ parse_args "$@"
+
+ echo >&2 ""
+ echo >&2 "╔══════════════════════════════════════════════════════╗"
+ echo >&2 "║ PeerTube Video Import v${SCRIPT_VERSION} ║"
+ echo >&2 "║ SecuBox Intelligence Module ║"
+ echo >&2 "╚══════════════════════════════════════════════════════╝"
+ echo >&2 ""
+
+ # Check dependencies
+ for dep in yt-dlp curl; do
+ if ! command -v "$dep" >/dev/null 2>&1; then
+ log_error "Required dependency not found: $dep"
+ exit 1
+ fi
+ done
+
+ import_video "$VIDEO_URL" "$LANGUAGES" "$CHANNEL_ID" "$PRIVACY" "$USERNAME" "$PASSWORD"
+ exit $?
+}
+
+# Run if not sourced
+if [ "${0##*/}" = "peertube-import" ] || [ "${0##*/}" = "sh" ]; then
+ main "$@"
+fi
diff --git a/package/secubox/secubox-app-peertube/files/www/cgi-bin/peertube-import b/package/secubox/secubox-app-peertube/files/www/cgi-bin/peertube-import
new file mode 100644
index 00000000..21102b98
--- /dev/null
+++ b/package/secubox/secubox-app-peertube/files/www/cgi-bin/peertube-import
@@ -0,0 +1,164 @@
+#!/bin/sh
+# CGI endpoint for PeerTube video import
+# Returns JSON response
+
+# Set headers
+printf "Content-Type: application/json\r\n"
+printf "Access-Control-Allow-Origin: *\r\n"
+printf "Access-Control-Allow-Methods: POST, OPTIONS\r\n"
+printf "Access-Control-Allow-Headers: Content-Type\r\n"
+printf "\r\n"
+
+# Handle OPTIONS (CORS preflight)
+if [ "$REQUEST_METHOD" = "OPTIONS" ]; then
+ exit 0
+fi
+
+# Only allow POST
+if [ "$REQUEST_METHOD" != "POST" ]; then
+ echo '{"error": "Method not allowed"}'
+ exit 0
+fi
+
+# Read input
+INPUT=$(cat)
+
+# Parse JSON (use jq if available, else jsonfilter)
+if command -v jq >/dev/null 2>&1; then
+ URL=$(echo "$INPUT" | jq -r '.url // empty')
+ LANGUAGES=$(echo "$INPUT" | jq -r '.languages // "fr,en"')
+ CHANNEL_ID=$(echo "$INPUT" | jq -r '.channel_id // "1"')
+ PRIVACY=$(echo "$INPUT" | jq -r '.privacy // "1"')
+else
+ URL=$(echo "$INPUT" | jsonfilter -e '@.url' 2>/dev/null)
+ LANGUAGES=$(echo "$INPUT" | jsonfilter -e '@.languages' 2>/dev/null)
+ CHANNEL_ID=$(echo "$INPUT" | jsonfilter -e '@.channel_id' 2>/dev/null)
+ PRIVACY=$(echo "$INPUT" | jsonfilter -e '@.privacy' 2>/dev/null)
+fi
+
+# Handle array of languages
+if echo "$LANGUAGES" | grep -q '^\['; then
+ if command -v jq >/dev/null 2>&1; then
+ LANGUAGES=$(echo "$INPUT" | jq -r '.languages | if type == "array" then join(",") else . end')
+ else
+ LANGUAGES="fr,en"
+ fi
+fi
+
+# Validate URL
+if [ -z "$URL" ]; then
+ echo '{"error": "URL is required"}'
+ exit 0
+fi
+
+# Sanitize URL (basic security check)
+case "$URL" in
+ http://*|https://*)
+ # Valid URL prefix
+ ;;
+ *)
+ echo '{"error": "Invalid URL format"}'
+ exit 0
+ ;;
+esac
+
+# Set defaults
+[ -z "$LANGUAGES" ] && LANGUAGES="fr,en"
+[ -z "$CHANNEL_ID" ] && CHANNEL_ID="1"
+[ -z "$PRIVACY" ] && PRIVACY="1"
+
+# Generate job ID
+JOB_ID="import_$(date +%s)_$$"
+STATUS_FILE="/tmp/peertube-import-${JOB_ID}.status"
+RESULT_FILE="/tmp/peertube-import-${JOB_ID}.json"
+LOG_FILE="/tmp/peertube-import-${JOB_ID}.log"
+PROGRESS_FILE="/tmp/peertube-import-${JOB_ID}.progress"
+
+# Check for import script
+if [ ! -x "/usr/sbin/peertube-import" ]; then
+ echo '{"error": "peertube-import not installed"}'
+ exit 0
+fi
+
+# Initialize status
+echo "starting" > "$STATUS_FILE"
+echo "0" > "$PROGRESS_FILE"
+
+# Start import in background
+(
+ echo "downloading" > "$STATUS_FILE"
+ echo "10" > "$PROGRESS_FILE"
+
+ # Run the import
+ OUTPUT=$(/usr/sbin/peertube-import \
+ --lang "$LANGUAGES" \
+ --channel "$CHANNEL_ID" \
+ --privacy "$PRIVACY" \
+ "$URL" 2>&1)
+ RC=$?
+
+ echo "$OUTPUT" >> "$LOG_FILE"
+
+ if [ $RC -eq 0 ]; then
+ echo "completed" > "$STATUS_FILE"
+ echo "100" > "$PROGRESS_FILE"
+
+ # Extract JSON result from output (last JSON block)
+ local result_json
+ result_json=$(echo "$OUTPUT" | grep -E '^\{' | tail -1)
+
+ if [ -n "$result_json" ]; then
+ # Parse and rebuild result
+ if command -v jq >/dev/null 2>&1; then
+ VIDEO_ID=$(echo "$result_json" | jq -r '.video_id // empty')
+ VIDEO_UUID=$(echo "$result_json" | jq -r '.video_uuid // empty')
+ VIDEO_URL=$(echo "$result_json" | jq -r '.video_url // empty')
+ TITLE=$(echo "$result_json" | jq -r '.title // "Imported Video"')
+ else
+ VIDEO_ID=$(echo "$result_json" | jsonfilter -e '@.video_id' 2>/dev/null)
+ VIDEO_UUID=$(echo "$result_json" | jsonfilter -e '@.video_uuid' 2>/dev/null)
+ VIDEO_URL=$(echo "$result_json" | jsonfilter -e '@.video_url' 2>/dev/null)
+ TITLE="Imported Video"
+ fi
+
+ cat > "$RESULT_FILE" << EOF
+{
+ "success": true,
+ "job_id": "$JOB_ID",
+ "video_id": $VIDEO_ID,
+ "video_uuid": "$VIDEO_UUID",
+ "video_url": "$VIDEO_URL",
+ "title": "$TITLE",
+ "status": "completed"
+}
+EOF
+ else
+ cat > "$RESULT_FILE" << EOF
+{
+ "success": true,
+ "job_id": "$JOB_ID",
+ "status": "completed",
+ "message": "Import completed but no result JSON found"
+}
+EOF
+ fi
+ else
+ echo "failed" > "$STATUS_FILE"
+ echo "0" > "$PROGRESS_FILE"
+
+ # Get last error from log
+ ERROR_MSG=$(tail -5 "$LOG_FILE" 2>/dev/null | tr '\n' ' ' | sed 's/"/\\"/g' | head -c 500)
+
+ cat > "$RESULT_FILE" << EOF
+{
+ "success": false,
+ "job_id": "$JOB_ID",
+ "status": "failed",
+ "error": "$ERROR_MSG"
+}
+EOF
+ fi
+) &
+
+# Return job ID for polling
+echo "{\"success\": true, \"message\": \"Import started\", \"job_id\": \"$JOB_ID\"}"
diff --git a/package/secubox/secubox-app-peertube/files/www/cgi-bin/peertube-import-status b/package/secubox/secubox-app-peertube/files/www/cgi-bin/peertube-import-status
new file mode 100644
index 00000000..13cd82ff
--- /dev/null
+++ b/package/secubox/secubox-app-peertube/files/www/cgi-bin/peertube-import-status
@@ -0,0 +1,69 @@
+#!/bin/sh
+# CGI endpoint for checking PeerTube import status
+# Returns JSON response
+
+# Set headers
+printf "Content-Type: application/json\r\n"
+printf "Access-Control-Allow-Origin: *\r\n"
+printf "\r\n"
+
+# Get job_id from query string or POST body
+JOB_ID=""
+
+if [ -n "$QUERY_STRING" ]; then
+ JOB_ID=$(echo "$QUERY_STRING" | sed -n 's/.*job_id=\([^&]*\).*/\1/p')
+fi
+
+if [ -z "$JOB_ID" ] && [ "$REQUEST_METHOD" = "POST" ]; then
+ INPUT=$(cat)
+ if command -v jq >/dev/null 2>&1; then
+ JOB_ID=$(echo "$INPUT" | jq -r '.job_id // empty')
+ else
+ JOB_ID=$(echo "$INPUT" | jsonfilter -e '@.job_id' 2>/dev/null)
+ fi
+fi
+
+if [ -z "$JOB_ID" ]; then
+ echo '{"error": "job_id is required"}'
+ exit 0
+fi
+
+# Sanitize job_id (only allow alphanumeric and underscore)
+JOB_ID=$(echo "$JOB_ID" | tr -cd 'a-zA-Z0-9_')
+
+STATUS_FILE="/tmp/peertube-import-${JOB_ID}.status"
+RESULT_FILE="/tmp/peertube-import-${JOB_ID}.json"
+LOG_FILE="/tmp/peertube-import-${JOB_ID}.log"
+PROGRESS_FILE="/tmp/peertube-import-${JOB_ID}.progress"
+
+# Check if job exists
+if [ ! -f "$STATUS_FILE" ]; then
+ echo '{"error": "Job not found", "job_id": "'"$JOB_ID"'"}'
+ exit 0
+fi
+
+STATUS=$(cat "$STATUS_FILE" 2>/dev/null || echo "unknown")
+PROGRESS=$(cat "$PROGRESS_FILE" 2>/dev/null || echo "0")
+
+# If completed or failed, return full result
+if [ -f "$RESULT_FILE" ]; then
+ if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
+ cat "$RESULT_FILE"
+ exit 0
+ fi
+fi
+
+# Otherwise return status with progress and logs
+LOGS=""
+if [ -f "$LOG_FILE" ]; then
+ LOGS=$(tail -3 "$LOG_FILE" 2>/dev/null | tr '\n' ' ' | sed 's/"/\\"/g' | head -c 300)
+fi
+
+cat << EOF
+{
+ "status": "$STATUS",
+ "job_id": "$JOB_ID",
+ "progress": $PROGRESS,
+ "logs": "$LOGS"
+}
+EOF