Filter sidebar to films/musique/clips vidéos/TV, fix music videos not being playable
All checks were successful
Publier l'image Docker / publish (push) Successful in 7s

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 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-09-14 00:40:07 +02:00
parent 4b2d4822ef
commit 85bcf946b4
3 changed files with 62 additions and 27 deletions

View file

@ -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.

View file

@ -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

View file

@ -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,12 +130,15 @@ app.get('/api/catalog/libraries', requireSession, async (req, res) => {
}
const data = await embyRes.json();
res.json({
libraries: (data.Items || []).map((v) => ({
libraries: (data.Items || [])
.filter((v) => VISIBLE_COLLECTION_TYPES.has(v.CollectionType))
.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
// "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 {