Show the queued show and its start time in the index.php sidebar
- Shows get a description and an icon upload; broadcasts get a scheduled_at start time. schema.sql adds the columns to existing databases and seeds the descriptions from the old hand-written sidebar. - The dashboard and cli/set_show.php take an optional date and Eastern start time when queuing; index.php shows it with the UTC conversion. - liveBroadcast() and currentShow() ignore broadcasts unseen for longer than the grace period, so a stopped poller can't leave a show stuck as live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3fb49e816c
commit
f659b09a0b
9 files changed
+297
-77
No files matched your search
+93
-40
@@ -5,26 +5,50 @@ require __DIR__ . '/../includes/admin_layout.php';
|
||||
$admin = requireAdmin();
|
||||
$db = getDB();
|
||||
|
||||
const BACKGROUND_DIR = __DIR__ . '/../images/backgrounds/';
|
||||
// Backgrounds and icons are both saved here
|
||||
const UPLOAD_DIR = __DIR__ . '/../images/backgrounds/';
|
||||
const MAX_BACKGROUND_BYTES = 5 * 1024 * 1024;
|
||||
const MAX_ICON_BYTES = 1024 * 1024;
|
||||
const MAX_DESCRIPTION_CHARS = 5000;
|
||||
// Uploads are identified by their content, not their filename, and saved under a generated name
|
||||
const BACKGROUND_TYPES = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
|
||||
const IMAGE_TYPES = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
|
||||
|
||||
function slugify(string $name): string {
|
||||
$slug = strtolower(str_replace(["'", '’'], '', $name));
|
||||
return trim(preg_replace('/[^a-z0-9]+/', '-', $slug), '-');
|
||||
}
|
||||
|
||||
function deleteBackground(?string $filename): void {
|
||||
if ($filename && is_file(BACKGROUND_DIR . basename($filename))) {
|
||||
unlink(BACKGROUND_DIR . basename($filename));
|
||||
function deleteUpload(?string $filename): void {
|
||||
if ($filename && is_file(UPLOAD_DIR . basename($filename))) {
|
||||
unlink(UPLOAD_DIR . basename($filename));
|
||||
}
|
||||
}
|
||||
|
||||
// Checks an optional image upload. Returns its file extension, or null if no file was sent or it was rejected
|
||||
// (rejections are added to $errors).
|
||||
function validateImageUpload(?array $upload, string $label, int $maxBytes, array &$errors): ?string {
|
||||
if (!$upload || $upload['error'] === UPLOAD_ERR_NO_FILE) return null;
|
||||
if (in_array($upload['error'], [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE], true) || $upload['size'] > $maxBytes) {
|
||||
$errors[] = "{$label}s can be up to " . ($maxBytes / 1024 / 1024) . ' MB.';
|
||||
return null;
|
||||
}
|
||||
if ($upload['error'] !== UPLOAD_ERR_OK) {
|
||||
$errors[] = "The {$label} failed to upload. Please try again.";
|
||||
return null;
|
||||
}
|
||||
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($upload['tmp_name']);
|
||||
$ext = IMAGE_TYPES[$mime] ?? null;
|
||||
if ($ext === null || getimagesize($upload['tmp_name']) === false) {
|
||||
$errors[] = "{$label}s must be JPEG, PNG or WebP.";
|
||||
return null;
|
||||
}
|
||||
return $ext;
|
||||
}
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$show = ['id' => 0, 'name' => '', 'slug' => '', 'background_image' => null];
|
||||
$show = ['id' => 0, 'name' => '', 'slug' => '', 'description' => '', 'icon_image' => null, 'background_image' => null];
|
||||
if ($id) {
|
||||
$stmt = $db->prepare('SELECT id, name, slug, background_image FROM shows WHERE id = ?');
|
||||
$stmt = $db->prepare('SELECT id, name, slug, description, icon_image, background_image FROM shows WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$show = $stmt->fetch();
|
||||
if (!$show) {
|
||||
@@ -38,13 +62,14 @@ $errors = [];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// PHP discards the whole form when a request exceeds post_max_size, so catch that before the CSRF check
|
||||
if (empty($_POST) && (int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
|
||||
$errors[] = 'That upload is too large. Background images can be up to ' . (MAX_BACKGROUND_BYTES / 1024 / 1024) . ' MB.';
|
||||
$errors[] = 'That upload is too large. Background images can be up to ' . (MAX_BACKGROUND_BYTES / 1024 / 1024) . ' MB and icons up to ' . (MAX_ICON_BYTES / 1024 / 1024) . ' MB.';
|
||||
} else {
|
||||
verifyCsrf();
|
||||
|
||||
if (($_POST['action'] ?? '') === 'delete' && $id) {
|
||||
$db->prepare('DELETE FROM shows WHERE id = ?')->execute([$id]);
|
||||
deleteBackground($show['background_image']);
|
||||
deleteUpload($show['background_image']);
|
||||
deleteUpload($show['icon_image']);
|
||||
flash("Deleted {$show['name']}.");
|
||||
header('Location: shows.php');
|
||||
exit;
|
||||
@@ -52,6 +77,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
$show['name'] = trim((string)($_POST['name'] ?? ''));
|
||||
$show['slug'] = trim((string)($_POST['slug'] ?? '')) ?: slugify($show['name']);
|
||||
// Normalise line endings so blank-line paragraph breaks are found on index.php
|
||||
$show['description'] = trim(str_replace("\r\n", "\n", (string)($_POST['description'] ?? '')));
|
||||
|
||||
if ($show['name'] === '' || mb_strlen($show['name']) > 255) {
|
||||
$errors[] = 'Name is required (up to 255 characters).';
|
||||
@@ -63,46 +90,55 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt->execute([$show['slug'], $id]);
|
||||
if ($stmt->fetchColumn() > 0) $errors[] = "Another show already uses the slug '{$show['slug']}'.";
|
||||
}
|
||||
if (mb_strlen($show['description']) > MAX_DESCRIPTION_CHARS) {
|
||||
$errors[] = 'The description can be up to ' . MAX_DESCRIPTION_CHARS . ' characters.';
|
||||
}
|
||||
|
||||
$upload = $_FILES['background'] ?? null;
|
||||
$uploadExt = null;
|
||||
if ($upload && $upload['error'] !== UPLOAD_ERR_NO_FILE) {
|
||||
if (in_array($upload['error'], [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE], true) || $upload['size'] > MAX_BACKGROUND_BYTES) {
|
||||
$errors[] = 'Background images can be up to ' . (MAX_BACKGROUND_BYTES / 1024 / 1024) . ' MB.';
|
||||
} elseif ($upload['error'] !== UPLOAD_ERR_OK) {
|
||||
$errors[] = 'The background image failed to upload. Please try again.';
|
||||
} else {
|
||||
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($upload['tmp_name']);
|
||||
$uploadExt = BACKGROUND_TYPES[$mime] ?? null;
|
||||
if ($uploadExt === null || getimagesize($upload['tmp_name']) === false) {
|
||||
$errors[] = 'Background images must be JPEG, PNG or WebP.';
|
||||
// column => [form field, label, size limit, filename suffix after the slug]
|
||||
$images = [
|
||||
'background_image' => ['background', 'background image', MAX_BACKGROUND_BYTES, ''],
|
||||
'icon_image' => ['icon', 'icon', MAX_ICON_BYTES, '-icon'],
|
||||
];
|
||||
$uploadExts = [];
|
||||
foreach ($images as $column => [$field, $label, $maxBytes]) {
|
||||
$uploadExts[$column] = validateImageUpload($_FILES[$field] ?? null, ucfirst($label), $maxBytes, $errors);
|
||||
}
|
||||
|
||||
$oldImages = ['background_image' => $show['background_image'], 'icon_image' => $show['icon_image']];
|
||||
if (!$errors) {
|
||||
foreach ($images as $column => [$field, $label, , $suffix]) {
|
||||
if ($uploadExts[$column] !== null) {
|
||||
$filename = $show['slug'] . $suffix . '-' . bin2hex(random_bytes(4)) . '.' . $uploadExts[$column];
|
||||
if (move_uploaded_file($_FILES[$field]['tmp_name'], UPLOAD_DIR . $filename)) {
|
||||
$show[$column] = $filename;
|
||||
} else {
|
||||
$errors[] = "The {$label} could not be saved. Check that the web server can write to images/backgrounds/.";
|
||||
}
|
||||
} elseif (!empty($_POST["remove_{$field}"])) {
|
||||
$show[$column] = null;
|
||||
}
|
||||
}
|
||||
// Don't leave a newly saved file behind if the other one failed
|
||||
if ($errors) {
|
||||
foreach ($oldImages as $column => $old) {
|
||||
if ($show[$column] !== $old) deleteUpload($show[$column]);
|
||||
$show[$column] = $old;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
$oldBackground = $show['background_image'];
|
||||
if ($uploadExt !== null) {
|
||||
$filename = $show['slug'] . '-' . bin2hex(random_bytes(4)) . '.' . $uploadExt;
|
||||
if (move_uploaded_file($upload['tmp_name'], BACKGROUND_DIR . $filename)) {
|
||||
$show['background_image'] = $filename;
|
||||
} else {
|
||||
$errors[] = 'The background image could not be saved. Check that the web server can write to images/backgrounds/.';
|
||||
}
|
||||
} elseif (!empty($_POST['remove_background'])) {
|
||||
$show['background_image'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
$description = $show['description'] === '' ? null : $show['description'];
|
||||
if ($id) {
|
||||
$db->prepare('UPDATE shows SET name = ?, slug = ?, background_image = ? WHERE id = ?')
|
||||
->execute([$show['name'], $show['slug'], $show['background_image'], $id]);
|
||||
$db->prepare('UPDATE shows SET name = ?, slug = ?, description = ?, icon_image = ?, background_image = ? WHERE id = ?')
|
||||
->execute([$show['name'], $show['slug'], $description, $show['icon_image'], $show['background_image'], $id]);
|
||||
} else {
|
||||
$db->prepare('INSERT INTO shows (name, slug, background_image) VALUES (?, ?, ?)')
|
||||
->execute([$show['name'], $show['slug'], $show['background_image']]);
|
||||
$db->prepare('INSERT INTO shows (name, slug, description, icon_image, background_image) VALUES (?, ?, ?, ?, ?)')
|
||||
->execute([$show['name'], $show['slug'], $description, $show['icon_image'], $show['background_image']]);
|
||||
}
|
||||
foreach ($oldImages as $column => $old) {
|
||||
if ($old !== $show[$column]) deleteUpload($old);
|
||||
}
|
||||
if ($oldBackground !== $show['background_image']) deleteBackground($oldBackground);
|
||||
|
||||
flash("Saved {$show['name']}.");
|
||||
header('Location: shows.php');
|
||||
@@ -129,6 +165,23 @@ adminHeader($id ? 'Edit Show' : 'Add a Show', $admin);
|
||||
<input type="text" name="slug" id="slug" value="<?= e($show['slug']) ?>" maxlength="64" pattern="[a-z0-9]+(-[a-z0-9]+)*" placeholder="Leave blank to generate from the name" />
|
||||
<p><small>Short name used by <code>cli/set_show.php</code>. Lowercase letters, numbers and dashes.</small></p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" id="description" rows="8" maxlength="<?= MAX_DESCRIPTION_CHARS ?>"><?= e($show['description'] ?? '') ?></textarea>
|
||||
<p><small>Shown in the sidebar of the main page while this show is queued or live. Plain text; leave a blank line between paragraphs.</small></p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="icon">Icon</label>
|
||||
<?php if ($show['icon_image']): ?>
|
||||
<p><img src="../images/backgrounds/<?= e(rawurlencode($show['icon_image'])) ?>" alt="Current icon" style="max-width:120px;border-radius:4px;display:block;"></p>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="icon" id="icon" accept="image/jpeg,image/png,image/webp" />
|
||||
<p><small>JPEG, PNG or WebP, up to <?= MAX_ICON_BYTES / 1024 / 1024 ?> MB. The show logo shown next to the description on the main page.</small></p>
|
||||
<?php if ($show['icon_image']): ?>
|
||||
<input type="checkbox" name="remove_icon" id="remove_icon" value="1" />
|
||||
<label for="remove_icon">Remove icon</label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="background">Background image</label>
|
||||
<?php if ($show['background_image']): ?>
|
||||
|
||||
Reference in new issue
Block a user