Add SQLite indexes for contenus page language/tag queries + WIP: Peertube orphans, dialogs, contact decoupling, context note, finality types

This commit is contained in:
Pontoporeia
2026-06-21 13:33:55 +02:00
parent 0d5e9dac19
commit 03c9c3566f
38 changed files with 1432 additions and 333 deletions

View File

@@ -0,0 +1,91 @@
<?php
/**
* Temp file statistics as an HTML fragment (admin).
*
* GET /admin/actions/cleanup-stats-fragment.php
*
* Returns an HTML fragment ready for HTMX swap into #tmp-cleanup-stats.
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
// Re-use the existing stats endpoint internally
ob_start();
require __DIR__ . '/cleanup-stats.php';
$json = ob_get_clean();
$d = json_decode($json, true);
$hasFilePond = ($d['filepond_stale_count'] ?? 0) > 0 || ($d['filepond_active_count'] ?? 0) > 0;
$hasTrash = ($d['trash_stale_count'] ?? 0) > 0 || ($d['trash_active_count'] ?? 0) > 0;
if (!$hasFilePond && !$hasTrash): ?>
<p style="margin:0;color:var(--accent-green)">✓ Aucun fichier temporaire.</p>
<?php return; endif; ?>
<?php if ($d['filepond_stale_count'] > 0): ?>
<p class="n-heading">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256" aria-hidden="true"><path d="M230.64,25.36a32,32,0,0,0-45.26,0q-.21.21-.42.45L131.55,88.22,121,77.64a24,24,0,0,0-33.95,0l-76.69,76.7a8,8,0,0,0,0,11.31l80,80a8,8,0,0,0,11.31,0L178.36,169a24,24,0,0,0,0-33.95l-10.58-10.57L230.19,71c.15-.14.31-.28.45-.43A32,32,0,0,0,230.64,25.36ZM96,228.69,79.32,212l22.34-22.35a8,8,0,0,0-11.31-11.31L68,200.68,55.32,188l22.34-22.35a8,8,0,0,0-11.31-11.31L44,176.68,27.31,160,72,115.31,140.69,184ZM219.52,59.1l-68.71,58.81a8,8,0,0,0-.46,11.74L167,146.34a8,8,0,0,1,0,11.31l-15,15L83.32,104l15-15a8,8,0,0,1,11.31,0l16.69,16.69a8,8,0,0,0,11.74-.46L196.9,36.48A16,16,0,0,1,219.52,59.1Z"></path></svg>
Téléversements abandonnés <span class="n-meta"><?= $d['filepond_stale_count'] ?> dossier(s) · <?= htmlspecialchars($d['filepond_stale_human']) ?></span>
</p>
<table class="n-table">
<thead><tr><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead>
<tbody>
<?php foreach ($d['filepond_stale_files'] as $f): ?>
<tr>
<td><strong><?= htmlspecialchars($f['name']) ?></strong></td>
<td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td>
<td style="white-space:nowrap">~<?= (int)$f['age_minutes'] ?> min</td>
<td style="white-space:nowrap">
<button type="button" class="btn btn--sm btn--danger" style="font-size:0.85em;padding:2px var(--space-xs)"
hx-post="/admin/actions/cleanup-tmp.php"
hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","filepond_dir":"<?= htmlspecialchars($f['name']) ?>"}'
hx-target="#tmp-cleanup-stats"
hx-swap="innerHTML"
hx-indicator="#tmp-cleanup-stats">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256" aria-hidden="true"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
Supprimer
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php if ($d['trash_stale_count'] > 0): ?>
<p class="n-heading">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256" aria-hidden="true"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
Corbeille <span class="n-meta"><?= $d['trash_stale_count'] ?> fichier(s) · <?= htmlspecialchars($d['trash_stale_human']) ?></span>
</p>
<table class="n-table">
<thead><tr><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead>
<tbody>
<?php foreach ($d['trash_stale_files'] as $f): ?>
<tr>
<td><strong><?= htmlspecialchars($f['name']) ?></strong></td>
<td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td>
<td style="white-space:nowrap">~<?= (int)$f['age_days'] ?> j</td>
<td style="white-space:nowrap">
<button type="button" class="btn btn--sm btn--danger" style="font-size:0.85em;padding:2px var(--space-xs)"
hx-post="/admin/actions/cleanup-tmp.php"
hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","trash_file":"<?= htmlspecialchars($f['name']) ?>"}'
hx-target="#tmp-cleanup-stats"
hx-swap="innerHTML"
hx-indicator="#tmp-cleanup-stats">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256" aria-hidden="true"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
Supprimer
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php if (($d['filepond_active_count'] ?? 0) > 0 || ($d['trash_active_count'] ?? 0) > 0): ?>
<p style="margin:var(--space-sm) 0 0 0;font-size:0.85em;color:var(--text-secondary)">Conservés :
<?php if ($d['filepond_active_count']) echo $d['filepond_active_count'] . ' téléversement(s) actif(s) (' . htmlspecialchars($d['filepond_active_human']) . '), '; ?>
<?php if ($d['trash_active_count']) echo $d['trash_active_count'] . ' fichier(s) récent(s) (' . htmlspecialchars($d['trash_active_human']) . ')'; ?>
</p>
<?php endif; ?>

View File

@@ -43,6 +43,55 @@ $details = [];
$db = new Database();
$pdo = $db->getPDO();
// ── Individual deletion mode ────────────────────────────────────────────
$individualFilepond = trim($_POST['filepond_dir'] ?? '');
$individualTrash = trim($_POST['trash_file'] ?? '');
if ($individualFilepond !== '' || $individualTrash !== '') {
if ($individualFilepond !== '') {
$dirPath = $filepondDir . '/' . basename($individualFilepond);
if (is_dir($dirPath) && str_starts_with(realpath($dirPath), realpath($filepondDir))) {
rmdirRecursive($dirPath);
$filepondRemoved = 1;
$details[] = "filepond/$individualFilepond: suppression manuelle";
} else {
$errors[] = 'Dossier introuvable : ' . htmlspecialchars($individualFilepond);
}
}
if ($individualTrash !== '') {
$filePath = $trashDir . '/' . basename($individualTrash);
if (is_file($filePath) && str_starts_with(realpath($filePath), realpath($trashDir))) {
if (@unlink($filePath)) {
$trashRemoved = 1;
$details[] = "_trash/$individualTrash: suppression manuelle";
} else {
$errors[] = 'Impossible de supprimer : ' . htmlspecialchars($individualTrash);
}
} else {
$errors[] = 'Fichier introuvable : ' . htmlspecialchars($individualTrash);
}
}
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// HTMX request: re-render the fragment
if (isset($_SERVER['HTTP_HX_REQUEST'])) {
header('HX-Trigger: refreshStats');
require __DIR__ . '/cleanup-stats-fragment.php';
exit;
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => true,
'filepond_removed' => $filepondRemoved,
'trash_removed' => $trashRemoved,
'errors' => $errors,
'details' => $details,
]);
exit;
}
// ── Determine PHP session save path ──────────────────────────────────────
$sessionSavePath = session_save_path();
if (!$sessionSavePath || $sessionSavePath === '') {

View File

@@ -0,0 +1,72 @@
<?php
/**
* PeerTube video deletion endpoint (admin).
*
* POST /admin/actions/peertube-delete.php
* Body: csrf_token + uuid
*
* Deletes a video from the PeerTube channel entirely.
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|| !isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
http_response_code(403);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'CSRF invalide.']);
exit;
}
$uuid = trim($_POST['uuid'] ?? '');
if ($uuid === '' || !preg_match('/^[a-zA-Z0-9\-_]+$/', $uuid)) {
http_response_code(400);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'UUID invalide.']);
exit;
}
require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/PeerTubeService.php';
$db = new Database();
if (!PeerTubeService::isConfigured($db)) {
http_response_code(503);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'PeerTube non configuré.']);
exit;
}
// Also remove any stale DB references to this UUID
$pdo = $db->getConnection();
$stmt = $pdo->prepare(
"SELECT id FROM thesis_files WHERE file_path = ?"
);
$stmt->execute(['peertube_ids:' . $uuid]);
$dbRefs = $stmt->fetchAll(PDO::FETCH_COLUMN);
$dbCleaned = count($dbRefs);
foreach ($dbRefs as $id) {
$pdo->prepare("DELETE FROM thesis_files WHERE id = ?")->execute([$id]);
}
$deleted = PeerTubeService::deleteVideo($db, $uuid);
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
if ($deleted) {
error_log("[peertube-delete] uuid=$uuid deleted" . ($dbCleaned > 0 ? " + $dbCleaned DB ref(s) cleaned" : ""));
if (isset($_SERVER['HTTP_HX_REQUEST'])) {
require __DIR__ . '/peertube-orphans-fragment.php';
exit;
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => true]);
} else {
error_log("[peertube-delete] uuid=$uuid delete failed" . ($dbCleaned > 0 ? " (cleaned $dbCleaned DB refs though)" : ""));
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'Échec de la suppression sur PeerTube (vérifiez les logs).']);
}
exit;

View File

@@ -0,0 +1,102 @@
<?php
/**
* PeerTube orphan check as HTML fragment (admin).
*
* GET /admin/actions/peertube-orphans-fragment.php
*
* Returns an HTML fragment ready for HTMX swap into #peertube-orphans-wrapper.
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
// Re-use the existing JSON endpoint internally
ob_start();
require __DIR__ . '/peertube-orphans.php';
$json = ob_get_clean();
$d = json_decode($json, true);
if (!($d['configured'] ?? false)): ?>
<div id="peertube-orphans-wrapper">
<details id="peertube-orphans-col" class="n-section" open>
<summary>Vidéos PeerTube orphelines</summary>
<div id="peertube-orphans-stats">
<p style="margin:0;color:var(--color-warning)">⚠️ PeerTube non configuré.</p>
</div>
</details>
</div>
<?php return; endif; ?>
<?php if (!empty($d['error'])): ?>
<div id="peertube-orphans-wrapper">
<details id="peertube-orphans-col" class="n-section" open>
<summary>Vidéos PeerTube orphelines</summary>
<div id="peertube-orphans-stats">
<p style="margin:0;color:var(--color-error)">✗ <?= htmlspecialchars($d['error']) ?></p>
</div>
</details>
</div>
<?php return; endif; ?>
<div id="peertube-orphans-wrapper">
<details id="peertube-orphans-col" class="n-section" open>
<summary>Vidéos PeerTube orphelines <span class="n-meta"><?= (int)($d['total_on_channel'] ?? 0) ?> vidéos · <?= (int)($d['total_linked'] ?? 0) ?> liées</span></summary>
<div id="peertube-orphans-stats">
<?php if (($d['orphan_count'] ?? 0) > 0): ?>
<table class="n-table">
<thead><tr><th>Nom</th><th>Date</th><th width="1%"></th></tr></thead>
<tbody>
<?php foreach ($d['orphans'] as $v): ?>
<tr>
<td>
<strong><?= htmlspecialchars($v['name']) ?></strong>
<span class="n-table__info" style="display:block"><?= htmlspecialchars($v['uuid']) ?></span>
</td>
<td style="white-space:nowrap"><?= htmlspecialchars(substr($v['createdAt'] ?? '', 0, 10)) ?></td>
<td style="white-space:nowrap">
<button type="button" class="btn btn--sm btn--danger" style="font-size:0.85em;padding:2px var(--space-xs)"
hx-post="/admin/actions/peertube-delete.php"
hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","uuid":"<?= htmlspecialchars($v['uuid']) ?>"}'
hx-target="#peertube-orphans-wrapper"
hx-swap="outerHTML"
hx-trigger="click"
hx-indicator="#peertube-orphans-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256" aria-hidden="true"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
Supprimer
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p style="margin:0;color:var(--accent-green)">✓ Aucune vidéo orpheline.</p>
<?php endif; ?>
</div>
</details>
<?php if (($d['stale_count'] ?? 0) > 0): ?>
<details id="peertube-stale-section" class="n-section" open>
<summary>Références DB obsolètes <span class="n-meta"><?= $d['stale_count'] ?></span></summary>
<p style="margin:0 0 var(--space-sm) 0;font-size:0.85em;color:var(--text-secondary)">Ces UUID sont référencés en base de données mais n'existent plus sur la chaîne PeerTube. Les TFE liés affichent des liens morts.</p>
<table class="n-table">
<thead><tr><th>UUID</th><th>TFE(s)</th></tr></thead>
<tbody>
<?php foreach ($d['stale_entries'] as $s): ?>
<tr>
<td style="word-break:break-all;color:var(--text-secondary)"><?= htmlspecialchars($s['uuid']) ?></td>
<td>
<?php if (!empty($s['theses'])): ?>
<?= implode(', ', array_map(function($t) {
$label = $t['identifier'] ?: '#' . $t['thesis_id'];
return '<a href="/admin/contenus-edit.php?id=' . (int)$t['thesis_id'] . '" target="_blank">' . htmlspecialchars($label) . '</a>';
}, $s['theses'])) ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</details>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,121 @@
<?php
/**
* PeerTube orphan video check endpoint (admin).
*
* GET /admin/actions/peertube-orphans.php
*
* Returns JSON with a list of PeerTube channel videos that are NOT linked to
* any TFE in the database.
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
exit;
}
require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/PeerTubeService.php';
$db = new Database();
if (!PeerTubeService::isConfigured($db)) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'configured' => false,
'error' => 'PeerTube non configuré.',
]);
exit;
}
// ── Collect all Peertube UUIDs linked in the DB ──────────────────────────
$pdo = $db->getPDO();
$dbUuids = [];
$linkedMap = []; // uuid → [thesis_id, thesis_title, thesis_identifier]
$stmt = $pdo->query(
"SELECT tf.file_path, tf.file_name, t.id AS thesis_id, t.title, t.identifier
FROM thesis_files tf
JOIN theses t ON t.id = tf.thesis_id
WHERE tf.file_path LIKE 'peertube_ids:%'
AND t.deleted_at IS NULL"
);
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$uuid = substr($row['file_path'], strlen('peertube_ids:'));
$dbUuids[$uuid] = true;
$linkedMap[$uuid][] = [
'thesis_id' => (int)$row['thesis_id'],
'title' => $row['title'],
'identifier' => $row['identifier'] ?? '',
];
}
// ── List all channel videos ──────────────────────────────────────────────
try {
$channelVideos = PeerTubeService::listChannelVideos($db);
} catch (\Throwable $e) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'configured' => true,
'error' => 'Erreur lors du listage des vidéos : ' . $e->getMessage(),
]);
exit;
}
// ── Find orphans: on channel but not in DB ───────────────────────────────
$orphans = [];
$linked = [];
foreach ($channelVideos as $v) {
$uuid = $v['shortUUID'] ?: $v['uuid'];
if ($uuid === '') {
continue;
}
if (isset($dbUuids[$uuid])) {
$linked[] = [
'uuid' => $uuid,
'name' => $v['name'],
'theses' => $linkedMap[$uuid] ?? [],
];
} else {
$orphans[] = [
'uuid' => $uuid,
'name' => $v['name'],
'createdAt' => $v['createdAt'],
];
}
}
// ── Find stale DB entries: in DB but not on channel ──────────────────────
$stale = [];
foreach ($dbUuids as $uuid => $_) {
$found = false;
foreach ($channelVideos as $v) {
if (($v['shortUUID'] ?: $v['uuid']) === $uuid) {
$found = true;
break;
}
}
if (!$found) {
$stale[] = [
'uuid' => $uuid,
'theses' => $linkedMap[$uuid] ?? [],
];
}
}
$totalOnChannel = count($channelVideos);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'configured' => true,
'channel_name' => PeerTubeService::getSettings($db)['channel_name'],
'total_on_channel' => $totalOnChannel,
'total_linked' => count($linked),
'orphan_count' => count($orphans),
'orphans' => $orphans,
'stale_count' => count($stale),
'stale_entries' => $stale,
]);

View File

@@ -0,0 +1,126 @@
<?php
/**
* PeerTube video relink endpoint (admin).
*
* POST /admin/actions/peertube-relink.php
* Body: JSON { thesis_id: 123, uuid: "bmpQZTUPv4ou8ufiwajV63" }
*
* Links an existing PeerTube video to a thesis by inserting a thesis_files row
* with file_path = 'peertube_ids:{uuid}'. Only videos that are on the channel
* but NOT linked to any TFE can be relinked.
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
function peertubeRelinkError(int $code, string $message): never {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode(['ok' => false, 'error' => $message]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
peertubeRelinkError(405, 'Méthode non autorisée.');
}
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!isset($_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $csrfHeader)) {
peertubeRelinkError(403, 'Token CSRF invalide.');
}
$body = json_decode(file_get_contents('php://input'), true);
if (!is_array($body)) {
peertubeRelinkError(400, 'JSON invalide.');
}
$thesisId = filter_var($body['thesis_id'] ?? '', FILTER_VALIDATE_INT);
$uuid = trim($body['uuid'] ?? '');
if (!$thesisId || $uuid === '') {
peertubeRelinkError(400, 'Paramètres invalides (thesis_id + uuid requis).');
}
// Validate UUID format (shortUUID or full UUID)
if (!preg_match('/^[a-zA-Z0-9\-_]+$/', $uuid)) {
peertubeRelinkError(400, 'UUID invalide.');
}
require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/PeerTubeService.php';
$db = new Database();
if (!PeerTubeService::isConfigured($db)) {
peertubeRelinkError(503, 'PeerTube non configuré.');
}
// Check thesis exists
$thesis = $db->getThesis($thesisId);
if (!$thesis) {
peertubeRelinkError(404, 'TFE introuvable.');
}
// Check this UUID is not already linked to this thesis
$pdo = $db->getConnection();
$stmt = $pdo->prepare(
"SELECT id FROM thesis_files
WHERE thesis_id = ? AND file_path = ?"
);
$stmt->execute([$thesisId, 'peertube_ids:' . $uuid]);
if ($stmt->fetch()) {
peertubeRelinkError(409, 'Cette vidéo est déjà liée à ce TFE.');
}
// Verify the video exists on the channel
$info = PeerTubeService::fetchVideoInfo($db, $uuid);
if ($info === null) {
peertubeRelinkError(404, 'Vidéo introuvable sur PeerTube.');
}
// Verify it's not already linked to another TFE
$stmt = $pdo->prepare(
"SELECT t.identifier FROM thesis_files tf
JOIN theses t ON t.id = tf.thesis_id
WHERE tf.file_path = ? AND t.deleted_at IS NULL"
);
$stmt->execute(['peertube_ids:' . $uuid]);
$existing = $stmt->fetch();
if ($existing) {
peertubeRelinkError(409,
'Cette vidéo est déjà liée au TFE ' . htmlspecialchars($existing['identifier'] ?? '?')
. '. Dé-liez-la d\'abord avant de la relier à un autre.'
);
}
// Determine file type from PeerTube info
$videoName = $info['name'] ?? $uuid;
$fileType = 'video'; // default
$catId = (int)($info['category']['id'] ?? 0);
if ($catId === 16) {
$fileType = 'audio';
}
$db->insertThesisFile(
$thesisId,
$fileType,
'peertube_ids:' . $uuid,
$videoName,
0, // size unknown (not on disk)
'video/mp4', // PeerTube streams HLS, mime is nominal
null,
null
);
$newId = $pdo->lastInsertId();
error_log("[peertube-relink] thesis_id=$thesisId uuid=$uuid file_type=$fileType new_id=$newId");
header('Content-Type: application/json');
echo json_encode([
'ok' => true,
'id' => (int)$newId,
'message' => 'Vidéo PeerTube reliée avec succès.',
]);
exit;