mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Add SQLite indexes for contenus page language/tag queries + WIP: Peertube orphans, dialogs, contact decoupling, context note, finality types
This commit is contained in:
91
app/public/admin/actions/cleanup-stats-fragment.php
Normal file
91
app/public/admin/actions/cleanup-stats-fragment.php
Normal 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; ?>
|
||||
@@ -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 === '') {
|
||||
|
||||
72
app/public/admin/actions/peertube-delete.php
Normal file
72
app/public/admin/actions/peertube-delete.php
Normal 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;
|
||||
102
app/public/admin/actions/peertube-orphans-fragment.php
Normal file
102
app/public/admin/actions/peertube-orphans-fragment.php
Normal 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>
|
||||
121
app/public/admin/actions/peertube-orphans.php
Normal file
121
app/public/admin/actions/peertube-orphans.php
Normal 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,
|
||||
]);
|
||||
126
app/public/admin/actions/peertube-relink.php
Normal file
126
app/public/admin/actions/peertube-relink.php
Normal 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;
|
||||
Reference in New Issue
Block a user