refactor(decouple): extract local-playback resolve substrate to store/

Pull findLocalPlaybackUrl/findLocalPlaybackEntry/hasLocalPersistentPlaybackBytes
+ the entry/index-key membership helpers (entryBelongsToServer,
indexKeyBelongsToServer, findFavoriteAutoEntry, hasLocal{Library,FavoriteAuto}Bytes)
out of features/offline/utils/offlineLibraryHelpers into a new core module
store/localPlaybackResolve.ts. The substrate depends only on authStore +
localPlaybackStore + serverIndexKey utils (no useOfflineStore), so the audio
core can resolve on-disk bytes without inverting into @/features/offline.

Repointed the audio-core consumers (crossfadePreload, playbackUrlRouting,
resolvePlaybackUrl, playTrackAction, promoteStreamCache, hotCachePrefetch,
hotCacheStore) + offline-internal callers + favorites hook; moved the two
barrel vi.mocks onto the new leaf module. offlineLibraryHelpers re-imports the
two primitives it still needs internally.

Decouple Step 2a. Removes the local-bytes offline edge from the audio core;
the resolve* media-resolution family (Step 2b) + orbit seam (Step 3) remain.

tsc 0, lint 0, full suite 319/2353 green.
This commit is contained in:
Psychotoxical
2026-06-30 09:10:18 +02:00
parent 36b0042d9a
commit bd5143d98c
18 changed files with 120 additions and 111 deletions
@@ -5,6 +5,7 @@ import type { CoverServerScope } from '@/cover/types';
import { useAuthStore } from '@/store/authStore';
import type { LocalPlaybackEntry, PinnedGroup, PinSource } from '@/store/localPlaybackStore';
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { findLocalPlaybackEntry, hasLocalLibraryBytes } from '@/store/localPlaybackResolve';
import { useOfflineStore, type OfflineAlbumMeta } from '@/features/offline/store/offlineStore';
import { resolveTrackCoverArtId, trackToSong } from '@/utils/library/advancedSearchLocal';
import { canonicalQueueServerKey, resolveIndexKey } from '@/utils/server/serverIndexKey';
@@ -34,100 +35,6 @@ export function resolveOfflineAlbumMeta(
return albums[`${indexKey}:${albumId}`] ?? albums[`${serverId}:${albumId}`];
}
function serverIndexKeysForServerId(serverId: string): string[] {
const servers = useAuthStore.getState().servers;
const server = servers.find(s => s.id === serverId);
const keys = new Set<string>();
if (server) {
const profileKey = serverIndexKeyForProfile(server);
if (profileKey) keys.add(profileKey);
keys.add(server.id);
}
keys.add(resolveIndexKey(serverId));
keys.add(serverId);
return [...keys].filter(Boolean);
}
export function entryBelongsToServer(entry: LocalPlaybackEntry, serverId: string): boolean {
return serverIndexKeysForServerId(serverId).includes(entry.serverIndexKey);
}
export function indexKeyBelongsToServer(serverIndexKey: string, serverId: string): boolean {
return serverIndexKeysForServerId(serverId).includes(serverIndexKey);
}
/** Resolve a library-tier row across legacy UUID / URL index-key variants. */
export function findLocalPlaybackEntry(
trackId: string,
serverId: string,
): LocalPlaybackEntry | null {
const lp = useLocalPlaybackStore.getState();
for (const key of serverIndexKeysForServerId(serverId)) {
const hit = lp.getEntry(trackId, key);
if (hit?.tier === 'library') return hit;
}
for (const entry of Object.values(lp.entries)) {
if (entry.trackId !== trackId || entry.tier !== 'library') continue;
if (entryBelongsToServer(entry, serverId)) return entry;
}
return null;
}
/** Index cache; run {@link reconcileLibraryTierForAlbum} / server reconcile so rows match disk. */
export function hasLocalLibraryBytes(trackId: string, serverId: string): boolean {
return !!findLocalPlaybackEntry(trackId, serverId)?.localPath;
}
/** Resolve a `favorite-auto` tier row across index-key variants. */
export function findFavoriteAutoEntry(
trackId: string,
serverId: string,
): LocalPlaybackEntry | null {
const lp = useLocalPlaybackStore.getState();
for (const key of serverIndexKeysForServerId(serverId)) {
const hit = lp.getEntry(trackId, key);
if (hit?.tier === 'favorite-auto') return hit;
}
for (const entry of Object.values(lp.entries)) {
if (entry.trackId !== trackId || entry.tier !== 'favorite-auto') continue;
if (entryBelongsToServer(entry, serverId)) return entry;
}
return null;
}
export function hasLocalFavoriteAutoBytes(trackId: string, serverId: string): boolean {
return !!findFavoriteAutoEntry(trackId, serverId)?.localPath;
}
/** Manual offline library or favorites auto-sync — skip redundant hot-cache prefetch/promote. */
export function hasLocalPersistentPlaybackBytes(trackId: string, serverId: string): boolean {
return hasLocalLibraryBytes(trackId, serverId) || hasLocalFavoriteAutoBytes(trackId, serverId);
}
/** Resolve `psysonic-local://` across legacy UUID / host index-key variants. */
export function findLocalPlaybackUrl(
trackId: string,
serverId: string,
tier: 'library' | 'ephemeral' | 'favorite-auto',
): string | null {
if (tier === 'library') {
const entry = findLocalPlaybackEntry(trackId, serverId);
if (entry?.localPath) return `psysonic-local://${entry.localPath}`;
return null;
}
if (tier === 'favorite-auto') {
const entry = findFavoriteAutoEntry(trackId, serverId);
if (entry?.localPath) return `psysonic-local://${entry.localPath}`;
return null;
}
const lp = useLocalPlaybackStore.getState();
for (const key of serverIndexKeysForServerId(serverId)) {
const url = lp.getLocalUrl(trackId, key, 'ephemeral');
if (url) return url;
}
return null;
}
/** Songs that still need a library-tier pin (used to skip redundant downloads). */
export function pendingOfflinePinSongs<T extends { id: string }>(
songs: T[],