refactor(lib): extract Track/QueueItemRef domain model to lib/media/trackTypes

Track is the app's normalized song model and QueueItemRef its thin queue
identity -- consumed app-wide (queue, cover, context menus, sharing, lucky-mix,
library browse), not a playback-internal detail. They lived in
features/playback/store/playerStoreTypes, forcing ~115 core/feature files to
reach into the playback feature for the central media type.

Move the two interfaces to lib/media/trackTypes; playerStoreTypes keeps
PlayerState and now imports them from lib. 115 importers repointed (mixed
'PlayerState, Track' lines split so PlayerState stays). Pure type move (erased at
runtime). tsc 0, lint 0.
This commit is contained in:
Psychotoxical
2026-06-30 17:16:56 +02:00
parent 2756cb7698
commit 9058abd340
117 changed files with 189 additions and 168 deletions
@@ -1,5 +1,5 @@
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import React, { useMemo, useState, useEffect } from 'react';
import { useTracklistColumns } from '@/utils/useTracklistColumns';
import { usePlayerStore } from '@/features/playback/store/playerStore';
@@ -1,7 +1,7 @@
import React from 'react';
import { AudioLines } from 'lucide-react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { formatLongDuration } from '@/lib/format/formatDuration';
+1 -1
View File
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { useSelectionStore } from '@/store/selectionStore';
import { useThemeStore } from '@/store/themeStore';
@@ -1,7 +1,7 @@
import type { TFunction } from 'i18next';
import { getSimilarSongs2, getTopSongs } from '@/lib/api/subsonicArtists';
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { runBulkPlayAll, runBulkShuffle } from '@/features/playback/utils/playback/runBulkPlay';
import { resolveAlbum, resolveMediaServerId } from '@/features/offline';
@@ -5,7 +5,7 @@ import { useLyrics, type WordLyricsLine } from '@/hooks/useLyrics';
import { useWordLyricsSync } from '@/hooks/useWordLyricsSync';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '@/features/playback/store/playbackProgress';
import type { LrcLine } from '@/api/lrclib';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { EaseScroller, targetForFraction } from '@/utils/ui/easeScroll';
// Fullscreen synced lyrics.
@@ -2,7 +2,7 @@ import { memo, useMemo, useSyncExternalStore } from 'react';
import { useTranslation } from 'react-i18next';
import { X } from 'lucide-react';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
import {
getQueueResolverVersion,
@@ -5,7 +5,7 @@ import { useAuthStore } from '@/store/authStore';
import { setTransitionMode, type TransitionMode } from '@/features/playback/utils/playback/playbackTransition';
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
import type { SubsonicOpenArtistRef } from '@/lib/api/subsonicTypes';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
export const MINI_WINDOW_LABEL = 'mini';
@@ -1,7 +1,7 @@
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
import type { MiniSyncPayload, MiniTrackInfo } from '@/features/miniPlayer/utils/miniPlayerBridge';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
/** Half-width of the mini initial-snapshot queue window (matches the bridge). */
const MINI_SNAPSHOT_HALF = 100;
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { queueSongStar } from '@/features/playback/store/pendingStarSync';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import type { TrackStats } from '@/music-network';
import { getMusicNetworkRuntime } from '@/music-network';
@@ -9,7 +9,7 @@ import { findLocalPlaybackEntry, hasLocalLibraryBytes } from '@/store/localPlayb
import { useOfflineStore, type OfflineAlbumMeta } from '@/features/offline/store/offlineStore';
import { resolveTrackCoverArtId, trackToSong } from '@/lib/library/advancedSearchLocal';
import { canonicalQueueServerKey, resolveIndexKey } from '@/utils/server/serverIndexKey';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { findServerByIdOrIndexKey, resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
import { serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
@@ -9,7 +9,8 @@ import {
} from '@/features/playback/store/engineState';
import { clearPreloadingIds } from '@/features/playback/store/gaplessPreloadState';
import { deriveNormalizationSnapshot } from '@/features/playback/store/normalizationSnapshot';
import type { PlayerState, QueueItemRef } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
import { seedQueueResolver } from '@/features/playback/store/queueTrackResolver';
import { canonicalQueueServerKey } from '@/utils/server/serverIndexKey';
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { QueueItemRef } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
const getPlayQueueForServerMock = vi.fn();
@@ -4,7 +4,7 @@ import { bindQueueServerId } from '@/features/playback/utils/playback/playbackSe
import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
import { toQueueItemRefs } from '@/features/playback/store/queueItemRef';
import { seedQueueResolver } from '@/features/playback/store/queueTrackResolver';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { preparePausedRestoreOnStartup } from '@/features/playback/store/pausedRestorePrepare';
@@ -1,5 +1,5 @@
import { scrobbleSong } from '@/lib/api/subsonicScrobble';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import {
playbackReportPlaying,
playbackReportStart,
@@ -28,7 +28,8 @@ import {
pushQueueUndoSnapshot,
type QueueUndoSnapshot,
} from '@/features/playback/store/queueUndo';
import type { PlayerState, QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import { savePlayQueue } from '@/lib/api/subsonicPlayQueue';
import { _resetQueueSyncForTest, flushPlayQueuePosition } from '@/features/playback/store/queueSync';
@@ -6,7 +6,7 @@ import { findLocalPlaybackUrl } from '@/store/localPlaybackResolve';
import { playbackCacheKeyForRef } from '@/features/playback/utils/playback/playbackServer';
import { resolvePlaybackUrl } from '@/features/playback/utils/playback/resolvePlaybackUrl';
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import {
hasPlannedCrossfade,
markPlannedCrossfade,
@@ -1,4 +1,4 @@
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { invoke } from '@tauri-apps/api/core';
import { setDeferHotCachePrefetch } from '@/utils/cache/hotCacheGate';
import {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { QueueItemRef } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import { create } from 'zustand';
import type { HotCacheEntry } from '@/features/playback/store/hotCacheStoreTypes';
import { useLocalPlaybackStore, type LocalPlaybackEntry } from '@/store/localPlaybackStore';
@@ -4,7 +4,7 @@
* = current track + next `LOUDNESS_BACKFILL_WINDOW_AHEAD` entries, with
* duplicates collapsed.
*/
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { describe, expect, it } from 'vitest';
import {
LOUDNESS_BACKFILL_WINDOW_AHEAD,
@@ -1,4 +1,4 @@
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
/**
* After a bulk enqueue (queue replace, append-many, lucky-mix) the runtime
* warms the loudness cache for the current track + the next N entries so
@@ -4,7 +4,7 @@
* guard, the window collection, and the no-sync-engine flag on each
* refresh call.
*/
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const hoisted = vi.hoisted(() => {
const auth = { normalizationEngine: 'loudness' as 'off' | 'replaygain' | 'loudness' };
@@ -1,4 +1,4 @@
import type { QueueItemRef } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import { useAuthStore } from '@/store/authStore';
import { collectLoudnessBackfillWindowTrackIds } from '@/features/playback/store/loudnessBackfillWindow';
import { refreshLoudnessForTrack } from '@/features/playback/store/loudnessRefresh';
+2 -1
View File
@@ -10,7 +10,8 @@ import {
setInfiniteQueueFetching,
} from '@/features/playback/store/infiniteQueueState';
import { isInOrbitSession } from '@/store/orbitRuntime';
import type { PlayerState, QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import { toQueueItemRefs } from '@/features/playback/store/queueItemRef';
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
import { seedQueueResolver } from '@/features/playback/store/queueTrackResolver';
@@ -4,7 +4,7 @@
* album-mode resolution). useAuthStore is mocked through a hoisted state
* object so each test can flip flags without rebuilding the store.
*/
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { authState } = vi.hoisted(() => ({
authState: {
@@ -1,4 +1,5 @@
import type { PlayerState, Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import { useAuthStore } from '@/store/authStore';
import { resolveReplayGainDb } from '@/features/playback/utils/audio/resolveReplayGainDb';
/**
@@ -1,4 +1,4 @@
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const hoisted = vi.hoisted(() => ({
@@ -1,4 +1,4 @@
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { getQueueTracksView } from '@/features/playback/store/queueTrackView';
import { scheduleHotCachePrefetchForTrack } from '@/hotCachePrefetch';
import { getPlaybackCacheServerKey } from '@/features/playback/utils/playback/playbackServer';
@@ -10,7 +10,7 @@ vi.mock('@/lib/api/subsonicStarRating', () => ({
}));
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { queueSongStar, queueSongRating, _resetPendingStarSyncForTest } from '@/features/playback/store/pendingStarSync';
import {
getCachedTrack,
@@ -22,7 +22,7 @@ vi.mock('@/features/playback/utils/playback/playbackServer', () => ({
getPlaybackServerId: vi.fn(() => 'server-1'),
}));
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
const testTrack: Track = {
id: 't1',
@@ -1,4 +1,4 @@
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { usePreviewStore } from '@/features/playback/store/previewStore';
import {
@@ -57,7 +57,8 @@ import {
playbackSourceHintForResolvedUrl,
recordEnginePlayUrl,
} from '@/features/playback/store/playbackUrlRouting';
import type { PlayerState, Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import { toQueueItemRefs } from '@/features/playback/store/queueItemRef';
import { getQueueTracksView, resolveQueueTrack } from '@/features/playback/store/queueTrackView';
import { seedQueueResolver } from '@/features/playback/store/queueTrackResolver';
+2 -1
View File
@@ -4,7 +4,8 @@ import { readInitialNetworkLovedCache, persistNetworkLovedCache } from '@/featur
import { readInitialPlayerPrefs, persistPlayerPrefs } from '@/features/playback/store/playerPrefsStorage';
import { createHydrationGatedStorage, createSafeJSONStorage } from '@/lib/util/safeStorage';
import { emitPlaybackProgress } from '@/features/playback/store/playbackProgress';
import type { PlayerState, QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import { toQueueItemRefs } from '@/features/playback/store/queueItemRef';
import { canonicalQueueServerKey } from '@/utils/server/serverIndexKey';
import { readInitialQueueVisibility } from '@/features/playback/store/queueVisibilityStorage';
@@ -1,56 +1,6 @@
import type { InternetRadioStation, SubsonicOpenArtistRef } from '@/lib/api/subsonicTypes';
import type { InternetRadioStation } from '@/lib/api/subsonicTypes';
import type { PlaybackSourceKind } from '@/features/playback/utils/playback/resolvePlaybackUrl';
export interface Track {
id: string;
title: string;
artist: string;
album: string;
albumId: string;
artistId?: string;
/** OpenSubsonic `artists` on the child song — multiple performers with ids. */
artists?: SubsonicOpenArtistRef[];
duration: number;
coverArt?: string;
discNumber?: number;
track?: number;
year?: number;
bitRate?: number;
suffix?: string;
userRating?: number;
replayGainTrackDb?: number;
replayGainAlbumDb?: number;
replayGainPeak?: number;
starred?: string;
genre?: string;
samplingRate?: number;
bitDepth?: number;
/** Subsonic `size` in bytes when provided by the server (helps hot-cache budgeting). */
size?: number;
/** Owning server profile id when the queue spans multiple servers (e.g. offline favorites). */
serverId?: string;
autoAdded?: boolean;
radioAdded?: boolean;
/** Inserted via "Play Next". Used by the preserve-order toggle to find the
* end of the current Play-Next streak. Stale flags behind queueIndex are
* harmless — the streak scan only looks forward from queueIndex+1. */
playNextAdded?: boolean;
}
/**
* Thin canonical queue item (queue thin-state plan, §5.10). Identity plus the
* queue-only flags; library metadata (title/artist/cover/…) is resolved from
* the local index or network on demand from Phase 2 on. `serverId` is per-item
* (day-1 schema for mixed-server queues); v1 fills it with the single playback
* server.
*/
export interface QueueItemRef {
serverId: string;
trackId: string;
autoAdded?: boolean;
radioAdded?: boolean;
playNextAdded?: boolean;
}
import type { Track, QueueItemRef } from '@/lib/media/trackTypes';
export interface PlayerState {
currentTrack: Track | null;
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
const invokeMock = vi.fn();
const setEntryMock = vi.fn();
@@ -1,5 +1,5 @@
import { buildStreamUrlForServer } from '@/lib/api/subsonicStreamUrl';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { invoke } from '@tauri-apps/api/core';
import { useHotCacheStore } from '@/features/playback/store/hotCacheStore';
import { getMediaDir } from '@/utils/media/mediaDir';
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { toQueueItemRefs } from './queueItemRef';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
describe('toQueueItemRefs', () => {
it('uses per-track serverId when present', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { stampTrackServerId } from '@/features/playback/utils/playback/trackServerScope';
import { canonicalQueueServerKey } from '@/utils/server/serverIndexKey';
@@ -3,7 +3,8 @@ import { orbitBulkGuard } from '@/store/orbitRuntime';
import { useAuthStore } from '@/store/authStore';
import { setIsAudioPaused } from '@/features/playback/store/engineState';
import { prefetchLoudnessForEnqueuedTracks } from '@/features/playback/store/loudnessPrefetch';
import type { PlayerState, QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import { toQueueItemRefs } from '@/features/playback/store/queueItemRef';
import { seedQueueResolver } from '@/features/playback/store/queueTrackResolver';
import { pushQueueUndoFromGetter } from '@/features/playback/store/queueUndo';
@@ -3,7 +3,7 @@ import { onInvoke } from '@/test/mocks/tauri';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { TrackRefDto } from '@/lib/api/library';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import {
getCachedTrack,
_resetQueueResolverForTest,
+1 -1
View File
@@ -1,6 +1,6 @@
import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { QueueItemRef } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import { canonicalQueueServerKey } from '@/utils/server/serverIndexKey';
import { resolveBatch } from './queueTrackResolver';
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
const { savePlayQueueMock, playerState, progressSnapshot, isSubsonicServerReachableMock } = vi.hoisted(() => ({
savePlayQueueMock: vi.fn(async () => undefined),
+1 -1
View File
@@ -1,5 +1,5 @@
import { savePlayQueue } from '@/lib/api/subsonicPlayQueue';
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { isSubsonicServerReachable } from '@/utils/network/subsonicNetworkGuard';
import {
filterQueueRefsForPlaybackServer,
@@ -3,7 +3,7 @@ import { onInvoke } from '@/test/mocks/tauri';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { TrackRefDto } from '@/lib/api/library';
import type { QueueItemRef } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import * as subsonic from '@/lib/api/subsonicLibrary';
import {
resolveBatch,
@@ -1,7 +1,7 @@
import { libraryGetTracksBatch, type TrackRefDto } from '@/lib/api/library';
import { getSongForServer } from '@/lib/api/subsonicLibrary';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
import { canonicalQueueServerKey } from '@/utils/server/serverIndexKey';
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { seedQueueResolver, _resetQueueResolverForTest } from '@/features/playback/store/queueTrackResolver';
import { resolveQueueTrack, getQueueTracksView } from './queueTrackView';
@@ -1,4 +1,4 @@
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { getCachedTrack, placeholderTrack, applyQueueOverrides } from './queueTrackResolver';
/**
@@ -4,7 +4,8 @@
* push, and (c) the scroll-top reader / consumer pair that QueuePanel uses
* to restore list scroll position after an undo/redo commit.
*/
import type { PlayerState, Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import { toQueueItemRefs } from '@/features/playback/store/queueItemRef';
import { beforeEach, describe, expect, it } from 'vitest';
import {
+2 -1
View File
@@ -1,4 +1,5 @@
import type { PlayerState, QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
/** Hard cap on undo/redo depth — keeps memory bounded for very long sessions. */
export const QUEUE_UNDO_MAX = 32;
@@ -4,7 +4,7 @@
* atSeconds > 0.05, the wantPlaying=false branch that issues audio_pause,
* and the generation-mismatch bail-out.
*/
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const hoisted = vi.hoisted(() => {
const auth = {
@@ -1,4 +1,4 @@
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { engineLoadTrackAtPosition } from '@/features/playback/store/engineLoadTrackAtPosition';
/**
@@ -4,7 +4,7 @@
* (pending-sync, F4) its optimistic patch + retry behaviour is covered in
* `pendingStarSync.test.ts`, so here we only assert the delegation + guards.
*/
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { queueSongRatingMock, recordSkipStarMock, playerStateGet } = vi.hoisted(() => {
const playerState = {
@@ -1,4 +1,4 @@
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { getCachedTrack } from '@/features/playback/store/queueTrackResolver';
@@ -1,4 +1,4 @@
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import {
getPlaybackServerId,
playbackProfileIdForTrack,
@@ -5,7 +5,7 @@
* Originally lived in `playerStore.ts`; extracted in M0 of the frontend
* refactor (2026-05-12).
*/
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { describe, expect, it } from 'vitest';
import { resolveReplayGainDb } from '@/features/playback/utils/audio/resolveReplayGainDb';
describe('resolveReplayGainDb', () => {
@@ -1,4 +1,4 @@
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
/**
* Resolve the ReplayGain dB value for a track based on the configured mode.
* In 'auto' mode, picks album-gain when an adjacent queue neighbour shares the
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import type { QueueItemRef } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import {
autodjJsTriggerAtSec,
computeAutodjJsOverlap,
@@ -1,5 +1,5 @@
import { DYNAMIC_OVERLAP_HARD_CAP_SEC, STANDARD_BLEND_SEC } from '@/utils/waveform/waveformSilence';
import type { QueueItemRef } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
export type QueueRepeatMode = 'off' | 'all' | 'one';
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { invoke } from '@tauri-apps/api/core';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import * as crossfadePreload from '@/features/playback/store/crossfadePreload';
import {
armInterruptHandoff,
@@ -1,5 +1,5 @@
import { invoke } from '@tauri-apps/api/core';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import {
INTERRUPT_BLEND_PREP_FADE_SEC,
isCrossfadeNextReady,
@@ -8,7 +8,7 @@
*/
import { getSimilarSongs2, getTopSongs } from '@/lib/api/subsonicArtists';
import { getRandomSongs } from '@/lib/api/subsonicLibrary';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Mock only the artist Subsonic API submodule (the pre-move target was
@@ -1,6 +1,6 @@
import { getSimilarSongs2, getTopSongs } from '@/lib/api/subsonicArtists';
import { getRandomSongs } from '@/lib/api/subsonicLibrary';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import {
enrichSongsForMixRatingFilter,
getMixMinRatingsConfigFromAuth,
@@ -5,7 +5,7 @@ import { libraryAdvancedSearch, libraryGetGenreAlbumCounts, type LibrarySortClau
import { fetchAllSongsByGenre, getGenres } from '@/lib/api/subsonicGenres';
import type { SubsonicGenre } from '@/lib/api/subsonicTypes';
import { libraryScopeForServer } from '@/lib/api/subsonicClient';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { shuffleArray } from '@/lib/util/shuffleArray';
import { trackToSong } from '@/lib/library/advancedSearchLocal';
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { makeSubsonicSong } from '@/test/helpers/factories';
import { onInvoke } from '@/test/mocks/tauri';
import { resetOrbitStore, resetPlayerStore } from '@/test/helpers/storeReset';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
// Spread the real module so registerMediaResolver stays callable — the offline
// barrel loads offlineMediaResolve transitively, which registers at module init.
@@ -4,7 +4,7 @@ import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { orbitSnapshot } from '@/store/orbitRuntime';
import { fadeOut } from '@/features/playback/utils/playback/fadeOut';
import { shouldAutodjInterruptBlend } from '@/features/playback/utils/playback/autodjManualBlend';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { shuffleArray } from '@/lib/util/shuffleArray';
export async function fetchAlbumTracks(albumId: string, serverId?: string): Promise<Track[]> {
@@ -8,7 +8,7 @@ import type { ServerProfile } from '@/store/authStoreTypes';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { switchActiveServer } from '@/utils/server/switchActiveServer';
import { sameQueueTrackId } from '@/features/playback/utils/playback/queueIdentity';
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { findServerByIdOrIndexKey, resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
import {
canonicalQueueServerKey,
@@ -4,7 +4,7 @@
* routing through the HTTP source) and the no-op detection in
* `queuesStructuralEqual` that prevents unnecessary store rewrites.
*/
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { describe, expect, it } from 'vitest';
import {
normalizeAnalysisTrackId,
@@ -1,4 +1,4 @@
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
/**
* Strip the `stream:` prefix that some Rust events attach to track ids when
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { runBulkEnqueue, runBulkPlayAll, runBulkShuffle } from '@/features/playback/utils/playback/runBulkPlay';
function track(id: string): Track {
@@ -1,4 +1,4 @@
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { shuffleArray } from '@/lib/util/shuffleArray';
/**
@@ -5,7 +5,7 @@
* playback queue. Originally lived in `playerStore.ts`; extracted in M0 of
* the frontend refactor (2026-05-12).
*/
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { describe, expect, it } from 'vitest';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { makeSubsonicSong } from '@/test/helpers/factories';
@@ -1,5 +1,5 @@
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { coerceOpenArtistRefs } from '@/lib/api/openArtistRefs';
import { activeServerProfileId } from '@/features/playback/utils/playback/trackServerScope';
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { primaryTrackArtistRef, resolveTrackArtistRefs } from '@/features/playback/utils/playback/trackArtistRefs';
describe('resolveTrackArtistRefs', () => {
@@ -1,5 +1,5 @@
import type { SubsonicOpenArtistRef } from '@/lib/api/subsonicTypes';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { coerceOpenArtistRefs } from '@/lib/api/openArtistRefs';
type TrackArtistFields = Pick<Track, 'artist' | 'artistId' | 'artists'>;
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import {
activeServerQueueTrackIds,
filterQueueRefsForActiveServer,
@@ -1,4 +1,4 @@
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { canonicalQueueServerKey } from '@/utils/server/serverIndexKey';
@@ -11,7 +11,7 @@ import {
} from 'lucide-react';
import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { previewInputFromSong, usePreviewStore } from '@/features/playback/store/previewStore';
import { useThemeStore } from '@/store/themeStore';
@@ -1,5 +1,5 @@
import { useCallback } from 'react';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { enqueuePlaylistAll, playPlaylistAll, shufflePlaylistAll } from '@/features/playlist/utils/playlistBulkPlayActions';
export interface PlaylistBulkPlayCallbacksDeps {
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { getDisplayedSongs, type PlaylistSortDir, type PlaylistSortKey } from '@/features/playlist/utils/playlistDisplayedSongs';
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { enqueuePlaylistAll, playPlaylistAll, shufflePlaylistAll } from '@/features/playlist/utils/playlistBulkPlayActions';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
// Only id/queue identity matters for these actions.
const tracks = [{ id: 'a' }, { id: 'b' }, { id: 'c' }] as unknown as Track[];
@@ -1,4 +1,4 @@
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
// No `touchPlaylist` here: playing/shuffling/enqueuing does not modify the
// playlist. Touching it bumps `lastModified`, which is the playlist detail
@@ -1,7 +1,7 @@
import React from 'react';
import { ChevronDown, FolderOpen, HardDrive, Music, Waves } from 'lucide-react';
import type { TFunction } from 'i18next';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import type { LoudnessLufsPreset, NormalizationEngine } from '@/store/authStoreTypes';
import type { PlaybackSourceKind } from '@/features/playback/utils/playback/resolvePlaybackUrl';
import {
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { useAuthStore } from '@/store/authStore';
import type { QueueItemRef } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import type { QueueDisplayMode } from '@/store/authStoreTypes';
import type { DurationMode } from '@/utils/componentHelpers/queuePanelHelpers';
import { formatLongDuration } from '@/lib/format/formatDuration';
+2 -1
View File
@@ -5,7 +5,8 @@ import type { TFunction } from 'i18next';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { useLuckyMixStore } from '@/store/luckyMixStore';
import type { QueueItemRef, PlayerState } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import type { QueueDisplayMode } from '@/store/authStoreTypes';
import { formatTrackTime } from '@/lib/format/formatDuration';
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
+1 -1
View File
@@ -2,7 +2,7 @@ import { Play } from 'lucide-react';
import { updatePlaylist } from '@/lib/api/subsonicPlaylists';
import { resolvePlaylist, resolveMediaServerId } from '@/features/offline';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { useState, useRef, useMemo } from 'react';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { useOrbitStore } from '@/features/orbit';
@@ -3,7 +3,7 @@ import {
Blend, Check, FolderOpen, Infinity as InfinityIcon, ListMusic, MoveRight, Save, Share2, Shuffle, Trash2, Waves,
} from 'lucide-react';
import type { TFunction } from 'i18next';
import type { QueueItemRef } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import type {
QueueToolbarButtonConfig,
QueueToolbarButtonId,
@@ -1,6 +1,6 @@
import React, { useLayoutEffect } from 'react';
import { registerQueueListScrollTopReader, consumePendingQueueListScrollTop } from '@/features/playback/store/queueUndo';
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
interface Args {
queue: QueueItemRef[];
@@ -3,7 +3,7 @@ import { resolveAlbum, resolveMediaServerId } from '@/features/offline';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { useDragDrop, registerQueueDragHitTest } from '@/lib/dnd/DragDropContext';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
/** Drag types that may be dropped into the queue panel. */
const QUEUE_DROP_TYPES = new Set(['song', 'album', 'queue_reorder']);
@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { Track } from '@/features/playback/store/playerStoreTypes';
import type { Track } from '@/lib/media/trackTypes';
import { getCachedTrack, _resetQueueResolverForTest } from '@/features/playback/store/queueTrackResolver';
import { seedQueue } from '@/test/helpers/factories';
import { useQueueTrackAt, useCurrentTrack, useQueueItems } from '@/features/queue/hooks/useQueueTracks';
+1 -1
View File
@@ -1,6 +1,6 @@
import { useMemo, useSyncExternalStore } from 'react';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { QueueItemRef, Track } from '@/features/playback/store/playerStoreTypes';
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
import {
getQueueResolverVersion,