Repository navigation
feat(web): play a set's recording with its tracklist following along - #274
Merged
Merged
Conversation
A set with a recording or a playable link gets a Play button in DjSets. Pressing it asks /sets/<did>/<rkey>/player.json where it plays, and the server picks, audio first: the set's recording record (an fm.plyr.track, from plyr.fm's CDN through its public API, or plyr's embed player when the track is gated; or a computer.localnetwork.dj.recording on the DJ's PDS), then its links (archive.org downloads play directly, the platforms in their own players). SetPlayer marks the track playing in the tracklist, starts a track when its start time or row is clicked, skips between tracks, and feeds the Media Session API. SoundCloud's and Mixcloud's widget APIs let it follow those players too. The mix report page streams the linked set's source, falling back to the mix's link, and picking a track there plays from it. /media/audio/<did>/<cid> streams a recording's blob from the DJ's PDS: only one an indexed dj.recording of an active project's manager references, as the audio type and size it declares, answering Range itself since getBlob ignores it. It stores nothing. /sets/<did>/<rkey>/chapters.json serves a set's tracklist as Podcasting 2.0 JSON chapters. embedUrl knows plyr.fm track pages, and the CSP adds media-src and the widget and plyr.fm hosts. The client bundle budget in tests/build.test.ts goes from 620 to 640 KiB of JavaScript: the player adds about 15 KB to every page's code together (628,219 bytes before, 643,120 after), and the build was already within 7 KB of the old limit.
# Conflicts: # apps/web/src/lib/nsid.ts
The set player's PDS recordings were written before computer.localnetwork.dj.recording existed, and disagreed with it in three places. The size cap was 300,000,000 bytes while the lexicon's audio maxSize is 314,572,800 (300 MiB), so a recording locallibrary uploaded near the cap wouldn't play. RECORDING_MAX_BYTES is now read from the lexicon file, and so is the accepted type list, matched with @atproto/oauth-scopes' BlobPermission the way a PDS matches a blob scope. That list takes video/mp4 besides audio/*: a PDS names a blob's type by sniffing it, and an M4A that doesn't brand itself as audio comes back as video/mp4. Those recordings now play, and /media/audio/<did>/<cid> sends them as audio/mp4. durationSeconds is optional in the lexicon and the player reads the length from the audio, so the schema no longer parses it. The recording service tests now index their records through HappyView's real dj/recording.lua gate instead of seeding them as written, and cover a video/mp4 recording, one at exactly 300 MiB and one a byte over, and one without durationSeconds. The README describes the audio route's limits as the lexicon's.
bnb
previously approved these changes
Oct 29, 2026
Main's stored-type-response ast-grep rule fails any response in apps/web/src whose content type comes from data. The set player's recording route set its type from the recording record, and the chapters route from an imported constant, so both tripped it. apps/web/src/lib/server/audio.ts sits beside images.ts and holds what the recording lexicon says about audio: RECORDING_TYPES, its accept list (audio/* and video/mp4) as the BlobPermission matcher the recording service already used, and RECORDING_MAX_BYTES, its maxSize. Its audioResponse lowercases the type, answers a plain 404 and cancels the body for a type outside that list, sends video/mp4 as audio/mp4, and sets the CSP and nosniff the route already sent. media/audio.ts keeps its Range, 206 and 416 handling, content-length, etag and cache-control, and hands the body and type to audioResponse. The recording service reads its type list and size cap from the new module instead of its own copies. The chapters route now writes application/json+chapters as a literal, and the CHAPTERS_TYPE constant it was the only user of is gone. The rule ignores audio.ts as it does images.ts, its note names both helpers, and its tests take an audioResponse call as valid. New tests cover the accepted types and their edges (video/webm, audiox/mpeg, audio/*), the 300 MiB cap, case, the video/mp4 mapping and refusals; the route's tests check a refused type cancels the PDS read. The README lists the rule after both stored-media bullets.
botandbang
marked this pull request as ready for review
October 29, 2026 19:02
Coverage Report for apps/web
File Coverage
SizeCompared with main. KiB are 1,024 bytes.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
bnb
approved these changes
Oct 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Sets on Local Network can now be played on the site, with their tracklist following along.
Added:
apps/web/src/lib/components/mix/SetPlayer.svelte): plays a set's recording and shows the track playing now, with play/pause, previous and next track, and a position bar marked at each track's start. It feeds the Media Session API: the track's title and artist, the set's name, plyr.fm artwork, and play, pause, seek and track actions.DjSets.sveltethat has a recording or a playable link. While the player is open, the set's tracklist unfolds, marks the track playing, and starts any track from its time or its row./projects/<slug>/mixes/<n>), the player replaces the platform embed. Picking a track in the timeline, tracklist, key wheel or tempo chart plays from it, and the tracklist marks the track playing.GET /sets/<did>/<rkey>/player.json, which picks where a set plays when someone presses Play (apps/web/src/lib/server/services/recordings.ts). It prefers audio the page plays itself over a platform's player: the set'srecording, either anfm.plyr.trackthrough plyr.fm's public API (apps/web/src/lib/server/plyr.ts: its file onaudio.plyr.fm, or plyr's embed player when the track is gated) or acomputer.localnetwork.dj.recordingon the DJ's PDS; then its links, where archive.org downloads play directly and SoundCloud, Mixcloud, YouTube and plyr.fm use their own players. Mixcloud's and SoundCloud's widget APIs (apps/web/src/lib/mix/widgets.ts) let the tracklist follow those players too.GET /media/audio/<did>/<cid>(apps/web/src/lib/server/media/audio.ts), which streams a recording's blob from the DJ's PDS: only a blob referenced by an indexeddj.recordingof an active project's manager, as the type and size that record declares, within the lexicon'sacceptlist andmaxSize(read fromlexicons/computer/localnetwork/dj/recording.json). Avideo/mp4blob, which a PDS reports for an M4A without its audio brand, goes out asaudio/mp4. It answers Range itself, sincegetBlobignores it, and stores nothing. HappyView doesn't serve blob bytes, so this read goes to the PDS with anast-grep-ignoresaying why.GET /sets/<did>/<rkey>/chapters.json, a set's tracklist as Podcasting 2.0 JSON chapters (apps/web/src/lib/mix/chapters.ts).Changed:
embedUrlinapps/web/src/lib/mix/links.tsturns plyr.fm track pages into plyr's embed player.apps/web/vite.config.tsadds amedia-srcfor the site,audio.plyr.fmand archive.org's download hosts, addswidget.mixcloud.comandw.soundcloud.comtoscript-src, andplyr.fmtoframe-src.MixEmbed.svelteexposes its iframe for the widget APIs.apps/web/src/lib/server/audio.tsholds the recording types and size from the lexicon and builds the audio response, refusing other types and servingvideo/mp4asaudio/mp4, so/media/audiopassestools/ast-grep/rules/stored-type-response.ymlthe wayimages.tsdoes; the rule ignores both helpers. The chapters route's content type is a literal.apps/web/README.mddescribes the player, the audio route and the CSP.The client JavaScript comes to 635.1 KiB, within main's 640 KiB budget.
How it was tested
source.test.ts,recordings.test.ts,plyr.test.ts), the audio route over a fake PDS (audio.test.ts: ranges across chunks, 416, refusing unreferenced CIDs, non-audio and oversize records and non-managers, the exact 300 MiB boundary,video/mp4served asaudio/mp4, the upstream read stopping after the range), and the chapters (chapters.test.ts). The recordings tests index their records through the realdj/recording.luagate.set-player.svelte.test.ts: the track playing as the recording plays on, seeking from the position bar and from the page, the keyboard, Media Session metadata, errors and retry, following Mixcloud's and SoundCloud's players, DjSets and the mix report.pnpm dev, at 390 and 1280 px: a demo set given an archive.org link played and seeked under the CSP, with the track in the system's media controls; the SoundCloud player and the mix report page opened; axe found no violations.pnpm -C apps/web verify,pnpm -C apps/web test:coverage(lines 94.68, statements 93.15, functions 93.59, branches 83.33), the rootpnpm verifyandast-grep scanpass.Screenshots
Phone (390 px) and wide (1280 px), from the dev site and Storybook, to attach before review. The site has no light theme on screen, and the player is hidden in print.
Checklist
apps/locallibrary/CHANGELOG.mdhas a line under Unreleased, if people using it will noticedocs/and--helpstill say what the code does