Failed to verify fix + reactivity & polling fixes for multi downloads

This commit is contained in:
Harvey
2026-04-19 03:18:50 +01:00
parent a63008c298
commit e70a76f489
10 changed files with 210 additions and 38 deletions
@@ -4,12 +4,15 @@
import { libraryStore } from '$lib/stores/library';
import { playerStore } from '$lib/stores/player.svelte';
const POLL_INTERVAL_MS = 15_000;
let completedPerJob = $derived.by(() => {
const { mbidSet } = $libraryStore;
const counts: Record<string, number> = {};
for (const job of batchDownloadStore.jobs) {
let done = 0;
for (const mbid of job.musicbrainzIds) {
if (libraryStore.isInLibrary(mbid)) done++;
if (mbidSet.has(mbid.toLowerCase())) done++;
}
counts[job.artistId] = done;
}
@@ -20,6 +23,19 @@
batchDownloadStore.jobs.every((job) => (completedPerJob[job.artistId] ?? 0) >= job.total)
);
// Poll library while batch downloads are in progress
$effect(() => {
if (!batchDownloadStore.hasActive || allComplete) return;
libraryStore.refresh();
const interval = setInterval(() => {
libraryStore.refresh();
}, POLL_INTERVAL_MS);
return () => clearInterval(interval);
});
$effect(() => {
if (allComplete && batchDownloadStore.jobs.length > 0) {
const timer = setTimeout(() => {
+2 -1
View File
@@ -182,7 +182,8 @@ export const API = {
suggest: (query: string, limit = 5) =>
`/api/v1/search/suggest?q=${encodeURIComponent(query.trim())}&limit=${limit}`
},
home: (source: string) => `/api/v1/home?source=${encodeURIComponent(source)}`,
home: (source?: string) =>
source ? `/api/v1/home?source=${encodeURIComponent(source)}` : '/api/v1/home',
homeIntegrationStatus: () => '/api/v1/home/integration-status',
discover: (source?: string) =>
source ? `/api/v1/discover?source=${encodeURIComponent(source)}` : '/api/v1/discover',
+117
View File
@@ -0,0 +1,117 @@
import { describe, expect, it, beforeEach, vi } from 'vitest';
import { PAGE_SOURCE_KEYS, API } from '$lib/constants';
vi.mock('$app/environment', () => ({ browser: true }));
const storage = new Map<string, string>();
const mockLocalStorage = {
getItem: vi.fn((key: string) => storage.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => storage.set(key, value)),
removeItem: vi.fn((key: string) => storage.delete(key)),
clear: vi.fn(() => storage.clear()),
get length() {
return storage.size;
},
key: vi.fn((_i: number) => null)
};
vi.stubGlobal('localStorage', mockLocalStorage);
vi.stubGlobal('window', globalThis);
describe('migratePageSourceKeys', () => {
let migratePageSourceKeys: (typeof import('$lib/stores/musicSource'))['migratePageSourceKeys'];
beforeEach(async () => {
vi.clearAllMocks();
storage.clear();
vi.resetModules();
const mod = await import('$lib/stores/musicSource');
migratePageSourceKeys = mod.migratePageSourceKeys;
});
it('converts raw "listenbrainz" to JSON-encoded string', () => {
storage.set(PAGE_SOURCE_KEYS.home, 'listenbrainz');
migratePageSourceKeys();
expect(storage.get(PAGE_SOURCE_KEYS.home)).toBe('"listenbrainz"');
});
it('converts raw "lastfm" to JSON-encoded string', () => {
storage.set(PAGE_SOURCE_KEYS.home, 'lastfm');
migratePageSourceKeys();
expect(storage.get(PAGE_SOURCE_KEYS.home)).toBe('"lastfm"');
});
it('leaves already-JSON-encoded values unchanged', () => {
storage.set(PAGE_SOURCE_KEYS.home, '"listenbrainz"');
migratePageSourceKeys();
expect(storage.get(PAGE_SOURCE_KEYS.home)).toBe('"listenbrainz"');
});
it('leaves invalid values unchanged', () => {
storage.set(PAGE_SOURCE_KEYS.home, 'plex');
migratePageSourceKeys();
expect(storage.get(PAGE_SOURCE_KEYS.home)).toBe('plex');
});
it('handles absent keys without error', () => {
expect(() => migratePageSourceKeys()).not.toThrow();
});
it('migrates all page source keys', () => {
for (const key of Object.values(PAGE_SOURCE_KEYS)) {
storage.set(key, 'lastfm');
}
migratePageSourceKeys();
for (const key of Object.values(PAGE_SOURCE_KEYS)) {
expect(storage.get(key)).toBe('"lastfm"');
}
});
});
describe('isMusicSource', () => {
let isMusicSource: (typeof import('$lib/stores/musicSource'))['isMusicSource'];
beforeEach(async () => {
vi.resetModules();
const mod = await import('$lib/stores/musicSource');
isMusicSource = mod.isMusicSource;
});
it('returns true for listenbrainz', () => {
expect(isMusicSource('listenbrainz')).toBe(true);
});
it('returns true for lastfm', () => {
expect(isMusicSource('lastfm')).toBe(true);
});
it('returns false for undefined', () => {
expect(isMusicSource(undefined)).toBe(false);
});
it('returns false for invalid string', () => {
expect(isMusicSource('plex')).toBe(false);
});
it('returns false for null', () => {
expect(isMusicSource(null)).toBe(false);
});
});
describe('API.home', () => {
it('includes source param when provided', () => {
expect(API.home('listenbrainz')).toBe('/api/v1/home?source=listenbrainz');
});
it('omits source param when undefined', () => {
expect(API.home(undefined)).toBe('/api/v1/home');
});
it('omits source param when called with no arguments', () => {
expect(API.home()).toBe('/api/v1/home');
});
it('encodes special characters in source', () => {
expect(API.home('listen brainz')).toBe('/api/v1/home?source=listen%20brainz');
});
});
+30 -3
View File
@@ -18,6 +18,23 @@ export function isMusicSource(value: unknown): value is MusicSource {
return value === 'listenbrainz' || value === 'lastfm';
}
/**
* Migrate old raw-string localStorage source values to JSON format.
* Before v1.3.0, setPageSource() stored raw strings (e.g. `listenbrainz`).
* PersistedState expects JSON (e.g. `"listenbrainz"`). Must run before
* any PersistedState constructor reads these keys.
*/
export function migratePageSourceKeys(): void {
if (!browser) return;
for (const key of Object.values(PAGE_SOURCE_KEYS)) {
const raw = localStorage.getItem(key);
if (raw === null) continue;
if (isMusicSource(raw)) {
localStorage.setItem(key, JSON.stringify(raw));
}
}
}
function readCachedSource(): MusicSource {
if (!browser) return DEFAULT_SOURCE;
const stored = localStorage.getItem(CACHED_SOURCE_KEY);
@@ -105,13 +122,23 @@ function createMusicSourceStore() {
function getPageSource(page: MusicSourcePage): MusicSource {
const fallbackSource = getSource();
if (!browser) return fallbackSource;
const storedSource = localStorage.getItem(getPageStorageKey(page));
return isMusicSource(storedSource) ? storedSource : fallbackSource;
const raw = localStorage.getItem(getPageStorageKey(page));
if (raw === null) return fallbackSource;
// Handle JSON-encoded values (new format)
try {
const parsed: unknown = JSON.parse(raw);
if (isMusicSource(parsed)) return parsed;
} catch {
// Fall through to raw check
}
// Handle raw string values (old format)
if (isMusicSource(raw)) return raw;
return fallbackSource;
}
function setPageSource(page: MusicSourcePage, source: MusicSource): void {
if (!browser) return;
localStorage.setItem(getPageStorageKey(page), source);
localStorage.setItem(getPageStorageKey(page), JSON.stringify(source));
}
return {