mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
feat: PeerTube integration — alternate audio/video labels, FilePond pools, shared SMTP credentials, channel by name, test button, resumable upload, embed improvements, fix alt labels/curl_close/deprecation
This commit is contained in:
23
TODO.md
23
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
|
- [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)
|
- [ ] **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)
|
## SQLite Backup & Data Integrity (docs/backup-plan.md)
|
||||||
|
|
||||||
### Phase 1 — WAL Mode
|
### Phase 1 — WAL Mode
|
||||||
|
|||||||
6
app/migrations/applied/029_peertube_labels.sql
Normal file
6
app/migrations/applied/029_peertube_labels.sql
Normal file
@@ -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 '';
|
||||||
6
app/migrations/applied/030_peertube_oauth.sql
Normal file
6
app/migrations/applied/030_peertube_oauth.sql
Normal file
@@ -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 '';
|
||||||
23
app/migrations/applied/031_shared_credentials.sql
Normal file
23
app/migrations/applied/031_shared_credentials.sql
Normal file
@@ -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;
|
||||||
7
app/migrations/applied/032_channel_name.sql
Normal file
7
app/migrations/applied/032_channel_name.sql
Normal file
@@ -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.
|
||||||
40
app/public/admin/actions/peertube-test.php
Normal file
40
app/public/admin/actions/peertube-test.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* peertube-test.php
|
||||||
|
*
|
||||||
|
* HTMX endpoint: tests PeerTube connectivity using the form's current values.
|
||||||
|
* Reads POST fields, performs a temporary settings update, tests, then returns
|
||||||
|
* an HTMX fragment with the result.
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../../../bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||||
|
AdminAuth::requireLogin();
|
||||||
|
|
||||||
|
require_once APP_ROOT . '/src/Database.php';
|
||||||
|
require_once APP_ROOT . '/src/SmtpRelay.php';
|
||||||
|
require_once APP_ROOT . '/src/PeerTubeService.php';
|
||||||
|
|
||||||
|
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||||
|
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo '<span style="color:var(--color-error);">Token CSRF invalide.</span>';
|
||||||
|
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 '<span style="color:var(--color-success);">✓ Connexion réussie — authentification et résolution de la chaîne OK.</span>';
|
||||||
|
} else {
|
||||||
|
echo '<span style="color:var(--color-error);">✗ ' . htmlspecialchars($test['error']) . '</span>';
|
||||||
|
}
|
||||||
@@ -138,17 +138,12 @@ if ($section === 'formulaire_restrictions') {
|
|||||||
$enabled = isset($_POST['peertube_upload_enabled']) ? '1' : '0';
|
$enabled = isset($_POST['peertube_upload_enabled']) ? '1' : '0';
|
||||||
$db->setSetting('peertube_upload_enabled', $enabled);
|
$db->setSetting('peertube_upload_enabled', $enabled);
|
||||||
|
|
||||||
// Credentials — only overwrite password when user typed something
|
// PeerTube-specific settings (auth uses SMTP credentials)
|
||||||
$data = [
|
$data = [
|
||||||
'instance_url' => $_POST['peertube_instance_url'] ?? '',
|
'instance_url' => $_POST['peertube_instance_url'] ?? '',
|
||||||
'username' => $_POST['peertube_username'] ?? '',
|
'channel_name' => $_POST['peertube_channel_name'] ?? '',
|
||||||
'channel_id' => $_POST['peertube_channel_id'] ?? 1,
|
|
||||||
'privacy' => $_POST['peertube_privacy'] ?? 1,
|
'privacy' => $_POST['peertube_privacy'] ?? 1,
|
||||||
];
|
];
|
||||||
$pwd = $_POST['peertube_password'] ?? '';
|
|
||||||
if ($pwd !== '') {
|
|
||||||
$data['password'] = $pwd;
|
|
||||||
}
|
|
||||||
PeerTubeService::updateSettings($db, $data);
|
PeerTubeService::updateSettings($db, $data);
|
||||||
$logger->logPeerTubeUpdate($enabled === '1');
|
$logger->logPeerTubeUpdate($enabled === '1');
|
||||||
App::flash('success', 'Paramètres PeerTube mis à jour.');
|
App::flash('success', 'Paramètres PeerTube mis à jour.');
|
||||||
|
|||||||
@@ -33,8 +33,16 @@
|
|||||||
"text/vtt",
|
"text/vtt",
|
||||||
"application/zip", "application/x-tar", "application/gzip"
|
"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é",
|
labelFileTypeNotAllowed: "Format non accepté",
|
||||||
fileValidateTypeLabelExpectedTypes: "PDF, Images, Vidéos, Audio, VTT, Archives",
|
fileValidateTypeLabelExpectedTypes: "PDF, Images, Vidéos, Audio, VTT, Archives",
|
||||||
|
fileValidateTypeLabelExpectedTypesPeerTube: "PDF, Images, VTT, Archives",
|
||||||
maxFileSize: "500MB",
|
maxFileSize: "500MB",
|
||||||
labelMaxFileSizeExceeded: "Fichier trop volumineux",
|
labelMaxFileSizeExceeded: "Fichier trop volumineux",
|
||||||
labelMaxFileSize: "Taille max: {filesize}",
|
labelMaxFileSize: "Taille max: {filesize}",
|
||||||
@@ -91,17 +99,37 @@
|
|||||||
labelMaxFileSize: "Taille max: {filesize}",
|
labelMaxFileSize: "Taille max: {filesize}",
|
||||||
allowMultiple: false
|
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
|
// Map input id → queue type
|
||||||
var INPUT_ID_TO_TYPE = {
|
var INPUT_ID_TO_TYPE = {
|
||||||
"tfe-files-input": "tfe",
|
"tfe-files-input": "tfe",
|
||||||
"tfe-files-input-2": "tfe",
|
"tfe-files-input-2": "tfe",
|
||||||
"video-files-input": "video",
|
"video-files-input": "video",
|
||||||
"audio-files-input": "audio",
|
"audio-files-input": "audio",
|
||||||
"annexe-files-input": "annexe",
|
"annexe-files-input": "annexe",
|
||||||
"couverture": "cover",
|
"couverture": "cover",
|
||||||
"note_intention": "note_intention",
|
"note_intention": "note_intention",
|
||||||
|
"peertube-video-input": "peertube_video",
|
||||||
|
"peertube-audio-input": "peertube_audio",
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────
|
||||||
@@ -167,6 +195,15 @@
|
|||||||
// Per-type max size overrides (for TFE: PDF=100MB, video/audio=2GB)
|
// Per-type max size overrides (for TFE: PDF=100MB, video/audio=2GB)
|
||||||
var perExtMax = cfg.perExtensionMaxSize || {};
|
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 {
|
return {
|
||||||
allowMultiple: cfg.allowMultiple,
|
allowMultiple: cfg.allowMultiple,
|
||||||
allowReorder: true,
|
allowReorder: true,
|
||||||
@@ -174,9 +211,9 @@
|
|||||||
storeAsFile: true,
|
storeAsFile: true,
|
||||||
|
|
||||||
// ── Native FilePond validation ──
|
// ── Native FilePond validation ──
|
||||||
acceptedFileTypes: cfg.acceptedFileTypes,
|
acceptedFileTypes: acceptedFileTypes,
|
||||||
labelFileTypeNotAllowed: cfg.labelFileTypeNotAllowed,
|
labelFileTypeNotAllowed: cfg.labelFileTypeNotAllowed,
|
||||||
fileValidateTypeLabelExpectedTypes: cfg.fileValidateTypeLabelExpectedTypes,
|
fileValidateTypeLabelExpectedTypes: expectedTypesLabel,
|
||||||
maxFileSize: cfg.maxFileSize,
|
maxFileSize: cfg.maxFileSize,
|
||||||
labelMaxFileSizeExceeded: cfg.labelMaxFileSizeExceeded,
|
labelMaxFileSizeExceeded: cfg.labelMaxFileSizeExceeded,
|
||||||
labelMaxFileSize: cfg.labelMaxFileSize,
|
labelMaxFileSize: cfg.labelMaxFileSize,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
require_once APP_ROOT . '/src/PeerTubeService.php';
|
require_once APP_ROOT . '/src/PeerTubeService.php';
|
||||||
$_ptDb = Database::getInstance();
|
$_ptDb = Database::getInstance();
|
||||||
$peerTubeEnabled = PeerTubeService::isEnabled($_ptDb);
|
$peerTubeEnabled = PeerTubeService::isEnabled($_ptDb);
|
||||||
|
$peerTubeSettings = PeerTubeService::getSettings($_ptDb);
|
||||||
|
|
||||||
$db = $_ptDb->getConnection();
|
$db = $_ptDb->getConnection();
|
||||||
|
|
||||||
@@ -98,6 +99,20 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
|||||||
hx-trigger="change"
|
hx-trigger="change"
|
||||||
hx-include="[name='formats[]'], [name='website_url'], [name='admin_mode'], [name='edit_mode'], [name='_cover']"
|
hx-include="[name='formats[]'], [name='website_url'], [name='admin_mode'], [name='edit_mode'], [name='_cover']"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
|
<?php elseif ((int)$opt['id'] === ($videoId ?? 0)): ?>
|
||||||
|
hx-post="<?= htmlspecialchars($hxPost) ?>"
|
||||||
|
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"
|
||||||
|
<?php elseif ((int)$opt['id'] === ($audioId ?? 0)): ?>
|
||||||
|
hx-post="<?= htmlspecialchars($hxPost) ?>"
|
||||||
|
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"
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
>
|
>
|
||||||
<?= htmlspecialchars($opt['name']) ?>
|
<?= htmlspecialchars($opt['name']) ?>
|
||||||
@@ -142,9 +157,13 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
|||||||
// ── Existing files ──
|
// ── Existing files ──
|
||||||
$_thesisFilesList = array_values(array_filter($_efiles, fn($f) => $f["file_type"] !== "cover"));
|
$_thesisFilesList = array_values(array_filter($_efiles, fn($f) => $f["file_type"] !== "cover"));
|
||||||
foreach ($_thesisFilesList as $_f):
|
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";
|
$_fType = $_f["file_type"] ?? "other";
|
||||||
$_fIcon = match (true) {
|
$_fIcon = match (true) {
|
||||||
|
$_fIsPeerTube && $_fType === "video" => "🎬",
|
||||||
|
$_fIsPeerTube && $_fType === "audio" => "🔊",
|
||||||
$_fType === "main" || $_fExt === "pdf" => "📄",
|
$_fType === "main" || $_fExt === "pdf" => "📄",
|
||||||
in_array($_fExt, ["jpg","jpeg","png","gif","webp"]) => "🖼️",
|
in_array($_fExt, ["jpg","jpeg","png","gif","webp"]) => "🖼️",
|
||||||
$_fType === "video" || in_array($_fExt, ["mp4","webm","mov","ogv"]) => "🎬",
|
$_fType === "video" || in_array($_fExt, ["mp4","webm","mov","ogv"]) => "🎬",
|
||||||
@@ -153,8 +172,8 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
|||||||
$_fType === "website" => "🌐",
|
$_fType === "website" => "🌐",
|
||||||
default => "📎",
|
default => "📎",
|
||||||
};
|
};
|
||||||
$_fIsExternal = str_starts_with($_f["file_path"] ?? "", "http://") || str_starts_with($_f["file_path"] ?? "", "https://");
|
$_fIsExternal = str_starts_with($_fPath, "http://") || str_starts_with($_fPath, "https://");
|
||||||
$_fLinkHref = $_fIsExternal ? htmlspecialchars($_f["file_path"]) : "/media?path=" . urlencode($_f["file_path"]);
|
$_fLinkHref = $_fIsPeerTube ? "#" : ($_fIsExternal ? htmlspecialchars($_fPath) : "/media?path=" . urlencode($_fPath));
|
||||||
?>
|
?>
|
||||||
<li class="admin-file-list-item" data-file-id="<?= (int)$_f["id"] ?>">
|
<li class="admin-file-list-item" data-file-id="<?= (int)$_f["id"] ?>">
|
||||||
<input type="hidden" name="file_sort_order[]" value="<?= (int)$_f["id"] ?>">
|
<input type="hidden" name="file_sort_order[]" value="<?= (int)$_f["id"] ?>">
|
||||||
@@ -222,9 +241,15 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
|||||||
name="queue_file[tfe][]"
|
name="queue_file[tfe][]"
|
||||||
multiple
|
multiple
|
||||||
class="tfe-file-picker"
|
class="tfe-file-picker"
|
||||||
<?= !$adminMode ? 'required' : '' ?>>
|
<?= !$adminMode ? 'required' : '' ?>
|
||||||
|
data-peertube-active="<?= $peerTubeEnabled ? '1' : '0' ?>">
|
||||||
<small class="admin-file-hint">
|
<small class="admin-file-hint">
|
||||||
|
<?php if ($peerTubeEnabled): ?>
|
||||||
|
PDF (max 100 MB) · Images (max 500 MB) · VTT · Archives (max 500 MB).
|
||||||
|
Vidéo & Audio → utilisez les emplacements dédiés ci-dessous.
|
||||||
|
<?php else: ?>
|
||||||
PDF (max 100 MB) · Images (max 500 MB) · Vidéo & Audio (max 2 GB) · VTT · Archives (max 500 MB).
|
PDF (max 100 MB) · Images (max 500 MB) · Vidéo & Audio (max 2 GB) · VTT · Archives (max 500 MB).
|
||||||
|
<?php endif; ?>
|
||||||
Glissez pour réordonner.
|
Glissez pour réordonner.
|
||||||
PDFs trop lourds ? <a href="https://www.bentopdf.com" target="_blank" rel="noopener">https://bentopdf.com/</a>
|
PDFs trop lourds ? <a href="https://www.bentopdf.com" target="_blank" rel="noopener">https://bentopdf.com/</a>
|
||||||
</small>
|
</small>
|
||||||
@@ -265,20 +290,21 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
|||||||
<div id="slot-siteweb" hidden></div>
|
<div id="slot-siteweb" hidden></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Slot: Video (disabled — video files are now uploaded via the TFE input) -->
|
<!-- Slot: Video (always visible when PeerTube enabled) -->
|
||||||
<!--
|
|
||||||
<?php if ($hasVideo): ?>
|
|
||||||
<?php if ($peerTubeEnabled): ?>
|
<?php if ($peerTubeEnabled): ?>
|
||||||
<div id="slot-video" class="admin-form-group">
|
<div id="slot-video" class="admin-form-group admin-files-fieldgroup">
|
||||||
<label for="peertube_video">Vidéo PeerTube<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
<label for="peertube-video-input">Vidéo<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||||
<div class="admin-file-input">
|
<div class="admin-file-input">
|
||||||
<input type="file" id="peertube_video" name="peertube_video"
|
<input type="file" id="peertube-video-input"
|
||||||
|
name="queue_file[peertube_video][]"
|
||||||
|
multiple
|
||||||
accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogv,.mov"
|
accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogv,.mov"
|
||||||
|
class="tfe-file-picker"
|
||||||
<?= !$adminMode ? 'required' : '' ?>>
|
<?= !$adminMode ? 'required' : '' ?>>
|
||||||
<small>MP4, WebM ou MOV. La vidéo sera hébergée sur PeerTube. Max 500 MB.</small>
|
<small class="admin-file-hint">MP4, WebM ou MOV. Max 500 MB. Glissez pour réordonner. Hébergé sur <a href="<?= htmlspecialchars($peerTubeSettings['instance_url']) ?>" target="_blank" rel="noopener">PeerTube</a>.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php elseif ($hasVideo): ?>
|
||||||
<div id="slot-video" class="admin-form-group admin-files-fieldgroup">
|
<div id="slot-video" class="admin-form-group admin-files-fieldgroup">
|
||||||
<label for="video-files-input">Vidéo<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
<label for="video-files-input">Vidéo<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||||
<div class="admin-file-input">
|
<div class="admin-file-input">
|
||||||
@@ -291,27 +317,25 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
|||||||
<small class="admin-file-hint">MP4, WebM ou MOV. Max 500 MB. Glissez pour réordonner.</small>
|
<small class="admin-file-hint">MP4, WebM ou MOV. Max 500 MB. Glissez pour réordonner.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div id="slot-video" hidden></div>
|
<div id="slot-video" hidden></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
-->
|
|
||||||
<div id="slot-video" hidden></div>
|
|
||||||
|
|
||||||
<!-- Slot: Audio (disabled — audio files are now uploaded via the TFE input) -->
|
<!-- Slot: Audio (always visible when PeerTube enabled) -->
|
||||||
<!--
|
|
||||||
<?php if ($hasAudio): ?>
|
|
||||||
<?php if ($peerTubeEnabled): ?>
|
<?php if ($peerTubeEnabled): ?>
|
||||||
<div id="slot-audio" class="admin-form-group">
|
<div id="slot-audio" class="admin-form-group admin-files-fieldgroup">
|
||||||
<label for="peertube_audio">Audio PeerTube<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
<label for="peertube-audio-input">Audio<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||||
<div class="admin-file-input">
|
<div class="admin-file-input">
|
||||||
<input type="file" id="peertube_audio" name="peertube_audio"
|
<input type="file" id="peertube-audio-input"
|
||||||
|
name="queue_file[peertube_audio][]"
|
||||||
|
multiple
|
||||||
accept="audio/mpeg,audio/ogg,audio/wav,audio/flac,audio/aac,audio/mp4,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a"
|
accept="audio/mpeg,audio/ogg,audio/wav,audio/flac,audio/aac,audio/mp4,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a"
|
||||||
|
class="tfe-file-picker"
|
||||||
<?= !$adminMode ? 'required' : '' ?>>
|
<?= !$adminMode ? 'required' : '' ?>>
|
||||||
<small>MP3, OGG, WAV, FLAC ou AAC. Le fichier sera hébergé sur PeerTube. Max 500 MB.</small>
|
<small class="admin-file-hint">MP3, OGG, WAV, FLAC ou AAC. Max 500 MB. Glissez pour réordonner. Hébergé sur <a href="<?= htmlspecialchars($peerTubeSettings['instance_url']) ?>" target="_blank" rel="noopener">PeerTube</a>.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php elseif ($hasAudio): ?>
|
||||||
<div id="slot-audio" class="admin-form-group admin-files-fieldgroup">
|
<div id="slot-audio" class="admin-form-group admin-files-fieldgroup">
|
||||||
<label for="audio-files-input">Audio<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
<label for="audio-files-input">Audio<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||||
<div class="admin-file-input">
|
<div class="admin-file-input">
|
||||||
@@ -324,12 +348,9 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
|||||||
<small class="admin-file-hint">MP3, OGG, WAV, FLAC ou AAC. Max 500 MB. Glissez pour réordonner.</small>
|
<small class="admin-file-hint">MP3, OGG, WAV, FLAC ou AAC. Max 500 MB. Glissez pour réordonner.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div id="slot-audio" hidden></div>
|
<div id="slot-audio" hidden></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
-->
|
|
||||||
<div id="slot-audio" hidden></div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -209,9 +209,11 @@ class ThesisCreateController
|
|||||||
$nextNum = $this->handleTfeQueueFiles($thesisId, $qAudio, $folderPath, $filePrefix, $nextNum);
|
$nextNum = $this->handleTfeQueueFiles($thesisId, $qAudio, $folderPath, $filePrefix, $nextNum);
|
||||||
$this->handleAnnexeQueueFiles($thesisId, $qAnnexe, $folderPath, $filePrefix);
|
$this->handleAnnexeQueueFiles($thesisId, $qAnnexe, $folderPath, $filePrefix);
|
||||||
|
|
||||||
// ── 5b. PeerTube video / audio uploads ────────────────────────────────
|
// ── 5b. PeerTube video / audio uploads (from FilePond queue) ──────────
|
||||||
$this->handlePeerTubeUpload($thesisId, $data['titre'], $files, 'peertube_video');
|
$qPTVideo = $this->extractFilesSubArray($queueFiles, 'peertube_video');
|
||||||
$this->handlePeerTubeUpload($thesisId, $data['titre'], $files, 'peertube_audio');
|
$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 ──────────────────────
|
// ── 6. Website URL — stored as thesis_files row ──────────────────────
|
||||||
$this->handleWebsiteUrl($thesisId, $post);
|
$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.
|
* Files arrive via PHP's nested $_FILES structure from
|
||||||
* @param string $title Title to use on PeerTube.
|
* <input name="queue_file[peertube_video][]">.
|
||||||
* @param array $files $_FILES array.
|
*
|
||||||
* @param string $inputName 'peertube_video' or 'peertube_audio'.
|
* @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 (!$uploads || !is_array($uploads['name'] ?? null)) {
|
||||||
if (!$upload || !isset($upload['error']) || $upload['error'] !== UPLOAD_ERR_OK) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,29 +604,37 @@ class ThesisCreateController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$count = count($uploads['name']);
|
||||||
$watchUrl = PeerTubeService::upload(
|
for ($i = 0; $i < $count; $i++) {
|
||||||
$this->db,
|
if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||||
$upload['tmp_name'],
|
continue;
|
||||||
$title,
|
}
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
$fileType = str_contains($inputName, 'audio') ? 'audio' : 'video';
|
try {
|
||||||
$this->db->insertThesisFile(
|
$result = PeerTubeService::upload(
|
||||||
$thesisId,
|
$this->db,
|
||||||
$fileType,
|
$uploads['tmp_name'][$i],
|
||||||
$watchUrl, // stored as the watch URL (no local file)
|
$title,
|
||||||
basename($upload['name']),
|
''
|
||||||
$upload['size'],
|
);
|
||||||
$upload['type'] ?? 'application/octet-stream',
|
|
||||||
null,
|
// Store as peertube_ids:{uuid} so the embed template can extract the UUID
|
||||||
null
|
$storedPath = 'peertube_ids:' . $result['uuid'];
|
||||||
);
|
$this->db->insertThesisFile(
|
||||||
error_log("ThesisCreateController: PeerTube upload OK → $watchUrl");
|
$thesisId,
|
||||||
} catch (\Throwable $e) {
|
$fileType,
|
||||||
error_log('ThesisCreateController: PeerTube upload failed — ' . $e->getMessage());
|
$storedPath,
|
||||||
// Non-fatal: the thesis is already saved; admin can re-upload manually.
|
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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -437,9 +437,11 @@ class ThesisEditController
|
|||||||
$this->handleAnnexeFiles($thesisId, $files['annexes'], $folderPath, $filePrefix, $post);
|
$this->handleAnnexeFiles($thesisId, $files['annexes'], $folderPath, $filePrefix, $post);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PeerTube video / audio uploads ────────────────────────────────────
|
// ── PeerTube video / audio uploads (from FilePond queue) ──────────────
|
||||||
$this->handlePeerTubeUpload($thesisId, trim($post['titre'] ?? ''), $files, 'peertube_video');
|
$qPTVideo = $this->extractFilesSubArray($queueFiles, 'peertube_video');
|
||||||
$this->handlePeerTubeUpload($thesisId, trim($post['titre'] ?? ''), $files, 'peertube_audio');
|
$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 ──────────────────────────────────────
|
// ── Website URL — add or update ──────────────────────────────────────
|
||||||
$this->handleWebsiteUrl($thesisId, $post);
|
$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.
|
* Files arrive via PHP's nested $_FILES structure from
|
||||||
* @param string $title Title to use on PeerTube.
|
* <input name="queue_file[peertube_video][]">.
|
||||||
* @param array $files $_FILES array.
|
*
|
||||||
* @param string $inputName 'peertube_video' or 'peertube_audio'.
|
* @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 (!$uploads || !is_array($uploads['name'] ?? null)) {
|
||||||
if (!$upload || !isset($upload['error']) || $upload['error'] !== UPLOAD_ERR_OK) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,28 +594,35 @@ class ThesisEditController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$count = count($uploads['name']);
|
||||||
$watchUrl = PeerTubeService::upload(
|
for ($i = 0; $i < $count; $i++) {
|
||||||
$this->db,
|
if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||||
$upload['tmp_name'],
|
continue;
|
||||||
$title,
|
}
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
$fileType = str_contains($inputName, 'audio') ? 'audio' : 'video';
|
try {
|
||||||
$this->db->insertThesisFile(
|
$result = PeerTubeService::upload(
|
||||||
$thesisId,
|
$this->db,
|
||||||
$fileType,
|
$uploads['tmp_name'][$i],
|
||||||
$watchUrl,
|
$title,
|
||||||
basename($upload['name']),
|
''
|
||||||
$upload['size'],
|
);
|
||||||
$upload['type'] ?? 'application/octet-stream',
|
|
||||||
null,
|
$storedPath = 'peertube_ids:' . $result['uuid'];
|
||||||
null
|
$this->db->insertThesisFile(
|
||||||
);
|
$thesisId,
|
||||||
error_log("ThesisEditController: PeerTube upload OK → $watchUrl");
|
$fileType,
|
||||||
} catch (\Throwable $e) {
|
$storedPath,
|
||||||
error_log('ThesisEditController: PeerTube upload failed — ' . $e->getMessage());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,108 +3,118 @@
|
|||||||
/**
|
/**
|
||||||
* PeerTubeService
|
* PeerTubeService
|
||||||
*
|
*
|
||||||
* Handles credential storage and video/audio uploads to a PeerTube instance
|
* Handles video/audio uploads to a PeerTube instance via its REST API.
|
||||||
* 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
|
|
||||||
*
|
*
|
||||||
* PeerTube API reference:
|
* Credentials are shared with SmtpRelay: the SMTP username/password are
|
||||||
* POST /api/v1/videos/upload — resumable / direct upload
|
* reused for PeerTube OAuth2 password-grant authentication. Only
|
||||||
* POST /api/v1/users/token — OAuth2 password grant
|
* PeerTube-specific settings (instance URL, channel name, privacy, labels)
|
||||||
|
* live in peertube_settings.
|
||||||
*
|
*
|
||||||
* The stored access token is refreshed automatically when it expires (401).
|
* The channel is stored by its full handle (name@host). The numeric ID
|
||||||
* Credentials (password + token) are encrypted at rest via Crypto.php.
|
* 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
|
class PeerTubeService
|
||||||
{
|
{
|
||||||
|
/** @var array<string,array{client_id:string,client_secret:string}> In-memory OAuth cache. */
|
||||||
|
private static array $oauthCache = [];
|
||||||
|
|
||||||
|
/** @var array<string,int> In-memory channel name → ID cache. */
|
||||||
|
private static array $channelCache = [];
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// DB CRUD
|
// 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
|
public static function getSettings(Database $db): array
|
||||||
{
|
{
|
||||||
$stmt = $db->getPDO()->prepare(
|
$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'
|
FROM peertube_settings WHERE id = 1 LIMIT 1'
|
||||||
);
|
);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$row = $stmt->fetch();
|
$row = $stmt->fetch() ?: [];
|
||||||
|
|
||||||
if ($row) {
|
require_once __DIR__ . '/SmtpRelay.php';
|
||||||
require_once __DIR__ . '/Crypto.php';
|
$smtp = SmtpRelay::getSettings($db);
|
||||||
$row['password'] = Crypto::decrypt($row['password']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $row ?: [
|
return [
|
||||||
'instance_url' => '',
|
'instance_url' => $row['instance_url'] ?? '',
|
||||||
'username' => '',
|
'username' => $smtp['username'] ?? '',
|
||||||
'password' => '',
|
'password' => $smtp['password'] ?? '',
|
||||||
'channel_id' => 1,
|
'channel_name' => $row['channel_name'] ?? '',
|
||||||
'privacy' => 1, // 1=Public, 2=Unlisted, 3=Private
|
'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
|
public static function updateSettings(Database $db, array $data): void
|
||||||
{
|
{
|
||||||
$current = self::getSettings($db);
|
$current = self::getSettings($db);
|
||||||
$merged = array_merge($current, $data);
|
$merged = array_merge($current, $data);
|
||||||
|
|
||||||
require_once __DIR__ . '/Crypto.php';
|
|
||||||
|
|
||||||
// Normalise instance URL: strip trailing slash
|
|
||||||
$instanceUrl = rtrim(trim($merged['instance_url']), '/');
|
$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)
|
$privacy = in_array((int)$merged['privacy'], [1, 2, 3], true)
|
||||||
? (int)$merged['privacy'] : 1;
|
? (int)$merged['privacy'] : 1;
|
||||||
|
$videoLabel = trim($merged['peertube_video_label'] ?? '');
|
||||||
|
$audioLabel = trim($merged['peertube_audio_label'] ?? '');
|
||||||
|
|
||||||
$pdo = $db->getPDO();
|
$pdo = $db->getPDO();
|
||||||
|
|
||||||
// Upsert row (id=1 is always the singleton row)
|
|
||||||
$exists = $pdo->query('SELECT COUNT(*) FROM peertube_settings WHERE id = 1')->fetchColumn();
|
$exists = $pdo->query('SELECT COUNT(*) FROM peertube_settings WHERE id = 1')->fetchColumn();
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
'UPDATE peertube_settings
|
'UPDATE peertube_settings
|
||||||
SET instance_url = :url,
|
SET instance_url = :url,
|
||||||
username = :user,
|
channel_name = :chan,
|
||||||
password = :pass,
|
privacy = :priv,
|
||||||
channel_id = :chan,
|
peertube_video_label = :vlabel,
|
||||||
privacy = :priv,
|
peertube_audio_label = :alabel,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = 1'
|
WHERE id = 1'
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
'INSERT INTO peertube_settings (id, instance_url, username, password, channel_id, privacy)
|
'INSERT INTO peertube_settings (id, instance_url, channel_name, privacy, peertube_video_label, peertube_audio_label)
|
||||||
VALUES (1, :url, :user, :pass, :chan, :priv)'
|
VALUES (1, :url, :chan, :priv, :vlabel, :alabel)'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
':url' => $instanceUrl,
|
':url' => $instanceUrl,
|
||||||
':user' => trim($merged['username']),
|
':chan' => $channelName,
|
||||||
':pass' => Crypto::encrypt($merged['password']),
|
':priv' => $privacy,
|
||||||
':chan' => $channelId,
|
':vlabel' => $videoLabel,
|
||||||
':priv' => $privacy,
|
':alabel' => $audioLabel,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether PeerTube credentials are fully configured.
|
* Whether PeerTube is fully configured.
|
||||||
*/
|
*/
|
||||||
public static function isConfigured(Database $db): bool
|
public static function isConfigured(Database $db): bool
|
||||||
{
|
{
|
||||||
$s = self::getSettings($db);
|
$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}
|
* @return array{ok:bool, error:string}
|
||||||
*/
|
*/
|
||||||
@@ -126,8 +136,15 @@ class PeerTubeService
|
|||||||
if ($s['instance_url'] === '') {
|
if ($s['instance_url'] === '') {
|
||||||
return ['ok' => false, 'error' => "URL de l'instance PeerTube non configurée."];
|
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 {
|
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' => ''];
|
return ['ok' => true, 'error' => ''];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return ['ok' => false, 'error' => $e->getMessage()];
|
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.
|
* @return array{uuid:string, watchUrl:string}
|
||||||
* @param string $title Video/audio title shown on PeerTube.
|
* @throws \RuntimeException
|
||||||
* @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(
|
public static function upload(
|
||||||
Database $db,
|
Database $db,
|
||||||
string $filePath,
|
string $filePath,
|
||||||
string $title,
|
string $title,
|
||||||
string $description = ''
|
string $description = ''
|
||||||
): string {
|
): array {
|
||||||
$s = self::getSettings($db);
|
$s = self::getSettings($db);
|
||||||
if ($s['instance_url'] === '') {
|
if ($s['instance_url'] === '') {
|
||||||
throw new \RuntimeException('PeerTube non configuré.');
|
throw new \RuntimeException('PeerTube non configuré.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = self::obtainToken($s);
|
$channelId = self::resolveChannelId($s);
|
||||||
|
if ($channelId === null) {
|
||||||
$baseUrl = $s['instance_url'];
|
throw new \RuntimeException('Chaîne PeerTube introuvable : ' . $s['channel_name']);
|
||||||
$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
|
$token = self::obtainToken($s);
|
||||||
|
$baseUrl = $s['instance_url'];
|
||||||
|
$fileSize = filesize($filePath);
|
||||||
$fileName = basename($filePath);
|
$fileName = basename($filePath);
|
||||||
$mimeType = (new \finfo(FILEINFO_MIME_TYPE))->file($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,
|
'Authorization: Bearer ' . $token,
|
||||||
'Content-Type: multipart/form-data; boundary=' . $boundary,
|
'Content-Type: application/json',
|
||||||
'Content-Length: ' . strlen($body),
|
'X-Upload-Content-Length: ' . $fileSize,
|
||||||
|
'X-Upload-Content-Type: ' . $mimeType,
|
||||||
|
'Content-Length: ' . strlen($initBody),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$json = json_decode($response['body'], true);
|
$initJson = json_decode($initResponse['body'], true);
|
||||||
|
if ($initResponse['status'] < 200 || $initResponse['status'] >= 300) {
|
||||||
if ($response['status'] < 200 || $response['status'] >= 300) {
|
$msg = $initJson['error'] ?? $initJson['detail'] ?? $initResponse['body'];
|
||||||
$msg = $json['error'] ?? $json['detail'] ?? $response['body'];
|
throw new \RuntimeException('PeerTube upload init failed (' . $initResponse['status'] . '): ' . $msg);
|
||||||
throw new \RuntimeException('PeerTube upload failed (' . $response['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) {
|
if ($shortUuid === null) {
|
||||||
throw new \RuntimeException('PeerTube upload: no video UUID in response.');
|
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
|
// 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.
|
* Obtain an OAuth2 access token via password grant.
|
||||||
*
|
|
||||||
* @throws \RuntimeException on failure.
|
|
||||||
*/
|
*/
|
||||||
private static function obtainToken(array $s): string
|
private static function obtainToken(array $s): string
|
||||||
{
|
{
|
||||||
|
$oauth = self::getOAuthClient($s['instance_url']);
|
||||||
$tokenUrl = $s['instance_url'] . '/api/v1/users/token';
|
$tokenUrl = $s['instance_url'] . '/api/v1/users/token';
|
||||||
|
|
||||||
$body = http_build_query([
|
$body = http_build_query([
|
||||||
'client_id' => self::getClientId($s),
|
'client_id' => $oauth['client_id'],
|
||||||
'client_secret' => self::getClientSecret($s),
|
'client_secret' => $oauth['client_secret'],
|
||||||
'grant_type' => 'password',
|
'grant_type' => 'password',
|
||||||
'response_type' => 'code',
|
'response_type' => 'code',
|
||||||
'username' => $s['username'],
|
'username' => $s['username'],
|
||||||
@@ -238,7 +424,6 @@ class PeerTubeService
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$json = json_decode($response['body'], true);
|
$json = json_decode($response['body'], true);
|
||||||
|
|
||||||
if ($response['status'] !== 200 || empty($json['access_token'])) {
|
if ($response['status'] !== 200 || empty($json['access_token'])) {
|
||||||
$msg = $json['error_description'] ?? $json['error'] ?? $response['body'];
|
$msg = $json['error_description'] ?? $json['error'] ?? $response['body'];
|
||||||
throw new \RuntimeException('PeerTube auth failed (' . $response['status'] . '): ' . $msg);
|
throw new \RuntimeException('PeerTube auth failed (' . $response['status'] . '): ' . $msg);
|
||||||
@@ -247,40 +432,16 @@ class PeerTubeService
|
|||||||
return $json['access_token'];
|
return $json['access_token'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// -------------------------------------------------------------------------
|
||||||
* Fetch the OAuth2 client_id from the PeerTube instance.
|
// HTTP helper
|
||||||
*/
|
// -------------------------------------------------------------------------
|
||||||
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.
|
* Minimal cURL HTTP helper.
|
||||||
*
|
*
|
||||||
* @return array{status:int, body:string}
|
* @return array{status:int, body:string}
|
||||||
*/
|
*/
|
||||||
private static function httpRequest(
|
public static function httpRequest(
|
||||||
string $url,
|
string $url,
|
||||||
string $method,
|
string $method,
|
||||||
string $body,
|
string $body,
|
||||||
@@ -306,12 +467,16 @@ class PeerTubeService
|
|||||||
if ($method === 'POST') {
|
if ($method === 'POST') {
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
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);
|
$responseBody = curl_exec($ch);
|
||||||
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
$error = curl_error($ch);
|
$error = curl_error($ch);
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($responseBody === false) {
|
if ($responseBody === false) {
|
||||||
throw new \RuntimeException('Erreur réseau PeerTube : ' . $error);
|
throw new \RuntimeException('Erreur réseau PeerTube : ' . $error);
|
||||||
|
|||||||
@@ -295,11 +295,12 @@ CREATE TABLE IF NOT EXISTS admin_audit_log (
|
|||||||
CREATE TABLE IF NOT EXISTS peertube_settings (
|
CREATE TABLE IF NOT EXISTS peertube_settings (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
instance_url TEXT NOT NULL DEFAULT '',
|
instance_url TEXT NOT NULL DEFAULT '',
|
||||||
username TEXT NOT NULL DEFAULT '',
|
|
||||||
password TEXT NOT NULL DEFAULT '',
|
|
||||||
channel_id INTEGER NOT NULL DEFAULT 1,
|
channel_id INTEGER NOT NULL DEFAULT 1,
|
||||||
privacy 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 (
|
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 ('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_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 ('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 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);
|
INSERT OR IGNORE INTO pages (slug, title, content, is_published) VALUES ('about', 'À propos', 'Contenu à venir', 1);
|
||||||
|
|||||||
@@ -779,6 +779,58 @@
|
|||||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
+%%%%%%% 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)
|
+\\\\\\\ 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'] ?? '';
|
++ $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'])) : '';
|
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||||
?>
|
?>
|
||||||
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">
|
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">
|
||||||
|
|||||||
@@ -222,6 +222,11 @@
|
|||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<p class="param-note">
|
||||||
|
ℹ L'authentification PeerTube utilise les mêmes identifiants que le
|
||||||
|
<strong>relay SMTP</strong> configuré ci-dessus.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="param-grid">
|
<div class="param-grid">
|
||||||
<div>
|
<div>
|
||||||
<label for="peertube_instance_url">URL de l'instance PeerTube</label>
|
<label for="peertube_instance_url">URL de l'instance PeerTube</label>
|
||||||
@@ -232,26 +237,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="peertube_username">Nom d'utilisateur</label>
|
<label for="peertube_channel_name">Nom de la chaîne</label>
|
||||||
<input type="text" id="peertube_username" name="peertube_username"
|
<input type="text" id="peertube_channel_name" name="peertube_channel_name"
|
||||||
value="<?= htmlspecialchars($peerTubeSettings['username']) ?>"
|
value="<?= htmlspecialchars($peerTubeSettings['channel_name'] ?? '') ?>"
|
||||||
autocomplete="username">
|
placeholder="xamxam_erg.be_channel@videos.erg.be">
|
||||||
</div>
|
<small>Identifiant complet de la chaîne (handle), ex : <code>nom_de_chaîne@hôte</code>.</small>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="peertube_password">Mot de passe</label>
|
|
||||||
<input type="password" id="peertube_password" name="peertube_password"
|
|
||||||
value=""
|
|
||||||
autocomplete="new-password"
|
|
||||||
placeholder="Laissez vide pour ne pas modifier">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="peertube_channel_id">ID de la chaîne</label>
|
|
||||||
<input type="number" id="peertube_channel_id" name="peertube_channel_id"
|
|
||||||
value="<?= (int)$peerTubeSettings['channel_id'] ?>"
|
|
||||||
min="1">
|
|
||||||
<small>Identifiant numérique de la chaîne PeerTube cible.</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -265,6 +255,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn--primary">Enregistrer</button>
|
<button type="submit" class="btn btn--primary">Enregistrer</button>
|
||||||
|
<button type="button" class="btn btn--secondary" id="peertube-test-btn"
|
||||||
|
hx-post="/admin/actions/peertube-test.php"
|
||||||
|
hx-target="#peertube-test-result"
|
||||||
|
hx-include="closest form"
|
||||||
|
hx-indicator="#peertube-test-spinner">
|
||||||
|
Tester la connexion
|
||||||
|
</button>
|
||||||
|
<span id="peertube-test-spinner" class="htmx-indicator" style="display:none;margin-left:var(--space-xs);">⏳</span>
|
||||||
|
<div id="peertube-test-result" style="margin-top:var(--space-xs);"></div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -95,8 +95,23 @@
|
|||||||
<?php
|
<?php
|
||||||
$mime = $f['mime_type'] ?? '';
|
$mime = $f['mime_type'] ?? '';
|
||||||
$isImage = str_starts_with($mime, 'image/');
|
$isImage = str_starts_with($mime, 'image/');
|
||||||
$mediaUrl = '/media?path=' . urlencode($f['file_path']);
|
$filePath = $f['file_path'] ?? '';
|
||||||
$fileName = htmlspecialchars($f['file_name'] ?? basename($f['file_path']));
|
$isPeerTube = str_starts_with($filePath, 'peertube_ids:');
|
||||||
|
if ($isPeerTube) {
|
||||||
|
$_ptUuid = substr($filePath, strlen('peertube_ids:'));
|
||||||
|
require_once APP_ROOT . '/src/PeerTubeService.php';
|
||||||
|
$_ptDb = new Database();
|
||||||
|
$_ptS = PeerTubeService::getSettings($_ptDb);
|
||||||
|
// Only link to watch page for public (1) and unlisted (2) videos
|
||||||
|
if ((int)$_ptS['privacy'] <= 2) {
|
||||||
|
$mediaUrl = PeerTubeService::getWatchUrl($_ptDb, $_ptUuid);
|
||||||
|
} else {
|
||||||
|
$mediaUrl = '#';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$mediaUrl = '/media?path=' . urlencode($filePath);
|
||||||
|
}
|
||||||
|
$fileName = htmlspecialchars($f['file_name'] ?? basename($filePath));
|
||||||
$fileType = htmlspecialchars($f['file_type']);
|
$fileType = htmlspecialchars($f['file_type']);
|
||||||
?>
|
?>
|
||||||
<li class="recap-file-item">
|
<li class="recap-file-item">
|
||||||
@@ -107,7 +122,9 @@
|
|||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="recap-file-icon">
|
<span class="recap-file-icon">
|
||||||
<?php
|
<?php
|
||||||
if ($mime === 'application/pdf') echo '📄';
|
if ($isPeerTube && $f['file_type'] === 'video') echo '🎬';
|
||||||
|
elseif ($isPeerTube && $f['file_type'] === 'audio') echo '🎵';
|
||||||
|
elseif ($mime === 'application/pdf') echo '📄';
|
||||||
elseif (str_starts_with($mime, 'video/')) echo '🎬';
|
elseif (str_starts_with($mime, 'video/')) echo '🎬';
|
||||||
elseif (str_starts_with($mime, 'audio/')) echo '🎵';
|
elseif (str_starts_with($mime, 'audio/')) echo '🎵';
|
||||||
elseif (in_array($mime, ['application/zip','application/x-zip-compressed'])) echo '🗜️';
|
elseif (in_array($mime, ['application/zip','application/x-zip-compressed'])) echo '🗜️';
|
||||||
|
|||||||
35
app/templates/partials/peertube-embed.php
Normal file
35
app/templates/partials/peertube-embed.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* peertube-embed.php
|
||||||
|
*
|
||||||
|
* Renders a PeerTube video/audio embed iframe from a stored peertube_ids:{uuid} value.
|
||||||
|
*
|
||||||
|
* Expected variables:
|
||||||
|
* string $peertubeUuid — the PeerTube video shortUUID
|
||||||
|
* string $title — title attribute for the iframe
|
||||||
|
* string $instanceUrl — base URL of the PeerTube instance (e.g. https://videos.erg.be)
|
||||||
|
* int $width — iframe width (default 560)
|
||||||
|
* int $height — iframe height (default 315)
|
||||||
|
*/
|
||||||
|
|
||||||
|
$peertubeUuid = $peertubeUuid ?? '';
|
||||||
|
$title = $title ?? 'PeerTube video';
|
||||||
|
$instanceUrl = $instanceUrl ?? '';
|
||||||
|
$width = $width ?? 560;
|
||||||
|
$height = $height ?? 315;
|
||||||
|
|
||||||
|
if ($peertubeUuid === '' || $instanceUrl === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$embedUrl = rtrim($instanceUrl, '/') . '/videos/embed/' . $peertubeUuid;
|
||||||
|
?>
|
||||||
|
<iframe
|
||||||
|
title="<?= htmlspecialchars($title) ?>"
|
||||||
|
width="<?= (int)$width ?>"
|
||||||
|
height="<?= (int)$height ?>"
|
||||||
|
src="<?= htmlspecialchars($embedUrl) ?>"
|
||||||
|
style="border: 0px;"
|
||||||
|
allow="fullscreen"
|
||||||
|
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
||||||
|
></iframe>
|
||||||
@@ -419,6 +419,22 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<?php elseif (!empty($data["files"])): ?>
|
<?php elseif (!empty($data["files"])): ?>
|
||||||
|
<?php
|
||||||
|
// Preload PeerTube instance URL once
|
||||||
|
$_ptHasAnyPeerTube = false;
|
||||||
|
foreach ($data['files'] as $_f) {
|
||||||
|
if (str_starts_with($_f['file_path'] ?? '', 'peertube_ids:')) {
|
||||||
|
$_ptHasAnyPeerTube = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$_ptInstanceUrl = '';
|
||||||
|
if ($_ptHasAnyPeerTube) {
|
||||||
|
require_once APP_ROOT . '/src/PeerTubeService.php';
|
||||||
|
$_ptSettings = PeerTubeService::getSettings(Database::getInstance());
|
||||||
|
$_ptInstanceUrl = $_ptSettings['instance_url'] ?? '';
|
||||||
|
}
|
||||||
|
?>
|
||||||
<?php foreach ($data["files"] as $file): ?>
|
<?php foreach ($data["files"] as $file): ?>
|
||||||
<?php
|
<?php
|
||||||
$ext = strtolower(pathinfo($file["file_path"] ?? '', PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($file["file_path"] ?? '', PATHINFO_EXTENSION));
|
||||||
@@ -445,7 +461,8 @@
|
|||||||
$caption = !empty($file["display_label"]) ? $file["display_label"] : ($file["description"] ?? '');
|
$caption = !empty($file["display_label"]) ? $file["display_label"] : ($file["description"] ?? '');
|
||||||
$filePath = $file['file_path'] ?? '';
|
$filePath = $file['file_path'] ?? '';
|
||||||
$isExternalUrl = str_starts_with($filePath, 'http://') || str_starts_with($filePath, 'https://');
|
$isExternalUrl = str_starts_with($filePath, 'http://') || str_starts_with($filePath, 'https://');
|
||||||
$mediaUrl = $isExternalUrl ? htmlspecialchars($filePath) : ('/media?path=' . urlencode($filePath));
|
$isPeerTube = str_starts_with($filePath, 'peertube_ids:');
|
||||||
|
$mediaUrl = $isPeerTube ? '' : ($isExternalUrl ? htmlspecialchars($filePath) : ('/media?path=' . urlencode($filePath)));
|
||||||
$fileName = htmlspecialchars($file["file_name"] ?? basename($filePath));
|
$fileName = htmlspecialchars($file["file_name"] ?? basename($filePath));
|
||||||
?>
|
?>
|
||||||
<figure>
|
<figure>
|
||||||
@@ -476,6 +493,14 @@
|
|||||||
<img src="<?= $mediaUrl ?>"
|
<img src="<?= $mediaUrl ?>"
|
||||||
alt="<?= htmlspecialchars($caption !== '' ? $caption : $data['title'] . ' — ' . ($data['authors'] ?? '')) ?>">
|
alt="<?= htmlspecialchars($caption !== '' ? $caption : $data['title'] . ' — ' . ($data['authors'] ?? '')) ?>">
|
||||||
<?php elseif ($isVideo): ?>
|
<?php elseif ($isVideo): ?>
|
||||||
|
<?php if ($isPeerTube): ?>
|
||||||
|
<?php
|
||||||
|
$peertubeUuid = substr($filePath, strlen('peertube_ids:'));
|
||||||
|
$title = $fileName;
|
||||||
|
$instanceUrl = $_ptInstanceUrl;
|
||||||
|
include APP_ROOT . '/templates/partials/peertube-embed.php';
|
||||||
|
?>
|
||||||
|
<?php else: ?>
|
||||||
<video width="100%" controls>
|
<video width="100%" controls>
|
||||||
<source src="<?= $mediaUrl ?>" type="video/<?= htmlspecialchars($ext === 'mov' ? 'mp4' : $ext) ?>">
|
<source src="<?= $mediaUrl ?>" type="video/<?= htmlspecialchars($ext === 'mov' ? 'mp4' : $ext) ?>">
|
||||||
<?php if ($_vttPath): ?>
|
<?php if ($_vttPath): ?>
|
||||||
@@ -484,7 +509,18 @@
|
|||||||
srclang="fr" label="Sous-titres" default>
|
srclang="fr" label="Sous-titres" default>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</video>
|
</video>
|
||||||
|
<?php endif; ?>
|
||||||
<?php elseif ($isAudio): ?>
|
<?php elseif ($isAudio): ?>
|
||||||
|
<?php if ($isPeerTube): ?>
|
||||||
|
<?php
|
||||||
|
$peertubeUuid = substr($filePath, strlen('peertube_ids:'));
|
||||||
|
$title = $fileName;
|
||||||
|
$instanceUrl = $_ptInstanceUrl;
|
||||||
|
$width = 560;
|
||||||
|
$height = 80;
|
||||||
|
include APP_ROOT . '/templates/partials/peertube-embed.php';
|
||||||
|
?>
|
||||||
|
<?php else: ?>
|
||||||
<audio controls class="tfe-audio">
|
<audio controls class="tfe-audio">
|
||||||
<source src="<?= $mediaUrl ?>" type="audio/<?= htmlspecialchars(match($ext) {
|
<source src="<?= $mediaUrl ?>" type="audio/<?= htmlspecialchars(match($ext) {
|
||||||
'mp3' => 'mpeg',
|
'mp3' => 'mpeg',
|
||||||
@@ -497,6 +533,7 @@
|
|||||||
}) ?>">
|
}) ?>">
|
||||||
Votre navigateur ne supporte pas la lecture audio.
|
Votre navigateur ne supporte pas la lecture audio.
|
||||||
</audio>
|
</audio>
|
||||||
|
<?php endif; ?>
|
||||||
<?php else: /* other — download only */ ?>
|
<?php else: /* other — download only */ ?>
|
||||||
<div class="tfe-download-file">
|
<div class="tfe-download-file">
|
||||||
<a href="<?= $mediaUrl ?>&download=1" class="tfe-download-link">
|
<a href="<?= $mediaUrl ?>&download=1" class="tfe-download-link">
|
||||||
|
|||||||
Reference in New Issue
Block a user