Add optional episode titles to queued and live broadcasts
Store an episode_title on each broadcast, settable from the dashboard or with set_show.php --title=, and show it under the show name on index.php and under the heading on live.php. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9bc9af87c7
commit
170ae6eed0
9 files changed
+67
-25
No files matched your search
@@ -27,7 +27,7 @@ Public pages:
|
||||
|
||||
Admin backend ([admin/](admin/)):
|
||||
- [admin/login.php](admin/login.php), [admin/logout.php](admin/logout.php) — Username/password login.
|
||||
- [admin/index.php](admin/index.php) — Dashboard: broadcast status, queue the next show (with an optional date and Eastern start time), switch the live show, clear the queue.
|
||||
- [admin/index.php](admin/index.php) — Dashboard: broadcast status, queue the next show (with an optional episode title, date and Eastern start time), switch the live show or its episode title, clear the queue.
|
||||
- [admin/shows.php](admin/shows.php), [admin/show_edit.php](admin/show_edit.php) — List, add, edit and delete shows, including the description and icon and background image uploads.
|
||||
|
||||
Shared includes:
|
||||
@@ -39,7 +39,7 @@ Shared includes:
|
||||
|
||||
Command-line tools (run over SSH from the site root):
|
||||
- [cli/poll_icecast.php](cli/poll_icecast.php) — Cron-run poller that records broadcasts and tracks.
|
||||
- [cli/set_show.php](cli/set_show.php) — Lists shows, or queues/sets the show by slug, optionally with a start time (`php cli/set_show.php <slug> 2026-10-30 19:30`). Same behaviour as the dashboard control.
|
||||
- [cli/set_show.php](cli/set_show.php) — Lists shows, or queues/sets the show by slug, optionally with a start time and episode title (`php cli/set_show.php <slug> 2026-10-30 19:30 --title="Ethereal and Enya"`). Same behaviour as the dashboard control.
|
||||
- [cli/create_admin.php](cli/create_admin.php) — Creates an admin account or resets its password.
|
||||
|
||||
Other:
|
||||
@@ -60,7 +60,7 @@ Other:
|
||||
MariaDB on localhost. Credentials live in `config/db_credentials.php`, which is gitignored and uploaded to the server separately. Tables (see [sql/schema.sql](sql/schema.sql)):
|
||||
|
||||
- `shows` — the shows, with a unique `slug`, a plain-text `description` (blank lines separate paragraphs) and optional `icon_image` and `background_image` (filenames in `images/backgrounds/`).
|
||||
- `broadcasts` — one row per streaming session: pending (`started_at` NULL, queued from the dashboard or `cli/set_show.php`), live (`ended_at` NULL), or ended. `scheduled_at` is the advertised start time shown on `index.php`; the broadcast still starts whenever the stream comes online.
|
||||
- `broadcasts` — one row per streaming session: pending (`started_at` NULL, queued from the dashboard or `cli/set_show.php`), live (`ended_at` NULL), or ended. `scheduled_at` is the advertised start time shown on `index.php`; the broadcast still starts whenever the stream comes online. `episode_title` is an optional per-episode title (e.g. "Ethereal and Enya") shown under the show name on `index.php` and under the heading on `live.php`.
|
||||
- `played_tracks` — tracks per broadcast. Cleared ten minutes after the broadcast ends; no long-term history is kept.
|
||||
- `admins` — admin accounts (username + password hash).
|
||||
- `login_attempts` — failed logins by IP, used for throttling.
|
||||
@@ -128,7 +128,7 @@ Server-only setup (one-time, or when the schema changes):
|
||||
|
||||
## Updating Show Information
|
||||
|
||||
The `index.php` sidebar is driven by the database. Before a show, queue it on the admin dashboard (or `php cli/set_show.php <slug> <date> <time>`) with its date and Eastern start time; the sidebar then shows its icon, name, start time (with the UTC conversion) and description, switches to "On the Air Now" while live (with the icon and a "Broadcasting Live" link pointing to `live.php`), and falls back to a short "nothing scheduled" note once nothing is queued. Edit descriptions and icons on the show's admin page.
|
||||
The `index.php` sidebar is driven by the database. Before a show, queue it on the admin dashboard (or `php cli/set_show.php <slug> <date> <time>`) with its date, Eastern start time and optional episode title; the sidebar then shows its icon, name, episode title, start time (with the UTC conversion) and description, switches to "On the Air Now" while live (with the icon and a "Broadcasting Live" link pointing to `live.php`), and falls back to a short "nothing scheduled" note once nothing is queued. Edit descriptions and icons on the show's admin page.
|
||||
|
||||
The commented-out show blocks in the `#one` section of `index.php` are the hand-written history from before this; leave them as they are.
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@ During your next stream, tracks should start appearing on `live.php` within a mi
|
||||
sudo -u my_webapp /usr/bin/php8.2 cli/set_show.php # list shows
|
||||
sudo -u my_webapp /usr/bin/php8.2 cli/set_show.php <slug> # queue one
|
||||
sudo -u my_webapp /usr/bin/php8.2 cli/set_show.php <slug> 2026-10-30 19:30 # queue one with a start time
|
||||
sudo -u my_webapp /usr/bin/php8.2 cli/set_show.php <slug> 2026-10-30 19:30 --title="Ethereal and Enya" # ...and an episode title
|
||||
```
|
||||
|
||||
2. Start streaming. Within a minute, the poller starts the queued broadcast. The live page's heading and background switch to the show, and tracks begin to appear.
|
||||
|
||||
+14
-8
@@ -25,14 +25,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$date = trim((string)($_POST['date'] ?? ''));
|
||||
$time = substr(trim((string)($_POST['time'] ?? '')), 0, 5);
|
||||
$scheduledAt = $date === '' && $time === '' ? null : stationTimeToUtc("$date $time");
|
||||
$episodeTitle = trim((string)($_POST['episode_title'] ?? ''));
|
||||
$label = $show['name'] . ($episodeTitle !== '' ? " ({$episodeTitle})" : '');
|
||||
if (!$show) {
|
||||
flash('Please choose a show.', 'error');
|
||||
} elseif (($date !== '' || $time !== '') && $scheduledAt === null) {
|
||||
flash('Please enter both a valid date and time, or leave both blank.', 'error');
|
||||
} elseif (setBroadcastShow($db, (int)$show['id'], $scheduledAt) === 'live') {
|
||||
flash("The live broadcast is now {$show['name']}.");
|
||||
} elseif (setBroadcastShow($db, (int)$show['id'], $scheduledAt, $episodeTitle) === 'live') {
|
||||
flash("The live broadcast is now {$label}.");
|
||||
} else {
|
||||
flash("{$show['name']} is queued" . ($scheduledAt ? ' for ' . describeScheduledAt($scheduledAt) : '')
|
||||
flash("{$label} is queued" . ($scheduledAt ? ' for ' . describeScheduledAt($scheduledAt) : '')
|
||||
. '. It will go live as soon as the stream starts.');
|
||||
}
|
||||
} elseif ($action === 'clear_queue') {
|
||||
@@ -54,9 +56,9 @@ adminHeader('Dashboard', $admin);
|
||||
<h3>Broadcast status</h3>
|
||||
<p>
|
||||
<?php if ($live): ?>
|
||||
<strong>Live now</strong><?= $current ? ': ' . e($current['name']) : ' (no show set)' ?>, since <?= e($live['started_at']) ?> UTC.
|
||||
<strong>Live now</strong><?= $current ? ': ' . e($current['name']) . ($current['episode_title'] ? ' — “' . e($current['episode_title']) . '”' : '') : ' (no show set)' ?>, since <?= e($live['started_at']) ?> UTC.
|
||||
<?php elseif ($current): ?>
|
||||
Offline. <strong><?= e($current['name']) ?></strong> is queued for the next broadcast<?= $current['scheduled_at'] ? ', scheduled for ' . e(describeScheduledAt($current['scheduled_at'])) : '' ?>.
|
||||
Offline. <strong><?= e($current['name']) ?></strong><?= $current['episode_title'] ? ' — “' . e($current['episode_title']) . '”' : '' ?> is queued for the next broadcast<?= $current['scheduled_at'] ? ', scheduled for ' . e(describeScheduledAt($current['scheduled_at'])) : '' ?>.
|
||||
<?php else: ?>
|
||||
Offline. No show is queued.
|
||||
<?php endif; ?>
|
||||
@@ -64,13 +66,13 @@ adminHeader('Dashboard', $admin);
|
||||
|
||||
<h3><?= $live ? 'Change the live show' : 'Queue the next show' ?></h3>
|
||||
<p><?= $live
|
||||
? 'Switch which show the live page and recently played list belong to.'
|
||||
: 'Choose the show before you start streaming. The broadcast goes live on the site automatically when the stream comes online. The show, and its start time if you enter one, appear on the main page until then.' ?></p>
|
||||
? 'Switch which show, and episode title, the live page and recently played list belong to.'
|
||||
: 'Choose the show before you start streaming. The broadcast goes live on the site automatically when the stream comes online. The show, and its episode title and start time if you enter them, appear on the main page until then.' ?></p>
|
||||
<form method="post" action="index.php">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="action" value="set_show" />
|
||||
<div class="row gtr-uniform gtr-50">
|
||||
<div class="<?= $live ? 'col-8' : 'col-12' ?> col-12-small">
|
||||
<div class="col-12">
|
||||
<select name="show_id" aria-label="Show" required>
|
||||
<option value="">Choose a show…</option>
|
||||
<?php foreach ($shows as $show): ?>
|
||||
@@ -78,6 +80,10 @@ adminHeader('Dashboard', $admin);
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="<?= $live ? 'col-8' : 'col-12' ?> col-12-small">
|
||||
<label for="episode_title">Episode title (optional)</label>
|
||||
<input type="text" name="episode_title" id="episode_title" maxlength="255" placeholder="e.g. Ethereal and Enya" value="<?= $current && $current['episode_title'] ? e($current['episode_title']) : '' ?>" />
|
||||
</div>
|
||||
<?php if (!$live): ?>
|
||||
<div class="col-6 col-12-xsmall">
|
||||
<label for="date">Date (optional)</label>
|
||||
|
||||
@@ -2656,6 +2656,10 @@ input, select, textarea {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#header #one .show-heading .episode-title {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
#footer .icons {
|
||||
|
||||
+21
-7
@@ -5,6 +5,8 @@
|
||||
// (or assign it to the live one if already streaming)
|
||||
// php cli/set_show.php <slug> 2026-10-30 19:30
|
||||
// queue it with an advertised start time (Eastern) for the main page
|
||||
// php cli/set_show.php <slug> 2026-10-30 19:30 --title="Ethereal and Enya"
|
||||
// also give the episode a title (works with or without a start time)
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(403);
|
||||
@@ -15,10 +17,20 @@ require __DIR__ . '/../includes/db.php';
|
||||
require __DIR__ . '/../includes/broadcasts.php';
|
||||
|
||||
$db = getDB();
|
||||
$slug = $argv[1] ?? null;
|
||||
// Pull out --title=... so the remaining arguments stay positional
|
||||
$episodeTitle = null;
|
||||
$args = [];
|
||||
foreach (array_slice($argv, 1) as $arg) {
|
||||
if (str_starts_with($arg, '--title=')) {
|
||||
$episodeTitle = substr($arg, 8);
|
||||
} else {
|
||||
$args[] = $arg;
|
||||
}
|
||||
}
|
||||
$slug = $args[0] ?? null;
|
||||
$scheduledAt = null;
|
||||
if (isset($argv[2])) {
|
||||
$scheduledAt = stationTimeToUtc($argv[2] . ' ' . ($argv[3] ?? ''));
|
||||
if (isset($args[1])) {
|
||||
$scheduledAt = stationTimeToUtc($args[1] . ' ' . ($args[2] ?? ''));
|
||||
if ($scheduledAt === null) {
|
||||
fwrite(STDERR, "Start time must be a valid date and time like 2026-10-30 19:30 (" . STATION_TIMEZONE_LABEL . ").\n");
|
||||
exit(1);
|
||||
@@ -31,7 +43,8 @@ if ($slug === null) {
|
||||
printf(" %-20s %s\n", $show['slug'], $show['name']);
|
||||
}
|
||||
$current = currentShow($db);
|
||||
echo "\nCurrent: " . ($current ? $current['name'] : '(none)') . (liveBroadcast($db) ? ' [live]' : '');
|
||||
echo "\nCurrent: " . ($current ? $current['name'] : '(none)')
|
||||
. ($current && $current['episode_title'] ? " \"{$current['episode_title']}\"" : '') . (liveBroadcast($db) ? ' [live]' : '');
|
||||
if ($current && $current['scheduled_at'] && !$current['started_at']) {
|
||||
echo ', scheduled for ' . utcToStationTime($current['scheduled_at'])->format('Y-m-d H:i') . ' ' . STATION_TIMEZONE_LABEL . " ({$current['scheduled_at']} UTC)";
|
||||
}
|
||||
@@ -47,8 +60,9 @@ if (!$show) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (setBroadcastShow($db, (int)$show['id'], $scheduledAt) === 'live') {
|
||||
echo "Live broadcast is now {$show['name']}." . ($scheduledAt ? ' (Start time ignored.)' : '') . "\n";
|
||||
$label = $show['name'] . (trim((string)$episodeTitle) !== '' ? ' "' . trim($episodeTitle) . '"' : '');
|
||||
if (setBroadcastShow($db, (int)$show['id'], $scheduledAt, $episodeTitle) === 'live') {
|
||||
echo "Live broadcast is now {$label}." . ($scheduledAt ? ' (Start time ignored.)' : '') . "\n";
|
||||
} else {
|
||||
echo "{$show['name']} is queued for the next broadcast" . ($scheduledAt ? " at {$argv[2]} {$argv[3]} " . STATION_TIMEZONE_LABEL . " ({$scheduledAt} UTC)" : '') . ".\n";
|
||||
echo "{$label} is queued for the next broadcast" . ($scheduledAt ? " at {$args[1]} {$args[2]} " . STATION_TIMEZONE_LABEL . " ({$scheduledAt} UTC)" : '') . ".\n";
|
||||
}
|
||||
@@ -38,10 +38,10 @@ function pendingBroadcast(PDO $db): ?array {
|
||||
}
|
||||
|
||||
// The show for the live broadcast, or the queued one if nothing is live yet,
|
||||
// with the broadcast's scheduled_at and started_at. Stale broadcasts are skipped as in liveBroadcast().
|
||||
// with the broadcast's scheduled_at, episode_title and started_at. Stale broadcasts are skipped as in liveBroadcast().
|
||||
function currentShow(PDO $db): ?array {
|
||||
$row = $db->query(
|
||||
'SELECT s.*, b.scheduled_at, b.started_at FROM broadcasts b
|
||||
'SELECT s.*, b.scheduled_at, b.episode_title, b.started_at FROM broadcasts b
|
||||
JOIN shows s ON s.id = b.show_id
|
||||
WHERE b.ended_at IS NULL
|
||||
AND (b.started_at IS NULL OR b.last_seen_at >= NOW() - INTERVAL ' . BROADCAST_GRACE_MINUTES . ' MINUTE)
|
||||
@@ -64,15 +64,18 @@ function utcToStationTime(string $utc): DateTimeImmutable {
|
||||
}
|
||||
|
||||
// Assigns a show to the live broadcast, or queues it for the next one if nothing is live.
|
||||
// $scheduledAt (UTC) is the advertised start time of a queued show. Returns 'live' or 'queued'.
|
||||
function setBroadcastShow(PDO $db, int $showId, ?string $scheduledAt = null): string {
|
||||
// $scheduledAt (UTC) is the advertised start time of a queued show; $episodeTitle is optional and replaces
|
||||
// any title already set (blank clears it). Returns 'live' or 'queued'.
|
||||
function setBroadcastShow(PDO $db, int $showId, ?string $scheduledAt = null, ?string $episodeTitle = null): string {
|
||||
$episodeTitle = trim((string)$episodeTitle);
|
||||
$episodeTitle = $episodeTitle === '' ? null : mb_substr($episodeTitle, 0, 255);
|
||||
$live = liveBroadcast($db);
|
||||
if ($live !== null) {
|
||||
$db->prepare('UPDATE broadcasts SET show_id = ? WHERE id = ?')->execute([$showId, $live['id']]);
|
||||
$db->prepare('UPDATE broadcasts SET show_id = ?, episode_title = ? WHERE id = ?')->execute([$showId, $episodeTitle, $live['id']]);
|
||||
return 'live';
|
||||
}
|
||||
clearQueuedShow($db);
|
||||
$db->prepare('INSERT INTO broadcasts (show_id, scheduled_at) VALUES (?, ?)')->execute([$showId, $scheduledAt]);
|
||||
$db->prepare('INSERT INTO broadcasts (show_id, scheduled_at, episode_title) VALUES (?, ?, ?)')->execute([$showId, $scheduledAt, $episodeTitle]);
|
||||
return 'queued';
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,9 @@ if ($show) {
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<h3><?= htmlspecialchars($show['name'], ENT_QUOTES, 'UTF-8') ?></h3>
|
||||
<?php if ($show['episode_title']): ?>
|
||||
<p class="episode-title"><em><?= htmlspecialchars($show['episode_title'], ENT_QUOTES, 'UTF-8') ?></em></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($show['started_at']): ?>
|
||||
<p><strong><a href="live.php">Broadcasting Live</a></strong></p>
|
||||
<?php elseif ($when): ?>
|
||||
|
||||
@@ -70,6 +70,11 @@ if ($show && $show['background_image'] && is_file(__DIR__ . '/images/backgrounds
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
.episode-title {
|
||||
font-size: 1.25em;
|
||||
font-style: italic;
|
||||
margin-top: -0.75em;
|
||||
}
|
||||
#recently-played td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -159,6 +164,9 @@ if ($show && $show['background_image'] && is_file(__DIR__ . '/images/backgrounds
|
||||
<section id="one">
|
||||
<header class="major">
|
||||
<h2>Recently Played<?php if ($show): ?> on <?= htmlspecialchars($show['name'], ENT_QUOTES, 'UTF-8') ?><?php endif; ?></h2>
|
||||
<?php if ($show && $show['episode_title']): ?>
|
||||
<p class="episode-title"><?= htmlspecialchars($show['episode_title'], ENT_QUOTES, 'UTF-8') ?></p>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
|
||||
@@ -25,6 +25,8 @@ CREATE TABLE IF NOT EXISTS broadcasts (
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
-- Advertised start time of a queued show; the broadcast still starts when the stream comes online
|
||||
scheduled_at DATETIME NULL,
|
||||
-- Optional title for this episode, e.g. "Ethereal and Enya", shown with the show name
|
||||
episode_title VARCHAR(255) NULL,
|
||||
started_at DATETIME NULL,
|
||||
last_seen_at DATETIME NULL,
|
||||
ended_at DATETIME NULL,
|
||||
@@ -37,6 +39,7 @@ CREATE TABLE IF NOT EXISTS broadcasts (
|
||||
ALTER TABLE shows ADD COLUMN IF NOT EXISTS description TEXT NULL AFTER name;
|
||||
ALTER TABLE shows ADD COLUMN IF NOT EXISTS icon_image VARCHAR(255) NULL AFTER description;
|
||||
ALTER TABLE broadcasts ADD COLUMN IF NOT EXISTS scheduled_at DATETIME NULL AFTER created_at;
|
||||
ALTER TABLE broadcasts ADD COLUMN IF NOT EXISTS episode_title VARCHAR(255) NULL AFTER scheduled_at;
|
||||
|
||||
-- Tracks are cleared ten minutes after their broadcast ends.
|
||||
CREATE TABLE IF NOT EXISTS played_tracks (
|
||||
|
||||
Reference in new issue
Block a user