diff --git a/src/features/offline/utils/offlineMediaResolve.ts b/src/features/offline/utils/offlineMediaResolve.ts index 0cb6940c..5e7a2722 100644 --- a/src/features/offline/utils/offlineMediaResolve.ts +++ b/src/features/offline/utils/offlineMediaResolve.ts @@ -11,6 +11,7 @@ import { useAuthStore } from '@/store/authStore'; import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard'; import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode'; import { libraryIsReady } from '@/utils/library/libraryReady'; +import { registerMediaResolver } from '@/store/mediaResolver'; import { loadAlbumFromLibraryIndex, loadArtistFromLibraryIndex, @@ -25,7 +26,16 @@ import { playlistsOfflineBrowseEnabled, } from '@/features/offline/utils/offlinePlaylistBrowse'; -export type ResolvedAlbum = { album: SubsonicAlbum; songs: SubsonicSong[] }; +// resolveAlbumForServer / resolveMediaServerId / resolveAlbumForActiveServer + +// the ResolvedAlbum type now live in the core seam; re-exported so the +// @/features/offline barrel keeps surfacing them for UI consumers. +import type { ResolvedAlbum } from '@/store/mediaResolver'; +export type { ResolvedAlbum }; +export { + resolveAlbumForServer, + resolveMediaServerId, + resolveAlbumForActiveServer, +} from '@/store/mediaResolver'; /** * Album detail / play / enqueue: the local SQLite index first when it is ready @@ -67,9 +77,6 @@ export async function resolveAlbum( } } -/** @deprecated Use {@link resolveAlbum}. */ -export const resolveAlbumForServer = resolveAlbum; - export async function resolveArtist( serverId: string, artistId: string, @@ -115,16 +122,7 @@ export async function resolvePlaylist( } } -export function resolveMediaServerId(explicit?: string | null): string | null { - return explicit ?? useAuthStore.getState().activeServerId; -} - -/** Resolve album for active server when `serverId` omitted. */ -export async function resolveAlbumForActiveServer( - albumId: string, - serverId?: string, -): Promise { - const sid = serverId ?? useAuthStore.getState().activeServerId; - if (!sid) return null; - return resolveAlbum(sid, albumId); -} +// Install the offline-aware policy into the core seam. Runs at module init, +// which happens at boot because AppShell eagerly imports the @/features/offline +// barrel (export * evaluates this module). +registerMediaResolver({ resolveAlbum, resolveArtist, resolvePlaylist }); diff --git a/src/store/mediaResolver.ts b/src/store/mediaResolver.ts new file mode 100644 index 00000000..d832c2b4 --- /dev/null +++ b/src/store/mediaResolver.ts @@ -0,0 +1,87 @@ +// Media-resolution seam. The audio core needs album/artist/playlist data that +// respects offline-browse mode, but that policy lives in @/features/offline and +// the core must not import a feature (iron rule). So the offline feature registers +// its offline-aware resolver here at boot (see offlineMediaResolve.ts module init, +// loaded eagerly via the @/features/offline barrel that AppShell imports); the +// audio core calls these neutral delegators. +// +// Default (unregistered) = network-only: a safety net that never runs in practice +// because the offline feature always registers at boot. It deliberately omits the +// library-index / offline-bytes branches (those live in the feature) — it only +// guarantees a sane fallback, not the full policy. +import type { SubsonicAlbum, SubsonicArtist, SubsonicPlaylist, SubsonicSong } from '@/lib/api/subsonicTypes'; +import { getAlbumForServer } from '@/lib/api/subsonicLibrary'; +import { getArtistForServer } from '@/lib/api/subsonicArtists'; +import { getPlaylistForServer } from '@/lib/api/subsonicPlaylists'; +import { useAuthStore } from '@/store/authStore'; + +export type ResolvedAlbum = { album: SubsonicAlbum; songs: SubsonicSong[] }; +export type ResolvedArtist = { artist: SubsonicArtist; albums: SubsonicAlbum[] }; +export type ResolvedPlaylist = { playlist: SubsonicPlaylist; songs: SubsonicSong[] }; + +export interface MediaResolver { + resolveAlbum(serverId: string, albumId: string): Promise; + resolveArtist(serverId: string, artistId: string): Promise; + resolvePlaylist(serverId: string, playlistId: string): Promise; +} + +let registered: MediaResolver | null = null; + +/** Offline feature installs its offline-aware policy here at module init. */ +export function registerMediaResolver(resolver: MediaResolver): void { + registered = resolver; +} + +async function netAlbum(serverId: string, albumId: string): Promise { + try { + const data = await getAlbumForServer(serverId, albumId); + return { album: data.album, songs: data.songs }; + } catch { + return null; + } +} + +async function netArtist(serverId: string, artistId: string): Promise { + try { + return await getArtistForServer(serverId, artistId); + } catch { + return null; + } +} + +async function netPlaylist(serverId: string, playlistId: string): Promise { + try { + return await getPlaylistForServer(serverId, playlistId); + } catch { + return null; + } +} + +export function resolveAlbum(serverId: string, albumId: string): Promise { + return (registered?.resolveAlbum ?? netAlbum)(serverId, albumId); +} + +export function resolveArtist(serverId: string, artistId: string): Promise { + return (registered?.resolveArtist ?? netArtist)(serverId, artistId); +} + +export function resolvePlaylist(serverId: string, playlistId: string): Promise { + return (registered?.resolvePlaylist ?? netPlaylist)(serverId, playlistId); +} + +/** @deprecated Use {@link resolveAlbum}. */ +export const resolveAlbumForServer = resolveAlbum; + +export function resolveMediaServerId(explicit?: string | null): string | null { + return explicit ?? useAuthStore.getState().activeServerId; +} + +/** Resolve album for active server when `serverId` omitted. */ +export async function resolveAlbumForActiveServer( + albumId: string, + serverId?: string, +): Promise { + const sid = serverId ?? useAuthStore.getState().activeServerId; + if (!sid) return null; + return resolveAlbum(sid, albumId); +} diff --git a/src/utils/mix/luckyMixHelpers.ts b/src/utils/mix/luckyMixHelpers.ts index 47af8da1..4d7ac033 100644 --- a/src/utils/mix/luckyMixHelpers.ts +++ b/src/utils/mix/luckyMixHelpers.ts @@ -1,6 +1,6 @@ import { getTopSongs } from '@/lib/api/subsonicArtists'; import { filterSongsToActiveLibrary, getAlbumList, getRandomSongs } from '@/lib/api/subsonicLibrary'; -import { resolveAlbumForActiveServer } from '@/features/offline'; +import { resolveAlbumForActiveServer } from '@/store/mediaResolver'; import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes'; import { filterSongsForLuckyMixRatings, diff --git a/src/utils/playback/fetchTracksForSource.ts b/src/utils/playback/fetchTracksForSource.ts index ad82b5c3..edc45794 100644 --- a/src/utils/playback/fetchTracksForSource.ts +++ b/src/utils/playback/fetchTracksForSource.ts @@ -5,7 +5,7 @@ import { resolveArtist, resolveMediaServerId, resolvePlaylist, -} from '@/features/offline'; +} from '@/store/mediaResolver'; export async function fetchTracksForSource(source: DeviceSyncSource): Promise { const serverId = resolveMediaServerId(); diff --git a/src/utils/playback/playAlbum.test.ts b/src/utils/playback/playAlbum.test.ts index da4111bc..2e8521d9 100644 --- a/src/utils/playback/playAlbum.test.ts +++ b/src/utils/playback/playAlbum.test.ts @@ -4,7 +4,10 @@ import { onInvoke } from '@/test/mocks/tauri'; import { resetOrbitStore, resetPlayerStore } from '@/test/helpers/storeReset'; import type { Track } from '../../store/playerStoreTypes'; -vi.mock('@/features/offline', () => ({ +// Spread the real module so registerMediaResolver stays callable — the offline +// barrel loads offlineMediaResolve transitively, which registers at module init. +vi.mock('@/store/mediaResolver', async (importOriginal) => ({ + ...(await importOriginal()), resolveAlbumForActiveServer: vi.fn(), })); @@ -12,7 +15,7 @@ vi.mock('./fadeOut', () => ({ fadeOut: vi.fn(async () => undefined), })); -import { resolveAlbumForActiveServer } from '@/features/offline'; +import { resolveAlbumForActiveServer } from '@/store/mediaResolver'; import { useOrbitStore } from '@/features/orbit'; import { usePlayerStore } from '../../store/playerStore'; import { playAlbum, playAlbumShuffled } from './playAlbum'; diff --git a/src/utils/playback/playAlbum.ts b/src/utils/playback/playAlbum.ts index fd2a8c4c..fb1d6b8a 100644 --- a/src/utils/playback/playAlbum.ts +++ b/src/utils/playback/playAlbum.ts @@ -1,5 +1,5 @@ import { usePlayerStore } from '../../store/playerStore'; -import { resolveAlbumForActiveServer } from '@/features/offline'; +import { resolveAlbumForActiveServer } from '@/store/mediaResolver'; import { songToTrack } from './songToTrack'; import { useOrbitStore } from '@/features/orbit'; import { fadeOut } from './fadeOut'; diff --git a/src/utils/playback/playArtistShuffled.ts b/src/utils/playback/playArtistShuffled.ts index 6636eb5e..6c43f8c3 100644 --- a/src/utils/playback/playArtistShuffled.ts +++ b/src/utils/playback/playArtistShuffled.ts @@ -1,4 +1,4 @@ -import { resolveAlbum, resolveArtist, resolveMediaServerId } from '@/features/offline'; +import { resolveAlbum, resolveArtist, resolveMediaServerId } from '@/store/mediaResolver'; import { songToTrack } from './songToTrack'; import { shuffleArray } from './shuffleArray'; import { usePlayerStore } from '../../store/playerStore'; diff --git a/src/utils/playback/playByOpaqueId.ts b/src/utils/playback/playByOpaqueId.ts index a85c66c6..1f2c8bf0 100644 --- a/src/utils/playback/playByOpaqueId.ts +++ b/src/utils/playback/playByOpaqueId.ts @@ -1,5 +1,5 @@ import { getSong } from '@/lib/api/subsonicLibrary'; -import { resolveAlbumForActiveServer } from '@/features/offline'; +import { resolveAlbumForActiveServer } from '@/store/mediaResolver'; import { songToTrack } from './songToTrack'; import { playAlbum } from './playAlbum'; import { playArtistShuffled } from './playArtistShuffled';