4 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| b45a243e59 |
Browse Emby playlists and launch one as a pre-queued party
Queued right after the ffmpeg burn-in work, asked for directly: see
what's inside a playlist, and launch it - pre-queued with the whole
thing, the same "one click, the rest queues itself" shape starting from
an episode already gets for its series.
- server/src/services/CatalogService.ts: VISIBLE_COLLECTION_TYPES gains
'playlists' - Emby's /Views already returns that CollectionType once
the user has any, so it appears in the sidebar through the exact same
code every other library already uses, nothing else needed there. New
getPlaylistItemsRaw/getPlaylistItems (Emby's own /Playlists/{id}/Items,
mirroring getSeriesEpisodes/getAlbumTracks). New exported
splitPlaylistByMediaType: a playlist can mix movies/episodes and songs,
this app's party model can't - decides video vs music from the first
item's type and drops anything that doesn't match (logged when it
does) - pure, unit-tested on its own.
- server/src/index.ts: new GET /api/catalog/playlists/:playlistId/items
route. POST /api/parties gains a sibling early branch for
item.Type === 'Playlist' (a new createPartyFromPlaylist helper,
deliberately NOT merged into the existing single-item path, which
didn't need touching) - builds a video QueueItem[] exactly like the
episode-queue path already does, or seeds MusicQueueService.init +
addTrack per track for a music result, the same mechanism
POST /api/parties/:id/tracks already uses.
- client/src/tiles.ts: one more DRILL_DOWN entry (Playlist -> its own
page) - a playlist was never directly streamable, same as
Series/MusicArtist/MusicAlbum before it.
- client/src/api/embyApi.ts: getPlaylistItems(). No new API for
*launching* one - createParty(playlistId) already does everything,
same as clicking any other item; the server decides what it means.
- client/src/views/playlist/Playlist.{ts,html,css} (new): a playlist's
contents, read-only past the one "Lancer cette playlist" button -
deliberately no per-item start/add the way album tracks or episode
rows get (an Emby item carries no back-reference to "which playlist
contains me" the way an Episode's SeriesId does, so "start from item
N, queue the rest of the playlist" isn't cheaply reconstructable the
same way - not asked for here anyway). The launch button branches on
the created party's mediaType, same check Player.ts's own dispatcher
already makes.
- client/src/app.ts: registers /playlist/:id.
- server/test/catalogService.test.ts: the new methods' target
URL/mapping, plus splitPlaylistByMediaType's own tests (homogeneous
video, homogeneous music, mixed, empty).
- docs/architecture.md, README.md: design + the scope decision (no
per-item actions) spelled out.
Verified: full typecheck+test+build on both projects (62/62 server
tests). The new page driven through the same mocked-fetch + real-bundle
harness used throughout this session, in a real browser: header/meta/
item list render correctly from mocked catalog data (including the
sidebar showing "Playlists" once a mocked library declares that
CollectionType), and clicking "Lancer cette playlist" reaches
POST /api/parties with the right id and correctly routes into the video
player for a video result. Actual playback past that point still needs
a real Emby server - same standing caveat as the rest of this app.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|||
| 26da8fc5dc |
Self-hosted ffmpeg pipeline for PGS (image) subtitle burn-in
Asked for directly, right after the client-side <track> redesign closed
every *text* subtitle case: PGS/VobSub/DVB subtitles (Blu-ray/DVD rips)
are bitmaps, not text - there's nothing for Emby's Stream.vtt conversion
to extract, so a native <track> fundamentally can't render one. Rather
than keep asking Emby's own transcode to burn these in (the exact thing
that kept failing in earlier sessions - missing fontconfig, then an
unrelated "Error starting ffmpeg"), this runs ffmpeg ourselves: a PGS
stream decodes like a second video track, and ffmpeg's `overlay` filter
composites it directly - no text rendering, no libass, no fontconfig
anywhere in this path.
One shared stream per PARTY, not per viewer - explicitly asked for after
the initial plan: every member plugs into the exact same server-emitted
stream, mirroring "host drives, everyone follows" rather than a
per-viewer model. Picking a burn-in subtitle is therefore a host-only,
party-wide choice, like play/pause/next already are.
- server/src/services/TranscodeService.ts (new): one ffmpeg session per
partyId. getOrStartSession negotiates PlaybackInfo (via the new
CatalogService.getMediaSource), converts Emby's absolute
MediaStreams[].Index into ffmpeg's type-relative 0:s:N/0🅰️N
(absoluteToRelativeIndex - pure, unit-tested), and spawns ffmpeg
against Emby's own direct/static file endpoint (TODO(verify) - no
Emby server reachable from this dev environment, see the function's
own comment). Output is real progressive HLS to a temp dir, served by
two new routes that poll briefly for the next file. endSession/
reapIdleSessions handle teardown; PartyService's destroy path and
advanceQueue both end a party's session immediately, the idle sweep
is the backstop.
- server/src/services/PartyService.ts: Party/PartyPublicState gain
burnInSubtitleIndex/burnInAudioIndex (null together = no burn-in);
advanceQueue clears them (a new item's indices mean nothing on the
old one).
- server/src/realtime/SocketGateway.ts: new host-only "setBurnInSubtitle"
handler broadcasting "burnInChanged" - same shape as "next"/
"itemChanged".
- server/src/services/CatalogService.ts: new getMediaSource() (id/
container/streams together - what TranscodeService needs); extended
the local PlaybackInfoResponse type with Container.
- server/src/Logger.ts: new Logger.transcode().
- server/src/index.ts: GET /api/catalog/burn/:partyId/master.m3u8 and
.../:segment (membership-checked, same pattern as the party REST
routes); a dedicated idle-reap interval for TranscodeService.
- server/Dockerfile: `apk add ffmpeg` in the runtime stage - no font
packages needed, unlike Emby's own text-subtitle burn-in.
- client/src/api/party.ts, client/src/api/embyApi.ts: setBurnInSubtitle/
onBurnInChanged on PartyConnection, burnInStreamUrl().
- client/src/views/player/Player.{html,css,ts}: the subtitle picker's
image-track options go from unconditionally disabled to host-only
selectable, labeled to say picking one restarts the stream for
everyone. Every member (host included) only ever reacts to the
resulting "burnInChanged" broadcast - never applies a pick
optimistically, same discipline the rest of the player already uses.
Audio/quality are disabled while a burn-in is active (one fixed
encode); a follower's subtitle select is disabled too (nothing left
for them to meaningfully pick).
- server/test/transcodeService.test.ts (new): absoluteToRelativeIndex,
session start/reuse/replace, endSession, reapIdleSessions -
child_process.spawn mocked via a small explicit test seam
(setSpawnForTesting), never invoking a real ffmpeg binary.
- server/test/socketGateway.test.ts, partyService.test.ts,
catalogService.test.ts: host-only gating + broadcast for
setBurnInSubtitle, advanceQueue's reset, toPublicState exposing the
new fields, getMediaSource.
- docs/architecture.md, README.md: design + an explicit "assumptions to
verify" note (the direct-source URL shape, first and foremost).
Verified: full typecheck+test+build on both projects (56/56 server
tests). No Emby server or ffmpeg binary is reachable from this dev
environment, so the actual command against a real PGS file is
unverified - flagged explicitly in the docs, same "TODO(verify)" spirit
already used elsewhere in this codebase for an Emby response shape that
can't be checked locally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|||
| 137977893a |
Render subtitles client-side (<track>) instead of asking Emby to burn them in
Six rounds of real-server debugging (see docs/architecture.md's player-
track-selection section) found three different, unrelated reasons a
server-side burn-in could fail - missing fonts, a bitmap format libass
fundamentally can't burn, ffmpeg failing outright on an otherwise
unremarkable file - none of which this app's own request ever caused or
could have fixed, all downstream of the one thing every attempt had in
common: asking Emby for a real-time video re-encode. Not needing that
re-encode at all closes the whole class of failure instead of chasing
each one.
- server/src/services/CatalogService.ts: new proxySubtitle asks Emby to
convert the track to WebVTT (GET /Videos/{id}/{mediaSourceId}/
Subtitles/{index}/Stream.vtt - a real Emby endpoint their own web
client already uses for this) - entirely separate from the video
transcode. applyPlaybackOverrides lost its SubtitleStreamIndex/
SubtitleMethod=Encode branch - the video request no longer needs to
know a subtitle was picked at all.
- server/src/types.ts, client/src/types.ts: PlaybackTrackOption's flag
renamed supportsBurnIn -> isTextBased - same underlying fact (image vs
text codec), but the name shouldn't keep describing a mechanism this
app no longer uses.
- server/src/index.ts: new GET /api/catalog/subtitle/:itemId/:subtitleIndex
route, grouped with the other cookie-authed media-element routes (a
<track> can't send a bearer header either).
- client/src/api/embyApi.ts: new subtitleUrl(); streamUrl no longer
carries SubtitleStreamIndex at all.
- client/src/views/player/Player.ts: the subtitle <select> no longer
reloads the video (reloadWithCurrentOptions, still used for audio/
quality, never touches subtitles now). New applySubtitleTrack()
removes/re-adds a fresh <track> and sets video.textTracks[0].mode =
'showing' synchronously right after appending - deliberately not
waiting on the <track>'s own "load" event, confirmed against a real
browser that it doesn't fire reliably at all (a known cross-browser
quirk), which would have left a subtitle silently never shown - the
exact symptom this whole saga started from. Re-applied on every video
(re)load so an audio/quality change (which does still reload the
video) doesn't drop whatever subtitle was showing.
- server/test/catalogService.test.ts: updated/new cases for
proxySubtitle and the renamed isTextBased flag; removed the test for
the now-deleted subtitle-forcing behavior.
Verified against the real compiled bundle: picking a text track adds one
<track> with the right URL and flips it to showing; "Aucun" removes it;
an image-coded track is listed but disabled; changing audio reloads the
video but the subtitle survives it, same element, still showing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|||
| 4a31ee741e |
Rewrite server + client in TypeScript, service-oriented server architecture
All checks were successful
Publier l'image Docker / publish (push) Successful in 18s
Full rewrite, requested directly. Two independent decisions since TypeScript can't compile itself away at runtime - "no build step" and "TypeScript" are mutually exclusive, not a small addition to the existing setup: - Server: plain tsc (no bundling need - Node resolves its own requires). server/dist mirrors server/src 1:1. - Client: esbuild (does need bundling) - picked over a heavier tool (Vite, webpack) since this app's whole client is a couple thousand lines; its built-in CSS-from-JS bundling and a `text` loader for importing an HTML template as a string cover everything needed via a ~30-line hand-written build script (client/build.mjs), not a config file for something built for much bigger apps. Both projects compile independently (no shared-types package - considered and rejected as more structural commitment than this app's size justifies; server/src/types.ts and client/src/types.ts have their own overlapping DTO copies instead). server/'s `npm run dev` uses tsx for a compile-free dev loop; `npm run build` (tsc) is what server/Dockerfile's build stage actually runs. client/'s `npm run watch` is esbuild's own watch mode. Server: one singleton service class per feature, generalizing the PartyManager/MusicQueueService pattern that already existed, rather than route handlers with logic inline: - AuthService (from embyAuth.js), CatalogService (NEW - previously ~15 route handlers' worth of Emby queries/DTO shaping inlined directly in index.js), PartyService (renamed from PartyManager), MusicQueueService (ported as-is, already service-shaped), SocketGateway (from socketHandler.js), EmbyClient (from embyProxy.js - low-level plumbing, not called a "service" since it has no state/feature of its own). - index.ts is now thin wiring only: every route reads the session, calls one service method, returns the result. - Real error-handling pass, not just a port: Express 4 doesn't catch a rejected Promise from an async handler on its own - a new asyncRoute() wrapper + a single error-handling middleware replace what used to be zero handling for a request that outright throws. CatalogService methods throw a CatalogFetchError (own HTTP status - getRawItem now preserves Emby's real 404 instead of a blanket 502) rather than touching res directly. Client: one folder per view (src/views/<name>/<Name>.ts + .html + .css) instead of one .js file with the markup inline as a template literal. A view's static shell moved to its .html (esbuild's text loader); per-item dynamic content (tile grids, episode/track lists) stays as template -literal-returning functions in the .ts, same as before. Genuinely shared CSS (tiles, the presence avatar, the series/album detail-header pattern, track-list rows) lives under src/styles/, imported by whichever views need it; src/styles/base.css holds the global primitives, imported once by app.ts. Deliberately not pushed to zero duplication/renaming shared classes across views for tidiness alone - not worth the risk in a change already this large. Player.ts keeps its pre-existing internal shape (the /party/:id dispatcher + the video renderer in one file, importing renderMusicPlayer from the sibling folder) - that pattern predates this rewrite and only needed types, not restructuring. Two real things the port itself caught, not just carried over: - A type bug: porting SocketGateway's "members" broadcast, reusing PartyService's PublicMember interface would have type-checked fine but was the wrong (larger) shape - the socket broadcast never included isPaused/positionTicks, only the REST response did. Caught by comparing against the original JS line by line, fixed with a dedicated SocketMemberDto type. - A real Express+TypeScript friction point: noUncheckedIndexedAccess (enabled in both tsconfigs - genuinely useful elsewhere, e.g. PartyService.advanceQueue's array indexing) makes every req.params.xxx read a type error, since Express's index-signature typing doesn't know a matched route's required :name segment is always present. Resolved once with a param(req, name) helper rather than 15 routes' worth of scattered assertions. Verified: both projects type-check cleanly in strict mode (tsc --noEmit - not just "esbuild didn't complain", which it wouldn't even with type errors present, since it transpiles without checking). Server: every unit/integration test from the JS version (party lifecycle logging, the music queue's timer/vote/pause behavior incl. real setTimeout auto-advance, the socket integration through SocketGateway+PartyService+ MusicQueueService together, the library-type filter, the playback -override forcing) re-run against the compiled server/dist output - identical results; the compiled server was actually started and confirmed listening cleanly. Client: since esbuild bundles everything into one app.js (no more individually-importable view modules), testing moved to driving the real compiled bundle through its actual boot -> login -> router -> view chain (mocked fetch/Socket.IO/hls.js, real clicks/hash navigation) - arguably more realistic than the old per-module approach. Confirmed this way: login, the sidebar, home's carousels, a video party (presence, host controls, leave), the full music hierarchy, a music party (presence, vote-skip, pause toggle, the inline browse-and-add panel), and the series page - all through client/dist's actual output, no thrown errors anywhere. Not verified: an actual `docker build` of the new multi-stage Dockerfile - this dev environment has no Docker at all. The build stage runs the exact npm run build commands already confirmed working locally, just inside node:20-alpine - the main remaining risk is something environment -specific, not the build commands themselves, but a real build is still the first thing to try after this lands. docs/architecture.md has the full writeup (new "TypeScript and a service-oriented architecture" section, plus a corrected top-level "Components" overview that had gone stale even before this change). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |