mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
refactor(player): E.5 — extract playback-URL routing into module (#568)
Three file-private helpers + their shared module-scoped track id move into src/store/playbackUrlRouting.ts: recordEnginePlayUrl, playbackSourceHintForResolvedUrl, shouldRebindPlaybackToHotCache. The `lastOpenedWithHttpTrackId` mutable goes with them — only those three read it. No external callers, no re-exports needed. Adds 12 focused tests covering the source-kind classifier (stream / hot / offline), the rebind decision across stream:-prefix forms, the empty-serverId / un-recorded edge cases, and the test-only reset helper. playerStore 3490 → 3470 LOC.
This commit is contained in:
committed by
GitHub
parent
5b89051817
commit
b68bddd034
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* 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 offline store actually has a local
|
||||||
|
* URL for the track.
|
||||||
|
*/
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const { offlineStoreState } = vi.hoisted(() => ({
|
||||||
|
offlineStoreState: { localUrlByKey: new Map<string, string>() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./offlineStore', () => ({
|
||||||
|
useOfflineStore: {
|
||||||
|
getState: () => ({
|
||||||
|
getLocalUrl: (trackId: string, serverId: string) =>
|
||||||
|
offlineStoreState.localUrlByKey.get(`${serverId}:${trackId}`) ?? null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../utils/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 offline store 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');
|
||||||
|
// No entry in offlineStoreState → resolvePlaybackUrl mock returns https://...
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { resolvePlaybackUrl, type PlaybackSourceKind } from '../utils/resolvePlaybackUrl';
|
||||||
|
import { sameQueueTrackId } from '../utils/queueIdentity';
|
||||||
|
import { useOfflineStore } from './offlineStore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers that classify the URL `audio_play` was last invoked with, so the
|
||||||
|
* runtime can rebind playback onto a freshly-promoted hot-cache entry the
|
||||||
|
* next time the same track plays.
|
||||||
|
*
|
||||||
|
* `lastOpenedWithHttpTrackId` remembers the most recent track id we asked
|
||||||
|
* the engine to fetch over HTTP (anything that isn't the `psysonic-local://`
|
||||||
|
* scheme). When the hot-cache later promotes that track to a local URL we
|
||||||
|
* compare against this id to decide whether a transparent rebind is worth
|
||||||
|
* triggering.
|
||||||
|
*/
|
||||||
|
let lastOpenedWithHttpTrackId: string | null = null;
|
||||||
|
|
||||||
|
export function recordEnginePlayUrl(trackId: string, url: string): void {
|
||||||
|
lastOpenedWithHttpTrackId = url.startsWith('psysonic-local://') ? null : trackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Matches `playTrack` / PlayerBar: stream vs hot-cache vs offline file from resolved `audio_play` URL. */
|
||||||
|
export function playbackSourceHintForResolvedUrl(trackId: string, serverId: string, url: string): PlaybackSourceKind {
|
||||||
|
if (!url.startsWith('psysonic-local://')) return 'stream';
|
||||||
|
return useOfflineStore.getState().getLocalUrl(trackId, serverId) ? 'offline' : 'hot';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldRebindPlaybackToHotCache(trackId: string, serverId: string): boolean {
|
||||||
|
if (!serverId) return false;
|
||||||
|
if (!lastOpenedWithHttpTrackId || !sameQueueTrackId(lastOpenedWithHttpTrackId, trackId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return resolvePlaybackUrl(trackId, serverId).startsWith('psysonic-local://');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test-only: clear the module-scoped track id so each test starts clean. */
|
||||||
|
export function _resetPlaybackUrlRoutingForTest(): void {
|
||||||
|
lastOpenedWithHttpTrackId = null;
|
||||||
|
}
|
||||||
@@ -39,6 +39,11 @@ import {
|
|||||||
subscribePlaybackProgress,
|
subscribePlaybackProgress,
|
||||||
type PlaybackProgressSnapshot,
|
type PlaybackProgressSnapshot,
|
||||||
} from './playbackProgress';
|
} from './playbackProgress';
|
||||||
|
import {
|
||||||
|
playbackSourceHintForResolvedUrl,
|
||||||
|
recordEnginePlayUrl,
|
||||||
|
shouldRebindPlaybackToHotCache,
|
||||||
|
} from './playbackUrlRouting';
|
||||||
|
|
||||||
// Re-export the playback-progress public surface so existing call sites
|
// Re-export the playback-progress public surface so existing call sites
|
||||||
// (PlayerBar, FullscreenPlayer, WaveformSeek, LyricsPane, MobilePlayerView,
|
// (PlayerBar, FullscreenPlayer, WaveformSeek, LyricsPane, MobilePlayerView,
|
||||||
@@ -1034,31 +1039,6 @@ async function promoteCompletedStreamToHotCache(track: Track, serverId: string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks the **actual** `audio_play` URL family: `getPlaybackSourceKind()` can
|
|
||||||
* report `hot` for RAM preload or disk index while the engine still uses HTTP.
|
|
||||||
* Rebind-to-local seeks need this, not the UI hint alone.
|
|
||||||
*/
|
|
||||||
let lastOpenedWithHttpTrackId: string | null = null;
|
|
||||||
|
|
||||||
function recordEnginePlayUrl(trackId: string, url: string): void {
|
|
||||||
lastOpenedWithHttpTrackId = url.startsWith('psysonic-local://') ? null : trackId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Matches `playTrack` / PlayerBar: stream vs hot-cache vs offline file from resolved `audio_play` URL. */
|
|
||||||
function playbackSourceHintForResolvedUrl(trackId: string, serverId: string, url: string): PlaybackSourceKind {
|
|
||||||
if (!url.startsWith('psysonic-local://')) return 'stream';
|
|
||||||
return useOfflineStore.getState().getLocalUrl(trackId, serverId) ? 'offline' : 'hot';
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldRebindPlaybackToHotCache(trackId: string, serverId: string): boolean {
|
|
||||||
if (!serverId) return false;
|
|
||||||
if (!lastOpenedWithHttpTrackId || !sameQueueTrackId(lastOpenedWithHttpTrackId, trackId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return resolvePlaybackUrl(trackId, serverId).startsWith('psysonic-local://');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track ID that has already been sent to audio_chain_preload (gapless chain).
|
// Track ID that has already been sent to audio_chain_preload (gapless chain).
|
||||||
let gaplessPreloadingId: string | null = null;
|
let gaplessPreloadingId: string | null = null;
|
||||||
// Track ID that has already been sent to audio_preload (byte pre-download).
|
// Track ID that has already been sent to audio_preload (byte pre-download).
|
||||||
|
|||||||
Reference in New Issue
Block a user