23c9125ad8
Backend CI / Lint (push) Waiting to run
Backend CI / Tests (push) Waiting to run
Squashes 26 incremental fork commits (Apr–May 2026) onto upstream main as a single
diff for cleaner cross-fork comparison. Original history preserved on the
pre-squash-backup tag locally.
Feature additions
─────────────────
• Inline single-track download via yt-dlp-worker proxy
New routes: POST /api/v1/track-download/search (source: youtube | spotify),
POST /api/v1/track-download, GET /api/v1/track-download/{id}. Frontend
TrackDownloadButton in album track list AND popular-songs row, with a per-button
source picker. Per-user rate limits live in the worker's SQLite store. On
completion the backend fires Lidarr RefreshArtist + Plex library refresh +
cache invalidation, and the popular-songs list auto-refreshes.
• Per-instance library pinning via MUSICSEERR_LIBRARY env
Backend stamps the library label server-side (music / music-personal /
music-shared); clients cannot override. Drives an instance-segregated
deployment of three musicseerr containers sharing one source tree.
• Lidarr-request flow (single-track requests via Lidarr indexers)
New routes: POST /api/v1/lidarr-request, GET /api/v1/lidarr-request/status.
Per-album asyncio.Lock keyed on album_mbid so rapid-clicks on the same album
serialize correctly. Cross-release track matcher with foreignTrackId →
foreignRecordingId → position+disc → exact-title → substring fallback chain,
evaluated per release (recording UUIDs frequently differ between album,
single, and deluxe edition releases of the same song). Flips
artist.monitored = True on request so Lidarr's WantedAlbums query reaches
the track. Full Lidarr-chain gate (artist AND album AND track) for the
status endpoint to avoid false-positive REQUESTED display. Persistent UI
state so button icons survive refresh and cross-album navigation.
• Privacy: show_now_playing toggle in Settings → Home
Default off. Plex /status/sessions returns active audio sessions across the
whole server with no library-section filter, so a shared instance leaks
every household member's listening activity. The merged store still emits
the user's local MusicSeerr playback bar; only server-derived sessions
(Plex / Jellyfin / Navidrome) are gated.
• Per-button visibility prefs for the track-row action cluster
Settings → Preferences → Download Options / Playback Buttons. Per-context
(popular_songs / album_page) force-off flags layered on top of the existing
source-availability gate.
• UX: wrap action cluster on mobile, hide LidarrRequestButton in tight
layouts, cross-album status-leak fix in AlbumTrackList ($effect keyed on
album.musicbrainz_id to rebuild lookup; map keyed by
"{albumMbid}:{position}:{disc}").
Test coverage
─────────────
Backend pytest: full suite green (2031/2031 as of squash). New: schema-default
tests for HomeSettings, lidarr_request_service cross-release matcher
regression test, singleton-registry expected-count bump to 59. Frontend
vitest: SettingsHome.svelte.spec covers new toggle, nowPlayingSessions
.svelte.spec covers the privacy gate (no fetch when off; fetches when on).
73 lines
2.0 KiB
TypeScript
73 lines
2.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { writable } from 'svelte/store';
|
|
|
|
vi.mock('$env/dynamic/public', () => ({
|
|
env: { PUBLIC_API_URL: '' }
|
|
}));
|
|
|
|
// integrationStore needs to be a real Svelte store — nowPlayingSessions uses
|
|
// `get(integrationStore)` to decide which sources to poll.
|
|
const integrationState = writable({
|
|
jellyfin: false,
|
|
navidrome: false,
|
|
plex: true,
|
|
loaded: true
|
|
});
|
|
vi.mock('$lib/stores/integration', () => ({
|
|
integrationStore: integrationState
|
|
}));
|
|
|
|
let showNowPlaying = false;
|
|
vi.mock('$lib/stores/homeSettings.svelte', () => ({
|
|
homeSettingsStore: {
|
|
get showNowPlaying() {
|
|
return showNowPlaying;
|
|
}
|
|
}
|
|
}));
|
|
|
|
describe('nowPlayingStore privacy gate', () => {
|
|
let originalFetch: typeof globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
originalFetch = globalThis.fetch;
|
|
Object.defineProperty(document, 'hidden', { value: false, configurable: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
vi.resetModules();
|
|
});
|
|
|
|
it('skips the network when showNowPlaying is false', async () => {
|
|
showNowPlaying = false;
|
|
const fetchSpy = vi.fn();
|
|
globalThis.fetch = fetchSpy;
|
|
|
|
const { nowPlayingStore } = await import('./nowPlayingSessions.svelte');
|
|
await nowPlayingStore.refresh();
|
|
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
expect(nowPlayingStore.sessions.length).toBe(0);
|
|
});
|
|
|
|
it('hits the configured source when showNowPlaying is true', async () => {
|
|
showNowPlaying = true;
|
|
const fetchSpy = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ sessions: [] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' }
|
|
})
|
|
);
|
|
globalThis.fetch = fetchSpy;
|
|
|
|
const { nowPlayingStore } = await import('./nowPlayingSessions.svelte');
|
|
await nowPlayingStore.refresh();
|
|
|
|
// Plex is the only integration configured in the mock above — exactly
|
|
// one fetch should land on the Plex sessions endpoint.
|
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
expect(String(fetchSpy.mock.calls[0][0])).toContain('plex');
|
|
});
|
|
});
|