Files
radio-free-elsewhere/CLAUDE.md
T
Daniel MesserandClaude Opus 5.5 babb71660c Add admin login
- admins and login_attempts tables
- cli/create_admin.php: create an admin or reset a password over SSH
  (no email involved)
- includes/auth.php: hardened admin sessions, login throttling (5
  failures per IP per 15 minutes) and CSRF helpers
- admin/: login, logout and a dashboard showing broadcast status
- includes/admin_layout.php: shared admin page layout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 07:13:51 -05:00

75 lines
5.7 KiB
Markdown

# Radio Free Elsewhere
## Project Overview
Radio Free Elsewhere is an Internet radio station streaming eclectic music (Celtic folk, ambient/space, surf rock, and more). It runs on Icecast hosted on a Yunohost server. Streams are live and occasional — a few times per week in the evenings, typically around 7:30pm Eastern, running roughly two hours. This is not a 24/7 automated station.
The shows have distinct themes and identities:
- **O'Morain's Pub** — Celtic folk
- **Cosmoeuphoria** — ambient and space music
- **Bisexual Surfers From the Twilight of Time** — surf rock and rockabilly
- **The Bollocks** - punk rock
- **The Spin** - eclectic tunes from all genres
## Scope
This repo covers the **frontend only** — the public-facing website. Icecast configuration, Yunohost server administration, and streaming software are out of scope. Do not look for or suggest changes to server-side streaming config.
## Key Files
- [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).
- [cli/create_admin.php](cli/create_admin.php) — Creates an admin account or resets its password.
- [admin/](admin/) — Admin backend. Every page calls `requireAdmin()` from [includes/auth.php](includes/auth.php) and uses `adminHeader()`/`adminFooter()` from [includes/admin_layout.php](includes/admin_layout.php).
## 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 `<style>` blocks.
- **Now-playing:** `index.php` and `live.php` fetch `now_playing.php` via JavaScript every 15 seconds and inject the result into `#now-playing`. The fetch uses `arrayBuffer` + `TextDecoder` to handle the UTF-8 encoding correctly on the client side.
## Database
MariaDB on localhost. Credentials live in `config/db_credentials.php`, which is gitignored and uploaded to the server separately; `config/db_credentials.example.php` is the template. Schema is in [sql/schema.sql](sql/schema.sql):
- `shows` — the shows, with an optional `background_image` (filename in `images/backgrounds/`).
- `broadcasts` — one row per streaming session: pending (queued by `cli/set_show.php`), live, or ended.
- `played_tracks` — tracks per broadcast. Cleared ten minutes after the broadcast ends; no long-term history is kept.
`cli/poll_icecast.php` runs from cron every minute and polls Icecast four times, 15 seconds apart. A broadcast starts when the stream comes online and ends after five minutes unseen (so brief drop-outs don't split a show). Before going live, run `php cli/set_show.php <slug>` to choose the show.
## Admin Authentication
Username and password only; the site sends no email. Accounts are created and passwords reset over SSH with `php cli/create_admin.php <username>`. Passwords are hashed with `password_hash()` (PASSWORD_DEFAULT). Sessions use a `rfe_admin` cookie scoped to the admin directory (HttpOnly, SameSite=Strict, Secure over HTTPS) and expire after four hours idle. Five failed logins from one IP within 15 minutes locks that IP out for the rest of the window. Every POST form must include `csrfField()` and its handler must call `verifyCsrf()` first.
## Icecast Integration
- **Stream URL:** `https://radio.cyberpunklibrarian.nohost.me/rfe`
- **Status API:** `https://radio.cyberpunklibrarian.nohost.me/status-json.xsl`
The status API returns JSON with track metadata under `icestats.source`. The `source` key may be a single object or an array depending on how many mountpoints are active; `now_playing.php` handles both cases.
## Known Encoding Quirk
Icecast double-encodes track metadata as Latin-1 interpreted as UTF-8. The fix in `decodeMetaField()` in [includes/icecast.php](includes/icecast.php) deliberately converts the string from UTF-8 to ISO-8859-1 to strip the outer encoding layer and recover the correct UTF-8 byte sequence. Do not remove or "simplify" this conversion — it is intentional and necessary for non-ASCII characters (accented names, etc.) to display correctly.
## Deployment
There is no CI/CD pipeline. Deployment is manual:
1. Commit and push changes to the self-hosted Gitea repository.
2. Upload changed files to the Yunohost web server via SFTP.
There is no staging environment — changes go straight to production.
## Updating Show Information
Show info lives in the `#one` section of `index.php`. Previous shows are commented out rather than deleted, so there's a history of past broadcasts in the markup. When updating for a new show, comment out the current show block and add the new one above it.