From 85bcf946b43a954dc03b5d161b5f118ab75acf13 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 14 Sep 2026 00:40:07 +0200 Subject: [PATCH] =?UTF-8?q?Filter=20sidebar=20to=20films/musique/clips=20v?= =?UTF-8?q?id=C3=A9os/TV,=20fix=20music=20videos=20not=20being=20playable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit server/src/index.js: GET /api/catalog/libraries now filters Emby's /Views response to VISIBLE_COLLECTION_TYPES (movies/tvshows/music/musicvideos) before returning it. Emby's /Views can return all sorts of things this app has no UI for at all - boxset collections, playlists, live TV, photos, books, a mixed-content folder with no CollectionType - none of which should ever have shown up in the sidebar. Filtering once at the source means every consumer (the sidebar, home.js's per-library carousels, direct navigation to /library/:id) is restricted the same way without repeating the check in each place; a stale link to a now-hidden library's id just falls into library.js's existing "Bibliothèque introuvable" state, since it looks the id up in this same filtered list. Found while making musicvideos a real visible category: its items are Emby's own 'MusicVideo' type (distinct from the generic 'Video'), which WATCH_PARTY_TYPES didn't include - every tile in a music-videos library would have rendered with no "Lancer une watch party" button at all, only "Voir sur Emby". Added it. Also fixed two stale doc/comment references while touching this code: the sidebar hasn't shown per-type icons since the earlier "no emoji" request (plain text links only), and views/home.js's party carousel has used the live lobby Socket.IO subscription instead of a GET /api/parties fetch since that endpoint was removed - both docs/architecture.md and an inline comment still described the old behavior. Verified: a plain filter/map unit test confirms exactly movies/tvshows/music/musicvideos survive against a mock /Views response seeded with one of every excluded type (photos/livetv/boxsets/playlists/ books/homevideos/mixed/no-type-at-all); WATCH_PARTY_TYPES.has('MusicVideo') checked directly; and mounting shell.js + library.js against a mocked already-filtered libraries response renders exactly the 4 expected sidebar links and a working "Lancer une watch party" button on a MusicVideo tile. docs/architecture.md has the full writeup. Co-Authored-By: Claude Sonnet 5 --- README.md | 4 +++- docs/architecture.md | 49 +++++++++++++++++++++++++++++--------------- server/src/index.js | 36 ++++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 1f2de89..7a0f057 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ including why this replaced an earlier Emby-plugin attempt (still in 1. Sign in with your Emby account (just username/password — the server address is fixed server-side, see Deploying below). -2. A sidebar lists your Emby libraries; the home page shows every ongoing +2. A sidebar lists your Emby libraries (movies, TV, music, music videos — + anything else Emby might have, like photos or a boxset collection, isn't + shown); the home page shows every ongoing watch party live (host, how long it's been running, member count) plus a "recently added" row per library, Emby-style. No join codes — if a party is running, it's already on the list, updating in real time. diff --git a/docs/architecture.md b/docs/architecture.md index fb66bab..15ff6a3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -459,33 +459,48 @@ collection type (movies / TV / music) as its own experience rather than one generic grid: - **`client/js/shell.js`** renders the persistent chrome once per login: a - sidebar (collections from `GET /api/catalog/libraries`, one icon per - `collectionType`) + a `#mainContent` element the router (`router.js`) - swaps on every navigation. `router.js` was made idempotent + sidebar (one plain-text link per library from `GET /api/catalog/libraries` + — no icons, no emoji prefix, per an explicit earlier request) + a + `#mainContent` element the router (`router.js`) swaps on every navigation. + `GET /api/catalog/libraries` itself only ever returns libraries whose + `CollectionType` is `movies`/`tvshows`/`music`/`musicvideos` + (`VISIBLE_COLLECTION_TYPES` in `server/src/index.js`) — Emby's `/Views` can + return all sorts of things this app has no UI for at all (boxset + collections, playlists, live TV, photos, books, a mixed-content folder + with no `CollectionType`), and filtering once at the source means the + sidebar, the home page's per-library carousels, and direct navigation to + `/library/:id` are all restricted the same way without repeating the + check in each place — a stale link to a since-hidden library's id just + renders `library.js`'s existing "Bibliothèque introuvable" state, since + it looks the id up in this same filtered list. `router.js` was made + idempotent (`start(root)` can be called again safely) specifically so login → shell boot can happen more than once in a page's lifetime without piling up duplicate `hashchange` listeners — the same class of bug the earlier Emby plugin work ran into with its page lifecycle, worth remembering. - **`views/home.js`** (route `/home`): a horizontal carousel of every active - party server-wide (`GET /api/parties`, deliberately not filtered to "your" - parties — the point is letting people discover and join one without a - code passed out of band) plus a join-by-code fallback, then one more - carousel per library of its recently-added items - (`GET /api/catalog/libraries/:id/recent`, Emby's own - `/Users/{id}/Items/Latest`) — skipping music (see below). + party server-wide (the live lobby subscription, not a fetch — see "Live + lobby, host handoff, no join codes" above) plus one more carousel per + library of its recently-added items (`GET /api/catalog/libraries/:id/recent`, + Emby's own `/Users/{id}/Items/Latest`) — skipping music (see below). - **`views/library.js`** (route `/library/:id`): dispatches purely on the - library's `collectionType`. `movies`/`tvshows`/anything else gets a tile + library's `collectionType`. `movies`/`tvshows`/`musicvideos` all get a tile grid (`GET /api/catalog/libraries/:id/items`, `Recursive=false` — Movies - as movies, TV as Series tiles); `music` gets an honest "not built yet" - message instead of a half-working page (see Non-goals). + and music videos as themselves, TV as Series tiles); `music` gets an + honest "not built yet" message instead of a half-working page (see + Non-goals). - **Tiles** (`client/js/tiles.js`, shared by the home carousels and the library grid so they can't drift apart): poster + name, hover-revealed overlay with "Lancer une watch party" (only for directly-playable types — - a Series tile gets "Voir les épisodes" instead, linking to `/series/:id`) - and "Voir sur Emby" (opens Emby's own web client via a `embyUrl` the - backend builds from `EMBY_SERVER_URL` + the session's `serverId`). - `:hover`/`:focus-within` reveal it on mouse/keyboard; `@media (hover: none)` - keeps it always visible on touch devices, which have no hover state at all. + `WATCH_PARTY_TYPES` in `server/src/index.js`, which had to grow a + `MusicVideo` entry once `musicvideos` became a visible library type, + otherwise every tile in it would render with no way to start a party at + all; a Series tile gets "Voir les épisodes" instead, linking to + `/series/:id`) and "Voir sur Emby" (opens Emby's own web client via a + `embyUrl` the backend builds from `EMBY_SERVER_URL` + the session's + `serverId`). `:hover`/`:focus-within` reveal it on mouse/keyboard; + `@media (hover: none)` keeps it always visible on touch devices, which + have no hover state at all. - **`views/series.js`** (route `/series/:id`): a detail header — poster, title + year, overview, a "Voir sur Emby" link — above season tabs (`GET /api/catalog/series/:id/seasons`, Emby's `/Shows/{id}/Seasons`) and diff --git a/server/src/index.js b/server/src/index.js index 5e83837..92522b4 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -111,7 +111,17 @@ app.get('/api/catalog/item/:itemId', requireSession, async (req, res) => { res.json(toItemDto(await embyRes.json(), req.embySession.serverId)); }); -// One tab per library, e.g. Films / Séries - see client/js/views/browse.js. +// Emby's /Views can return all sorts of things beyond actual media +// libraries - boxset collections, playlists, live TV, photos, books, a +// mixed-content folder with no CollectionType at all - none of which this +// app has any UI for. Only these four are ever shown, in the sidebar or +// anywhere else: everything else is dropped here, at the source, so every +// consumer (shell.js's sidebar, home.js's carousels, direct navigation to +// /library/:id) is filtered the same way without each having to repeat the +// same check. +const VISIBLE_COLLECTION_TYPES = new Set(['movies', 'tvshows', 'music', 'musicvideos']); + +// One entry per library, shown in the sidebar - see client/js/shell.js. app.get('/api/catalog/libraries', requireSession, async (req, res) => { const embyRes = await embyFetch(`/Users/${req.embySession.userId}/Views`, req.embySession.embyToken); if (!embyRes.ok) { @@ -120,13 +130,16 @@ app.get('/api/catalog/libraries', requireSession, async (req, res) => { } const data = await embyRes.json(); res.json({ - libraries: (data.Items || []).map((v) => ({ - id: v.Id, - name: v.Name, - // "movies" / "tvshows" / "music" / others - drives which browsing - // experience client/js/views/library.js renders. See docs/architecture.md. - collectionType: v.CollectionType || null - })) + libraries: (data.Items || []) + .filter((v) => VISIBLE_COLLECTION_TYPES.has(v.CollectionType)) + .map((v) => ({ + id: v.Id, + name: v.Name, + // "movies" / "tvshows" / "music" / "musicvideos" - drives which + // browsing experience client/js/views/library.js renders. See + // docs/architecture.md. + collectionType: v.CollectionType + })) }); }); @@ -335,7 +348,12 @@ app.get('/api/catalog/stream/:itemId/raw', async (req, res) => { await proxyEmbyResponse(req, res, pathAndQuery, session.embyToken, itemId); }); -const WATCH_PARTY_TYPES = new Set(['Movie', 'Episode', 'Video']); +// 'MusicVideo' is Emby's own item type for a musicvideos library's +// contents (distinct from the generic 'Video', which is home videos) - +// needed now that musicvideos is one of the visible library types (see +// VISIBLE_COLLECTION_TYPES above), otherwise every tile in it would render +// with no "Lancer une watch party" button at all. +const WATCH_PARTY_TYPES = new Set(['Movie', 'Episode', 'Video', 'MusicVideo']); function toItemDto(item, serverId) { return {