mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
bd5143d98c
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.
110 lines
4.2 KiB
TypeScript
110 lines
4.2 KiB
TypeScript
/**
|
|
* Playback-URL routing characterization. The non-trivial behaviour is
|
|
* (a) the module-scoped `lastOpenedWithHttpTrackId` only persists for HTTP
|
|
* URLs (offline / hot-cache plays clear it), (b) the rebind check matches
|
|
* across the `stream:` / bare id forms, and (c) the source-kind classifier
|
|
* picks 'offline' vs 'hot' only when the library index has a local file.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const { offlineStoreState } = vi.hoisted(() => ({
|
|
offlineStoreState: { localUrlByKey: new Map<string, string>() },
|
|
}));
|
|
|
|
vi.mock('@/store/localPlaybackResolve', () => ({
|
|
findLocalPlaybackUrl: (trackId: string, serverId: string, tier: string) => {
|
|
if (tier !== 'library') return null;
|
|
return offlineStoreState.localUrlByKey.has(`${serverId}:${trackId}`)
|
|
? `psysonic-local://${serverId}/${trackId}`
|
|
: null;
|
|
},
|
|
}));
|
|
|
|
vi.mock('../utils/playback/resolvePlaybackUrl', () => ({
|
|
resolvePlaybackUrl: vi.fn((trackId: string, serverId: string) => {
|
|
if (offlineStoreState.localUrlByKey.has(`${serverId}:${trackId}`)) {
|
|
return `psysonic-local://${serverId}/${trackId}`;
|
|
}
|
|
return `https://mock/${serverId}/${trackId}`;
|
|
}),
|
|
}));
|
|
|
|
import {
|
|
_resetPlaybackUrlRoutingForTest,
|
|
playbackSourceHintForResolvedUrl,
|
|
recordEnginePlayUrl,
|
|
shouldRebindPlaybackToHotCache,
|
|
} from './playbackUrlRouting';
|
|
|
|
beforeEach(() => {
|
|
offlineStoreState.localUrlByKey.clear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
_resetPlaybackUrlRoutingForTest();
|
|
});
|
|
|
|
describe('playbackSourceHintForResolvedUrl', () => {
|
|
it("classifies non-local URLs as 'stream'", () => {
|
|
expect(playbackSourceHintForResolvedUrl('t1', 'srv', 'https://mock/srv/t1')).toBe('stream');
|
|
});
|
|
|
|
it("classifies psysonic-local:// as 'offline' when the library index has the file", () => {
|
|
offlineStoreState.localUrlByKey.set('srv:t1', 'file:///cache/t1.mp3');
|
|
expect(playbackSourceHintForResolvedUrl('t1', 'srv', 'psysonic-local://srv/t1')).toBe('offline');
|
|
});
|
|
|
|
it("classifies psysonic-local:// as 'hot' when no offline copy exists", () => {
|
|
expect(playbackSourceHintForResolvedUrl('t1', 'srv', 'psysonic-local://srv/t1')).toBe('hot');
|
|
});
|
|
});
|
|
|
|
describe('recordEnginePlayUrl + shouldRebindPlaybackToHotCache', () => {
|
|
it('records HTTP URLs and rebinds when the resolved URL later goes local', () => {
|
|
recordEnginePlayUrl('t1', 'https://mock/srv/t1');
|
|
offlineStoreState.localUrlByKey.set('srv:t1', 'file:///cache/t1.mp3');
|
|
expect(shouldRebindPlaybackToHotCache('t1', 'srv')).toBe(true);
|
|
});
|
|
|
|
it('does not rebind when the recorded URL was already local', () => {
|
|
recordEnginePlayUrl('t1', 'psysonic-local://srv/t1');
|
|
offlineStoreState.localUrlByKey.set('srv:t1', 'file:///cache/t1.mp3');
|
|
expect(shouldRebindPlaybackToHotCache('t1', 'srv')).toBe(false);
|
|
});
|
|
|
|
it('matches the recorded id across the stream: prefix', () => {
|
|
recordEnginePlayUrl('stream:t1', 'https://mock/srv/stream:t1');
|
|
offlineStoreState.localUrlByKey.set('srv:t1', 'file:///cache/t1.mp3');
|
|
expect(shouldRebindPlaybackToHotCache('t1', 'srv')).toBe(true);
|
|
});
|
|
|
|
it('returns false for a different track id', () => {
|
|
recordEnginePlayUrl('t1', 'https://mock/srv/t1');
|
|
offlineStoreState.localUrlByKey.set('srv:t2', 'file:///cache/t2.mp3');
|
|
expect(shouldRebindPlaybackToHotCache('t2', 'srv')).toBe(false);
|
|
});
|
|
|
|
it('returns false when serverId is empty', () => {
|
|
recordEnginePlayUrl('t1', 'https://mock/srv/t1');
|
|
expect(shouldRebindPlaybackToHotCache('t1', '')).toBe(false);
|
|
});
|
|
|
|
it('returns false when nothing has been recorded yet', () => {
|
|
expect(shouldRebindPlaybackToHotCache('t1', 'srv')).toBe(false);
|
|
});
|
|
|
|
it('returns false when the resolved URL is still HTTP (no hot-cache entry)', () => {
|
|
recordEnginePlayUrl('t1', 'https://mock/srv/t1');
|
|
expect(shouldRebindPlaybackToHotCache('t1', 'srv')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('_resetPlaybackUrlRoutingForTest', () => {
|
|
it('clears the recorded id', () => {
|
|
recordEnginePlayUrl('t1', 'https://mock/srv/t1');
|
|
_resetPlaybackUrlRoutingForTest();
|
|
offlineStoreState.localUrlByKey.set('srv:t1', 'file:///cache/t1.mp3');
|
|
expect(shouldRebindPlaybackToHotCache('t1', 'srv')).toBe(false);
|
|
});
|
|
});
|