chore: weekly status maintenance (#417)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>

authored by claude[bot] claude[bot] and committed by GitHub 042d20e4 be087ae9

Changed files
+237 -179
.status_history
+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
··· 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

This is a binary file and will not be displayed.