Seven domain-eng splits peel ~200 LOC of read endpoints out of
`api/subsonic.ts`:
- `subsonicStreamUrl.ts` — `buildStreamUrl`, `coverArtCacheKey`,
`buildCoverArtUrl`, `buildDownloadUrl` (token-signed URL builders
for the four /rest endpoints we hand to the browser).
- `subsonicStarRating.ts` — `getStarred`, `star`, `unstar`,
`setRating`, `probeEntityRatingSupport`. `setRating` still triggers
the lazy `navidromeBrowse` cache invalidation; the same-folder
lazy import path is preserved.
- `subsonicSearch.ts` — `search`, `searchSongsPaged`.
- `subsonicScrobble.ts` — `scrobbleSong`, `reportNowPlaying`,
`getNowPlaying`.
- `subsonicAlbumInfo.ts` — `getAlbumInfo2`.
- `subsonicLyrics.ts` — `getLyricsBySongId`.
- `subsonicGenres.ts` — `getGenres`, `getAlbumsByGenre`.
63 external call sites migrated to direct imports. Four `vi.mock`
targets in the store-level tests pointed at `../api/subsonic` and
were updated to the new module paths.
Pure code-move. subsonic.ts: 762 → 561 LOC (−201).
Three domain-eng modules peel ~316 LOC of fetch/mapping out of
`api/subsonic.ts`:
- `subsonicLibrary.ts` — browse + random + per-song fetch
(`getMusicDirectory`, `getMusicIndexes`, `getMusicFolders`,
`getRandomAlbums`, `getAlbumList`, `getRandomSongs`,
`getRandomSongsFiltered`, `getSong`, `getAlbum`,
`filterSongsToActiveLibrary`, `similarSongsRequestCount`, plus
the private `albumIdsInActiveLibraryScope` cache).
- `subsonicArtists.ts` — artist endpoints (`getArtists`, `getArtist`,
`getArtistInfo`, `getTopSongs`, `getSimilarSongs2`,
`getSimilarSongs`). Uses Library's `filterSongsToActiveLibrary` and
`similarSongsRequestCount` for the per-library scoping fallback.
- `subsonicRatings.ts` — `parseSubsonicEntityStarRating` parser plus
`prefetchArtistUserRatings` and `prefetchAlbumUserRatings` workers
with the shared 7-min cache. Calls back into Library/Artists for
the per-id fetch.
51 external call sites migrated to direct imports. No re-export
shims in `subsonic.ts`. Statistics endpoints still in `subsonic.ts`
keep their `RATING_CACHE_TTL` constant locally (same 7-min window).
subsonic.ts: 1078 → 762 LOC (−316).
39 new tests targeting the response normalization paths the F3 PR (#544)
deferred. Mocks axios at the module boundary; pins:
- api() helper envelope: unwrap subsonic-response on status=ok, throw
"Invalid response" without envelope, throw the server message on
status=failed, throw a generic on failed-without-message, propagate
network failures.
- song-array vs single-object normalization paths:
- getMusicDirectory normalizes child (object -> [object]), empty -> [].
- getMusicIndexes flattens index -> artist arrays (object or array).
- getMusicFolders coerces numeric ids to strings + defaults name.
- getRandomSongs pass-through behaviour pinned (no normalization --
Navidrome always returns the array form).
- collection-shape contracts: getAlbum splits { album, songs } + empty
fallback when album.song is absent, getStarred returns empty arrays
on missing starred2 / missing fields / pass-through arrays.
- single endpoint behaviours: getSong null on failure, getTopSongs []
+ slice to 5, getArtists flatten + empty, search whitespace-query
short-circuit (no HTTP), getAlbumInfo2 null on error, ping
true/false based on status.
- pingWithCredentials (explicit-URL path): full response (type +
serverVersion + openSubsonic), http:// prepend when scheme missing,
trailing-slash strip before /rest/ping.view, ok=false on any
failure / status=failed, openSubsonic defaults to false when omitted.
subsonic.ts coverage 12.66% -> 31.87% lines (+19pp). The remaining
surface lives in niche endpoints (playlist mutations, statistics
overview/aggregates, internet radio CRUD, cover-art uploads, ratings
prefetch). Not gate-eligible yet; a follow-up could push to ~60% but
diminishing returns relative to other backlog items.
Frontend suite: 412 -> 451 tests (+39).