diff --git a/client/src/tiles.ts b/client/src/tiles.ts index 878cc80..4d85f70 100644 --- a/client/src/tiles.ts +++ b/client/src/tiles.ts @@ -96,15 +96,26 @@ export function wireTileButtons(container: ParentNode, onStart: (itemId: string) } /** - * Renders `items` into `container` a chunk at a time, across animation - * frames, instead of one `innerHTML` write for the whole list - a library - * grid can hand back up to 300 tiles at once (CatalogService.getLibraryItems/ - * getArtists), and building + inserting/laying out ~300 posters in a - * single synchronous task is exactly what a "the page freezes while it - * loads" report looks like on a slower device. Splitting the same HTML - * across a few chunks keeps every individual task well under a frame's - * budget, so the tab stays responsive (and the loading spinner keeps - * animating) while it works through the rest. + * Renders `items` into `container` a chunk at a time instead of one + * `innerHTML` write for the whole list - a library grid can legitimately + * hand back over a thousand tiles now that CatalogService.getLibraryItems/ + * getArtists no longer cap themselves (confirmed against the real server: + * one library alone has 1229 artists - see their own comments), and + * building + inserting/laying out that many in a single synchronous task + * is exactly what a "the page freezes while it loads" report looks like + * on a slower device. Splitting the same HTML across a few chunks keeps + * every individual task well under a frame's budget, so the tab stays + * responsive (and the loading spinner keeps animating) while it works + * through the rest. + * + * Scheduled with `setTimeout(fn, 0)`, not `requestAnimationFrame` - rAF + * callbacks are deliberately paused by the browser for a backgrounded/ + * hidden tab, which is a completely ordinary thing to do while a large + * library is still loading (switching tabs, minimizing the window) and + * would otherwise stall the render indefinitely, looking exactly like + * "some of the data is missing" rather than "still loading". setTimeout + * keeps running in a background tab (only throttled, never paused), so + * this always finishes on its own. * * Uses `insertAdjacentHTML`, not repeated `innerHTML +=` (which would * re-parse and re-render everything already inserted on every chunk). @@ -120,7 +131,7 @@ export function renderTilesInChunks(container: HTMLElement, items: ItemDto[], ch if (!chunk.length) return; container.insertAdjacentHTML('beforeend', chunk.map(tileHtml).join('')); i += chunkSize; - if (i < items.length) requestAnimationFrame(renderNextChunk); + if (i < items.length) setTimeout(renderNextChunk, 0); } renderNextChunk(); } diff --git a/server/src/services/CatalogService.ts b/server/src/services/CatalogService.ts index c1691f4..9bb13fe 100644 --- a/server/src/services/CatalogService.ts +++ b/server/src/services/CatalogService.ts @@ -199,11 +199,13 @@ class CatalogServiceClass { // structure, same reasoning as Shows -> Seasons -> Episodes above. async getArtists(session: EmbySession, libraryId: string): Promise { - // Limit added for the same reason as getLibraryItems's own - unbounded - // on a large music library, this used to hand back every artist at - // once for the client to render in a single pass (see tiles.ts's - // renderTilesInChunks for the render-side half of that fix). - const params = new URLSearchParams({ ParentId: libraryId, UserId: session.userId, SortBy: 'SortName', Limit: '300' }); + // No Limit, deliberately - see getLibraryItems's own comment below for + // why one very nearly shipped here and got pulled: confirmed directly + // against the real server that this library alone has 1229 artists, + // so a 300 cap would have silently dropped 75% of them, not just + // trimmed an edge case. Rendering that many is tiles.ts's + // renderTilesInChunks's job, not this query's. + const params = new URLSearchParams({ ParentId: libraryId, UserId: session.userId, SortBy: 'SortName' }); const data = await this.fetchJson(`/Artists?${params}`, session, 'Impossible de récupérer les artistes.'); return (data.Items || []).map((a) => this.toItemDto(a, session.serverId)); } @@ -238,17 +240,29 @@ class CatalogServiceClass { // renders tileHtml's poster/name/year (views/library, tiles.ts), which // never reads it. Traced here after "the catalog API is too slow, the // front freezes while it loads" - Emby happily serializes a full - // synopsis for all 300 items, and the client happily JSON.parses all - // of it, entirely to be thrown away unread. Contrast getSeriesEpisodes/ - // getPlaylistItemsRaw, which keep Overview because their own views - // (Series/Playlist) actually display it. + // synopsis for every item anyway, and the client happily JSON.parses + // all of it, entirely to be thrown away unread. Contrast + // getSeriesEpisodes/getPlaylistItemsRaw, which keep Overview because + // their own views (Series/Playlist) actually display it. + // + // No Limit - there used to be one here (300), presumably meant as the + // same defensive cap getArtists briefly grew (see its own comment). + // Confirmed directly against the real server that this app's own + // "Films" library alone has 421 items - past that cap, so it was + // quietly serving an incomplete grid with no error and no hint + // anything was missing, reported as "the catalog is broken, not all + // the data is there". Emby returns everything when Limit is omitted + // (confirmed: TotalRecordCount matched Items.length exactly with no + // Limit param, for both this library and the 1229-artist one above) - + // rendering that many tiles without freezing the tab is + // renderTilesInChunks's job (tiles.ts), not something this query + // should be truncating data to work around. const params = new URLSearchParams({ ParentId: libraryId, Recursive: 'false', // top-level: Movies as movies, TV as Series tiles (not every episode) SortBy: 'SortName', Fields: 'ProductionYear,AlbumId', - ImageTypeLimit: '1', - Limit: '300' + ImageTypeLimit: '1' }); const data = await this.fetchJson(`/Users/${session.userId}/Items?${params}`, session, 'Impossible de récupérer le contenu de cette bibliothèque.'); return (data.Items || []).map((i) => this.toItemDto(i, session.serverId));