Files
Psychotoxical-psysonic/src/localPlaybackInvalidation.ts
T
Psychotoxical 7e9cb60763 refactor(api): co-locate server-protocol clients into lib/api
Move the Subsonic + Navidrome protocol clients and the library IPC facade
(subsonic*, navidrome*, library.ts + tests) from src/api/ into src/lib/api/,
the feature-free infra layer (plan M4, decision §10.3: shared client core → lib/).
Pure move: deep-path import specifiers @/api/* -> @/lib/api/* across ~150
consumers; no behaviour change. Domain REST (lyrics/events) and cover/analysis
infra stay in src/api/ pending their own M4 placement.

tsc 0, lint 0/0, full suite 319 files / 2353 tests green.
2026-06-30 07:53:25 +02:00

65 lines
2.8 KiB
TypeScript

import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';
import { libraryGetTrack } from '@/lib/api/library';
import { useAuthStore } from './store/authStore';
import { useLocalPlaybackStore } from './store/localPlaybackStore';
import { layoutFingerprintFromLibraryTrack } from './utils/media/mediaLayout';
import { getMediaDir } from './utils/media/mediaDir';
import { runLegacyOfflineFileMigration } from '@/features/offline';
import { reconcileLibraryTierForServer } from '@/features/offline';
import { resolveServerIdForIndexKey } from './utils/server/serverLookup';
import { serverIndexKeyFromUrl } from './utils/server/serverIndexKey';
async function invalidateEntriesForLibraryServer(libraryServerId: string): Promise<void> {
const store = useLocalPlaybackStore.getState();
const mediaDir = getMediaDir();
const targets = Object.values(store.entries).filter(
e =>
(e.tier === 'library' || e.tier === 'favorite-auto')
&& resolveServerIdForIndexKey(e.serverIndexKey) === libraryServerId,
);
for (const entry of targets) {
const track = await libraryGetTrack(libraryServerId, entry.trackId).catch(() => null);
if (!track) {
await invoke('delete_media_file', { localPath: entry.localPath, mediaDir }).catch(() => {});
store.removeEntry(entry.trackId, entry.serverIndexKey, 'sync-track-removed');
continue;
}
if (!entry.layoutFingerprint) continue;
const nextFp = layoutFingerprintFromLibraryTrack(track, entry.suffix);
if (nextFp !== entry.layoutFingerprint) {
await invoke('delete_media_file', { localPath: entry.localPath, mediaDir }).catch(() => {});
store.removeEntry(entry.trackId, entry.serverIndexKey, 'sync-layout-changed');
}
}
}
function serverIndexKeyForLibraryId(libraryServerId: string): string | undefined {
const server = useAuthStore.getState().servers.find(s => s.id === libraryServerId);
if (!server) return undefined;
return serverIndexKeyFromUrl(server.url) || server.id;
}
/** Drop stale local files after library sync; relocate legacy offline bytes when index is ready. */
export function initLocalPlaybackInvalidation(): () => void {
let unlisten: (() => void) | null = null;
void listen<{ serverId?: string }>('library:sync-idle', ({ payload }) => {
const scopeId = payload?.serverId?.trim();
if (!scopeId) return;
void (async () => {
const profileId = resolveServerIdForIndexKey(scopeId) || scopeId;
const indexKey = serverIndexKeyForLibraryId(profileId);
await runLegacyOfflineFileMigration(indexKey);
await reconcileLibraryTierForServer(profileId);
await invalidateEntriesForLibraryServer(profileId);
})();
}).then(fn => {
unlisten = fn;
});
return () => {
unlisten?.();
unlisten = null;
};
}