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
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:
parent
4b2d4822ef
commit
85bcf946b4
3 changed files with 62 additions and 27 deletions
|
|
@ -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
|
1. Sign in with your Emby account (just username/password — the server
|
||||||
address is fixed server-side, see Deploying below).
|
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
|
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
|
"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.
|
is running, it's already on the list, updating in real time.
|
||||||
|
|
|
||||||
|
|
@ -459,33 +459,48 @@ collection type (movies / TV / music) as its own experience rather than one
|
||||||
generic grid:
|
generic grid:
|
||||||
|
|
||||||
- **`client/js/shell.js`** renders the persistent chrome once per login: a
|
- **`client/js/shell.js`** renders the persistent chrome once per login: a
|
||||||
sidebar (collections from `GET /api/catalog/libraries`, one icon per
|
sidebar (one plain-text link per library from `GET /api/catalog/libraries`
|
||||||
`collectionType`) + a `#mainContent` element the router (`router.js`)
|
— no icons, no emoji prefix, per an explicit earlier request) + a
|
||||||
swaps on every navigation. `router.js` was made idempotent
|
`#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
|
(`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
|
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
|
duplicate `hashchange` listeners — the same class of bug the earlier Emby
|
||||||
plugin work ran into with its page lifecycle, worth remembering.
|
plugin work ran into with its page lifecycle, worth remembering.
|
||||||
- **`views/home.js`** (route `/home`): a horizontal carousel of every active
|
- **`views/home.js`** (route `/home`): a horizontal carousel of every active
|
||||||
party server-wide (`GET /api/parties`, deliberately not filtered to "your"
|
party server-wide (the live lobby subscription, not a fetch — see "Live
|
||||||
parties — the point is letting people discover and join one without a
|
lobby, host handoff, no join codes" above) plus one more carousel per
|
||||||
code passed out of band) plus a join-by-code fallback, then one more
|
library of its recently-added items (`GET /api/catalog/libraries/:id/recent`,
|
||||||
carousel per library of its recently-added items
|
Emby's own `/Users/{id}/Items/Latest`) — skipping music (see below).
|
||||||
(`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
|
- **`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
|
grid (`GET /api/catalog/libraries/:id/items`, `Recursive=false` — Movies
|
||||||
as movies, TV as Series tiles); `music` gets an honest "not built yet"
|
and music videos as themselves, TV as Series tiles); `music` gets an
|
||||||
message instead of a half-working page (see Non-goals).
|
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
|
- **Tiles** (`client/js/tiles.js`, shared by the home carousels and the
|
||||||
library grid so they can't drift apart): poster + name, hover-revealed
|
library grid so they can't drift apart): poster + name, hover-revealed
|
||||||
overlay with "Lancer une watch party" (only for directly-playable types —
|
overlay with "Lancer une watch party" (only for directly-playable types —
|
||||||
a Series tile gets "Voir les épisodes" instead, linking to `/series/:id`)
|
`WATCH_PARTY_TYPES` in `server/src/index.js`, which had to grow a
|
||||||
and "Voir sur Emby" (opens Emby's own web client via a `embyUrl` the
|
`MusicVideo` entry once `musicvideos` became a visible library type,
|
||||||
backend builds from `EMBY_SERVER_URL` + the session's `serverId`).
|
otherwise every tile in it would render with no way to start a party at
|
||||||
`:hover`/`:focus-within` reveal it on mouse/keyboard; `@media (hover: none)`
|
all; a Series tile gets "Voir les épisodes" instead, linking to
|
||||||
keeps it always visible on touch devices, which have no hover state at all.
|
`/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,
|
- **`views/series.js`** (route `/series/:id`): a detail header — poster,
|
||||||
title + year, overview, a "Voir sur Emby" link — above season tabs
|
title + year, overview, a "Voir sur Emby" link — above season tabs
|
||||||
(`GET /api/catalog/series/:id/seasons`, Emby's `/Shows/{id}/Seasons`) and
|
(`GET /api/catalog/series/:id/seasons`, Emby's `/Shows/{id}/Seasons`) and
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,17 @@ app.get('/api/catalog/item/:itemId', requireSession, async (req, res) => {
|
||||||
res.json(toItemDto(await embyRes.json(), req.embySession.serverId));
|
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) => {
|
app.get('/api/catalog/libraries', requireSession, async (req, res) => {
|
||||||
const embyRes = await embyFetch(`/Users/${req.embySession.userId}/Views`, req.embySession.embyToken);
|
const embyRes = await embyFetch(`/Users/${req.embySession.userId}/Views`, req.embySession.embyToken);
|
||||||
if (!embyRes.ok) {
|
if (!embyRes.ok) {
|
||||||
|
|
@ -120,13 +130,16 @@ app.get('/api/catalog/libraries', requireSession, async (req, res) => {
|
||||||
}
|
}
|
||||||
const data = await embyRes.json();
|
const data = await embyRes.json();
|
||||||
res.json({
|
res.json({
|
||||||
libraries: (data.Items || []).map((v) => ({
|
libraries: (data.Items || [])
|
||||||
id: v.Id,
|
.filter((v) => VISIBLE_COLLECTION_TYPES.has(v.CollectionType))
|
||||||
name: v.Name,
|
.map((v) => ({
|
||||||
// "movies" / "tvshows" / "music" / others - drives which browsing
|
id: v.Id,
|
||||||
// experience client/js/views/library.js renders. See docs/architecture.md.
|
name: v.Name,
|
||||||
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);
|
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) {
|
function toItemDto(item, serverId) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue