Prevents PostgreSQL errors when favorites API is called without
authentication. Functions now return early (nil or 0) instead of
generating invalid SQL with NIL in WHERE clause.
- Fix find-track-by-title to parse 'Artist - Title' format from Icecast
and search both artist and title columns in tracks table
- Fix favorites API alist key mismatch (TRACK-TITLE not TRACK_TITLE)
- Fix favorites cache to update UI after loading
- Fix race condition where star reverted after clicking
- Add aget-profile helper for Postmodern uppercase key lookup
The migration defines 'completed' as INTEGER but the code was inserting
TRUE/FALSE boolean values. PostgreSQL rejects this type mismatch.
Changed (if completed "TRUE" "FALSE") to (if completed 1 0)
- Mark Internet-Radio.com listing as complete
- Mark Listener Requests (library tracks, add to library) as complete
- Mark all Themed streams as complete (low orbit, deep space, darker ambient, underworld)
- Add user playlist creation, editing, and track management
- Add library browser for adding tracks to playlists
- Add playlist submission workflow for station airing
- Add admin review interface with preview, approve, reject
- Generate M3U files on approval in playlists/user-submissions/
- Include user-submissions in playlist scheduler dropdown
- Use playlist description as PHASE tag in M3U
- Add database migration for user_playlists table
- Update TODO-next-features.org to mark feature complete
Removed the Recently Played UI section from profile as redundant.
The listening history backend and APIs remain intact for future use.
Previous commit (0359e59) preserves the full implementation.
Track Requests:
- Database table for user track requests (migration 007)
- API endpoints for submit, approve, reject, play
- Front page UI for submitting requests
- Shows recently played requests section
Listening History:
- Auto-records tracks when playing (with 60s deduplication)
- Recently Played section on profile (has date formatting issues)
- Activity chart showing listening patterns by day
- Load More Tracks pagination
Profile Improvements:
- Fixed 401 errors returning proper JSON
- Fixed PostgreSQL boolean type for completed column
- Added offset parameter to recent-tracks API
Note: Recently Played section has date formatting issues showing
'20397 days ago' - may be removed in future commit if not needed.
The listening history backend works correctly.
For production: run migrations/007-track-requests.sql
Avatars:
- Add avatar_path column to USERS table (migration 006)
- Upload API endpoint /api/asteroid/user/avatar/upload
- Profile page shows avatar with hover-to-change overlay
- Default SVG avatar for users without uploaded image
- Avatars stored in static/avatars/ directory
Fixes:
- 401 errors now return proper JSON instead of 500
- SQL escaping for history recording (single quotes)
- Added debug logging for history/record API
- Avatar container has background color for visibility
For production: run migrations/006-user-avatars.sql
- New API endpoint /api/asteroid/user/activity for daily aggregation
- Bar chart showing tracks played per day (last 30 days)
- Hover tooltips show exact date and count
- Total tracks summary below chart
- Green gradient bars matching site theme
Listening History:
- Auto-record tracks when they change (logged-in users only)
- Track stored by title (no tracks table dependency)
- Profile page shows real recent tracks, top artists, listening stats
- APIs: /api/asteroid/user/history, /user/listening-stats, /user/recent-tracks, /user/top-artists
Favorites Fixes:
- Remove favorite now uses title instead of track-id
- Fixed response parsing to show green success message
- Profile page remove button works correctly
Migration Script Updated:
- track_title column added to both tables
- track-id now optional (nullable)
- Unique index on (user-id, track_title)
- No foreign key to tracks table (title-based storage)
For production: run migrations/005-user-favorites-history.sql
- Add user_favorites and listening_history database tables
- Add migration 005-user-favorites-history.sql
- Create user-profile.lisp with favorites/history API endpoints
- Add star button (☆/★) to Now Playing on main page
- Add star button to frame player bar
- Add Favorites section to profile page
- Show login prompt when unauthenticated user clicks star
- Use gold color (#ffcc00) for favorited state (space theme)
- Fix require-authentication to properly detect API routes
- Support title-based favorites (no track DB required)
- Add cl-time-to-unix helper to convert CL universal time to Unix epoch
- Store last-login as UTC time for correct timezone conversion
- Handle Unix epoch in JavaScript (detect seconds vs milliseconds)
- Add /api/asteroid/channel-name endpoint for live channel name updates
- Update front-page.js and stream-player.js to poll server for channel name
instead of relying on localStorage (fixes issue where listeners don't see
playlist changes until page refresh)
- Add footer with Internet Radio directory link and craftering webring links
- Fix last-login display bug: use proper ISO 8601 timestamp format and
parse as Date string instead of Unix epoch
- Store schedule in PostgreSQL (playlist_schedule table)
- Load schedule from database on startup
- Admin UI: add/update schedule entries with hour and playlist dropdowns
- Admin UI: delete buttons for each schedule entry
- Available playlists populated from playlists directory
- Changes persist across server restarts
The scheduler now loads the appropriate playlist immediately when the
server starts, not just at the next scheduled time. This ensures the
stream plays the correct time-based playlist right away.
- Add server time info (UTC) to scheduler status API
- Add scheduler section to admin.ctml with:
- Server time display (UTC)
- Current playlist indicator
- Enable/Disable/Load Current buttons
- Schedule table showing all time slots
- Add ParenScript functions for scheduler controls
- Auto-refresh scheduler status every 30 seconds
- Highlight active playlist in schedule table
- Fix channel name not updating in frame player when playlist changes
- Use localStorage polling (2s interval) for cross-frame communication
- Fix API response access: use bracket notation for 'channel-name' property
- Add skip command after playlist load to trigger crossfade to new playlist
- Add #PHASE metadata to Asteroid-Low-Orbit.m3u playlist
The arrow variable was referencing country-row before it was defined
because let binds all variables simultaneously. Changed to let* for
sequential binding so country-row is available when binding arrow.
- Separate Channel selector (Curated/Shuffle) from Quality selector (bitrate)
- Add channel selector to frame player, front page, and popout player
- Dynamic curated channel name from playlist #PHASE: metadata
- Channel selection syncs across all player contexts via localStorage
- Quality selector disabled when Shuffle channel selected (fixed bitrate)
- Fix reconnectStream to use channel-aware config
- Consistent CSS styling for selector heights
- Add shuffle source to Liquidsoap config (96kbps MP3)
- Add shuffle option to all UI quality selectors (frame, popout, front page)
- Make now-playing APIs mount-aware for correct metadata display
- Implement separate recently-played lists for curated vs shuffle streams
- Speed up now-playing and recently-played refresh on stream change
- Fix clean shutdown of stats polling thread (positional timeout arg)
- Widen quality selector dropdown for shuffle label
- Fix listener_count accumulation bug: use GREATEST() instead of + to track
peak concurrent listeners per day rather than cumulative count
- Migrate all inline JavaScript from templates to ParenScript:
- admin.ctml: listener stats, geo stats, password reset -> admin.lisp
- audio-player-frame.ctml: stream player logic -> stream-player.lisp (new)
- popout-player.ctml: popout player logic -> stream-player.lisp (shared)
- frameset-wrapper.ctml: frame-busting -> frameset-utils.lisp (new)
- profile.ctml: removed redundant inline init (already in profile.lisp)
- Fix API route detection: handle URIs with or without leading slash
- Add routes to serve new ParenScript-generated JS files
- Update ASDF system definition with new ParenScript components
Tested locally for 8+ hours with no issues.
- Add get-geo-stats-by-city function for city-level queries
- Add /api/asteroid/stats/geo/cities endpoint
- Add expandable country rows in admin geo stats table
- Click country to expand/collapse city breakdown
- Update update-geo-stats to accept optional city parameter
- Update get-cached-geo to cache and return city along with country
- Update collect-geo-stats-for-mount and collect-geo-stats-from-web-listeners
to track by country+city
- Revert migration to keep UNIQUE(date, country_code, city) constraint
- Detect when browser pauses muted stream (throttling)
- Auto-reconnect after 3 seconds when paused while muted
- Apply to all three players for consistency
- Implement isReconnecting flag to prevent duplicate reconnect attempts
- Add exponential backoff for error retries (3s, 6s, 12s, max 30s)
- Retry indefinitely until stream returns
- Handle error, stalled, and ended events consistently
- Reset state on successful playback
- Apply same logic to frame player, popout player, and front-page player
one of the accepted remediations for 'stuck stuttering stream' is to
change the burst size for the icecast server. Changing this value as a
test to see if it helps the problem.
- Update update-geo-stats to accept optional city parameter
- Update get-cached-geo to cache and return city along with country
- Update collect-geo-stats-for-mount and collect-geo-stats-from-web-listeners
to track by country+city
- Revert migration to keep UNIQUE(date, country_code, city) constraint
Instead of relying on Icecast's listener IPs (which show proxy IPs
behind HAProxy), capture real client IPs from X-Forwarded-For header
when users visit the front page or audio player frame.
Radiance automatically extracts X-Forwarded-For into (remote *request*).
Changes:
- Add *web-listeners* hash table to track visitors with real IPs
- Add register-web-listener to capture IPs during page requests
- Add collect-geo-stats-from-web-listeners for polling
- Call register-web-listener from front-page and audio-player-frame
- Filter out private/internal IPs (172.x, 192.168.x, 10.x, 127.x)
- Remove session requirement - use IP hash as key for anonymous visitors