3 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| efaa7f55f0 |
Add music: Artist/Album/Track browsing + participative listen-party queue
All checks were successful
Publier l'image Docker / publish (push) Successful in 7s
The last deferred piece from the original spec. Chosen permission model (asked, not assumed): anyone can add a track to the shared queue any time; anyone can pause/resume directly; skipping the current track needs a strict majority vote of currently connected members. No host for playback - a genuinely different shape of shared state than video's "host drives, everyone follows", which is why it's a separate service (server/src/musicQueueService.js, MusicQueueService) rather than more branches in partyManager.js/socketHandler.js's existing video-shaped logic. Server: - musicQueueService.js: owns a music party's queue/current-track/pause/ vote-skip state. Server-authoritative timing (no client is "the host" reporting position) - a wall-clock startedUtc plus the track's known duration is the only state; auto-advance is one setTimeout scheduled for exactly when that runs out, not any client reporting "it ended" (which could race across several players finishing near-simultaneously with no host to arbitrate). attachIo(io, onTrackChanged), called once at startup like PartyManager's own wiring, lets it broadcast "queueUpdated" from inside that timer's callback, which has no socket/request in scope at all. - The timer is the one new leak surface this adds, given how much this project has already had to fix real leaks - destroy(party) clears it, wired into both of partyManager.js's existing destruction paths (leave's last-member case, removeIdleParties' sweep). - Party gained mediaType: 'video'|'music' (inferred server-side from the clicked item's Emby Type - the client never has to know or say which). - Three new catalog endpoints (Artists -> Albums -> Tracks), same reasoning as the existing Shows -> Seasons -> Episodes: a real Emby endpoint per level, not generic recursive /Items guessing. - Audio streams through the *same* /master.m3u8 route as video (Emby's PlaybackInfo negotiation isn't video-specific) - the one difference is the hand-built-fallback path prefix (/Audio/ vs /Videos/), which the client tags via a mediaType=audio query hint rather than costing an extra Emby round trip to detect server-side. - Adding a track is REST (POST /api/parties/:partyId/tracks), not a socket event like voteSkip/musicPlaystate - it needs to work from the player's inline browse-and-add panel regardless of realtime-connection specifics, and MusicQueueService's own broadcast reaches everyone connected either way. Client: - player.js's renderPartyPlayer is now a small dispatcher: fetches the party once, then delegates to renderVideoPlayer or the new renderMusicPlayer (views/musicPlayer.js) based on mediaType. Can't be async despite awaiting that fetch - route()'s contract needs its cleanup function back synchronously (router.js's `typeof cleanup === 'function'` check would silently skip a Promise, reintroducing the exact leak class already fixed once) - solved with a mutable box returned immediately, filled in once the fetch resolves. - musicPlayer.js: not full-viewport/theater-mode like the video player (no image to declutter around, sidebar stays up) - which is what makes its own inline Artist -> Album -> Track browser work for "add tracks while listening" without navigating away and losing player state or threading party context through query params across three route levels. - New views/artist.js, views/album.js (library.js's music branch renders an artist grid instead of the old placeholder). tiles.js's DRILL_DOWN table generalizes the old single seriesHref special-case now that there are three drill-down targets, not two. - party.js: addTrackToQueue (REST) alongside voteSkip/setMusicPaused (socket) on the existing connectParty() connection object. Real bug found while testing, not just the mock's fault: applyMusicState used to update the queue list/vote button *after* the current-track branch, which calls loadTrack() - and loadTrack()'s hls.js setup can throw. Since JS doesn't roll back earlier statements when a later one throws but does abort everything after it, an exception there took the unrelated queue list and vote tally down with it, and on the first render escaped all the way to the dispatcher's catch, which alerts and bounces back to /home - discarding the whole view over what's really just "this one track's audio failed to load". Fixed by reordering (queue/vote/icon updates - which can't fail - before the track-loading branch, mirroring how player.js's onItemChanged already updates title/next-button before calling loadVideo) and wrapping loadTrack's hls.js setup in its own try/catch so a failure there logs and stops instead of propagating. Verified server-side directly against the real shipped modules (not reimplemented mocks): 18 checks against musicQueueService.js alone (auto -start, queueing, vote-skip majority, pause/resume elapsed-time math, a real setTimeout auto-advance, timer cleanup on destroy - instrumented setTimeout/clearTimeout counting, same technique as the earlier client -side leak tests) and 7 more through socketHandler.js + partyManager.js together (no-ops on a video party, a non-host pausing/resuming, a two-vote majority skip, the resulting broadcast really reaching the room). Client-side against the real local server with mocked fetch/Hls/ io: the full browsing chain, party creation, the dispatcher choosing the right player (and still choosing video when mediaType is absent - back -compat), now-playing/vote-tally/queue rendering and updates including a live track change, play/pause and vote-skip emitting the right events, the inline browse-and-add panel POSTing the right payload, and the crash fix re-confirmed fixed via the same broken scenario that first found it. Not verified: the real Emby round trip for any of this (Artists/albums -tracks response shapes, audio PlaybackInfo/TranscodingUrl behavior, actual playback quality) - same limit as everything else in this project tested without live Emby access. docs/architecture.md has the full writeup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| b6019bd211 |
Live lobby, host handoff reflected live, remove join codes, fix player scroll
All checks were successful
Publier l'image Docker / publish (push) Successful in 7s
Three requested changes plus one found-in-passing bug fix: 1. Home page's "watch parties en cours" is now a live Socket.IO subscription (connectLobby -> "lobby" room), not a one-shot fetch - updates the instant anything changes anywhere (create/join/leave). Host name + member count come from the snapshot; elapsed time ticks purely client-side from a createdUtc timestamp (no point pushing a recomputed duration every second for every viewer). home.js returns this subscription from its router cleanup, same leak-prevention pattern as the player's socket. 2. Host handoff is now live end to end, not just correct in the data model. PartyManager.leave already promoted another member and destroyed an empty party (that part predates this change) - what was missing was any way for an already-open follower player to find out it became host. applyLeave (shared by the socket leave/ disconnect handlers and the REST leave endpoint, so this and the lobby broadcast only live in one place) now emits a personal "hostChanged" to the new host's own socket; player.js's becomeHost() unlocks controls live. becomeHost is written to tolerate being called more than once safely (addEventListener no-ops on a repeat identical listener; the heartbeat setInterval is explicitly guarded since it has no such idempotence - calling it twice would leak the first interval, the exact bug class the previous pass fixed). 3. No more join codes - PartyManager no longer generates or tracks one. The live lobby list is the only discovery mechanism now; a code copied out of band doesn't add anything on top of a list that's already always current. POST /api/parties/:partyId/join replaces :joinCode; the "Rejoindre avec un code" UI is gone. Found while touching this: the home page's "Rejoindre" button used to navigate to the player WITHOUT ever calling the join endpoint first - it loaded fine (GET /api/parties/:id never checked membership) but the realtime "join" was silently rejected server-side (not an actual member), so sync never worked for anyone joining this way. Fixed to await joinParty() before navigating, matching how the library grid's tiles already did it. Also: .playerWrap was width:100%/height:100vh inside .mainContent's own padding, so the two stacked and made the page taller than the viewport - an outer scrollbar despite the video already being full- height. Changed to position:fixed/inset:0 so it fills the real viewport regardless of ancestor padding. Verified live against a real running server (see session history): lobby subscription renders host/member-count/ticking-elapsed correctly from a faked "parties" push (createdUtc 2:05 in the past rendered "2:06" after ~1s more, proving the client-side tick is real, not static); the join-then-navigate fix calls the join endpoint before navigating; .playerWrap computes to position:fixed with all four insets at 0. docs/architecture.md has the full writeup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| f563f5b0a2 |
Emby-style UI: sidebar, carousels, series drill-down, episode queue, player tracks
All checks were successful
Publier l'image Docker / publish (push) Successful in 7s
Big UI pass towards looking/behaving like Emby. Deferred: music (needs
its own player + a real-time collaborative playlist - different enough
subsystem to be its own pass; sidebar/routing already treat it as a
first-class collectionType, page just says "not yet" for now).
Layout:
- client/js/shell.js: persistent sidebar (one entry per Emby library,
from GET /api/catalog/libraries which now also returns
collectionType) + a #mainContent the router swaps on navigation.
router.js made idempotent (start() safe to call more than once) so
a login -> shell boot can happen twice in one page lifetime (logout
then back in) without piling up duplicate hashchange listeners.
- app.js's boot() renders either the bare login screen or the full
shell depending on session state; login.js calls it again on
success instead of navigate() (no router running yet at that point).
Browsing, by collection type:
- views/home.js (route /home): a carousel of every active party
server-wide, a join-by-code fallback, then one "recently added"
carousel per non-music library (GET /api/catalog/libraries/:id/recent,
Emby's Items/Latest).
- views/library.js (route /library/:id): dispatches on collectionType -
movies/tvshows get the tile grid (Series tiles link into /series/:id
instead of starting a party directly), music gets an honest
placeholder.
- views/series.js (route /series/:id): season tabs + episode list
(new GET /api/catalog/series/:id/seasons and .../episodes, Emby's
/Shows/{id}/Seasons and /Episodes).
- tiles.js: poster-tile rendering shared by home.js and library.js so
they can't drift apart.
Episode queue / "next episode":
- POST /api/parties now builds a queue when starting from an episode
(fetches the rest of the series, from that episode onward) -
partyManager.js gained queue/queueIndex/advanceQueue.
- New WS message {type:"next"} (host-only) advances the queue and
broadcasts {type:"itemChanged", itemId, itemName, ...}; every client
reloads the player against the new item. player.js shows "Épisode
suivant" when hasNext, and auto-sends next() on the video's `ended`
event.
Player: language/subtitles/quality:
- New GET /api/catalog/item/:id/playback-options exposes PlaybackInfo's
MediaStreams as audio/subtitle track lists.
- master.m3u8 now forwards AudioStreamIndex/SubtitleStreamIndex/
MaxStreamingBitrate to PlaybackInfo; player.js has three <select>s
(host-only) that reload the stream at the same position on change.
Subtitles end up burned into the transcode (via SubtitleStreamIndex),
not a toggleable track - simpler than sidecar WebVTT for this pass.
"Quality" is a MaxStreamingBitrate ceiling, not true adaptive bitrate.
Verified by driving a browser against mocked /api/* responses (see
session history): sidebar renders/highlights correctly, home carousels
render (parties + one row per non-music library, music correctly
skipped), library grid dispatch works, series page season-tab
switching + episode list render and the season/episode navigation
flow works end to end. Real Emby response shapes for all of this
(Views' CollectionType, Seasons/Episodes, Items/Latest, PlaybackInfo's
MediaStreams) and the queue/track-picker protocol are NOT yet
confirmed against a live server - see docs/architecture.md.
Removed client/js/views/browse.js, superseded by home.js + library.js.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|