Commit graph

5 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>
2026-09-14 01:49:22 +02:00
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>
2026-09-13 22:44:49 +02:00
07613c1cec Redesign home page: library tabs, poster tiles, ongoing parties list
All checks were successful
Publier l'image Docker / publish (push) Successful in 7s
- New backend endpoints: GET /api/catalog/libraries (Emby's
  /Users/{id}/Views), GET /api/catalog/libraries/:id/items (top-level,
  Recursive=false - movies as movies, TV as Series tiles), GET
  /api/parties (list every active party server-wide, for the home
  page's "watch parties en cours" section)
- Item DTO gained `type` and `canWatchParty` (only Movie/Episode/Video
  are directly streamable - a Series tile only gets "Voir sur Emby")
  and `embyUrl`, a deep link into Emby's own web client built from
  EMBY_SERVER_URL + serverId (now captured at login)
- Party creation now fetches and stores the item's name/poster so the
  home page's party list doesn't need a separate lookup per party
- client/js/views/browse.js rewritten: ongoing-parties section, a
  join-by-code fallback, one tab per library, and a tile grid with a
  hover-revealed two-button overlay (start party / view on Emby) - the
  first only shown for playable types
- Removed itemDetail.js/the /item/:id route (tiles act directly now,
  no intermediate detail page); player.js simplified to use
  party.itemName instead of a redundant item lookup
- app.css: full-width home layout (no max-width cap, as asked), CSS
  grid handling all the responsive reflow, :hover/:focus-within reveal
  for the tile overlay with an always-visible (hover: none) fallback
  for touch devices

Verified by actually driving a browser (mocked fetch standing in for
/api/parties and /api/catalog/* - real login still needs real
credentials): tab switching swaps content correctly, the overlay
reveals per-tile only, a Series tile correctly omits the watch-party
button, and the touch fallback shows both buttons inline without
hover. See docs/architecture.md for exactly what that does and doesn't
confirm (real Emby /Views and item-list response shapes are still
unverified).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 21:54:14 +02:00
b6818655b1 Proxy all Emby access through our own backend; drop server-url field; use Emby's real theme colors
All checks were successful
Publier l'image Docker / publish (push) Successful in 6s
Three changes requested:

1. Server proxies Emby, browser never talks to it directly. Login is
   now POST /api/auth/login {username,password} on our own backend;
   the backend calls Emby's AuthenticateByName server-side and mints
   its own opaque session (httpOnly cookie), storing
   {embyToken,userId,userName} in memory. The browser never sees or
   holds a real Emby access token. Every other call - search, item
   details, images, HLS streaming, party actions - goes to our own
   /api/* origin, authenticated by that cookie; server/src/embyProxy.js
   forwards to Emby server-side and streams the response back
   (including a playlist-rewrite pass for HLS .m3u8 so hls.js never
   gets handed a URL pointing at Emby directly). This also sidesteps
   the CORS problem the previous direct-to-Emby design had.

2. EMBY_SERVER_URL is backend-only config (.env.example); the login
   form no longer asks for a server address at all.

3. client/css/app.css now uses colors read directly off the live Emby
   server's own dark theme (--theme-primary-color-* etc, fetched via a
   JS probe against the running client, not guessed): accent green
   #52b54b/#6ccf65, near-black background #151216.

Tested for real: ran the server locally against the actual
EMBY_SERVER_URL and submitted intentionally-wrong credentials through
the UI - confirmed the full round trip (browser -> our backend -> real
Emby -> real 401 -> our error message) with no CORS issue, since it's
all server-to-server now. Login itself hasn't been exercised with real
credentials, so catalog/streaming remain unverified - see
docs/architecture.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 21:36:26 +02:00
6ff9aa29e2 Pivot to a standalone app - Emby plugin hit a hard wall
No plugin API exists for adding a button to Emby's player or item
detail page (confirmed by an Emby developer on their own forum, and
by research since). The only way to reach that would be patching
Emby's shipped web client files directly - outside the plugin,
breaks on every image update, needs manual reapplication. Not worth
the ongoing cost, per discussion.

New direction: a standalone app with its own player. Reads Emby's
catalog/streams directly from the browser (no plugin, no menu
integration fights); a small Node backend only coordinates party
membership and relays playback state over WebSocket - it never
touches a media byte. Auth is Emby-only, as chosen: the browser signs
in against Emby directly and this backend validates whatever token it
gets handed by asking Emby itself (GET /Users/{userId}), never seeing
a password.

Control model changed too: now that we own the player, host controls
playback and everyone else follows (event-driven play/pause/seek +
a 3s heartbeat, drift-corrected on the follower side) - the old
plugin's "everyone keeps their own controls, shared reference
position" was specifically a concession to nudging pre-existing Emby
app sessions we didn't control, which no longer applies.

- server/: Express + ws backend (party lifecycle, WebSocket protocol,
  Emby token validation)
- client/: plain-JS/ES-modules frontend (login, search, item detail,
  party player) - no build step
- Smoke-tested locally end to end against a stubbed Emby URL: server
  starts, routing/auth-gating works, error handling degrades
  gracefully (network failure, invalid token) - see the session
  transcript for the actual browser test run
- docs/architecture.md rewritten around the new design, including why
  the plugin was abandoned and a fresh "assumptions to verify" list
  (CORS between this app's origin and Emby's is the biggest unknown)
- src/WatchParty/ (the Emby plugin) kept as-is for reference, no
  longer the active direction

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 20:48:55 +02:00