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);
';
+}
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');