#!/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 (both manual and auto-generated) yt-dlp --write-sub --write-auto-sub \ --sub-lang "$languages" \ --sub-format vtt \ --convert-subs vtt \ --skip-download \ -o "$output_dir/${slug}" \ "$url" >&2 2>&1 || true # List downloaded subtitle files (catch all .vtt files in case filename differs) local count=0 for vtt in "$output_dir"/*.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"/*.vtt; do [ -f "$vtt" ] || continue # Extract language code from filename # Format: name.fr.vtt or name.en-US.vtt or name.en.vtt local filename filename=$(basename "$vtt") local lang # Extract language code before .vtt extension lang=$(echo "$filename" | sed -E 's/.*\.([a-zA-Z]{2}(-[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