diff --git a/TODO.md b/TODO.md index e720d79..e7626de 100644 --- a/TODO.md +++ b/TODO.md @@ -14,6 +14,29 @@ - [x] **Migration idempotency** — `CREATE INDEX` / `CREATE TRIGGER` / `CREATE VIEW` now use `IF NOT EXISTS` in schema.sql and generate-schema.py; migrate.sh no longer fails on re-run - [ ] **Database readonly** — intermittent permission issue after deploy (added deploy-nginx recipe; permissions should be fixed by --chown + deploy-server.sh) +## PeerTube Alternate Labels & FilePond Pools + +- [x] Add `peertube_video_label` and `peertube_audio_label` columns (migration 029) +- [x] Update PeerTubeService getSettings/updateSettings for new fields +- [x] Add label fields to parametres.php admin form +- [x] Handle label saving in admin/actions/settings.php +- [x] Uncomment video/audio slots in fichiers-fragment.php with FilePond pools when PeerTube enabled +- [x] Register `peertube_video` / `peertube_audio` queue types in file-upload-filepond.js +- [x] Update handlePeerTubeUpload → handlePeerTubeQueueFiles in both create/edit controllers +- [x] When PeerTube active, restrict TFE pool to PDF/images/VTT/archives only (no video/audio) +- [x] Add HTMX swap attributes to Vidéo/Audio format checkboxes for live toggling +- [x] Store PeerTube uploads as `peertube_ids:{uuid}` in thesis_files.file_path +- [x] Create `templates/partials/peertube-embed.php` iframe embed template +- [x] Render PeerTube embeds in public thesis view (tfe.php) +- [x] Handle PeerTube files in admin recapitulatif.php and fichiers-fragment.php +- [x] Shared SMTP credentials — remove username/password from peertube_settings (migration 031) +- [x] PeerTubeService reads credentials from SmtpRelay +- [x] OAuth client_id/secret fetched on-demand and cached in-memory (no DB storage) +- [x] Resumable upload protocol (POST init + PUT chunks) in PeerTubeService::upload() +- [x] Admin recapitulatif: show real PeerTube watch links (public/unlisted only) +- [x] Optimize public thesis view: load PeerTube instance URL once before file loop +- [ ] Test end-to-end: activate PeerTube, set labels, submit form with video/audio files + ## SQLite Backup & Data Integrity (docs/backup-plan.md) ### Phase 1 — WAL Mode diff --git a/app/migrations/applied/029_peertube_labels.sql b/app/migrations/applied/029_peertube_labels.sql new file mode 100644 index 0000000..38eaa4a --- /dev/null +++ b/app/migrations/applied/029_peertube_labels.sql @@ -0,0 +1,6 @@ +-- Migration 029: PeerTube alternate labels +-- Adds peertube_video_label and peertube_audio_label columns to peertube_settings. +-- These override the default "Vidéo" / "Audio" format labels when PeerTube is active. + +ALTER TABLE peertube_settings ADD COLUMN peertube_video_label TEXT NOT NULL DEFAULT ''; +ALTER TABLE peertube_settings ADD COLUMN peertube_audio_label TEXT NOT NULL DEFAULT ''; diff --git a/app/migrations/applied/030_peertube_oauth.sql b/app/migrations/applied/030_peertube_oauth.sql new file mode 100644 index 0000000..5450085 --- /dev/null +++ b/app/migrations/applied/030_peertube_oauth.sql @@ -0,0 +1,6 @@ +-- Migration 030: Store PeerTube OAuth client credentials +-- Instead of fetching client_id/client_secret from the API on every token request, +-- store them once. The admin fetches them manually or we auto-fetch on first save. + +ALTER TABLE peertube_settings ADD COLUMN oauth_client_id TEXT NOT NULL DEFAULT ''; +ALTER TABLE peertube_settings ADD COLUMN oauth_client_secret TEXT NOT NULL DEFAULT ''; diff --git a/app/migrations/applied/031_shared_credentials.sql b/app/migrations/applied/031_shared_credentials.sql new file mode 100644 index 0000000..83d7257 --- /dev/null +++ b/app/migrations/applied/031_shared_credentials.sql @@ -0,0 +1,23 @@ +-- Migration 031: Remove redundant username/password from peertube_settings. +-- PeerTube now shares SMTP credentials. Also removes oauth_client_id/secret +-- since those are fetched on-demand from the PeerTube API. + +-- SQLite doesn't support DROP COLUMN natively in older versions. +-- We rebuild the table without the dropped columns. + +CREATE TABLE peertube_settings_new ( + id INTEGER PRIMARY KEY CHECK (id = 1), + instance_url TEXT NOT NULL DEFAULT '', + channel_id INTEGER NOT NULL DEFAULT 1, + privacy INTEGER NOT NULL DEFAULT 1, + peertube_video_label TEXT NOT NULL DEFAULT '', + peertube_audio_label TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO peertube_settings_new (id, instance_url, channel_id, privacy, peertube_video_label, peertube_audio_label, updated_at) +SELECT id, instance_url, channel_id, privacy, peertube_video_label, peertube_audio_label, updated_at +FROM peertube_settings; + +DROP TABLE peertube_settings; +ALTER TABLE peertube_settings_new RENAME TO peertube_settings; diff --git a/app/migrations/applied/032_channel_name.sql b/app/migrations/applied/032_channel_name.sql new file mode 100644 index 0000000..7f3777a --- /dev/null +++ b/app/migrations/applied/032_channel_name.sql @@ -0,0 +1,7 @@ +-- Migration 032: Change peertube_settings.channel_id to channel_name. +-- Channel is now identified by its full handle (name@host) instead of numeric ID. +-- The ID is resolved via the PeerTube API at upload time. + +ALTER TABLE peertube_settings ADD COLUMN channel_name TEXT NOT NULL DEFAULT ''; +-- Copy existing values if any (unlikely to have useful data since channel_id=1 is the default) +-- Drop is not supported; channel_id column will be ignored by the application. diff --git a/app/public/admin/actions/peertube-test.php b/app/public/admin/actions/peertube-test.php new file mode 100644 index 0000000..5687c6d --- /dev/null +++ b/app/public/admin/actions/peertube-test.php @@ -0,0 +1,40 @@ +Token CSRF invalide.'; + exit; +} + +$db = new Database(); + +// Save current settings so we can test with the form values +$data = [ + 'instance_url' => $_POST['peertube_instance_url'] ?? '', + 'channel_name' => $_POST['peertube_channel_name'] ?? '', + 'privacy' => $_POST['peertube_privacy'] ?? 1, +]; +PeerTubeService::updateSettings($db, $data); + +$test = PeerTubeService::test($db); + +if ($test['ok']) { + echo '✓ Connexion réussie — authentification et résolution de la chaîne OK.'; +} else { + echo '✗ ' . htmlspecialchars($test['error']) . ''; +} diff --git a/app/public/admin/actions/settings.php b/app/public/admin/actions/settings.php index e6e4d0a..df6094a 100644 --- a/app/public/admin/actions/settings.php +++ b/app/public/admin/actions/settings.php @@ -138,17 +138,12 @@ if ($section === 'formulaire_restrictions') { $enabled = isset($_POST['peertube_upload_enabled']) ? '1' : '0'; $db->setSetting('peertube_upload_enabled', $enabled); - // Credentials — only overwrite password when user typed something + // PeerTube-specific settings (auth uses SMTP credentials) $data = [ 'instance_url' => $_POST['peertube_instance_url'] ?? '', - 'username' => $_POST['peertube_username'] ?? '', - 'channel_id' => $_POST['peertube_channel_id'] ?? 1, + 'channel_name' => $_POST['peertube_channel_name'] ?? '', '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.'); diff --git a/app/public/assets/js/file-upload-filepond.js b/app/public/assets/js/file-upload-filepond.js index c011178..9e685a5 100644 --- a/app/public/assets/js/file-upload-filepond.js +++ b/app/public/assets/js/file-upload-filepond.js @@ -33,8 +33,16 @@ "text/vtt", "application/zip", "application/x-tar", "application/gzip" ], + // When PeerTube is active, exclude video/audio from TFE pool + acceptedFileTypesPeerTube: [ + "image/jpeg", "image/png", "image/gif", "image/webp", + "application/pdf", + "text/vtt", + "application/zip", "application/x-tar", "application/gzip" + ], labelFileTypeNotAllowed: "Format non accepté", fileValidateTypeLabelExpectedTypes: "PDF, Images, Vidéos, Audio, VTT, Archives", + fileValidateTypeLabelExpectedTypesPeerTube: "PDF, Images, VTT, Archives", maxFileSize: "500MB", labelMaxFileSizeExceeded: "Fichier trop volumineux", labelMaxFileSize: "Taille max: {filesize}", @@ -91,17 +99,37 @@ labelMaxFileSize: "Taille max: {filesize}", allowMultiple: false }, + peertube_video: { + acceptedFileTypes: ["video/mp4", "video/webm", "video/ogg", "video/quicktime"], + labelFileTypeNotAllowed: "Format non accepté", + fileValidateTypeLabelExpectedTypes: "MP4, WebM, OGV, MOV", + maxFileSize: "500MB", + labelMaxFileSizeExceeded: "Fichier trop volumineux", + labelMaxFileSize: "Taille max: {filesize}", + allowMultiple: true + }, + peertube_audio: { + acceptedFileTypes: ["audio/mpeg", "audio/ogg", "audio/flac", "audio/x-wav", "audio/aac", "audio/mp4"], + labelFileTypeNotAllowed: "Format non accepté", + fileValidateTypeLabelExpectedTypes: "MP3, OGG, FLAC, WAV, AAC, M4A", + maxFileSize: "500MB", + labelMaxFileSizeExceeded: "Fichier trop volumineux", + labelMaxFileSize: "Taille max: {filesize}", + allowMultiple: true + }, }; // Map input id → queue type var INPUT_ID_TO_TYPE = { - "tfe-files-input": "tfe", - "tfe-files-input-2": "tfe", - "video-files-input": "video", - "audio-files-input": "audio", - "annexe-files-input": "annexe", - "couverture": "cover", - "note_intention": "note_intention", + "tfe-files-input": "tfe", + "tfe-files-input-2": "tfe", + "video-files-input": "video", + "audio-files-input": "audio", + "annexe-files-input": "annexe", + "couverture": "cover", + "note_intention": "note_intention", + "peertube-video-input": "peertube_video", + "peertube-audio-input": "peertube_audio", }; // ── Helpers ─────────────────────────────────────────────────────────── @@ -167,6 +195,15 @@ // Per-type max size overrides (for TFE: PDF=100MB, video/audio=2GB) var perExtMax = cfg.perExtensionMaxSize || {}; + // When PeerTube is active, restrict TFE pool to PDF/text only + var peerTubeActive = queueType === "tfe" && input.dataset.peertubeActive === "1"; + var acceptedFileTypes = peerTubeActive && cfg.acceptedFileTypesPeerTube + ? cfg.acceptedFileTypesPeerTube + : cfg.acceptedFileTypes; + var expectedTypesLabel = peerTubeActive && cfg.fileValidateTypeLabelExpectedTypesPeerTube + ? cfg.fileValidateTypeLabelExpectedTypesPeerTube + : cfg.fileValidateTypeLabelExpectedTypes; + return { allowMultiple: cfg.allowMultiple, allowReorder: true, @@ -174,9 +211,9 @@ storeAsFile: true, // ── Native FilePond validation ── - acceptedFileTypes: cfg.acceptedFileTypes, + acceptedFileTypes: acceptedFileTypes, labelFileTypeNotAllowed: cfg.labelFileTypeNotAllowed, - fileValidateTypeLabelExpectedTypes: cfg.fileValidateTypeLabelExpectedTypes, + fileValidateTypeLabelExpectedTypes: expectedTypesLabel, maxFileSize: cfg.maxFileSize, labelMaxFileSizeExceeded: cfg.labelMaxFileSizeExceeded, labelMaxFileSize: cfg.labelMaxFileSize, diff --git a/app/public/partage/fichiers-fragment.php b/app/public/partage/fichiers-fragment.php index 886a783..9013845 100644 --- a/app/public/partage/fichiers-fragment.php +++ b/app/public/partage/fichiers-fragment.php @@ -24,6 +24,7 @@ require_once APP_ROOT . '/src/PeerTubeService.php'; $_ptDb = Database::getInstance(); $peerTubeEnabled = PeerTubeService::isEnabled($_ptDb); +$peerTubeSettings = PeerTubeService::getSettings($_ptDb); $db = $_ptDb->getConnection(); @@ -98,6 +99,20 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']); hx-trigger="change" hx-include="[name='formats[]'], [name='website_url'], [name='admin_mode'], [name='edit_mode'], [name='_cover']" hx-swap="outerHTML" + + hx-post="" + hx-target="#slot-video" + hx-select="#slot-video" + hx-trigger="change" + hx-include="[name='formats[]'], [name='admin_mode'], [name='edit_mode'], [name='_cover']" + hx-swap="outerHTML" + + hx-post="" + hx-target="#slot-audio" + hx-select="#slot-audio" + hx-trigger="change" + hx-include="[name='formats[]'], [name='admin_mode'], [name='edit_mode'], [name='_cover']" + hx-swap="outerHTML" > @@ -142,9 +157,13 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']); // ── Existing files ── $_thesisFilesList = array_values(array_filter($_efiles, fn($f) => $f["file_type"] !== "cover")); foreach ($_thesisFilesList as $_f): - $_fExt = strtolower(pathinfo($_f["file_path"] ?? "", PATHINFO_EXTENSION)); + $_fPath = $_f["file_path"] ?? ""; + $_fIsPeerTube = str_starts_with($_fPath, "peertube_ids:"); + $_fExt = strtolower(pathinfo($_fPath, PATHINFO_EXTENSION)); $_fType = $_f["file_type"] ?? "other"; $_fIcon = match (true) { + $_fIsPeerTube && $_fType === "video" => "🎬", + $_fIsPeerTube && $_fType === "audio" => "🔊", $_fType === "main" || $_fExt === "pdf" => "📄", in_array($_fExt, ["jpg","jpeg","png","gif","webp"]) => "🖼️", $_fType === "video" || in_array($_fExt, ["mp4","webm","mov","ogv"]) => "🎬", @@ -153,8 +172,8 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']); $_fType === "website" => "🌐", default => "📎", }; - $_fIsExternal = str_starts_with($_f["file_path"] ?? "", "http://") || str_starts_with($_f["file_path"] ?? "", "https://"); - $_fLinkHref = $_fIsExternal ? htmlspecialchars($_f["file_path"]) : "/media?path=" . urlencode($_f["file_path"]); + $_fIsExternal = str_starts_with($_fPath, "http://") || str_starts_with($_fPath, "https://"); + $_fLinkHref = $_fIsPeerTube ? "#" : ($_fIsExternal ? htmlspecialchars($_fPath) : "/media?path=" . urlencode($_fPath)); ?>
  • "> "> @@ -222,9 +241,15 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']); name="queue_file[tfe][]" multiple class="tfe-file-picker" - > + + data-peertube-active=""> + + PDF (max 100 MB) · Images (max 500 MB) · VTT · Archives (max 500 MB). + Vidéo & Audio → utilisez les emplacements dédiés ci-dessous. + PDF (max 100 MB) · Images (max 500 MB) · Vidéo & Audio (max 2 GB) · VTT · Archives (max 500 MB). + Glissez pour réordonner. PDFs trop lourds ? https://bentopdf.com/ @@ -265,20 +290,21 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']); - - -
    - +
    +
    - > - MP4, WebM ou MOV. La vidéo sera hébergée sur PeerTube. Max 500 MB. + MP4, WebM ou MOV. Max 500 MB. Glissez pour réordonner. Hébergé sur PeerTube.
    - +
    @@ -291,27 +317,25 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']); MP4, WebM ou MOV. Max 500 MB. Glissez pour réordonner.
    - - --> - - - -
    - +
    +
    - > - MP3, OGG, WAV, FLAC ou AAC. Le fichier sera hébergé sur PeerTube. Max 500 MB. + MP3, OGG, WAV, FLAC ou AAC. Max 500 MB. Glissez pour réordonner. Hébergé sur PeerTube.
    - +
    @@ -324,12 +348,9 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']); MP3, OGG, WAV, FLAC ou AAC. Max 500 MB. Glissez pour réordonner.
    - - --> -
    diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index a7004c2..fb98971 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -209,9 +209,11 @@ class ThesisCreateController $nextNum = $this->handleTfeQueueFiles($thesisId, $qAudio, $folderPath, $filePrefix, $nextNum); $this->handleAnnexeQueueFiles($thesisId, $qAnnexe, $folderPath, $filePrefix); - // ── 5b. PeerTube video / audio uploads ──────────────────────────────── - $this->handlePeerTubeUpload($thesisId, $data['titre'], $files, 'peertube_video'); - $this->handlePeerTubeUpload($thesisId, $data['titre'], $files, 'peertube_audio'); + // ── 5b. PeerTube video / audio uploads (from FilePond queue) ────────── + $qPTVideo = $this->extractFilesSubArray($queueFiles, 'peertube_video'); + $qPTAudio = $this->extractFilesSubArray($queueFiles, 'peertube_audio'); + $this->handlePeerTubeQueueFiles($thesisId, $data['titre'], $qPTVideo, 'video'); + $this->handlePeerTubeQueueFiles($thesisId, $data['titre'], $qPTAudio, 'audio'); // ── 6. Website URL — stored as thesis_files row ────────────────────── $this->handleWebsiteUrl($thesisId, $post); @@ -581,17 +583,19 @@ class ThesisCreateController } /** - * Upload a video or audio file to PeerTube when the feature is enabled. + * Upload PeerTube video/audio files from FilePond queue. * - * @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'. + * Files arrive via PHP's nested $_FILES structure from + * . + * + * @param int $thesisId Thesis to attach the results to. + * @param string $title Title to use on PeerTube. + * @param array|null $uploads Flat $_FILES-style array from extractFilesSubArray(). + * @param string $fileType 'video' or 'audio'. */ - protected function handlePeerTubeUpload(int $thesisId, string $title, array $files, string $inputName): void + protected function handlePeerTubeQueueFiles(int $thesisId, string $title, ?array $uploads, string $fileType): void { - $upload = $files[$inputName] ?? null; - if (!$upload || !isset($upload['error']) || $upload['error'] !== UPLOAD_ERR_OK) { + if (!$uploads || !is_array($uploads['name'] ?? null)) { return; } @@ -600,29 +604,37 @@ class ThesisCreateController return; } - try { - $watchUrl = PeerTubeService::upload( - $this->db, - $upload['tmp_name'], - $title, - '' - ); + $count = count($uploads['name']); + for ($i = 0; $i < $count; $i++) { + if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { + continue; + } - $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. + try { + $result = PeerTubeService::upload( + $this->db, + $uploads['tmp_name'][$i], + $title, + '' + ); + + // Store as peertube_ids:{uuid} so the embed template can extract the UUID + $storedPath = 'peertube_ids:' . $result['uuid']; + $this->db->insertThesisFile( + $thesisId, + $fileType, + $storedPath, + basename($uploads['name'][$i]), + $uploads['size'][$i], + $uploads['type'][$i] ?? 'application/octet-stream', + null, + null + ); + error_log("ThesisCreateController: PeerTube upload OK → " . $result['watchUrl']); + } catch (\Throwable $e) { + error_log('ThesisCreateController: PeerTube upload failed — ' . $e->getMessage()); + // Non-fatal: thesis already saved; admin can re-upload manually. + } } } diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index cb8b9e5..fd8e589 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -437,9 +437,11 @@ class ThesisEditController $this->handleAnnexeFiles($thesisId, $files['annexes'], $folderPath, $filePrefix, $post); } - // ── PeerTube video / audio uploads ──────────────────────────────────── - $this->handlePeerTubeUpload($thesisId, trim($post['titre'] ?? ''), $files, 'peertube_video'); - $this->handlePeerTubeUpload($thesisId, trim($post['titre'] ?? ''), $files, 'peertube_audio'); + // ── PeerTube video / audio uploads (from FilePond queue) ────────────── + $qPTVideo = $this->extractFilesSubArray($queueFiles, 'peertube_video'); + $qPTAudio = $this->extractFilesSubArray($queueFiles, 'peertube_audio'); + $this->handlePeerTubeQueueFiles($thesisId, trim($post['titre'] ?? ''), $qPTVideo, 'video'); + $this->handlePeerTubeQueueFiles($thesisId, trim($post['titre'] ?? ''), $qPTAudio, 'audio'); // ── Website URL — add or update ────────────────────────────────────── $this->handleWebsiteUrl($thesisId, $post); @@ -571,17 +573,19 @@ class ThesisEditController } /** - * Upload a video or audio file to PeerTube when the feature is enabled. + * Upload PeerTube video/audio files from FilePond queue. * - * @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'. + * Files arrive via PHP's nested $_FILES structure from + * . + * + * @param int $thesisId Thesis to attach the results to. + * @param string $title Title to use on PeerTube. + * @param array|null $uploads Flat $_FILES-style array from extractFilesSubArray(). + * @param string $fileType 'video' or 'audio'. */ - private function handlePeerTubeUpload(int $thesisId, string $title, array $files, string $inputName): void + private function handlePeerTubeQueueFiles(int $thesisId, string $title, ?array $uploads, string $fileType): void { - $upload = $files[$inputName] ?? null; - if (!$upload || !isset($upload['error']) || $upload['error'] !== UPLOAD_ERR_OK) { + if (!$uploads || !is_array($uploads['name'] ?? null)) { return; } @@ -590,28 +594,35 @@ class ThesisEditController return; } - try { - $watchUrl = PeerTubeService::upload( - $this->db, - $upload['tmp_name'], - $title, - '' - ); + $count = count($uploads['name']); + for ($i = 0; $i < $count; $i++) { + if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { + continue; + } - $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()); + try { + $result = PeerTubeService::upload( + $this->db, + $uploads['tmp_name'][$i], + $title, + '' + ); + + $storedPath = 'peertube_ids:' . $result['uuid']; + $this->db->insertThesisFile( + $thesisId, + $fileType, + $storedPath, + basename($uploads['name'][$i]), + $uploads['size'][$i], + $uploads['type'][$i] ?? 'application/octet-stream', + null, + null + ); + error_log("ThesisEditController: PeerTube upload OK → " . $result['watchUrl']); + } catch (\Throwable $e) { + error_log('ThesisEditController: PeerTube upload failed — ' . $e->getMessage()); + } } } diff --git a/app/src/PeerTubeService.php b/app/src/PeerTubeService.php index 28307da..87193f6 100644 --- a/app/src/PeerTubeService.php +++ b/app/src/PeerTubeService.php @@ -3,108 +3,118 @@ /** * PeerTubeService * - * Handles credential storage and video/audio uploads to a PeerTube instance - * via its REST API. Follows the same patterns as SmtpRelay: - * - Static CRUD on a dedicated settings table (peertube_settings) - * - A feature-flag setting (peertube_upload_enabled) in site_settings - * - An upload() method that POSTs a file to the PeerTube /api/v1/videos/upload - * endpoint and returns the resulting watch URL + * Handles video/audio uploads to a PeerTube instance via its REST API. * - * PeerTube API reference: - * POST /api/v1/videos/upload — resumable / direct upload - * POST /api/v1/users/token — OAuth2 password grant + * Credentials are shared with SmtpRelay: the SMTP username/password are + * reused for PeerTube OAuth2 password-grant authentication. Only + * PeerTube-specific settings (instance URL, channel name, privacy, labels) + * live in peertube_settings. * - * The stored access token is refreshed automatically when it expires (401). - * Credentials (password + token) are encrypted at rest via Crypto.php. + * The channel is stored by its full handle (name@host). The numeric ID + * is resolved via GET /api/v1/video-channels/{handle} at upload time. + * + * OAuth client_id / client_secret are fetched once from the PeerTube + * instance's /api/v1/oauth-clients/local endpoint and cached in-memory + * per process lifetime. + * + * Upload uses the resumable protocol: + * POST /api/v1/videos/upload-resumable — init + * PUT /api/v1/videos/upload-resumable — send chunk + * DELETE /api/v1/videos/upload-resumable — cancel */ class PeerTubeService { + /** @var array In-memory OAuth cache. */ + private static array $oauthCache = []; + + /** @var array In-memory channel name → ID cache. */ + private static array $channelCache = []; + // ------------------------------------------------------------------------- // DB CRUD // ------------------------------------------------------------------------- /** - * Return current PeerTube settings from the DB. + * Return PeerTube settings merged with SMTP credentials. * - * @return array{instance_url:string,username:string,password:string,channel_id:int,privacy:int} + * @return array{instance_url:string,username:string,password:string,channel_name:string,privacy:int,peertube_video_label:string,peertube_audio_label:string} */ public static function getSettings(Database $db): array { $stmt = $db->getPDO()->prepare( - 'SELECT instance_url, username, password, channel_id, privacy + 'SELECT instance_url, channel_name, privacy, + peertube_video_label, peertube_audio_label FROM peertube_settings WHERE id = 1 LIMIT 1' ); $stmt->execute(); - $row = $stmt->fetch(); + $row = $stmt->fetch() ?: []; - if ($row) { - require_once __DIR__ . '/Crypto.php'; - $row['password'] = Crypto::decrypt($row['password']); - } + require_once __DIR__ . '/SmtpRelay.php'; + $smtp = SmtpRelay::getSettings($db); - return $row ?: [ - 'instance_url' => '', - 'username' => '', - 'password' => '', - 'channel_id' => 1, - 'privacy' => 1, // 1=Public, 2=Unlisted, 3=Private + return [ + 'instance_url' => $row['instance_url'] ?? '', + 'username' => $smtp['username'] ?? '', + 'password' => $smtp['password'] ?? '', + 'channel_name' => $row['channel_name'] ?? '', + 'privacy' => (int)($row['privacy'] ?? 1), + 'peertube_video_label' => $row['peertube_video_label'] ?? '', + 'peertube_audio_label' => $row['peertube_audio_label'] ?? '', ]; } /** - * Upsert PeerTube settings. + * Upsert PeerTube-specific 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']); + $channelName = trim($merged['channel_name'] ?? $current['channel_name'] ?? ''); $privacy = in_array((int)$merged['privacy'], [1, 2, 3], true) ? (int)$merged['privacy'] : 1; + $videoLabel = trim($merged['peertube_video_label'] ?? ''); + $audioLabel = trim($merged['peertube_audio_label'] ?? ''); $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 + SET instance_url = :url, + channel_name = :chan, + privacy = :priv, + peertube_video_label = :vlabel, + peertube_audio_label = :alabel, + 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)' + 'INSERT INTO peertube_settings (id, instance_url, channel_name, privacy, peertube_video_label, peertube_audio_label) + VALUES (1, :url, :chan, :priv, :vlabel, :alabel)' ); } $stmt->execute([ - ':url' => $instanceUrl, - ':user' => trim($merged['username']), - ':pass' => Crypto::encrypt($merged['password']), - ':chan' => $channelId, - ':priv' => $privacy, + ':url' => $instanceUrl, + ':chan' => $channelName, + ':priv' => $privacy, + ':vlabel' => $videoLabel, + ':alabel' => $audioLabel, ]); } /** - * Whether PeerTube credentials are fully configured. + * Whether PeerTube is fully configured. */ public static function isConfigured(Database $db): bool { $s = self::getSettings($db); - return $s['instance_url'] !== '' && $s['username'] !== ''; + return $s['instance_url'] !== '' && $s['username'] !== '' && $s['channel_name'] !== ''; } /** @@ -116,7 +126,7 @@ class PeerTubeService } /** - * Test connectivity: obtain a token without uploading anything. + * Test connectivity: obtain a token and resolve the channel. * * @return array{ok:bool, error:string} */ @@ -126,8 +136,15 @@ class PeerTubeService if ($s['instance_url'] === '') { return ['ok' => false, 'error' => "URL de l'instance PeerTube non configurée."]; } + if ($s['channel_name'] === '') { + return ['ok' => false, 'error' => 'Nom de la chaîne PeerTube non configuré.']; + } try { - self::obtainToken($s); + $token = self::obtainToken($s); + $chId = self::resolveChannelId($s); + if ($chId === null) { + return ['ok' => false, 'error' => "Chaîne « {$s['channel_name']} » introuvable sur l'instance."]; + } return ['ok' => true, 'error' => '']; } catch (\Throwable $e) { return ['ok' => false, 'error' => $e->getMessage()]; @@ -135,97 +152,266 @@ class PeerTubeService } // ------------------------------------------------------------------------- - // Upload + // Upload — resumable protocol // ------------------------------------------------------------------------- /** - * Upload a local file to PeerTube. + * Upload a local file to PeerTube using the resumable upload protocol. * - * @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. + * @return array{uuid:string, watchUrl:string} + * @throws \RuntimeException */ public static function upload( Database $db, string $filePath, string $title, string $description = '' - ): string { + ): array { $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"; + $channelId = self::resolveChannelId($s); + if ($channelId === null) { + throw new \RuntimeException('Chaîne PeerTube introuvable : ' . $s['channel_name']); } - // File part + $token = self::obtainToken($s); + $baseUrl = $s['instance_url']; + $fileSize = filesize($filePath); $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, [ + // ── Step 1: Initialize resumable upload ── + $initUrl = $baseUrl . '/api/v1/videos/upload-resumable'; + $initData = [ + 'channelId' => $channelId, + 'name' => $title, + 'privacy' => (int)$s['privacy'], + 'waitTranscoding' => false, + 'filename' => $fileName, + ]; + if ($description !== '') { + $initData['description'] = $description; + } + $initBody = json_encode($initData); + + $initResponse = self::httpRequest($initUrl, 'POST', $initBody, [ 'Authorization: Bearer ' . $token, - 'Content-Type: multipart/form-data; boundary=' . $boundary, - 'Content-Length: ' . strlen($body), + 'Content-Type: application/json', + 'X-Upload-Content-Length: ' . $fileSize, + 'X-Upload-Content-Type: ' . $mimeType, + 'Content-Length: ' . strlen($initBody), ]); - $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); + $initJson = json_decode($initResponse['body'], true); + if ($initResponse['status'] < 200 || $initResponse['status'] >= 300) { + $msg = $initJson['error'] ?? $initJson['detail'] ?? $initResponse['body']; + throw new \RuntimeException('PeerTube upload init failed (' . $initResponse['status'] . '): ' . $msg); } - $shortUuid = $json['video']['shortUUID'] ?? $json['video']['uuid'] ?? null; + // Small files may complete in one shot + $shortUuid = $initJson['video']['shortUUID'] ?? $initJson['video']['uuid'] ?? null; + if ($shortUuid && !isset($initJson['video']['id'])) { + $watchUrl = rtrim($baseUrl, '/') . '/videos/watch/' . $shortUuid; + return ['uuid' => $shortUuid, 'watchUrl' => $watchUrl]; + } + + $uploadId = $initJson['video']['id'] ?? null; + if (!$uploadId) { + throw new \RuntimeException('PeerTube upload init: no upload session returned.'); + } + + // ── Step 2: Send chunks ── + $chunkUrl = $baseUrl . '/api/v1/videos/upload-resumable?upload_id=' . urlencode((string)$uploadId); + + $fh = fopen($filePath, 'rb'); + if (!$fh) { + throw new \RuntimeException('Cannot open file for resumable upload.'); + } + + $chunkSize = 4 * 1024 * 1024; + $offset = 0; + $lastResponse = null; + + while ($offset < $fileSize) { + $chunk = fread($fh, $chunkSize); + $chunkLen = strlen($chunk); + $end = $offset + $chunkLen - 1; + + $resp = self::httpRequest($chunkUrl, 'PUT', $chunk, [ + 'Authorization: Bearer ' . $token, + 'Content-Type: application/octet-stream', + 'Content-Range: bytes ' . $offset . '-' . $end . '/' . $fileSize, + 'Content-Length: ' . $chunkLen, + ], 600); + + $offset += $chunkLen; + + if ($resp['status'] >= 200 && $resp['status'] < 300) { + $json = json_decode($resp['body'], true); + if (isset($json['video']['shortUUID']) || isset($json['video']['uuid'])) { + $lastResponse = $resp; + break; + } + } elseif ($resp['status'] === 308) { + continue; + } else { + fclose($fh); + try { + self::httpRequest($chunkUrl, 'DELETE', '', [ + 'Authorization: Bearer ' . $token, 'Content-Length: 0', + ], 10); + } catch (\Throwable $e) { /* ignore */ } + $errJson = json_decode($resp['body'], true); + $msg = $errJson['error'] ?? $errJson['detail'] ?? $resp['body']; + throw new \RuntimeException('PeerTube chunk upload failed (' . $resp['status'] . '): ' . $msg); + } + } + fclose($fh); + + if (!$lastResponse) { + throw new \RuntimeException('PeerTube upload: no completion response.'); + } + + $finalJson = json_decode($lastResponse['body'], true); + $shortUuid = $finalJson['video']['shortUUID'] ?? $finalJson['video']['uuid'] ?? null; if ($shortUuid === null) { throw new \RuntimeException('PeerTube upload: no video UUID in response.'); } - return rtrim($baseUrl, '/') . '/videos/watch/' . $shortUuid; + $watchUrl = rtrim($baseUrl, '/') . '/videos/watch/' . $shortUuid; + return ['uuid' => $shortUuid, 'watchUrl' => $watchUrl]; + } + + // ------------------------------------------------------------------------- + // Fetch video info / watch URL + // ------------------------------------------------------------------------- + + /** + * Fetch video metadata from PeerTube by UUID or shortUUID. + */ + public static function fetchVideoInfo(Database $db, string $uuid): ?array + { + $s = self::getSettings($db); + if ($s['instance_url'] === '') { + return null; + } + try { + $token = self::obtainToken($s); + $url = $s['instance_url'] . '/api/v1/videos/' . urlencode($uuid); + $resp = self::httpRequest($url, 'GET', '', [ + 'Authorization: Bearer ' . $token, + ], 10); + if ($resp['status'] !== 200) { + return null; + } + return json_decode($resp['body'], true); + } catch (\Throwable $e) { + error_log('PeerTubeService::fetchVideoInfo failed: ' . $e->getMessage()); + return null; + } + } + + /** + * Get the watch URL for a PeerTube video by UUID. + */ + public static function getWatchUrl(Database $db, string $uuid): string + { + $s = self::getSettings($db); + return rtrim($s['instance_url'], '/') . '/videos/watch/' . $uuid; + } + + // ------------------------------------------------------------------------- + // Channel resolution + // ------------------------------------------------------------------------- + + /** + * Resolve a channel handle (name@host) to its numeric ID via the PeerTube API. + * + * GET /api/v1/video-channels/{nameWithHost} + * + * @return int|null The channel numeric ID, or null if not found. + */ + public static function resolveChannelId(array $s): ?int + { + $name = $s['channel_name'] ?? ''; + if ($name === '' || empty($s['instance_url'])) { + return null; + } + + $cacheKey = $s['instance_url'] . '|' . $name; + if (isset(self::$channelCache[$cacheKey])) { + return self::$channelCache[$cacheKey]; + } + + try { + $token = self::obtainToken($s); + $url = rtrim($s['instance_url'], '/') . '/api/v1/video-channels/' . urlencode($name); + $resp = self::httpRequest($url, 'GET', '', [ + 'Authorization: Bearer ' . $token, + ], 10); + + if ($resp['status'] !== 200) { + return null; + } + $json = json_decode($resp['body'], true); + $id = (int)($json['id'] ?? 0); + if ($id > 0) { + self::$channelCache[$cacheKey] = $id; + return $id; + } + return null; + } catch (\Throwable $e) { + error_log('PeerTubeService::resolveChannelId failed: ' . $e->getMessage()); + return null; + } } // ------------------------------------------------------------------------- // Internal helpers // ------------------------------------------------------------------------- + /** + * Fetch OAuth client credentials from the PeerTube instance. + * Results are cached in-memory per process. + */ + private static function getOAuthClient(string $instanceUrl): array + { + $key = $instanceUrl; + if (isset(self::$oauthCache[$key])) { + return self::$oauthCache[$key]; + } + + $url = rtrim($instanceUrl, '/') . '/api/v1/oauth-clients/local'; + $response = self::httpRequest($url, 'GET', '', [], 10); + + $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'] . ').' + ); + } + + self::$oauthCache[$key] = [ + 'client_id' => $json['client_id'], + 'client_secret' => $json['client_secret'], + ]; + return self::$oauthCache[$key]; + } + /** * Obtain an OAuth2 access token via password grant. - * - * @throws \RuntimeException on failure. */ private static function obtainToken(array $s): string { + $oauth = self::getOAuthClient($s['instance_url']); $tokenUrl = $s['instance_url'] . '/api/v1/users/token'; $body = http_build_query([ - 'client_id' => self::getClientId($s), - 'client_secret' => self::getClientSecret($s), + 'client_id' => $oauth['client_id'], + 'client_secret' => $oauth['client_secret'], 'grant_type' => 'password', 'response_type' => 'code', 'username' => $s['username'], @@ -238,7 +424,6 @@ class PeerTubeService ]); $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); @@ -247,40 +432,16 @@ class PeerTubeService 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; - } + // ------------------------------------------------------------------------- + // HTTP helper + // ------------------------------------------------------------------------- /** * Minimal cURL HTTP helper. * * @return array{status:int, body:string} */ - private static function httpRequest( + public static function httpRequest( string $url, string $method, string $body, @@ -306,12 +467,16 @@ class PeerTubeService if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } elseif ($method === 'PUT') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } elseif ($method === 'DELETE') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); } $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); diff --git a/app/storage/schema.sql b/app/storage/schema.sql index f75798c..65df00a 100644 --- a/app/storage/schema.sql +++ b/app/storage/schema.sql @@ -295,11 +295,12 @@ CREATE TABLE IF NOT EXISTS admin_audit_log ( CREATE TABLE IF NOT EXISTS peertube_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, instance_url TEXT NOT NULL DEFAULT '', - username TEXT NOT NULL DEFAULT '', - password TEXT NOT NULL DEFAULT '', channel_id INTEGER NOT NULL DEFAULT 1, privacy INTEGER NOT NULL DEFAULT 1, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + peertube_video_label TEXT NOT NULL DEFAULT '', + peertube_audio_label TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + channel_name TEXT NOT NULL DEFAULT '' ); CREATE TABLE IF NOT EXISTS audit_log ( @@ -557,7 +558,7 @@ INSERT OR IGNORE INTO site_settings (key, value) VALUES ('access_type_interne_en INSERT OR IGNORE INTO site_settings (key, value) VALUES ('access_type_libre_enabled', '0'); INSERT OR IGNORE INTO site_settings (key, value) VALUES ('objet_frart_enabled', '0'); INSERT OR IGNORE INTO site_settings (key, value) VALUES ('objet_these_enabled', '0'); -INSERT OR IGNORE INTO site_settings (key, value) VALUES ('peertube_upload_enabled', '0'); +INSERT OR IGNORE INTO site_settings (key, value) VALUES ('peertube_upload_enabled', '1'); INSERT OR IGNORE INTO site_settings (key, value) VALUES ('restricted_files_enabled', '1'); INSERT OR IGNORE INTO pages (slug, title, content, is_published) VALUES ('about', 'À propos', 'Contenu à venir', 1); diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index 21d6276..10c3f36 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -779,6 +779,58 @@ +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +\\\\\\\ to: qrtmmwro c96116c4 "fix: make schema.sql fully idempotent — add IF NOT EXISTS to all CREATE INDEX, CREATE TRIGGER, and CREATE VIEW statements" (rebased revision) ++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: qrtmmwro c96116c4 "fix: make schema.sql fully idempotent — add IF NOT EXISTS to all CREATE INDEX, CREATE TRIGGER, and CREATE VIEW statements" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: sxpsrqsl 9b084ccb "feat: add PeerTube alternate audio/video labels and FilePond pools" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: sxpsrqsl 32184a7a "feat: add PeerTube alternate audio/video labels and FilePond pools" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: sxpsrqsl 32184a7a "feat: add PeerTube alternate audio/video labels and FilePond pools" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: qmoswsvt 5f00501e "feat: shared SMTP credentials + resumable PeerTube upload + embed improvements" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: qmoswsvt 5cc189d2 "feat: shared SMTP credentials + resumable PeerTube upload + embed improvements" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: qmoswsvt 5cc189d2 "feat: shared SMTP credentials + resumable PeerTube upload + embed improvements" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: mqoyqups 641a4db3 "feat: PeerTube channel by name, test button, always-visible FilePond pools" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: mqoyqups 03f89c7d "feat: PeerTube channel by name, test button, always-visible FilePond pools" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: mqoyqups 03f89c7d "feat: PeerTube channel by name, test button, always-visible FilePond pools" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: rxwmppwn 3060cae8 "fix: remove alt labels, fix curl_close deprecation, fix PeerTube description param" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: rxwmppwn 5ae93b41 "fix: remove alt labels, fix curl_close deprecation, fix PeerTube description param" (rebased revision) +++ $linkName = $link['name'] ?? ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ?> diff --git a/app/templates/admin/parametres.php b/app/templates/admin/parametres.php index 99ee3d7..dc29ad9 100644 --- a/app/templates/admin/parametres.php +++ b/app/templates/admin/parametres.php @@ -222,6 +222,11 @@ +

    + ℹ L'authentification PeerTube utilise les mêmes identifiants que le + relay SMTP configuré ci-dessus. +

    +
    @@ -232,26 +237,11 @@
    - - -
    - -
    - - -
    - -
    - - - Identifiant numérique de la chaîne PeerTube cible. + + + Identifiant complet de la chaîne (handle), ex : nom_de_chaîne@hôte.
    @@ -265,6 +255,15 @@
    + + +
    diff --git a/app/templates/admin/recapitulatif.php b/app/templates/admin/recapitulatif.php index dc6632e..e20b880 100644 --- a/app/templates/admin/recapitulatif.php +++ b/app/templates/admin/recapitulatif.php @@ -95,8 +95,23 @@
  • @@ -107,7 +122,9 @@ + diff --git a/app/templates/public/tfe.php b/app/templates/public/tfe.php index 18bda9d..1b7f01d 100644 --- a/app/templates/public/tfe.php +++ b/app/templates/public/tfe.php @@ -419,6 +419,22 @@ })(); +
    @@ -476,6 +493,14 @@ <?= htmlspecialchars($caption !== '' ? $caption : $data['title'] . ' — ' . ($data['authors'] ?? '')) ?> + + + + + + + +