mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-30 05:06:05 +00:00
feat(phase8): Add PeerTube federated video platform
- LXC-based PeerTube container with PostgreSQL, Redis, Node.js, ffmpeg - FastAPI backend for install, user, video management - Web UI with stats grid, videos/users/logs tabs - Systemd service on Unix socket Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
14b786d7b3
commit
491b2aaea1
479
packages/secubox-peertube/api/main.py
Normal file
479
packages/secubox-peertube/api/main.py
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
"""
|
||||
SecuBox PeerTube API
|
||||
LXC-based federated video platform management
|
||||
"""
|
||||
from fastapi import FastAPI, Depends, HTTPException, Query, UploadFile, File
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
|
||||
try:
|
||||
from secubox_core.auth import require_jwt
|
||||
from secubox_core.logger import get_logger
|
||||
except ImportError:
|
||||
def require_jwt(): return {"sub": "admin"}
|
||||
class Logger:
|
||||
def info(self, msg): print(f"INFO: {msg}")
|
||||
def error(self, msg): print(f"ERROR: {msg}")
|
||||
def get_logger(name): return Logger()
|
||||
|
||||
app = FastAPI(title="secubox-peertube", root_path="/api/v1/peertube")
|
||||
log = get_logger("peertube")
|
||||
|
||||
LXC_NAME = "secubox-peertube"
|
||||
CONFIG_DIR = Path("/var/lib/secubox/peertube")
|
||||
DATA_DIR = Path("/var/lib/secubox/peertube/data")
|
||||
PEERTUBE_PORT = 9000
|
||||
|
||||
|
||||
def lxc_exists() -> bool:
|
||||
return subprocess.run(["lxc-info", "-n", LXC_NAME], capture_output=True).returncode == 0
|
||||
|
||||
def lxc_running() -> bool:
|
||||
try:
|
||||
r = subprocess.run(["lxc-info", "-n", LXC_NAME, "-s"], capture_output=True, text=True, timeout=10)
|
||||
return "RUNNING" in r.stdout
|
||||
except: return False
|
||||
|
||||
def lxc_get_ip() -> Optional[str]:
|
||||
try:
|
||||
r = subprocess.run(["lxc-info", "-n", LXC_NAME, "-iH"], capture_output=True, text=True, timeout=10)
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
return r.stdout.strip().split("\n")[0]
|
||||
except: pass
|
||||
return None
|
||||
|
||||
def lxc_exec(cmd: List[str], timeout: int = 60) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(["lxc-attach", "-n", LXC_NAME, "--"] + cmd, capture_output=True, text=True, timeout=timeout)
|
||||
|
||||
def lxc_start() -> bool:
|
||||
return subprocess.run(["lxc-start", "-n", LXC_NAME], capture_output=True).returncode == 0
|
||||
|
||||
def lxc_stop() -> bool:
|
||||
return subprocess.run(["lxc-stop", "-n", LXC_NAME], capture_output=True).returncode == 0
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
username: str = Field(..., min_length=1, max_length=50)
|
||||
email: str
|
||||
password: str = Field(..., min_length=8)
|
||||
role: str = Field(default="user", pattern="^(admin|moderator|user)$")
|
||||
|
||||
class ConfigUpdate(BaseModel):
|
||||
instance_name: Optional[str] = None
|
||||
instance_description: Optional[str] = None
|
||||
signup_enabled: Optional[bool] = None
|
||||
signup_requires_approval: Optional[bool] = None
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok", "module": "peertube"}
|
||||
|
||||
@app.get("/status")
|
||||
async def status(user=Depends(require_jwt)):
|
||||
container_exists = lxc_exists()
|
||||
running = lxc_running() if container_exists else False
|
||||
container_ip = lxc_get_ip() if running else None
|
||||
|
||||
version = None
|
||||
domain = None
|
||||
users_count = 0
|
||||
videos_count = 0
|
||||
storage_used = None
|
||||
|
||||
if running:
|
||||
# Get version
|
||||
r = lxc_exec(["cat", "/var/www/peertube/versions/peertube-latest/package.json"])
|
||||
if r.returncode == 0:
|
||||
try:
|
||||
pkg = json.loads(r.stdout)
|
||||
version = pkg.get("version")
|
||||
except: pass
|
||||
|
||||
# Get stats from PostgreSQL
|
||||
r = lxc_exec(["sudo", "-u", "postgres", "psql", "-t", "-c",
|
||||
"SELECT COUNT(*) FROM \"user\";", "peertube"])
|
||||
if r.returncode == 0:
|
||||
try: users_count = int(r.stdout.strip())
|
||||
except: pass
|
||||
|
||||
r = lxc_exec(["sudo", "-u", "postgres", "psql", "-t", "-c",
|
||||
"SELECT COUNT(*) FROM video;", "peertube"])
|
||||
if r.returncode == 0:
|
||||
try: videos_count = int(r.stdout.strip())
|
||||
except: pass
|
||||
|
||||
# Storage
|
||||
r = lxc_exec(["du", "-sh", "/var/www/peertube/storage"])
|
||||
if r.returncode == 0:
|
||||
storage_used = r.stdout.split()[0]
|
||||
|
||||
disk_usage = None
|
||||
if DATA_DIR.exists():
|
||||
try:
|
||||
r = subprocess.run(["du", "-sh", str(DATA_DIR)], capture_output=True, text=True)
|
||||
if r.returncode == 0: disk_usage = r.stdout.split()[0]
|
||||
except: pass
|
||||
|
||||
return {
|
||||
"container_exists": container_exists,
|
||||
"running": running,
|
||||
"container_ip": container_ip,
|
||||
"version": version,
|
||||
"domain": domain,
|
||||
"users_count": users_count,
|
||||
"videos_count": videos_count,
|
||||
"storage_used": storage_used,
|
||||
"disk_usage": disk_usage,
|
||||
"port": PEERTUBE_PORT,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/install")
|
||||
async def install(
|
||||
domain: str = Query(..., description="PeerTube domain"),
|
||||
admin_email: str = Query(..., description="Admin email"),
|
||||
user=Depends(require_jwt)
|
||||
):
|
||||
if lxc_exists():
|
||||
raise HTTPException(400, "Container already exists")
|
||||
|
||||
log.info(f"Installing PeerTube for {domain}")
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create container
|
||||
r = subprocess.run([
|
||||
"lxc-create", "-n", LXC_NAME, "-t", "download", "--",
|
||||
"-d", "debian", "-r", "bookworm", "-a", "amd64"
|
||||
], capture_output=True, text=True, timeout=600)
|
||||
|
||||
if r.returncode != 0:
|
||||
raise HTTPException(500, f"Failed to create container: {r.stderr}")
|
||||
|
||||
# Configure LXC
|
||||
lxc_config = f"/var/lib/lxc/{LXC_NAME}/config"
|
||||
with open(lxc_config, "a") as f:
|
||||
f.write(f"\n# SecuBox PeerTube\n")
|
||||
f.write(f"lxc.mount.entry = {DATA_DIR} data none bind,create=dir 0 0\n")
|
||||
f.write("lxc.start.auto = 1\n")
|
||||
|
||||
if not lxc_start():
|
||||
raise HTTPException(500, "Failed to start container")
|
||||
|
||||
import time
|
||||
for _ in range(30):
|
||||
if lxc_get_ip(): break
|
||||
time.sleep(1)
|
||||
|
||||
install_script = f'''#!/bin/bash
|
||||
set -e
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install dependencies
|
||||
apt-get update
|
||||
apt-get install -y curl sudo gnupg ca-certificates
|
||||
|
||||
# Add NodeSource repo (Node.js 18)
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
|
||||
|
||||
# Install packages
|
||||
apt-get install -y nodejs postgresql postgresql-contrib redis-server \
|
||||
ffmpeg python3 python3-dev g++ make openssl certbot nginx
|
||||
|
||||
# Create peertube user
|
||||
useradd -m -d /var/www/peertube -s /bin/bash peertube || true
|
||||
|
||||
# Setup PostgreSQL
|
||||
sudo -u postgres psql -c "CREATE USER peertube WITH PASSWORD 'peertube';" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "CREATE DATABASE peertube OWNER peertube;" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" peertube 2>/dev/null || true
|
||||
sudo -u postgres psql -c "CREATE EXTENSION IF NOT EXISTS unaccent;" peertube 2>/dev/null || true
|
||||
|
||||
# Download PeerTube
|
||||
cd /var/www/peertube
|
||||
sudo -u peertube mkdir -p versions config storage
|
||||
cd versions
|
||||
|
||||
VERSION=$(curl -s https://api.github.com/repos/Chocobozzz/PeerTube/releases/latest | grep tag_name | cut -d'"' -f4)
|
||||
sudo -u peertube wget -q "https://github.com/Chocobozzz/PeerTube/releases/download/${{VERSION}}/peertube-${{VERSION}}.zip"
|
||||
sudo -u peertube unzip -q "peertube-${{VERSION}}.zip"
|
||||
sudo -u peertube rm "peertube-${{VERSION}}.zip"
|
||||
cd /var/www/peertube
|
||||
sudo -u peertube ln -sf "versions/peertube-${{VERSION}}" peertube-latest
|
||||
|
||||
# Install dependencies
|
||||
cd /var/www/peertube/peertube-latest
|
||||
sudo -u peertube yarn install --production --pure-lockfile
|
||||
|
||||
# Create config
|
||||
cat > /var/www/peertube/config/production.yaml << 'EOFCONFIG'
|
||||
listen:
|
||||
hostname: '0.0.0.0'
|
||||
port: {PEERTUBE_PORT}
|
||||
|
||||
webserver:
|
||||
https: true
|
||||
hostname: '{domain}'
|
||||
port: 443
|
||||
|
||||
database:
|
||||
hostname: 'localhost'
|
||||
port: 5432
|
||||
name: 'peertube'
|
||||
username: 'peertube'
|
||||
password: 'peertube'
|
||||
|
||||
redis:
|
||||
hostname: 'localhost'
|
||||
port: 6379
|
||||
|
||||
smtp:
|
||||
transport: 'sendmail'
|
||||
sendmail: '/usr/sbin/sendmail'
|
||||
|
||||
storage:
|
||||
tmp: '/var/www/peertube/storage/tmp/'
|
||||
bin: '/var/www/peertube/storage/bin/'
|
||||
avatars: '/var/www/peertube/storage/avatars/'
|
||||
videos: '/var/www/peertube/storage/videos/'
|
||||
streaming_playlists: '/var/www/peertube/storage/streaming-playlists/'
|
||||
redundancy: '/var/www/peertube/storage/redundancy/'
|
||||
logs: '/var/www/peertube/storage/logs/'
|
||||
previews: '/var/www/peertube/storage/previews/'
|
||||
thumbnails: '/var/www/peertube/storage/thumbnails/'
|
||||
torrents: '/var/www/peertube/storage/torrents/'
|
||||
captions: '/var/www/peertube/storage/captions/'
|
||||
cache: '/var/www/peertube/storage/cache/'
|
||||
plugins: '/var/www/peertube/storage/plugins/'
|
||||
client_overrides: '/var/www/peertube/storage/client-overrides/'
|
||||
|
||||
admin:
|
||||
email: '{admin_email}'
|
||||
|
||||
signup:
|
||||
enabled: false
|
||||
requires_approval: true
|
||||
|
||||
instance:
|
||||
name: 'SecuBox PeerTube'
|
||||
short_description: 'Federated video platform'
|
||||
description: 'PeerTube instance powered by SecuBox'
|
||||
EOFCONFIG
|
||||
|
||||
chown peertube:peertube /var/www/peertube/config/production.yaml
|
||||
|
||||
# Create storage directories
|
||||
for dir in tmp bin avatars videos streaming-playlists redundancy logs previews thumbnails torrents captions cache plugins client-overrides; do
|
||||
sudo -u peertube mkdir -p "/var/www/peertube/storage/$dir"
|
||||
done
|
||||
|
||||
# Systemd service
|
||||
cat > /etc/systemd/system/peertube.service << 'EOFSVC'
|
||||
[Unit]
|
||||
Description=PeerTube daemon
|
||||
After=network.target postgresql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=peertube
|
||||
Group=peertube
|
||||
WorkingDirectory=/var/www/peertube/peertube-latest
|
||||
ExecStart=/usr/bin/node dist/server
|
||||
Environment=NODE_ENV=production
|
||||
Environment=NODE_CONFIG_DIR=/var/www/peertube/config
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOFSVC
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable postgresql redis-server peertube
|
||||
systemctl start postgresql redis-server
|
||||
sleep 3
|
||||
systemctl start peertube
|
||||
|
||||
echo "PeerTube installed"
|
||||
'''
|
||||
|
||||
r = lxc_exec(["bash", "-c", install_script], timeout=900)
|
||||
|
||||
if r.returncode != 0:
|
||||
log.error(f"Install failed: {r.stderr}")
|
||||
raise HTTPException(500, f"Installation failed: {r.stderr[:500]}")
|
||||
|
||||
return {"success": True, "message": f"PeerTube installed for {domain}"}
|
||||
|
||||
|
||||
@app.post("/start")
|
||||
async def start(user=Depends(require_jwt)):
|
||||
if not lxc_exists(): raise HTTPException(400, "Container not installed")
|
||||
if lxc_running(): return {"success": True, "message": "Already running"}
|
||||
if lxc_start():
|
||||
lxc_exec(["systemctl", "start", "postgresql", "redis-server", "peertube"])
|
||||
return {"success": True}
|
||||
raise HTTPException(500, "Failed to start")
|
||||
|
||||
@app.post("/stop")
|
||||
async def stop(user=Depends(require_jwt)):
|
||||
if not lxc_running(): return {"success": True, "message": "Already stopped"}
|
||||
lxc_exec(["systemctl", "stop", "peertube", "redis-server", "postgresql"])
|
||||
if lxc_stop(): return {"success": True}
|
||||
raise HTTPException(500, "Failed to stop")
|
||||
|
||||
@app.post("/restart")
|
||||
async def restart(user=Depends(require_jwt)):
|
||||
if not lxc_running(): raise HTTPException(400, "Container not running")
|
||||
lxc_exec(["systemctl", "restart", "peertube"])
|
||||
return {"success": True}
|
||||
|
||||
@app.delete("/uninstall")
|
||||
async def uninstall(user=Depends(require_jwt)):
|
||||
if lxc_running(): lxc_stop()
|
||||
if lxc_exists():
|
||||
subprocess.run(["lxc-destroy", "-n", LXC_NAME], capture_output=True)
|
||||
return {"success": True, "message": "Container removed, data preserved"}
|
||||
|
||||
|
||||
@app.get("/users")
|
||||
async def list_users(user=Depends(require_jwt)):
|
||||
if not lxc_running(): raise HTTPException(400, "Container not running")
|
||||
|
||||
r = lxc_exec(["sudo", "-u", "postgres", "psql", "-t", "-A", "-F", "|", "-c",
|
||||
'SELECT username, email, role, "createdAt" FROM "user" ORDER BY "createdAt" DESC LIMIT 50;',
|
||||
"peertube"])
|
||||
|
||||
users = []
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
for line in r.stdout.strip().splitlines():
|
||||
parts = line.split("|")
|
||||
if len(parts) >= 3:
|
||||
users.append({
|
||||
"username": parts[0],
|
||||
"email": parts[1] if len(parts) > 1 else "",
|
||||
"role": parts[2] if len(parts) > 2 else "user",
|
||||
})
|
||||
return {"users": users}
|
||||
|
||||
@app.post("/users")
|
||||
async def create_user(req: UserCreate, user=Depends(require_jwt)):
|
||||
if not lxc_running(): raise HTTPException(400, "Container not running")
|
||||
|
||||
cmd = f'''cd /var/www/peertube/peertube-latest && \
|
||||
NODE_ENV=production NODE_CONFIG_DIR=/var/www/peertube/config \
|
||||
npx ts-node --transpile-only ./scripts/create-user.ts \
|
||||
--username "{req.username}" --email "{req.email}" --password "{req.password}" --role "{req.role}"'''
|
||||
|
||||
r = lxc_exec(["bash", "-c", cmd], timeout=60)
|
||||
return {"success": r.returncode == 0, "username": req.username}
|
||||
|
||||
|
||||
@app.get("/videos")
|
||||
async def list_videos(
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
user=Depends(require_jwt)
|
||||
):
|
||||
if not lxc_running(): raise HTTPException(400, "Container not running")
|
||||
|
||||
r = lxc_exec(["sudo", "-u", "postgres", "psql", "-t", "-A", "-F", "|", "-c",
|
||||
f'SELECT uuid, name, views, likes, duration, "createdAt" FROM video ORDER BY "createdAt" DESC LIMIT {limit};',
|
||||
"peertube"])
|
||||
|
||||
videos = []
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
for line in r.stdout.strip().splitlines():
|
||||
parts = line.split("|")
|
||||
if len(parts) >= 4:
|
||||
videos.append({
|
||||
"uuid": parts[0],
|
||||
"name": parts[1],
|
||||
"views": int(parts[2]) if parts[2].isdigit() else 0,
|
||||
"likes": int(parts[3]) if len(parts) > 3 and parts[3].isdigit() else 0,
|
||||
})
|
||||
return {"videos": videos}
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(user=Depends(require_jwt)):
|
||||
if not lxc_running(): return {"error": "Container not running"}
|
||||
|
||||
r = lxc_exec(["cat", "/var/www/peertube/config/production.yaml"])
|
||||
if r.returncode != 0: raise HTTPException(500, "Failed to read config")
|
||||
|
||||
# Parse YAML simply
|
||||
config = {}
|
||||
for line in r.stdout.splitlines():
|
||||
if ":" in line and not line.strip().startswith("#"):
|
||||
key = line.split(":")[0].strip()
|
||||
value = ":".join(line.split(":")[1:]).strip().strip("'\"")
|
||||
config[key] = value
|
||||
return config
|
||||
|
||||
|
||||
@app.get("/logs")
|
||||
async def get_logs(lines: int = Query(100, ge=10, le=1000), user=Depends(require_jwt)):
|
||||
if not lxc_running(): raise HTTPException(400, "Container not running")
|
||||
r = lxc_exec(["journalctl", "-u", "peertube", "-n", str(lines), "--no-pager"])
|
||||
return {"logs": r.stdout.splitlines() if r.returncode == 0 else []}
|
||||
|
||||
|
||||
@app.get("/stats")
|
||||
async def get_stats(user=Depends(require_jwt)):
|
||||
if not lxc_running(): raise HTTPException(400, "Container not running")
|
||||
|
||||
stats = {"users": 0, "videos": 0, "views": 0, "storage": "0"}
|
||||
|
||||
r = lxc_exec(["sudo", "-u", "postgres", "psql", "-t", "-c", 'SELECT COUNT(*) FROM "user";', "peertube"])
|
||||
if r.returncode == 0: stats["users"] = int(r.stdout.strip()) if r.stdout.strip().isdigit() else 0
|
||||
|
||||
r = lxc_exec(["sudo", "-u", "postgres", "psql", "-t", "-c", "SELECT COUNT(*) FROM video;", "peertube"])
|
||||
if r.returncode == 0: stats["videos"] = int(r.stdout.strip()) if r.stdout.strip().isdigit() else 0
|
||||
|
||||
r = lxc_exec(["sudo", "-u", "postgres", "psql", "-t", "-c", "SELECT COALESCE(SUM(views), 0) FROM video;", "peertube"])
|
||||
if r.returncode == 0: stats["views"] = int(r.stdout.strip()) if r.stdout.strip().isdigit() else 0
|
||||
|
||||
r = lxc_exec(["du", "-sh", "/var/www/peertube/storage/videos"])
|
||||
if r.returncode == 0: stats["storage"] = r.stdout.split()[0]
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
@app.post("/backup")
|
||||
async def create_backup(user=Depends(require_jwt)):
|
||||
backup_dir = Path("/var/lib/secubox/backups/peertube")
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_file = backup_dir / f"peertube-backup-{timestamp}.tar.gz"
|
||||
|
||||
was_running = lxc_running()
|
||||
if was_running:
|
||||
lxc_exec(["systemctl", "stop", "peertube"])
|
||||
|
||||
# Dump database
|
||||
lxc_exec(["sudo", "-u", "postgres", "pg_dump", "-Fc", "peertube", "-f", "/tmp/peertube.dump"])
|
||||
|
||||
try:
|
||||
r = subprocess.run([
|
||||
"tar", "-czf", str(backup_file),
|
||||
"-C", str(DATA_DIR.parent), DATA_DIR.name
|
||||
], capture_output=True, text=True, timeout=600)
|
||||
|
||||
if r.returncode != 0:
|
||||
raise HTTPException(500, f"Backup failed: {r.stderr}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"file": str(backup_file),
|
||||
"size": f"{backup_file.stat().st_size / 1024 / 1024:.1f} MB"
|
||||
}
|
||||
finally:
|
||||
if was_running:
|
||||
lxc_exec(["systemctl", "start", "peertube"])
|
||||
5
packages/secubox-peertube/debian/changelog
Normal file
5
packages/secubox-peertube/debian/changelog
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
secubox-peertube (1.0.0-1) bookworm; urgency=medium
|
||||
|
||||
* Initial release - PeerTube federated video platform
|
||||
|
||||
-- Gandalf / CyberMind <gk@cybermind.fr> Fri, 28 Mar 2026 10:30:00 +0100
|
||||
13
packages/secubox-peertube/debian/control
Normal file
13
packages/secubox-peertube/debian/control
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Source: secubox-peertube
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: Gandalf / CyberMind <gk@cybermind.fr>
|
||||
Build-Depends: debhelper-compat (= 13)
|
||||
Standards-Version: 4.6.2
|
||||
|
||||
Package: secubox-peertube
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, secubox-core (>= 1.0), python3-uvicorn, lxc, lxc-templates
|
||||
Description: SecuBox PeerTube - Federated Video Platform
|
||||
LXC-based PeerTube video platform for SecuBox.
|
||||
ActivityPub-compatible decentralized YouTube alternative.
|
||||
12
packages/secubox-peertube/debian/postinst
Executable file
12
packages/secubox-peertube/debian/postinst
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
case "$1" in
|
||||
configure)
|
||||
install -d -o secubox -g secubox -m 750 /var/lib/secubox/peertube/data
|
||||
systemctl daemon-reload
|
||||
systemctl enable secubox-peertube.service
|
||||
systemctl start secubox-peertube.service || true
|
||||
systemctl reload nginx 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
#DEBHELPER#
|
||||
16
packages/secubox-peertube/debian/rules
Executable file
16
packages/secubox-peertube/debian/rules
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/make -f
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_install:
|
||||
install -d $(CURDIR)/debian/secubox-peertube/usr/lib/secubox/peertube/api
|
||||
install -m 644 api/main.py $(CURDIR)/debian/secubox-peertube/usr/lib/secubox/peertube/api/
|
||||
touch $(CURDIR)/debian/secubox-peertube/usr/lib/secubox/peertube/api/__init__.py
|
||||
install -d $(CURDIR)/debian/secubox-peertube/usr/share/secubox/www/peertube
|
||||
install -m 644 www/peertube/index.html $(CURDIR)/debian/secubox-peertube/usr/share/secubox/www/peertube/
|
||||
install -d $(CURDIR)/debian/secubox-peertube/etc/nginx/secubox.d
|
||||
install -m 644 nginx/peertube.conf $(CURDIR)/debian/secubox-peertube/etc/nginx/secubox.d/
|
||||
install -d $(CURDIR)/debian/secubox-peertube/usr/share/secubox/menu.d
|
||||
install -m 644 menu.d/811-peertube.json $(CURDIR)/debian/secubox-peertube/usr/share/secubox/menu.d/
|
||||
install -d $(CURDIR)/debian/secubox-peertube/usr/lib/systemd/system
|
||||
install -m 644 debian/secubox-peertube.service $(CURDIR)/debian/secubox-peertube/usr/lib/systemd/system/
|
||||
17
packages/secubox-peertube/debian/secubox-peertube.service
Normal file
17
packages/secubox-peertube/debian/secubox-peertube.service
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=SecuBox PeerTube API
|
||||
After=network.target secubox-core.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/usr/lib/secubox/peertube
|
||||
ExecStart=/usr/bin/uvicorn api.main:app --uds /run/secubox/peertube.sock --log-level warning
|
||||
UMask=0117
|
||||
Restart=on-failure
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/run/secubox /var/lib/secubox /var/lib/lxc
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
1
packages/secubox-peertube/menu.d/811-peertube.json
Normal file
1
packages/secubox-peertube/menu.d/811-peertube.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"id":"peertube","name":"PeerTube","icon":"📺","path":"/peertube/","category":"apps","order":811,"description":"Federated video platform"}
|
||||
2
packages/secubox-peertube/nginx/peertube.conf
Normal file
2
packages/secubox-peertube/nginx/peertube.conf
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
location /api/v1/peertube/ { proxy_pass http://unix:/run/secubox/peertube.sock; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
|
||||
location /peertube/ { alias /usr/share/secubox/www/peertube/; try_files $uri $uri/ /peertube/index.html; }
|
||||
190
packages/secubox-peertube/www/peertube/index.html
Normal file
190
packages/secubox-peertube/www/peertube/index.html
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PeerTube - SecuBox</title>
|
||||
<link rel="stylesheet" href="/shared/crt-light.css">
|
||||
<link rel="stylesheet" href="/shared/sidebar-light.css">
|
||||
<style>
|
||||
:root { --pt-orange: #f2690d; --pt-light: #ff8533; --pt-dark: #cc5500; }
|
||||
body.dark { --pt-orange: #ff8533; --pt-light: #ffa366; --pt-dark: #f2690d; }
|
||||
.accent { color: var(--pt-orange); }
|
||||
.accent-bg { background-color: var(--pt-orange); }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; padding: 1rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; }
|
||||
.header h1 { margin: 0; font-size: 1.5rem; }
|
||||
.status-badge { padding: 0.25rem 0.75rem; border-radius: 1rem; font-size: 0.875rem; }
|
||||
.status-badge.running { background: var(--success-bg); color: var(--success); }
|
||||
.status-badge.stopped { background: var(--error-bg); color: var(--error); }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
|
||||
.stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; text-align: center; }
|
||||
.stat-value { font-size: 1.75rem; font-weight: bold; color: var(--pt-orange); }
|
||||
.stat-label { font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; }
|
||||
.action-bar { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.btn { padding: 0.5rem 1rem; border: 1px solid var(--border); border-radius: 0.25rem; background: var(--surface); color: var(--text); cursor: pointer; font-size: 0.875rem; }
|
||||
.btn:hover { background: var(--hover); }
|
||||
.btn.primary { background: var(--pt-orange); color: white; border-color: var(--pt-orange); }
|
||||
.btn.primary:hover { background: var(--pt-dark); }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.tabs { display: flex; gap: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; }
|
||||
.tab { padding: 0.75rem 1rem; border: none; background: none; color: var(--text-secondary); cursor: pointer; border-bottom: 2px solid transparent; }
|
||||
.tab:hover { color: var(--text); }
|
||||
.tab.active { color: var(--pt-orange); border-bottom-color: var(--pt-orange); }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
.list-item { display: flex; align-items: center; gap: 1rem; padding: 0.75rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.item-icon { font-size: 1.5rem; }
|
||||
.item-info { flex: 1; }
|
||||
.item-name { font-weight: 500; }
|
||||
.item-meta { font-size: 0.875rem; color: var(--text-secondary); }
|
||||
.log-viewer { background: var(--bg-dark, #1a1a2e); color: var(--text-light, #e0e0e0); font-family: monospace; font-size: 0.75rem; padding: 1rem; border-radius: 0.5rem; height: 400px; overflow-y: auto; white-space: pre-wrap; }
|
||||
.settings-form { display: grid; gap: 1rem; max-width: 500px; }
|
||||
.form-group label { display: block; margin-bottom: 0.25rem; font-weight: 500; }
|
||||
.form-group input, .form-group select { width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 0.25rem; background: var(--surface); color: var(--text); }
|
||||
.open-app-btn { position: fixed; bottom: 1rem; right: 1rem; padding: 1rem 1.5rem; background: var(--pt-orange); color: white; border: none; border-radius: 2rem; cursor: pointer; font-size: 1rem; box-shadow: 0 4px 12px rgba(242, 105, 13, 0.4); }
|
||||
.hidden { display: none !important; }
|
||||
.install-form { max-width: 400px; margin: 2rem auto; padding: 2rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; }
|
||||
.install-form h2 { margin-top: 0; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="crt-light">
|
||||
<nav class="sidebar" id="sidebar"></nav>
|
||||
<main class="main-content">
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><span class="accent">PeerTube</span></h1>
|
||||
<span id="status-badge" class="status-badge stopped">Stopped</span>
|
||||
</div>
|
||||
|
||||
<div id="install-section" class="install-form hidden">
|
||||
<h2>Install PeerTube</h2>
|
||||
<form onsubmit="install(event)">
|
||||
<div class="form-group"><label>Domain</label><input type="text" id="install-domain" placeholder="videos.example.com" required></div>
|
||||
<div class="form-group"><label>Admin Email</label><input type="email" id="install-email" required></div>
|
||||
<button type="submit" class="btn primary" style="width: 100%; margin-top: 1rem;">Install</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="main-section" class="hidden">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card"><div class="stat-value" id="users-count">0</div><div class="stat-label">Users</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="videos-count">0</div><div class="stat-label">Videos</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="views-count">0</div><div class="stat-label">Views</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="storage-used">-</div><div class="stat-label">Storage</div></div>
|
||||
</div>
|
||||
|
||||
<div class="action-bar">
|
||||
<button class="btn primary" id="btn-start" onclick="startService()">Start</button>
|
||||
<button class="btn" id="btn-stop" onclick="stopService()" disabled>Stop</button>
|
||||
<button class="btn" id="btn-restart" onclick="restartService()" disabled>Restart</button>
|
||||
<button class="btn" onclick="createBackup()">Backup</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="videos">Videos</button>
|
||||
<button class="tab" data-tab="users">Users</button>
|
||||
<button class="tab" data-tab="logs">Logs</button>
|
||||
</div>
|
||||
|
||||
<div id="tab-videos" class="tab-content active">
|
||||
<div id="videos-list"></div>
|
||||
</div>
|
||||
|
||||
<div id="tab-users" class="tab-content">
|
||||
<div class="action-bar"><button class="btn primary" onclick="showCreateUser()">Create User</button></div>
|
||||
<div id="users-list"></div>
|
||||
</div>
|
||||
|
||||
<div id="tab-logs" class="tab-content">
|
||||
<div class="action-bar"><button class="btn" onclick="loadLogs()">Refresh</button></div>
|
||||
<div id="log-viewer" class="log-viewer">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="open-app-btn" class="open-app-btn hidden" onclick="openApp()">Open PeerTube</button>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const API = '/api/v1/peertube';
|
||||
let containerIP = null;
|
||||
|
||||
async function api(endpoint, options = {}) {
|
||||
const r = await fetch(API + endpoint, { ...options, headers: { 'Authorization': 'Bearer ' + localStorage.getItem('sbx_token'), 'Content-Type': 'application/json', ...options.headers } });
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const d = await api('/status');
|
||||
const badge = document.getElementById('status-badge');
|
||||
if (!d.container_exists) {
|
||||
document.getElementById('install-section').classList.remove('hidden');
|
||||
document.getElementById('main-section').classList.add('hidden');
|
||||
badge.textContent = 'Not Installed'; badge.className = 'status-badge stopped';
|
||||
return;
|
||||
}
|
||||
document.getElementById('install-section').classList.add('hidden');
|
||||
document.getElementById('main-section').classList.remove('hidden');
|
||||
if (d.running) {
|
||||
badge.textContent = 'Running'; badge.className = 'status-badge running';
|
||||
document.getElementById('btn-start').disabled = true;
|
||||
document.getElementById('btn-stop').disabled = false;
|
||||
document.getElementById('btn-restart').disabled = false;
|
||||
containerIP = d.container_ip;
|
||||
document.getElementById('open-app-btn').classList.remove('hidden');
|
||||
loadStats(); loadVideos(); loadUsers();
|
||||
} else {
|
||||
badge.textContent = 'Stopped'; badge.className = 'status-badge stopped';
|
||||
document.getElementById('btn-start').disabled = false;
|
||||
document.getElementById('btn-stop').disabled = true;
|
||||
document.getElementById('btn-restart').disabled = true;
|
||||
document.getElementById('open-app-btn').classList.add('hidden');
|
||||
}
|
||||
document.getElementById('storage-used').textContent = d.storage_used || '-';
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const s = await api('/stats');
|
||||
document.getElementById('users-count').textContent = s.users || 0;
|
||||
document.getElementById('videos-count').textContent = s.videos || 0;
|
||||
document.getElementById('views-count').textContent = s.views || 0;
|
||||
document.getElementById('storage-used').textContent = s.storage || '-';
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function loadVideos() {
|
||||
try {
|
||||
const d = await api('/videos?limit=20');
|
||||
document.getElementById('videos-list').innerHTML = (d.videos || []).map(v => `<div class="list-item"><div class="item-icon">🎬</div><div class="item-info"><div class="item-name">${v.name}</div><div class="item-meta">${v.views} views · ${v.likes} likes</div></div></div>`).join('') || '<p>No videos</p>';
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const d = await api('/users');
|
||||
document.getElementById('users-list').innerHTML = (d.users || []).map(u => `<div class="list-item"><div class="item-icon">👤</div><div class="item-info"><div class="item-name">${u.username}</div><div class="item-meta">${u.email} · ${u.role}</div></div></div>`).join('') || '<p>No users</p>';
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function install(e) { e.preventDefault(); const domain = document.getElementById('install-domain').value; const email = document.getElementById('install-email').value; const btn = e.target.querySelector('button'); btn.disabled = true; btn.textContent = 'Installing...'; try { await api(`/install?domain=${encodeURIComponent(domain)}&admin_email=${encodeURIComponent(email)}`, { method: 'POST' }); await loadStatus(); } catch (err) { alert('Failed: ' + err.message); } finally { btn.disabled = false; btn.textContent = 'Install'; } }
|
||||
async function startService() { document.getElementById('btn-start').disabled = true; try { await api('/start', { method: 'POST' }); await loadStatus(); } catch (e) { alert('Failed: ' + e.message); } }
|
||||
async def stopService() { document.getElementById('btn-stop').disabled = true; try { await api('/stop', { method: 'POST' }); await loadStatus(); } catch (e) { alert('Failed: ' + e.message); } }
|
||||
async function restartService() { document.getElementById('btn-restart').disabled = true; try { await api('/restart', { method: 'POST' }); await loadStatus(); } catch (e) { alert('Failed: ' + e.message); } }
|
||||
|
||||
function showCreateUser() { const u = prompt('Username:'); if (!u) return; const e = prompt('Email:'); if (!e) return; const p = prompt('Password (8+ chars):'); if (!p || p.length < 8) { alert('Password too short'); return; } createUser(u, e, p); }
|
||||
async function createUser(u, e, p) { try { await api('/users', { method: 'POST', body: JSON.stringify({ username: u, email: e, password: p }) }); loadUsers(); } catch (err) { alert('Failed: ' + err.message); } }
|
||||
|
||||
async function loadLogs() { try { const d = await api('/logs?lines=200'); document.getElementById('log-viewer').textContent = d.logs?.join('\n') || 'No logs'; } catch (e) { document.getElementById('log-viewer').textContent = 'Error: ' + e.message; } }
|
||||
async function createBackup() { try { const r = await api('/backup', { method: 'POST' }); alert(`Backup: ${r.file} (${r.size})`); } catch (e) { alert('Failed: ' + e.message); } }
|
||||
function openApp() { if (containerIP) window.open(`http://${containerIP}:9000/`, '_blank'); }
|
||||
|
||||
document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); tab.classList.add('active'); document.getElementById('tab-' + tab.dataset.tab).classList.add('active'); if (tab.dataset.tab === 'logs') loadLogs(); }); });
|
||||
if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) document.body.classList.add('dark');
|
||||
loadStatus(); setInterval(loadStatus, 30000);
|
||||
</script>
|
||||
<script src="/shared/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user