Public-facing page at / shows the current show's playlist with tracks
obscured until the admin marks them as announced. Tracks reveal in
real-time via a new unauthenticated /ws/public WebSocket. Server-side
censorship on /public/playlist strips track details from unannounced
items and sanitizes announced tracks to only expose frontend-needed
fields (no raw_json, track_id, etc). Past episodes are browsable with
fully revealed but sanitized tracklists.
Also fixes RuntimeError on backfill shutdown by closing the httpx
client on the same event loop that created it.
Made-with: Cursor
Add a persistent "announced" checkbox after each track's Announce button.
The state is stored in a new `announced` column on `show_tracks` and is
auto-set when the Announce button is pressed. The checkbox is also freely
togglable, and announced tracks have their Announce button disabled.
Also fixes .env leakage in test_config.py (pass _env_file=None) and adds
tests for the new DB method, API endpoint, and announce side-effect.
Made-with: Cursor
Add a "Send IRC Message" section to the admin dashboard that sends a
configurable privmsg to any IRC nick or channel via all connected bots.
New POST /admin/ping endpoint broadcasts a "privmsg" WebSocket message
type, handled by both Limnoria and Sopel plugins.
Also enable pydantic-settings .env file loading (python-dotenv) and
add .env.example documenting all NTR_* configuration variables.
Made-with: Cursor
Decouple the like-window boundary (Wed 10pm ET) from when the system
rotates to a new show. NTR_SHOW_ROTATION_DELAY_HOURS=2 keeps the
previous week's show visible during the ~2 hour recording, then creates
the new show at midnight.
Made-with: Cursor
SoundCloud began rejecting the fabricated pagination cursor with 500
errors. Fixed cursor user_id padding (zfill 22→20) to match the
documented format, added 5xx retry with exponential backoff in _api_get,
and added a fallback in fetch_likes that drops the fabricated cursor
when it causes persistent 500s.
Made-with: Cursor
README was missing IRC command reference, plugin installation/config
guidance, and referenced nonexistent plugin READMEs. The WebSocket
docs in api.md were stale — subscribe message now documents role and
client_id fields, status message now includes the clients array.
Made-with: Cursor
Replace the link column with a Copy button that copies
"Title by Artist - URL" to clipboard. Replace the current/previous
show layout with a horizontal scrollable tab bar showing all shows
from the database, most recent first. Tabs lazy-load and cache
show data on click.
Made-with: Cursor
Users behind reverse proxies naturally configure https:// URLs.
The websocket-client library requires ws:// or wss:// schemes.
Both plugins now auto-convert before connecting.
Made-with: Cursor
The dashboard's own WS connection was being counted as a bot subscriber,
causing "1 bot connected" with no bots actually present. Now WS clients
send a role ("bot" or "viewer") in the subscribe message. Only bots count
toward the subscriber total. Bot plugins also send a configurable client_id
so the dashboard shows which specific bots are connected.
Made-with: Cursor
- Add web_user, web_password, secret_key as optional params to create_app
- Conditionally mount dashboard router when all three are set
- Pass settings from main.py to create_app
- Add tests for no dashboard/login routes when config absent
Made-with: Cursor
- Login/logout/dashboard with HMAC-signed session cookies
- POST /admin/announce with session or bearer auth
- WS /ws/announce for subscribe/broadcast
- Static stubs: login.html, dashboard.html
Made-with: Cursor
13-task TDD plan covering WebSocket manager, dashboard auth, announce
endpoint, styled UI, and bot plugin WS clients for both Sopel and Limnoria.
Made-with: Cursor
IRC plugins now format datetimes as "Wed Mar 11, 10:00 PM EDT" instead
of raw ISO 8601. Configurable timezone defaults to America/New_York.
Adds !lastshow N command to both Limnoria and Sopel plugins, returning
track N from the previous week's show via existing API endpoints.
Made-with: Cursor
- Fix format_playlist truncation: +4 → +5 to account for ", ..." suffix
- Add API error logging to all Sopel command handlers (matching Limnoria)
- Add long-track truncation test case
Made-with: Cursor
Allows looking up shows by episode number instead of internal DB ID,
enabling IRC bot commands like !playlist 530 to resolve directly.
Made-with: Cursor
Adds a --init mode that seeds the database with past shows from a given
anchor episode/date forward, batch-fetching likes from SoundCloud and
partitioning them into weekly buckets. Episode numbers are tracked in
the shows table and auto-incremented by the poller for new shows.
Includes full API documentation (docs/api.md) and updated README.
Made-with: Cursor
- create_app now accepts show_day/show_hour params instead of
hardcoding Wednesday 22:00
- main.py passes config values through to create_app
- Removed soundcloud_url from AddTrackRequest (not implemented);
track_id is now required
Made-with: Cursor
Nick is the host; if he unlikes a track, the service should respect
that and remove it from the show playlist. Positions re-compact after
removal. The tracks table retains the record for historical reference.
Made-with: Cursor