From 74dfb3c1460867637a2a50c3480bceeb7d7d6686 Mon Sep 17 00:00:00 2001 From: Daniel Messer Date: Thu, 29 Oct 2026 06:45:44 -0500 Subject: [PATCH] Add recently played tracking with shows and broadcasts - sql/schema.sql: shows, broadcasts and played_tracks tables, seeded with the five shows - cli/poll_icecast.php: cron-run poller that records broadcasts and tracks every 15 seconds while streaming; tracks are cleared ten minutes after a broadcast ends - cli/set_show.php: queue the next show, or assign one to the live broadcast - recently_played.php: track history rows for live.php, with album art and album title linking to Last.fm - live.php: recently played list, show name in heading, and per-show header background - includes/icecast.php: Icecast parsing, album art and Last.fm helpers shared by now_playing.php and the poller - includes/db.php: use UTC for the database session Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 21 +++++++- cli/poll_icecast.php | 29 ++++++++++ cli/set_show.php | 44 +++++++++++++++ images/backgrounds/.gitkeep | 0 includes/broadcasts.php | 105 ++++++++++++++++++++++++++++++++++++ includes/db.php | 2 + includes/icecast.php | 62 +++++++++++++++++++++ live.php | 54 +++++++++++++++++-- now_playing.php | 61 ++++++--------------- recently_played.php | 41 ++++++++++++++ sql/schema.sql | 48 +++++++++++++++++ 11 files changed, 417 insertions(+), 50 deletions(-) create mode 100644 cli/poll_icecast.php create mode 100644 cli/set_show.php create mode 100644 images/backgrounds/.gitkeep create mode 100644 includes/broadcasts.php create mode 100644 includes/icecast.php create mode 100644 recently_played.php create mode 100644 sql/schema.sql diff --git a/CLAUDE.md b/CLAUDE.md index 993f4b3..c372f45 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,12 +19,29 @@ This repo covers the **frontend only** — the public-facing website. Icecast co - [index.php](index.php) — Main page. Contains the audio player, now-playing widget, upcoming schedule section, and about section. Show information is updated here manually before each broadcast. - [now_playing.php](now_playing.php) — Fetches the currently playing track from the Icecast JSON status API and returns it as HTML. Called by the frontend every 15 seconds via `setInterval`. +- [live.php](live.php) — Page for active broadcasts. Player on the left, recently played list on the right; the header background and heading change with the current show. +- [recently_played.php](recently_played.php) — Returns the current broadcast's track history as table rows for `live.php`. +- [includes/icecast.php](includes/icecast.php) — Shared Icecast fetching/parsing, album art lookup and Last.fm link helpers. +- [includes/db.php](includes/db.php) — `getDB()` returns a shared PDO connection (session time zone UTC). +- [includes/broadcasts.php](includes/broadcasts.php) — Broadcast lifecycle and track history logic. +- [cli/poll_icecast.php](cli/poll_icecast.php) — Cron-run poller that records broadcasts and tracks. +- [cli/set_show.php](cli/set_show.php) — Sets which show is airing next (or now). ## Architecture - **Backend:** PHP only. No Composer, no package manager, no build step. - **Frontend:** HTML5 UP Strata template with custom CSS additions. No npm, no webpack, no preprocessor — edit CSS directly in `assets/css/main.css` or inline ` + + + @@ -90,9 +123,24 @@ console.error('Error fetching song data:', error); }); } + function fetchRecentlyPlayed() { + fetch('recently_played.php') + .then(response => response.arrayBuffer()) + .then(buffer => { + const list = document.getElementById('recently-played'); + list.innerHTML = new TextDecoder('utf-8').decode(buffer); + // Show play times in the listener's own time zone + list.querySelectorAll('time[datetime]').forEach(el => { + el.textContent = new Date(el.getAttribute('datetime')).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); + }); + }) + .catch(error => console.error('Error fetching recently played:', error)); + } // Fetch immediately and then every 15 seconds fetchNowPlaying(); + fetchRecentlyPlayed(); setInterval(fetchNowPlaying, 15000); + setInterval(fetchRecentlyPlayed, 15000);

Add Radio Free Elsewhere to VLC or your favourite audio streaming app.

@@ -104,12 +152,13 @@
-

Recently Played

+

Recently Played on

+ @@ -117,9 +166,8 @@ - - +
Time Title Artist
Track history coming soon.Loading recently played tracks...
diff --git a/now_playing.php b/now_playing.php index 18049e4..bad9604 100644 --- a/now_playing.php +++ b/now_playing.php @@ -1,58 +1,29 @@ Please check back later!"; } else { - $data = json_decode($response, true); + ['title' => $title, 'artist' => $artist, 'album' => $album] = $track; - if (isset($data['icestats']['source'])) { - $source = $data['icestats']['source']; - $currentStream = (!is_array($source) || isset($source['title'])) ? $source : $source[0]; + $artUrl = lookupAlbumArt($artist, $album); + if ($artUrl !== '') { + echo 'Album art'; + } - $raw = decodeMetaField($currentStream, 'title'); - $parts = array_map('trim', explode(' - ', $raw, 3)); - $title = $parts[0] ?? ''; - $artist = $parts[1] ?? ''; - $album = $parts[2] ?? ''; + echo htmlspecialchars($title ?: 'No song metadata available', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + if ($artist !== '') echo '
' . htmlspecialchars($artist, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + if ($album !== '') echo '
' . htmlspecialchars($album, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - // Look up album art from iTunes Search API - $artUrl = ''; - if ($artist !== '' && $album !== '') { - $query = urlencode($artist . ' ' . $album); - $itunesResponse = @file_get_contents("https://itunes.apple.com/search?term={$query}&entity=album&media=music&limit=1"); - if ($itunesResponse !== false) { - $itunesData = json_decode($itunesResponse, true); - if (!empty($itunesData['results'][0]['artworkUrl100'])) { - $artUrl = str_replace('100x100bb', '400x400bb', $itunesData['results'][0]['artworkUrl100']); - } - } - } - - if ($artUrl !== '') { - echo 'Album art'; - } - - echo htmlspecialchars($title ?: 'No song metadata available', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - if ($artist !== '') echo '
' . htmlspecialchars($artist, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - if ($album !== '') echo '
' . htmlspecialchars($album, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - - if ($artist !== '' && $title !== '') { - $lastfmUrl = 'https://www.last.fm/music/' . rawurlencode($artist) . '/_/' . rawurlencode($title); - echo '
More on Last.fm →'; - } - - } else { - echo "RFE is currently offline!
Please check back later!"; + if ($artist !== '' && $title !== '') { + $lastfmUrl = lastfmTrackUrl($artist, $title); + echo '
More on Last.fm →'; } } ?> diff --git a/recently_played.php b/recently_played.php new file mode 100644 index 0000000..dfa6a20 --- /dev/null +++ b/recently_played.php @@ -0,0 +1,41 @@ +Unable to load recently played tracks.'; + exit; +} + +if (!$tracks) { + echo 'Nothing played yet. Tracks will appear here once the show is under way.'; + exit; +} + +foreach ($tracks as $t) { + // Album art and album title link to the album on Last.fm + $albumUrl = ($t['artist'] !== '' && $t['album'] !== '') ? lastfmAlbumUrl($t['artist'], $t['album']) : ''; + $openLink = $albumUrl !== '' ? '' : ''; + $closeLink = $albumUrl !== '' ? '' : ''; + + // Stored in UTC; live.php reformats this in the listener's local time + $iso = str_replace(' ', 'T', $t['played_at']) . 'Z'; + $fallback = substr($t['played_at'], 11, 5) . ' UTC'; + + echo ''; + echo '' . ($t['art_url'] !== '' ? $openLink . 'Album art for ' . e($t['album']) . '' . $closeLink : '') . ''; + echo ''; + echo '' . e($t['title']) . ''; + echo '' . e($t['artist']) . ''; + echo '' . $openLink . e($t['album']) . $closeLink . ''; + echo ''; +} diff --git a/sql/schema.sql b/sql/schema.sql new file mode 100644 index 0000000..9d74e2e --- /dev/null +++ b/sql/schema.sql @@ -0,0 +1,48 @@ +-- Radio Free Elsewhere database schema. +-- Apply with: mysql -u -p < sql/schema.sql +-- All DATETIME values are stored in UTC (includes/db.php sets the session time zone). + +CREATE TABLE IF NOT EXISTS shows ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + slug VARCHAR(64) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + -- Filename in images/backgrounds/ used as the live.php header background; NULL keeps the default + background_image VARCHAR(255) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- One row per streaming session. +-- Pending (queued by cli/set_show.php): started_at IS NULL +-- Live: started_at set, ended_at IS NULL +-- Ended: ended_at set +CREATE TABLE IF NOT EXISTS broadcasts ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + show_id INT UNSIGNED NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + started_at DATETIME NULL, + last_seen_at DATETIME NULL, + ended_at DATETIME NULL, + INDEX (started_at), + INDEX (ended_at), + FOREIGN KEY (show_id) REFERENCES shows (id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Tracks are cleared ten minutes after their broadcast ends. +CREATE TABLE IF NOT EXISTS played_tracks ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + broadcast_id INT UNSIGNED NOT NULL, + played_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + title VARCHAR(255) NOT NULL, + artist VARCHAR(255) NOT NULL DEFAULT '', + album VARCHAR(255) NOT NULL DEFAULT '', + art_url VARCHAR(512) NOT NULL DEFAULT '', + INDEX (broadcast_id, played_at), + FOREIGN KEY (broadcast_id) REFERENCES broadcasts (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT IGNORE INTO shows (slug, name) VALUES + ('omorains-pub', 'O''Morain''s Pub'), + ('cosmoeuphoria', 'Cosmoeuphoria'), + ('bisexual-surfers', 'Bisexual Surfers From the Twilight of Time'), + ('the-bollocks', 'The Bollocks'), + ('the-spin', 'The Spin');