From 183c549c95ab1266f48fdcede26868f630f37760 Mon Sep 17 00:00:00 2001 From: Niko Huuskonen Date: Fri, 28 Aug 2026 09:34:11 +0300 Subject: [PATCH] fix: make jellyfin cache read faster and async MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - This addresses the issue where client freezes for a bit until the whole cache gets applied. Also changed the hydration to happen in chunks, reducing time complexity from O(n²) to O(n²/K) where K is chunk size - Changed global queue restoring to be O(n) instead of O(n²) Signed-off-by: Niko Huuskonen --- .../logic/MediaSource/JellyfinMediaSource.ts | 12 ++++++++--- src/renderer/logic/queue.ts | 20 +++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/renderer/logic/MediaSource/JellyfinMediaSource.ts b/src/renderer/logic/MediaSource/JellyfinMediaSource.ts index a7ce025b..e24bb5dc 100644 --- a/src/renderer/logic/MediaSource/JellyfinMediaSource.ts +++ b/src/renderer/logic/MediaSource/JellyfinMediaSource.ts @@ -112,7 +112,7 @@ export class JellyfinMediaSource extends MediaSource { this.accessToken = cache.accessToken; this.userId = cache.userId; this.lastSyncedAt = cache.lastSyncedAt; - this.hydrateFromCache(cache.items); + await this.hydrateFromCache(cache.items); if (await this.verifySession()) { this.isConnected.value = true; @@ -452,8 +452,14 @@ export class JellyfinMediaSource extends MediaSource { this.isSyncing.value = false; } - private hydrateFromCache(items: JellyfinItem[]) { - items.forEach((item) => this.amethyst.player.queue.add(this.createTrackFromJellyfinItem(item))); + // Batches and yields between chunks so hydrating a large cached library doesn't + // freeze the renderer's main thread for the whole loop in one synchronous turn + private async hydrateFromCache(items: JellyfinItem[]) { + for (let i = 0; i < items.length; i += PAGE_SIZE) { + const chunk = items.slice(i, i + PAGE_SIZE).map((item) => this.createTrackFromJellyfinItem(item)); + await this.amethyst.player.queue.add(chunk); + await new Promise((resolve) => setTimeout(resolve, 0)); + } } private upsertTrack(item: JellyfinItem, existingById: Map) { diff --git a/src/renderer/logic/queue.ts b/src/renderer/logic/queue.ts index f427bf62..b5ce7e9e 100644 --- a/src/renderer/logic/queue.ts +++ b/src/renderer/logic/queue.ts @@ -59,8 +59,9 @@ export class Queue { public constructor(private amethyst: Amethyst, paths?: string[]) { if (paths) this.add(paths); else { - // load saved queue from local storage - this.savedQueue.value.forEach((item) => { + // load saved queue from local storage, built up front and added in one batch so + // restoring a large queue doesn't re-serialize the whole list to storage per track + const tracks = this.savedQueue.value.map((item) => { const track = new Track(this.amethyst, item.path); if (item.type == MediaSourceType.Subsonic) { @@ -71,8 +72,10 @@ export class Queue { track.sourceType = MediaSourceType.Jellyfin; } - this.add(track); + return track; }); + + this.add(tracks); } } @@ -151,16 +154,17 @@ export class Queue { } /** - * Adds a track to the queue - * @param item A filepath to the track + * Adds a track, or a batch of filepaths/tracks under a single localStorage sync, to the queue + * @param item A filepath, a track, or an array of either */ - public async add(item: (string | string[]) | Track) { + public async add(item: (string | string[]) | Track | Track[]) { if (item instanceof Track) { this.list.value.set(item.path, item); } else if (item instanceof Array) { - const paths = Array.from(item); - paths.forEach((path) => this.list.value.set(path, new Track(this.amethyst, path))); + item.forEach((entry) => entry instanceof Track + ? this.list.value.set(entry.path, entry) + : this.list.value.set(entry, new Track(this.amethyst, entry))); } else if (typeof item === "string") { this.list.value.set(item, new Track(this.amethyst, item));