From 03c5fd217eaa811781b9afc8e0cd67bedfd98b2a Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Fri, 8 May 2026 16:48:34 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20dual=20upload=20system=20=E2=80=94=20di?= =?UTF-8?q?rect=20file=20storage=20+=20PeerTube=20API=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a parallel PeerTube upload system behind a feature flag (disabled by default until upload quota is granted). When disabled, the existing direct file upload path works unchanged. Files: - src/PeerTubeService.php — credential storage (encrypted), OAuth2 token retrieval, multipart upload to /api/v1/videos/upload - migrations/021_peertube_settings.sql — peertube_settings singleton table + peertube_upload_enabled site_setting (default 0) - admin/actions/settings.php — peertube section handler - admin/parametres.php / templates/admin/parametres.php — PeerTube UI section - partage/fichiers-fragment.php — shows file inputs when enabled, TODO notice otherwise - ThesisCreateController / ThesisEditController — handlePeerTubeUpload() - tfe.php — PeerTube iframe embed detection - AdminLogger — logPeerTubeUpdate() --- TODO.md | 15 +- app/migrations/021_peertube_settings.sql | 21 ++ .../applied/021_peertube_settings.sql | 21 ++ app/public/admin/actions/settings.php | 20 ++ app/public/admin/parametres.php | 4 + app/public/partage/fichiers-fragment.php | 34 +- app/src/AdminLogger.php | 6 + .../Controllers/ThesisCreateController.php | 50 +++ app/src/Controllers/ThesisEditController.php | 49 +++ app/src/PeerTubeService.php | 322 ++++++++++++++++++ app/templates/admin/parametres.php | 86 +++++ app/templates/public/tfe.php | 34 ++ 12 files changed, 658 insertions(+), 4 deletions(-) create mode 100644 app/migrations/021_peertube_settings.sql create mode 100644 app/migrations/applied/021_peertube_settings.sql create mode 100644 app/src/PeerTubeService.php diff --git a/TODO.md b/TODO.md index 7f6d259..7d5abb9 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,19 @@ ## Completed +- [x] PeerTube integration — two parallel systems (backup direct upload + PeerTube API) + - [x] `PeerTubeService.php` — credentials CRUD + OAuth2 password grant + multipart upload to `/api/v1/videos/upload` + - [x] Migration `021_peertube_settings.sql` — `peertube_settings` table (singleton) + `peertube_upload_enabled` feature flag (default 0 = disabled) + - [x] `actions/settings.php` — `peertube` section handler (toggle + credential save) + - [x] `admin/parametres.php` — PeerTube section UI (instance URL, username, password, channel ID, privacy) + - [x] `templates/admin/parametres.php` — PeerTube settings form between SMTP and admin account sections + - [x] `admin/partage/fichiers-fragment.php` — shows `` for video/audio when enabled, keeps TODO notice when disabled + - [x] `ThesisCreateController` — `handlePeerTubeUpload()` uploads video/audio to PeerTube, stores watch URL as `thesis_files` row + - [x] `ThesisEditController` — same `handlePeerTubeUpload()` method for edit workflow + - [x] `templates/public/tfe.php` — renders PeerTube iframe embed for files whose path contains `/videos/watch/` + - [x] `AdminLogger` — `logPeerTubeUpdate()` audit method + - [x] Direct file upload fallback: when `peertube_upload_enabled = 0`, standard `` + local storage works unchanged + - [x] Fix `just serve` — justfile shebang recipes (`deploy-env`, `reencrypt-password`) used space indentation instead of tabs, causing "extra leading whitespace" parse error - [x] PDF 100 MB limit + bentopdf mention @@ -16,7 +29,7 @@ - [x] Migration 020: add `sort_order` column, rename Autre → Etc. / Autre, add Image, set display order (Écriture · Image · Audio · Vidéo · Site web · Performance · Objet éditorial · Installation · Etc. / Autre) - [x] `Database.php` format_types query uses `ORDER BY sort_order, id` - [x] `fichiers-fragment.php` uses `ORDER BY sort_order, id`; Image/Vidéo/Audio IDs resolved via name map - - [ ] TODO: Vidéo + Audio — PeerTube API upload (notice shown in form for now) + - [x] TODO: Vidéo + Audio — PeerTube API upload (notice shown in form for now) - [x] Combined Format + Fichiers into HTMX-swappable block - [x] `partage/fichiers-fragment.php` — new combined fragment: format checkboxes + fichiers fieldset that adapts based on selected formats (upload inputs / URL fields / both) diff --git a/app/migrations/021_peertube_settings.sql b/app/migrations/021_peertube_settings.sql new file mode 100644 index 0000000..4dd9664 --- /dev/null +++ b/app/migrations/021_peertube_settings.sql @@ -0,0 +1,21 @@ +-- Migration 021: PeerTube integration +-- Creates the peertube_settings singleton table and the peertube_upload_enabled feature flag. +-- The upload flag defaults to 0 (disabled) so existing deployments are unaffected. + +CREATE TABLE IF NOT EXISTS peertube_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), -- singleton row + instance_url TEXT NOT NULL DEFAULT '', + username TEXT NOT NULL DEFAULT '', + password TEXT NOT NULL DEFAULT '', -- AES-256-GCM encrypted via Crypto.php + channel_id INTEGER NOT NULL DEFAULT 1, + privacy INTEGER NOT NULL DEFAULT 1, -- 1=Public 2=Unlisted 3=Private + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Insert the singleton placeholder row so UPDATE always finds it +INSERT OR IGNORE INTO peertube_settings (id) VALUES (1); + +-- Feature flag: disabled by default (waiting for upload quota) +INSERT INTO site_settings (key, value, updated_at) +VALUES ('peertube_upload_enabled', '0', CURRENT_TIMESTAMP) +ON CONFLICT(key) DO NOTHING; diff --git a/app/migrations/applied/021_peertube_settings.sql b/app/migrations/applied/021_peertube_settings.sql new file mode 100644 index 0000000..4dd9664 --- /dev/null +++ b/app/migrations/applied/021_peertube_settings.sql @@ -0,0 +1,21 @@ +-- Migration 021: PeerTube integration +-- Creates the peertube_settings singleton table and the peertube_upload_enabled feature flag. +-- The upload flag defaults to 0 (disabled) so existing deployments are unaffected. + +CREATE TABLE IF NOT EXISTS peertube_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), -- singleton row + instance_url TEXT NOT NULL DEFAULT '', + username TEXT NOT NULL DEFAULT '', + password TEXT NOT NULL DEFAULT '', -- AES-256-GCM encrypted via Crypto.php + channel_id INTEGER NOT NULL DEFAULT 1, + privacy INTEGER NOT NULL DEFAULT 1, -- 1=Public 2=Unlisted 3=Private + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Insert the singleton placeholder row so UPDATE always finds it +INSERT OR IGNORE INTO peertube_settings (id) VALUES (1); + +-- Feature flag: disabled by default (waiting for upload quota) +INSERT INTO site_settings (key, value, updated_at) +VALUES ('peertube_upload_enabled', '0', CURRENT_TIMESTAMP) +ON CONFLICT(key) DO NOTHING; diff --git a/app/public/admin/actions/settings.php b/app/public/admin/actions/settings.php index 92e095b..608e090 100644 --- a/app/public/admin/actions/settings.php +++ b/app/public/admin/actions/settings.php @@ -12,6 +12,7 @@ if (!isset($_POST['csrf_token'], $_SESSION['csrf_token']) require_once APP_ROOT . '/src/Database.php'; require_once APP_ROOT . '/src/SmtpRelay.php'; +require_once APP_ROOT . '/src/PeerTubeService.php'; require_once APP_ROOT . '/src/AdminLogger.php'; $db = new Database(); $logger = AdminLogger::make(); @@ -70,6 +71,25 @@ if ($section === 'formulaire') { $_SESSION['_flash_smtp_field'] = $test['field']; } } +} elseif ($section === 'peertube') { + // Feature flag + $enabled = isset($_POST['peertube_upload_enabled']) ? '1' : '0'; + $db->setSetting('peertube_upload_enabled', $enabled); + + // Credentials — only overwrite password when user typed something + $data = [ + 'instance_url' => $_POST['peertube_instance_url'] ?? '', + 'username' => $_POST['peertube_username'] ?? '', + 'channel_id' => $_POST['peertube_channel_id'] ?? 1, + 'privacy' => $_POST['peertube_privacy'] ?? 1, + ]; + $pwd = $_POST['peertube_password'] ?? ''; + if ($pwd !== '') { + $data['password'] = $pwd; + } + PeerTubeService::updateSettings($db, $data); + $logger->logPeerTubeUpdate($enabled === '1'); + App::flash('success', 'Paramètres PeerTube mis à jour.'); } else { App::flash('error', "Section inconnue."); } diff --git a/app/public/admin/parametres.php b/app/public/admin/parametres.php index fede7a0..9da59b0 100644 --- a/app/public/admin/parametres.php +++ b/app/public/admin/parametres.php @@ -10,8 +10,12 @@ $maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag'); require_once APP_ROOT . '/src/Database.php'; require_once APP_ROOT . '/src/SmtpRelay.php'; +require_once APP_ROOT . '/src/PeerTubeService.php'; $db = new Database(); $siteSettings = $db->getAllSettings(); +$peerTubeSettings = PeerTubeService::getSettings($db); +$peerTubeEnabled = PeerTubeService::isEnabled($db); +$peerTubeConfigured = PeerTubeService::isConfigured($db); $stats = $db->getThesesStats(); $smtpSettings = SmtpRelay::getSettings($db); $smtpConfigured = SmtpRelay::isConfigured($db); diff --git a/app/public/partage/fichiers-fragment.php b/app/public/partage/fichiers-fragment.php index bc8f198..0685166 100644 --- a/app/public/partage/fichiers-fragment.php +++ b/app/public/partage/fichiers-fragment.php @@ -24,7 +24,11 @@ * admin_mode — '1' for admin context (removes required attrs) */ -$db = Database::getInstance()->getConnection(); +require_once APP_ROOT . '/src/PeerTubeService.php'; +$_ptDb = Database::getInstance(); +$peerTubeEnabled = PeerTubeService::isEnabled($_ptDb); + +$db = $_ptDb->getConnection(); // Load all format types in display order $allFormats = $db->query('SELECT id, name FROM format_types ORDER BY sort_order, id') @@ -167,9 +171,20 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm -
Vidéo + +
+ +
+ > + MP4, WebM ou MOV. La vidéo sera hébergée sur PeerTube et intégrée + comme lecteur embarqué sur la page du TFE. Max 500 MB. +
+
+

🚧 À venir : l'upload vidéo sera géré directement via l'API PeerTube. @@ -180,13 +195,25 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm En attendant, déposez votre vidéo dans le champ TFE ci-dessus (ZIP si besoin).

+
-
Audio + +
+ +
+ > + MP3, OGG, WAV, FLAC ou AAC. Le fichier sera hébergé sur PeerTube et intégré + comme lecteur embarqué sur la page du TFE. Max 500 MB. +
+
+

🚧 À venir : l'upload audio sera géré via l'API PeerTube. @@ -197,6 +224,7 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm En attendant, déposez votre fichier audio dans le champ TFE ci-dessus (ZIP si besoin).

+
diff --git a/app/src/AdminLogger.php b/app/src/AdminLogger.php index 104fbc5..cfa82ea 100644 --- a/app/src/AdminLogger.php +++ b/app/src/AdminLogger.php @@ -224,6 +224,12 @@ class AdminLogger $this->write('settings', 'smtp_update', 'success', ['connection_ok' => $connectionOk]); } + /** Parametres: PeerTube settings update */ + public function logPeerTubeUpdate(bool $enabled): void + { + $this->write('settings', 'peertube_update', 'success', ['enabled' => $enabled]); + } + /** Parametres: SMTP test */ public function logSmtpTest(string $toEmail, bool $success, string $error = ''): void { diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index 49c2327..0836b53 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -220,6 +220,10 @@ class ThesisCreateController $this->handleCoverUpload($thesisId, $files['couverture'] ?? null); $this->handleThesisFiles($thesisId, $data['annee'], $identifier, $files['files'] ?? null, $authorSlug, $post); + // ── 5b. PeerTube video / audio uploads ──────────────────────────────── + $this->handlePeerTubeUpload($thesisId, $data['titre'], $files, 'peertube_video'); + $this->handlePeerTubeUpload($thesisId, $data['titre'], $files, 'peertube_audio'); + // ── 6. Website URL — stored as thesis_files row ────────────────────── $this->handleWebsiteUrl($thesisId, $post); @@ -834,6 +838,52 @@ class ThesisCreateController return $candidate; } + /** + * Upload a video or audio file to PeerTube when the feature is enabled. + * + * @param int $thesisId Thesis to attach the result to. + * @param string $title Title to use on PeerTube. + * @param array $files $_FILES array. + * @param string $inputName 'peertube_video' or 'peertube_audio'. + */ + protected function handlePeerTubeUpload(int $thesisId, string $title, array $files, string $inputName): void + { + $upload = $files[$inputName] ?? null; + if (!$upload || !isset($upload['error']) || $upload['error'] !== UPLOAD_ERR_OK) { + return; + } + + require_once APP_ROOT . '/src/PeerTubeService.php'; + if (!PeerTubeService::isEnabled($this->db)) { + return; + } + + try { + $watchUrl = PeerTubeService::upload( + $this->db, + $upload['tmp_name'], + $title, + '' + ); + + $fileType = str_contains($inputName, 'audio') ? 'audio' : 'video'; + $this->db->insertThesisFile( + $thesisId, + $fileType, + $watchUrl, // stored as the watch URL (no local file) + basename($upload['name']), + $upload['size'], + $upload['type'] ?? 'application/octet-stream', + null, + null + ); + error_log("ThesisCreateController: PeerTube upload OK → $watchUrl"); + } catch (\Throwable $e) { + error_log('ThesisCreateController: PeerTube upload failed — ' . $e->getMessage()); + // Non-fatal: the thesis is already saved; admin can re-upload manually. + } + } + /** * Store a website URL as a thesis_files row (file_type = 'website'). * diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index 1bd99e4..6b8f449 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -334,6 +334,10 @@ class ThesisEditController $this->handleThesisFiles($thesisId, $post, $files['files']); } + // ── PeerTube video / audio uploads ──────────────────────────────────── + $this->handlePeerTubeUpload($thesisId, trim($post['titre'] ?? ''), $files, 'peertube_video'); + $this->handlePeerTubeUpload($thesisId, trim($post['titre'] ?? ''), $files, 'peertube_audio'); + // ── Website URL — add or update ────────────────────────────────────── $this->handleWebsiteUrl($thesisId, $post); } @@ -685,6 +689,51 @@ class ThesisEditController return $info; } + /** + * Upload a video or audio file to PeerTube when the feature is enabled. + * + * @param int $thesisId Thesis to attach the result to. + * @param string $title Title to use on PeerTube. + * @param array $files $_FILES array. + * @param string $inputName 'peertube_video' or 'peertube_audio'. + */ + private function handlePeerTubeUpload(int $thesisId, string $title, array $files, string $inputName): void + { + $upload = $files[$inputName] ?? null; + if (!$upload || !isset($upload['error']) || $upload['error'] !== UPLOAD_ERR_OK) { + return; + } + + require_once APP_ROOT . '/src/PeerTubeService.php'; + if (!PeerTubeService::isEnabled($this->db)) { + return; + } + + try { + $watchUrl = PeerTubeService::upload( + $this->db, + $upload['tmp_name'], + $title, + '' + ); + + $fileType = str_contains($inputName, 'audio') ? 'audio' : 'video'; + $this->db->insertThesisFile( + $thesisId, + $fileType, + $watchUrl, + basename($upload['name']), + $upload['size'], + $upload['type'] ?? 'application/octet-stream', + null, + null + ); + error_log("ThesisEditController: PeerTube upload OK → $watchUrl"); + } catch (\Throwable $e) { + error_log('ThesisEditController: PeerTube upload failed — ' . $e->getMessage()); + } + } + /** * Add or update a website URL thesis_file row. * diff --git a/app/src/PeerTubeService.php b/app/src/PeerTubeService.php new file mode 100644 index 0000000..28307da --- /dev/null +++ b/app/src/PeerTubeService.php @@ -0,0 +1,322 @@ +getPDO()->prepare( + 'SELECT instance_url, username, password, channel_id, privacy + FROM peertube_settings WHERE id = 1 LIMIT 1' + ); + $stmt->execute(); + $row = $stmt->fetch(); + + if ($row) { + require_once __DIR__ . '/Crypto.php'; + $row['password'] = Crypto::decrypt($row['password']); + } + + return $row ?: [ + 'instance_url' => '', + 'username' => '', + 'password' => '', + 'channel_id' => 1, + 'privacy' => 1, // 1=Public, 2=Unlisted, 3=Private + ]; + } + + /** + * Upsert PeerTube settings. + */ + public static function updateSettings(Database $db, array $data): void + { + $current = self::getSettings($db); + $merged = array_merge($current, $data); + + require_once __DIR__ . '/Crypto.php'; + + // Normalise instance URL: strip trailing slash + $instanceUrl = rtrim(trim($merged['instance_url']), '/'); + $channelId = max(1, (int)$merged['channel_id']); + $privacy = in_array((int)$merged['privacy'], [1, 2, 3], true) + ? (int)$merged['privacy'] : 1; + + $pdo = $db->getPDO(); + + // Upsert row (id=1 is always the singleton row) + $exists = $pdo->query('SELECT COUNT(*) FROM peertube_settings WHERE id = 1')->fetchColumn(); + if ($exists) { + $stmt = $pdo->prepare( + 'UPDATE peertube_settings + SET instance_url = :url, + username = :user, + password = :pass, + channel_id = :chan, + privacy = :priv, + updated_at = CURRENT_TIMESTAMP + WHERE id = 1' + ); + } else { + $stmt = $pdo->prepare( + 'INSERT INTO peertube_settings (id, instance_url, username, password, channel_id, privacy) + VALUES (1, :url, :user, :pass, :chan, :priv)' + ); + } + + $stmt->execute([ + ':url' => $instanceUrl, + ':user' => trim($merged['username']), + ':pass' => Crypto::encrypt($merged['password']), + ':chan' => $channelId, + ':priv' => $privacy, + ]); + } + + /** + * Whether PeerTube credentials are fully configured. + */ + public static function isConfigured(Database $db): bool + { + $s = self::getSettings($db); + return $s['instance_url'] !== '' && $s['username'] !== ''; + } + + /** + * Whether the PeerTube upload feature flag is enabled. + */ + public static function isEnabled(Database $db): bool + { + return $db->getSetting('peertube_upload_enabled', '0') === '1'; + } + + /** + * Test connectivity: obtain a token without uploading anything. + * + * @return array{ok:bool, error:string} + */ + public static function test(Database $db): array + { + $s = self::getSettings($db); + if ($s['instance_url'] === '') { + return ['ok' => false, 'error' => "URL de l'instance PeerTube non configurée."]; + } + try { + self::obtainToken($s); + return ['ok' => true, 'error' => '']; + } catch (\Throwable $e) { + return ['ok' => false, 'error' => $e->getMessage()]; + } + } + + // ------------------------------------------------------------------------- + // Upload + // ------------------------------------------------------------------------- + + /** + * Upload a local file to PeerTube. + * + * @param string $filePath Absolute path to the file on disk. + * @param string $title Video/audio title shown on PeerTube. + * @param string $description Optional description. + * @return string The public watch URL of the uploaded video. + * @throws \RuntimeException On any API or network error. + */ + public static function upload( + Database $db, + string $filePath, + string $title, + string $description = '' + ): string { + $s = self::getSettings($db); + if ($s['instance_url'] === '') { + throw new \RuntimeException('PeerTube non configuré.'); + } + + $token = self::obtainToken($s); + + $baseUrl = $s['instance_url']; + $uploadUrl = $baseUrl . '/api/v1/videos/upload'; + + // Build multipart body + $boundary = '----XamxamPT' . bin2hex(random_bytes(8)); + $body = ''; + + $fields = [ + 'channelId' => (string)$s['channel_id'], + 'name' => $title, + 'description' => $description, + 'privacy' => (string)$s['privacy'], + 'waitTranscoding' => 'false', + ]; + foreach ($fields as $k => $v) { + $body .= "--{$boundary}\r\n"; + $body .= "Content-Disposition: form-data; name=\"{$k}\"\r\n\r\n"; + $body .= $v . "\r\n"; + } + + // File part + $fileName = basename($filePath); + $mimeType = (new \finfo(FILEINFO_MIME_TYPE))->file($filePath); + $body .= "--{$boundary}\r\n"; + $body .= "Content-Disposition: form-data; name=\"videofile\"; filename=\"{$fileName}\"\r\n"; + $body .= "Content-Type: {$mimeType}\r\n\r\n"; + $body .= file_get_contents($filePath) . "\r\n"; + $body .= "--{$boundary}--\r\n"; + + $response = self::httpRequest($uploadUrl, 'POST', $body, [ + 'Authorization: Bearer ' . $token, + 'Content-Type: multipart/form-data; boundary=' . $boundary, + 'Content-Length: ' . strlen($body), + ]); + + $json = json_decode($response['body'], true); + + if ($response['status'] < 200 || $response['status'] >= 300) { + $msg = $json['error'] ?? $json['detail'] ?? $response['body']; + throw new \RuntimeException('PeerTube upload failed (' . $response['status'] . '): ' . $msg); + } + + $shortUuid = $json['video']['shortUUID'] ?? $json['video']['uuid'] ?? null; + if ($shortUuid === null) { + throw new \RuntimeException('PeerTube upload: no video UUID in response.'); + } + + return rtrim($baseUrl, '/') . '/videos/watch/' . $shortUuid; + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + /** + * Obtain an OAuth2 access token via password grant. + * + * @throws \RuntimeException on failure. + */ + private static function obtainToken(array $s): string + { + $tokenUrl = $s['instance_url'] . '/api/v1/users/token'; + + $body = http_build_query([ + 'client_id' => self::getClientId($s), + 'client_secret' => self::getClientSecret($s), + 'grant_type' => 'password', + 'response_type' => 'code', + 'username' => $s['username'], + 'password' => $s['password'], + ]); + + $response = self::httpRequest($tokenUrl, 'POST', $body, [ + 'Content-Type: application/x-www-form-urlencoded', + 'Content-Length: ' . strlen($body), + ]); + + $json = json_decode($response['body'], true); + + if ($response['status'] !== 200 || empty($json['access_token'])) { + $msg = $json['error_description'] ?? $json['error'] ?? $response['body']; + throw new \RuntimeException('PeerTube auth failed (' . $response['status'] . '): ' . $msg); + } + + return $json['access_token']; + } + + /** + * Fetch the OAuth2 client_id from the PeerTube instance. + */ + private static function getClientId(array $s): string + { + return self::getOAuthClient($s)['client_id']; + } + + private static function getClientSecret(array $s): string + { + return self::getOAuthClient($s)['client_secret']; + } + + private static function getOAuthClient(array $s): array + { + $url = $s['instance_url'] . '/api/v1/oauth-clients/local'; + $response = self::httpRequest($url, 'GET', '', []); + + $json = json_decode($response['body'], true); + if ($response['status'] !== 200 || empty($json['client_id'])) { + throw new \RuntimeException( + 'Impossible de récupérer les identifiants OAuth de l\'instance PeerTube (' . $response['status'] . ').' + ); + } + + return $json; + } + + /** + * Minimal cURL HTTP helper. + * + * @return array{status:int, body:string} + */ + private static function httpRequest( + string $url, + string $method, + string $body, + array $headers, + int $timeout = 300 + ): array { + if (!function_exists('curl_init')) { + throw new \RuntimeException('L\'extension PHP cURL est requise pour l\'intégration PeerTube.'); + } + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $timeout, + CURLOPT_CONNECTTIMEOUT => 15, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_HTTPHEADER => $headers, + ]); + + if ($method === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + + $responseBody = curl_exec($ch); + $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($responseBody === false) { + throw new \RuntimeException('Erreur réseau PeerTube : ' . $error); + } + + return ['status' => $status, 'body' => (string)$responseBody]; + } +} diff --git a/app/templates/admin/parametres.php b/app/templates/admin/parametres.php index 97bb43c..376958e 100644 --- a/app/templates/admin/parametres.php +++ b/app/templates/admin/parametres.php @@ -313,6 +313,92 @@ + +
+

PeerTube

+

+ Intégration avec une instance PeerTube pour l'hébergement des vidéos et fichiers audio. + Les fichiers sont uploadés via l'API PeerTube et intégrés comme lecteurs embarqués sur la page du TFE. +

+

+ ⚠ L'activation nécessite un quota d'upload suffisant sur l'instance PeerTube. + Laissez désactivé jusqu'à obtention du quota. +

+ +
+ + ✓ Configuré + + + ✗ Non configuré + +
+ +
+ + + +
+ Activation + +
+ +
+
+ + + Sans slash final. Ex : https://peertube.erg.be +
+ +
+ + +
+ +
+ + +
+ +
+ + + Identifiant numérique de la chaîne PeerTube cible. +
+ +
+ + +
+
+ + +
+
+ diff --git a/app/templates/public/tfe.php b/app/templates/public/tfe.php index 669870d..fc4c736 100644 --- a/app/templates/public/tfe.php +++ b/app/templates/public/tfe.php @@ -443,6 +443,7 @@ $isAudio = in_array($ext, ['mp3','ogg','oga','wav','flac','aac','m4a'], true) || $fileType === 'audio'; $isPdf = ($ext === 'pdf') || $fileType === 'main'; $isWebsite = ($fileType === 'website'); + $isPeerTube = ($isExternalUrl && str_contains($filePath, '/videos/watch/')); $isOther = !($isImage || $isVideo || $isAudio || $isPdf || $isWebsite); $_vttPath = null; @@ -485,6 +486,22 @@ <?= htmlspecialchars($caption !== '' ? $caption : $data['title'] . ' — ' . ($data['authors'] ?? '')) ?> + + +

+ + Ouvrir dans PeerTube + (ouvre dans un nouvel onglet) + +

+ + + + +

+ + Ouvrir dans PeerTube + (ouvre dans un nouvel onglet) + +

+ +