+179
.status_history/2025-11.md
+179
.status_history/2025-11.md
···
696
696
697
697
---
698
698
699
+
### copyright moderation system (PRs #382, #384, Nov 29-30, 2025)
700
+
701
+
**motivation**: detect potential copyright violations in uploaded tracks to avoid DMCA issues and protect the platform.
702
+
703
+
**what shipped**:
704
+
- **moderation service** (Rust/Axum on Fly.io):
705
+
- standalone service at `plyr-moderation.fly.dev`
706
+
- integrates with AuDD enterprise API for audio fingerprinting
707
+
- scans audio URLs and returns matches with metadata (artist, title, album, ISRC, timecode)
708
+
- auth via `X-Moderation-Key` header
709
+
- **backend integration** (PR #382):
710
+
- `ModerationSettings` in config (service URL, auth token, timeout)
711
+
- moderation client module (`backend/_internal/moderation.py`)
712
+
- fire-and-forget background task on track upload
713
+
- stores results in `copyright_scans` table
714
+
- scan errors stored as "clear" so tracks aren't stuck unscanned
715
+
- **flagging fix** (PR #384):
716
+
- AuDD enterprise API returns no confidence scores (all 0)
717
+
- changed from score threshold to presence-based flagging: `is_flagged = !matches.is_empty()`
718
+
- removed unused `score_threshold` config
719
+
- **backfill script** (`scripts/scan_tracks_copyright.py`):
720
+
- scans existing tracks that haven't been checked
721
+
- `--max-duration` flag to skip long DJ sets (estimated from file size)
722
+
- `--dry-run` mode to preview what would be scanned
723
+
- supports dev/staging/prod environments
724
+
- **review workflow**:
725
+
- `copyright_scans` table has `resolution`, `reviewed_at`, `reviewed_by`, `review_notes` columns
726
+
- resolution values: `violation`, `false_positive`, `original_artist`
727
+
- SQL queries for dashboard: flagged tracks, unreviewed flags, violations list
728
+
729
+
**initial review results** (25 flagged tracks):
730
+
- 8 violations (actual copyright issues)
731
+
- 11 false positives (fingerprint noise)
732
+
- 6 original artists (people uploading their own distributed music)
733
+
734
+
**impact**:
735
+
- automated copyright detection on upload
736
+
- manual review workflow for flagged content
737
+
- protection against DMCA takedown requests
738
+
- clear audit trail with resolution status
739
+
740
+
---
741
+
742
+
### platform stats and media session integration (PRs #359-379, Nov 27-29, 2025)
743
+
744
+
**motivation**: show platform activity at a glance, improve playback experience across devices, and give users control over their data.
745
+
746
+
**what shipped**:
747
+
- **platform stats endpoint and UI** (PRs #376, #378, #379):
748
+
- `GET /stats` returns total plays, tracks, and artists
749
+
- stats bar displays in homepage header (e.g., "1,691 plays • 55 tracks • 8 artists")
750
+
- skeleton loading animation while fetching
751
+
- responsive layout: visible in header on wide screens, collapses to menu on narrow
752
+
- end-of-list animation on homepage
753
+
- **Media Session API** (PR #371):
754
+
- provides track metadata to CarPlay, lock screens, Bluetooth devices, macOS control center
755
+
- artwork display with fallback to artist avatar
756
+
- play/pause, prev/next, seek controls all work from system UI
757
+
- position state syncs scrubbers on external interfaces
758
+
- **browser tab title** (PR #374):
759
+
- shows "track - artist • plyr.fm" while playing
760
+
- persists across page navigation
761
+
- reverts to page title when playback stops
762
+
- **timed comments** (PR #359):
763
+
- comments capture timestamp when added during playback
764
+
- clickable timestamp buttons seek to that moment
765
+
- compact scrollable comments section on track pages
766
+
- **constellation integration** (PR #360):
767
+
- queries constellation.microcosm.blue backlink index
768
+
- enables network-wide like counts (not just plyr.fm internal)
769
+
- environment-aware namespace handling
770
+
- **account deletion** (PR #363):
771
+
- explicit confirmation flow (type handle to confirm)
772
+
- deletes all plyr.fm data (tracks, albums, likes, comments, preferences)
773
+
- optional ATProto record cleanup with clear warnings about orphaned references
774
+
775
+
**impact**:
776
+
- platform stats give visitors immediate sense of activity
777
+
- media session makes plyr.fm tracks controllable from car/lock screen/control center
778
+
- timed comments enable discussion at specific moments in tracks
779
+
- account deletion gives users full control over their data
780
+
781
+
---
782
+
783
+
### developer tokens with independent OAuth grants (PR #367, Nov 28, 2025)
784
+
785
+
**motivation**: programmatic API access (scripts, CLIs, automation) needed tokens that survive browser logout and don't become stale when browser sessions refresh.
786
+
787
+
**what shipped**:
788
+
- **OAuth-based dev tokens**: each developer token gets its own OAuth authorization flow
789
+
- user clicks "create token" → redirected to PDS for authorization → token created with independent credentials
790
+
- tokens have their own DPoP keypair, access/refresh tokens - completely separate from browser session
791
+
- **cookie isolation**: dev token exchange doesn't set browser cookie
792
+
- added `is_dev_token` flag to ExchangeToken model
793
+
- /auth/exchange skips Set-Cookie for dev token flows
794
+
- prevents logout from deleting dev tokens (critical bug fixed during implementation)
795
+
- **token management UI**: portal → "your data" → "developer tokens"
796
+
- create with optional name and expiration (30/90/180/365 days or never)
797
+
- list active tokens with creation/expiration dates
798
+
- revoke individual tokens
799
+
- **API endpoints**:
800
+
- `POST /auth/developer-token/start` - initiates OAuth flow, returns auth_url
801
+
- `GET /auth/developer-tokens` - list user's tokens
802
+
- `DELETE /auth/developer-tokens/{prefix}` - revoke by 8-char prefix
803
+
804
+
**security properties**:
805
+
- tokens are full sessions with encrypted OAuth credentials (Fernet)
806
+
- each token refreshes independently (no staleness from browser session refresh)
807
+
- revokable individually without affecting browser or other tokens
808
+
- explicit OAuth consent required at PDS for each token created
809
+
810
+
**testing verified**:
811
+
- created token → uploaded track → logged out → deleted track with token ✓
812
+
- browser logout doesn't affect dev tokens ✓
813
+
- token works across browser sessions ✓
814
+
- staging deployment tested end-to-end ✓
815
+
816
+
**documentation**: see `docs/authentication.md` "developer tokens" section
817
+
818
+
---
819
+
820
+
### oEmbed endpoint for Leaflet.pub embeds (PRs #355-358, Nov 25, 2025)
821
+
822
+
**motivation**: plyr.fm tracks embedded in Leaflet.pub (via iframely) showed a black HTML5 audio box instead of our custom embed player.
823
+
824
+
**what shipped**:
825
+
- **oEmbed endpoint** (PR #355): `/oembed` returns proper embed HTML with iframe
826
+
- follows oEmbed spec with `type: "rich"` and iframe in `html` field
827
+
- discovery link in track page `<head>` for automatic detection
828
+
- **iframely domain registration**: registered plyr.fm on iframely.com (free tier)
829
+
- this was the key fix - iframely now returns our embed iframe as `links.player[0]`
830
+
- API key: stored in 1password (iframely account)
831
+
832
+
**debugging journey** (PRs #356-358):
833
+
- initially tried `og:video` meta tags to hint iframe embed - didn't work
834
+
- tried removing `og:audio` to force oEmbed fallback - resulted in no player link
835
+
- discovered iframely requires domain registration to trust oEmbed providers
836
+
- after registration, iframely correctly returns embed iframe URL
837
+
838
+
**current state**:
839
+
- oEmbed endpoint working: `curl https://api.plyr.fm/oembed?url=https://plyr.fm/track/92`
840
+
- iframely returns `links.player[0].href = "https://plyr.fm/embed/track/92"` (our embed)
841
+
- Leaflet.pub should show proper embeds (pending their cache expiry)
842
+
843
+
**impact**:
844
+
- plyr.fm tracks can be embedded in Leaflet.pub and other iframely-powered services
845
+
- proper embed player with cover art instead of raw HTML5 audio
846
+
847
+
---
848
+
849
+
### export & upload reliability (PRs #337-344, Nov 24, 2025)
850
+
851
+
**motivation**: exports were failing silently on large files (OOM), uploads showed incorrect progress, and SSE connections triggered false error toasts.
852
+
853
+
**what shipped**:
854
+
- **database-backed jobs** (PR #337): moved upload/export tracking from in-memory to postgres
855
+
- jobs table persists state across server restarts
856
+
- enables reliable progress tracking via SSE polling
857
+
- **streaming exports** (PR #343): fixed OOM on large file exports
858
+
- previously loaded entire files into memory via `response["Body"].read()`
859
+
- now streams to temp files, adds to zip from disk (constant memory)
860
+
- 90-minute WAV files now export successfully on 1GB VM
861
+
- **progress tracking fix** (PR #340): upload progress was receiving bytes but treating as percentage
862
+
- `UploadProgressTracker` now properly converts bytes to percentage
863
+
- upload progress bar works correctly again
864
+
- **UX improvements** (PRs #338-339, #341-342, #344):
865
+
- export filename now includes date (`plyr-tracks-2025-11-24.zip`)
866
+
- toast notification on track deletion
867
+
- fixed false "lost connection" error when SSE completes normally
868
+
- progress now shows "downloading track X of Y" instead of confusing count
869
+
870
+
**impact**:
871
+
- exports work for arbitrarily large files (limited by disk, not RAM)
872
+
- upload progress displays correctly
873
+
- job state survives server restarts
874
+
- clearer progress messaging during exports
875
+
876
+
---
877
+
699
878
archived from STATUS.md on 2025-12-01
+58
-179
STATUS.md
+58
-179
STATUS.md
···
41
41
42
42
## recent work
43
43
44
+
### now-playing API for teal.fm/Piper integration (PR #416, Dec 1, 2025)
45
+
46
+
**motivation**: enable Piper (teal.fm) to display what users are currently listening to on plyr.fm
47
+
48
+
**what shipped**:
49
+
- **now-playing endpoint** (`GET /now-playing/{did}`):
50
+
- returns currently playing track for a given user DID
51
+
- includes track metadata (title, artist, album, cover art)
52
+
- includes playback position and timestamp
53
+
- public endpoint (no auth required)
54
+
- returns 404 when user isn't playing anything
55
+
- **playback tracking**:
56
+
- stores last playback state in `now_playing` table
57
+
- updated when users interact with player
58
+
- includes track_id, position, timestamp, user DID
59
+
- **privacy considerations**:
60
+
- opt-in via user preferences (future enhancement)
61
+
- currently public for all users who play tracks
62
+
- DIDs are already public identifiers
63
+
64
+
**impact**:
65
+
- enables cross-platform integrations (Piper can show "listening to X on plyr.fm")
66
+
- lays groundwork for richer presence features
67
+
- demonstrates plyr.fm as an API-first platform
68
+
69
+
---
70
+
71
+
### admin UI improvements for moderation (PRs #408-414, Dec 1, 2025)
72
+
73
+
**motivation**: improve usability of copyright moderation admin UI based on real-world usage
74
+
75
+
**what shipped**:
76
+
- **reason selection for false positives** (PR #408):
77
+
- dropdown menu when marking tracks as false positive
78
+
- options: "fingerprint noise", "original artist", "fair use", "other"
79
+
- stores reason in `review_notes` field
80
+
- multi-step confirmation to prevent accidental clicks
81
+
- **UI polish** (PR #414):
82
+
- artist/track links open in new tabs for easy verification
83
+
- better visual hierarchy and spacing
84
+
- improved button states and hover effects
85
+
- **AuDD score normalization** (PR #413):
86
+
- AuDD enterprise returns scores as 0-100 range (not 0-1)
87
+
- added score display to admin UI for transparency
88
+
- filter controls to show only high-confidence matches
89
+
- **form submission fix** (PR #412):
90
+
- switched from FormData to URLSearchParams
91
+
- fixes htmx POST request encoding
92
+
- ensures resolution actions work correctly
93
+
94
+
**impact**:
95
+
- faster moderation workflow (one-click access to verify tracks)
96
+
- better audit trail (reasons tracked for false positive resolutions)
97
+
- more transparent (shows match confidence scores)
98
+
- more reliable (form submission works consistently)
99
+
100
+
---
101
+
44
102
### ATProto labeler and admin UI improvements (PRs #385-395, Nov 29-Dec 1, 2025)
45
103
46
104
**motivation**: integrate with ATProto labeling protocol for proper copyright violation signaling, and improve admin tooling for reviewing flagged content.
···
77
135
- `moderation/static/admin.js` - auth handling (~40 lines)
78
136
- htmx endpoints: `/admin/flags-html`, `/admin/resolve-htmx`
79
137
- server-rendered HTML partials for flag cards
80
-
81
-
---
82
-
83
-
### copyright moderation system (PRs #382, #384, Nov 29-30, 2025)
84
-
85
-
**motivation**: detect potential copyright violations in uploaded tracks to avoid DMCA issues and protect the platform.
86
-
87
-
**what shipped**:
88
-
- **moderation service** (Rust/Axum on Fly.io):
89
-
- standalone service at `plyr-moderation.fly.dev`
90
-
- integrates with AuDD enterprise API for audio fingerprinting
91
-
- scans audio URLs and returns matches with metadata (artist, title, album, ISRC, timecode)
92
-
- auth via `X-Moderation-Key` header
93
-
- **backend integration** (PR #382):
94
-
- `ModerationSettings` in config (service URL, auth token, timeout)
95
-
- moderation client module (`backend/_internal/moderation.py`)
96
-
- fire-and-forget background task on track upload
97
-
- stores results in `copyright_scans` table
98
-
- scan errors stored as "clear" so tracks aren't stuck unscanned
99
-
- **flagging fix** (PR #384):
100
-
- AuDD enterprise API returns no confidence scores (all 0)
101
-
- changed from score threshold to presence-based flagging: `is_flagged = !matches.is_empty()`
102
-
- removed unused `score_threshold` config
103
-
- **backfill script** (`scripts/scan_tracks_copyright.py`):
104
-
- scans existing tracks that haven't been checked
105
-
- `--max-duration` flag to skip long DJ sets (estimated from file size)
106
-
- `--dry-run` mode to preview what would be scanned
107
-
- supports dev/staging/prod environments
108
-
- **review workflow**:
109
-
- `copyright_scans` table has `resolution`, `reviewed_at`, `reviewed_by`, `review_notes` columns
110
-
- resolution values: `violation`, `false_positive`, `original_artist`
111
-
- SQL queries for dashboard: flagged tracks, unreviewed flags, violations list
112
-
113
-
**initial review results** (25 flagged tracks):
114
-
- 8 violations (actual copyright issues)
115
-
- 11 false positives (fingerprint noise)
116
-
- 6 original artists (people uploading their own distributed music)
117
-
118
-
**impact**:
119
-
- automated copyright detection on upload
120
-
- manual review workflow for flagged content
121
-
- protection against DMCA takedown requests
122
-
- clear audit trail with resolution status
123
-
124
-
---
125
-
126
-
### platform stats and media session integration (PRs #359-379, Nov 27-29, 2025)
127
-
128
-
**motivation**: show platform activity at a glance, improve playback experience across devices, and give users control over their data.
129
-
130
-
**what shipped**:
131
-
- **platform stats endpoint and UI** (PRs #376, #378, #379):
132
-
- `GET /stats` returns total plays, tracks, and artists
133
-
- stats bar displays in homepage header (e.g., "1,691 plays • 55 tracks • 8 artists")
134
-
- skeleton loading animation while fetching
135
-
- responsive layout: visible in header on wide screens, collapses to menu on narrow
136
-
- end-of-list animation on homepage
137
-
- **Media Session API** (PR #371):
138
-
- provides track metadata to CarPlay, lock screens, Bluetooth devices, macOS control center
139
-
- artwork display with fallback to artist avatar
140
-
- play/pause, prev/next, seek controls all work from system UI
141
-
- position state syncs scrubbers on external interfaces
142
-
- **browser tab title** (PR #374):
143
-
- shows "track - artist • plyr.fm" while playing
144
-
- persists across page navigation
145
-
- reverts to page title when playback stops
146
-
- **timed comments** (PR #359):
147
-
- comments capture timestamp when added during playback
148
-
- clickable timestamp buttons seek to that moment
149
-
- compact scrollable comments section on track pages
150
-
- **constellation integration** (PR #360):
151
-
- queries constellation.microcosm.blue backlink index
152
-
- enables network-wide like counts (not just plyr.fm internal)
153
-
- environment-aware namespace handling
154
-
- **account deletion** (PR #363):
155
-
- explicit confirmation flow (type handle to confirm)
156
-
- deletes all plyr.fm data (tracks, albums, likes, comments, preferences)
157
-
- optional ATProto record cleanup with clear warnings about orphaned references
158
-
159
-
**impact**:
160
-
- platform stats give visitors immediate sense of activity
161
-
- media session makes plyr.fm tracks controllable from car/lock screen/control center
162
-
- timed comments enable discussion at specific moments in tracks
163
-
- account deletion gives users full control over their data
164
-
165
-
---
166
-
167
-
### developer tokens with independent OAuth grants (PR #367, Nov 28, 2025)
168
-
169
-
**motivation**: programmatic API access (scripts, CLIs, automation) needed tokens that survive browser logout and don't become stale when browser sessions refresh.
170
-
171
-
**what shipped**:
172
-
- **OAuth-based dev tokens**: each developer token gets its own OAuth authorization flow
173
-
- user clicks "create token" → redirected to PDS for authorization → token created with independent credentials
174
-
- tokens have their own DPoP keypair, access/refresh tokens - completely separate from browser session
175
-
- **cookie isolation**: dev token exchange doesn't set browser cookie
176
-
- added `is_dev_token` flag to ExchangeToken model
177
-
- /auth/exchange skips Set-Cookie for dev token flows
178
-
- prevents logout from deleting dev tokens (critical bug fixed during implementation)
179
-
- **token management UI**: portal → "your data" → "developer tokens"
180
-
- create with optional name and expiration (30/90/180/365 days or never)
181
-
- list active tokens with creation/expiration dates
182
-
- revoke individual tokens
183
-
- **API endpoints**:
184
-
- `POST /auth/developer-token/start` - initiates OAuth flow, returns auth_url
185
-
- `GET /auth/developer-tokens` - list user's tokens
186
-
- `DELETE /auth/developer-tokens/{prefix}` - revoke by 8-char prefix
187
-
188
-
**security properties**:
189
-
- tokens are full sessions with encrypted OAuth credentials (Fernet)
190
-
- each token refreshes independently (no staleness from browser session refresh)
191
-
- revokable individually without affecting browser or other tokens
192
-
- explicit OAuth consent required at PDS for each token created
193
-
194
-
**testing verified**:
195
-
- created token → uploaded track → logged out → deleted track with token ✓
196
-
- browser logout doesn't affect dev tokens ✓
197
-
- token works across browser sessions ✓
198
-
- staging deployment tested end-to-end ✓
199
-
200
-
**documentation**: see `docs/authentication.md` "developer tokens" section
201
-
202
-
---
203
-
204
-
### oEmbed endpoint for Leaflet.pub embeds (PRs #355-358, Nov 25, 2025)
205
-
206
-
**motivation**: plyr.fm tracks embedded in Leaflet.pub (via iframely) showed a black HTML5 audio box instead of our custom embed player.
207
-
208
-
**what shipped**:
209
-
- **oEmbed endpoint** (PR #355): `/oembed` returns proper embed HTML with iframe
210
-
- follows oEmbed spec with `type: "rich"` and iframe in `html` field
211
-
- discovery link in track page `<head>` for automatic detection
212
-
- **iframely domain registration**: registered plyr.fm on iframely.com (free tier)
213
-
- this was the key fix - iframely now returns our embed iframe as `links.player[0]`
214
-
- API key: stored in 1password (iframely account)
215
-
216
-
**debugging journey** (PRs #356-358):
217
-
- initially tried `og:video` meta tags to hint iframe embed - didn't work
218
-
- tried removing `og:audio` to force oEmbed fallback - resulted in no player link
219
-
- discovered iframely requires domain registration to trust oEmbed providers
220
-
- after registration, iframely correctly returns embed iframe URL
221
-
222
-
**current state**:
223
-
- oEmbed endpoint working: `curl https://api.plyr.fm/oembed?url=https://plyr.fm/track/92`
224
-
- iframely returns `links.player[0].href = "https://plyr.fm/embed/track/92"` (our embed)
225
-
- Leaflet.pub should show proper embeds (pending their cache expiry)
226
-
227
-
**impact**:
228
-
- plyr.fm tracks can be embedded in Leaflet.pub and other iframely-powered services
229
-
- proper embed player with cover art instead of raw HTML5 audio
230
-
231
-
---
232
-
233
-
### export & upload reliability (PRs #337-344, Nov 24, 2025)
234
-
235
-
**motivation**: exports were failing silently on large files (OOM), uploads showed incorrect progress, and SSE connections triggered false error toasts.
236
-
237
-
**what shipped**:
238
-
- **database-backed jobs** (PR #337): moved upload/export tracking from in-memory to postgres
239
-
- jobs table persists state across server restarts
240
-
- enables reliable progress tracking via SSE polling
241
-
- **streaming exports** (PR #343): fixed OOM on large file exports
242
-
- previously loaded entire files into memory via `response["Body"].read()`
243
-
- now streams to temp files, adds to zip from disk (constant memory)
244
-
- 90-minute WAV files now export successfully on 1GB VM
245
-
- **progress tracking fix** (PR #340): upload progress was receiving bytes but treating as percentage
246
-
- `UploadProgressTracker` now properly converts bytes to percentage
247
-
- upload progress bar works correctly again
248
-
- **UX improvements** (PRs #338-339, #341-342, #344):
249
-
- export filename now includes date (`plyr-tracks-2025-11-24.zip`)
250
-
- toast notification on track deletion
251
-
- fixed false "lost connection" error when SSE completes normally
252
-
- progress now shows "downloading track X of Y" instead of confusing count
253
-
254
-
**impact**:
255
-
- exports work for arbitrarily large files (limited by disk, not RAM)
256
-
- upload progress displays correctly
257
-
- job state survives server restarts
258
-
- clearer progress messaging during exports
259
138
260
139
---
261
140
update.wav
update.wav
This is a binary file and will not be displayed.