refactor(lib): move songToTrack + pure server-scope helpers to lib/media; split trackServerScope

songToTrack (the canonical Subsonic-song -> Track mapper) and the pure
server-scope helpers (activeServerProfileId, stampTrackServerId/s,
isMultiServerQueue, profileIdFromQueueRef) operate only on the lib/media model +
authStore + server-key utils -- no playback-store read. Move them to lib/media so
the ~58 app-wide consumers (cover, context menus, sharing, lucky-mix, library
browse, pages, hooks) depend on lib, not on the playback feature.

trackServerScope is split: the store-reading queue helpers (queueItemRefAt,
filterQueueRefs*, activeServerQueueTrackIds) stay in
features/playback/utils/playback/trackServerScope and build on the pure lib half.

With Track/QueueItemRef (prior commit), songToTrack and shuffleArray now in lib,
the media-domain model is fully out of features/playback. The remaining
playerStoreTypes imports are PlayerState only (the store shape). tsc 0, lint 0,
full suite 2353/2353, build OK.
This commit is contained in:
Psychotoxical
2026-06-30 17:24:29 +02:00
parent 9058abd340
commit 572dce4703
65 changed files with 123 additions and 102 deletions
@@ -2,7 +2,7 @@ import React from 'react';
import { AudioLines } from 'lucide-react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/lib/media/trackTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { formatLongDuration } from '@/lib/format/formatDuration';
interface Props {
+1 -1
View File
@@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom';
import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/lib/media/trackTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useSelectionStore } from '@/store/selectionStore';
import { useThemeStore } from '@/store/themeStore';
import { previewInputFromSong, usePreviewStore } from '@/features/playback/store/previewStore';
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef } from 'react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { useSelectionStore } from '@/store/selectionStore';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
interface UseAlbumTrackListSelectionArgs {
songs: SubsonicSong[];
+1 -1
View File
@@ -4,7 +4,7 @@ import { queueSongStar, queueSongRating } from '@/features/playback/store/pendin
import { getAlbumForServer } from '@/lib/api/subsonicLibrary';
import { getArtistInfo } from '@/lib/api/subsonicArtists';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { shuffleArray } from '@/lib/util/shuffleArray';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
+1 -1
View File
@@ -1,6 +1,6 @@
import { buildDownloadUrl } from '@/lib/api/subsonicStreamUrl';
import { resolveAlbum } from '@/features/offline';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useState, useEffect, useLayoutEffect, useRef, useMemo } from 'react';
import AlbumCard from '@/features/album/components/AlbumCard';
import { albumGridWarmCovers, coverDisplayCssPxForAlbumGrid } from '@/cover/layoutSizes';
+1 -1
View File
@@ -1,7 +1,7 @@
import { buildDownloadUrl } from '@/lib/api/subsonicStreamUrl';
import { resolveAlbum } from '@/features/offline';
import type { SubsonicAlbum } from '@/lib/api/subsonicTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import AlbumCard from '@/features/album/components/AlbumCard';
import { LOSSLESS_MODE_QUERY } from '@/lib/library/losslessMode';
@@ -5,7 +5,7 @@ import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { previewInputFromSong, usePreviewStore } from '@/features/playback/store/previewStore';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { formatTrackTime } from '@/lib/format/formatDuration';
import ArtistTopTrackCover from '@/features/artist/components/ArtistTopTrackCover';
import { topSongAlbumForCover } from '@/features/artist/components/topSongAlbumForCover';
@@ -2,7 +2,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 '@/lib/media/trackTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { runBulkPlayAll, runBulkShuffle } from '@/features/playback/utils/playback/runBulkPlay';
import { resolveAlbum, resolveMediaServerId } from '@/features/offline';
@@ -4,7 +4,7 @@ import { ListPlus, Play, SlidersHorizontal, X } from 'lucide-react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { useSelectionStore } from '@/store/selectionStore';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { AddToPlaylistSubmenu } from '@/components/ContextMenu';
import GenreFilterBar from '@/components/GenreFilterBar';
@@ -12,7 +12,7 @@ import { useSelectionStore } from '@/store/selectionStore';
import { useThemeStore } from '@/store/themeStore';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { appendServerQuery } from '@/utils/navigation/detailServerScope';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
import { useElementClientHeightById } from '@/lib/hooks/useResizeClientHeight';
+1 -1
View File
@@ -11,7 +11,7 @@ import { Music, EyeOff, LayoutGrid, RotateCcw, Eye } from 'lucide-react';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { useAuthStore } from '@/store/authStore';
import { useLyricsStore } from '@/store/lyricsStore';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useRadioMetadata } from '@/features/radio';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
@@ -1,5 +1,5 @@
import { getSong } from '@/lib/api/subsonicLibrary';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useEffect, useRef, useState } from 'react';
import { X, RefreshCw, Shuffle, Settings2, Share2, HelpCircle, Activity } from 'lucide-react';
import { useTranslation } from 'react-i18next';
+1 -1
View File
@@ -1,5 +1,5 @@
import { getSong } from '@/lib/api/subsonicLibrary';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useEffect, useRef } from 'react';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { useAuthStore } from '@/store/authStore';
+1 -1
View File
@@ -1,5 +1,5 @@
import { getSong } from '@/lib/api/subsonicLibrary';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useEffect } from 'react';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
+1 -1
View File
@@ -1,6 +1,6 @@
import { createPlaylist, deletePlaylist, getPlaylist, getPlaylists, updatePlaylist } from '@/lib/api/subsonicPlaylists';
import { getSong } from '@/lib/api/subsonicLibrary';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useAuthStore } from '@/store/authStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
+1 -1
View File
@@ -26,7 +26,7 @@ vi.mock('@/store/authStore', () => ({ useAuthStore: { getState: () => ({}) } }))
vi.mock('@/features/playback/store/playerStore', () => ({ usePlayerStore: { getState: () => ({ enqueue: vi.fn() }) } }));
vi.mock('@/lib/api/subsonicPlaylists', () => ({ createPlaylist: vi.fn(), deletePlaylist: vi.fn() }));
vi.mock('@/lib/api/subsonicLibrary', () => ({ getSong: vi.fn() }));
vi.mock('@/features/playback/utils/playback/songToTrack', () => ({ songToTrack: vi.fn() }));
vi.mock('@/lib/media/songToTrack', () => ({ songToTrack: vi.fn() }));
import { updateOrbitSettings } from '@/features/orbit/utils/host';
+1 -1
View File
@@ -1,6 +1,6 @@
import { createPlaylist, deletePlaylist } from '@/lib/api/subsonicPlaylists';
import { getSong } from '@/lib/api/subsonicLibrary';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useAuthStore } from '@/store/authStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
@@ -11,7 +11,7 @@ vi.mock('@/utils/server/serverLookup', () => ({
resolveServerIdForIndexKey: (id: string) => id,
}));
vi.mock('@/features/playback/utils/playback/songToTrack', () => ({
vi.mock('@/lib/media/songToTrack', () => ({
songToTrack: (s: { id: string }) => ({
id: s.id,
title: s.id,
@@ -1,5 +1,5 @@
import { getPlayQueueForServer, type PlayQueueResult } from '@/lib/api/subsonicPlayQueue';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { bindQueueServerId } from '@/features/playback/utils/playback/playbackServer';
import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
import { toQueueItemRefs } from '@/features/playback/store/queueItemRef';
+1 -1
View File
@@ -1,7 +1,7 @@
import { getSimilarSongs2, getTopSongs } from '@/lib/api/subsonicArtists';
import { invoke } from '@tauri-apps/api/core';
import { buildInfiniteQueueCandidates } from '@/features/playback/utils/playback/buildInfiniteQueueCandidates';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { ensureQueueServerPinned } from '@/features/playback/utils/playback/playbackServer';
import { useAuthStore } from '@/store/authStore';
import { setIsAudioPaused } from '@/features/playback/store/engineState';
@@ -39,7 +39,7 @@ vi.mock('@/features/playback/store/queueTrackView', () => ({
vi.mock('@/features/playback/utils/playback/buildInfiniteQueueCandidates', () => ({
buildInfiniteQueueCandidates: vi.fn(() => Promise.resolve([])),
}));
vi.mock('@/features/playback/utils/playback/songToTrack', () => ({ songToTrack: (s: unknown) => s }));
vi.mock('@/lib/media/songToTrack', () => ({ songToTrack: (s: unknown) => s }));
vi.mock('@/features/playback/utils/playback/playbackServer', () => ({ ensureQueueServerPinned: () => null }));
vi.mock('@/features/playback/store/queueTrackResolver', () => ({ seedQueueResolver: vi.fn() }));
vi.mock('@/features/playback/store/queueItemRef', () => ({ toQueueItemRefs: () => [] }));
@@ -26,7 +26,7 @@ import {
playbackProfileIdForTrack,
shouldBindQueueServerForPlay,
} from '@/features/playback/utils/playback/playbackServer';
import { stampTrackServerId, stampTrackServerIds } from '@/features/playback/utils/playback/trackServerScope';
import { stampTrackServerId, stampTrackServerIds } from '@/lib/media/trackServerScope';
import {
findLocalPlaybackUrl,
hasLocalPersistentPlaybackBytes,
+1 -1
View File
@@ -1,5 +1,5 @@
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { stampTrackServerId } from '@/features/playback/utils/playback/trackServerScope';
import { stampTrackServerId } from '@/lib/media/trackServerScope';
import { canonicalQueueServerKey } from '@/utils/server/serverIndexKey';
/**
@@ -2,7 +2,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 '@/lib/media/trackTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
import { canonicalQueueServerKey } from '@/utils/server/serverIndexKey';
import { trackToSong } from '@/lib/library/advancedSearchLocal';
+1 -1
View File
@@ -9,7 +9,7 @@ import {
import { resolvePlaybackUrl } from '@/features/playback/utils/playback/resolvePlaybackUrl';
import { resolveReplayGainDb } from '@/features/playback/utils/audio/resolveReplayGainDb';
import { audioPlayHiResBlendArgs } from '@/utils/audio/hiResCrossfadeResample';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useAuthStore } from '@/store/authStore';
import {
bumpPlayGeneration,
@@ -7,7 +7,7 @@ import {
passesMixMinRatings,
} from '@/utils/mix/mixRatingFilter';
import { shuffleArray } from '@/lib/util/shuffleArray';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
/**
* Infinite queue source strategy (Instant Mix-like):
* 1) Prefer artist-driven candidates (Top + Similar) around the current track.
@@ -6,7 +6,7 @@ import { fetchAllSongsByGenre, getGenres } from '@/lib/api/subsonicGenres';
import type { SubsonicGenre } from '@/lib/api/subsonicTypes';
import { libraryScopeForServer } from '@/lib/api/subsonicClient';
import type { Track } from '@/lib/media/trackTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { shuffleArray } from '@/lib/util/shuffleArray';
import { trackToSong } from '@/lib/library/advancedSearchLocal';
import { type AlbumBrowseSort } from '@/lib/library/albumBrowseSort';
@@ -1,6 +1,6 @@
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { resolveAlbumForActiveServer } from '@/store/mediaResolver';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { orbitSnapshot } from '@/store/orbitRuntime';
import { fadeOut } from '@/features/playback/utils/playback/fadeOut';
import { shouldAutodjInterruptBlend } from '@/features/playback/utils/playback/autodjManualBlend';
@@ -1,5 +1,5 @@
import { resolveAlbum, resolveArtist, resolveMediaServerId } from '@/store/mediaResolver';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { shuffleArray } from '@/lib/util/shuffleArray';
import { usePlayerStore } from '@/features/playback/store/playerStore';
/**
@@ -1,6 +1,6 @@
import { getSong } from '@/lib/api/subsonicLibrary';
import { resolveAlbumForActiveServer } from '@/store/mediaResolver';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { playAlbum } from '@/features/playback/utils/playback/playAlbum';
import { playArtistShuffled } from '@/features/playback/utils/playback/playArtistShuffled';
import { usePlayerStore } from '@/features/playback/store/playerStore';
@@ -1,5 +1,5 @@
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { fadeOut } from '@/features/playback/utils/playback/fadeOut';
import { shouldAutodjInterruptBlend } from '@/features/playback/utils/playback/autodjManualBlend';
@@ -18,11 +18,13 @@ import {
} from '@/utils/server/serverIndexKey';
import {
activeServerProfileId,
filterQueueRefsForServerProfile,
isMultiServerQueue,
profileIdFromQueueRef,
queueItemRefAt,
stampTrackServerIds,
} from '@/lib/media/trackServerScope';
import {
filterQueueRefsForServerProfile,
queueItemRefAt,
} from '@/features/playback/utils/playback/trackServerScope';
function playbackServerFromRef(ref: QueueItemRef): ServerProfile | undefined {
@@ -1,105 +0,0 @@
/**
* Pure-helper characterization for `songToTrack`.
*
* Maps a Subsonic song record into the internal `Track` shape used by the
* playback queue. Originally lived in `playerStore.ts`; extracted in M0 of
* the frontend refactor (2026-05-12).
*/
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';
describe('songToTrack', () => {
it('maps required Subsonic fields verbatim', () => {
const song = makeSubsonicSong({
id: 's1',
title: 'Hello',
artist: 'World',
album: 'Sample',
albumId: 'a1',
duration: 240,
});
const t = songToTrack(song);
expect(t.id).toBe('s1');
expect(t.title).toBe('Hello');
expect(t.artist).toBe('World');
expect(t.album).toBe('Sample');
expect(t.albumId).toBe('a1');
expect(t.duration).toBe(240);
});
it('copies optional fields when present', () => {
const song = makeSubsonicSong({
id: 's2',
artistId: 'ar-1',
track: 5,
year: 2024,
bitRate: 320,
suffix: 'flac',
userRating: 4,
starred: '2026-05-01T00:00:00Z',
genre: 'Rock',
samplingRate: 48000,
bitDepth: 24,
size: 9_000_000,
coverArt: 's2',
});
const t = songToTrack(song);
expect(t.artistId).toBe('ar-1');
expect(t.track).toBe(5);
expect(t.year).toBe(2024);
expect(t.bitRate).toBe(320);
expect(t.suffix).toBe('flac');
expect(t.userRating).toBe(4);
expect(t.starred).toBe('2026-05-01T00:00:00Z');
expect(t.genre).toBe('Rock');
expect(t.samplingRate).toBe(48000);
expect(t.bitDepth).toBe(24);
expect(t.size).toBe(9_000_000);
expect(t.coverArt).toBe('s2');
});
it('flattens replayGain into replayGainTrackDb / AlbumDb / Peak', () => {
const song = makeSubsonicSong({
replayGain: {
trackGain: -6.5,
albumGain: -7.1,
trackPeak: 0.98,
albumPeak: 0.99,
},
});
const t = songToTrack(song);
expect(t.replayGainTrackDb).toBe(-6.5);
expect(t.replayGainAlbumDb).toBe(-7.1);
expect(t.replayGainPeak).toBe(0.98);
// albumPeak is intentionally not surfaced — only trackPeak.
expect((t as Track & { replayGainAlbumPeak?: number }).replayGainAlbumPeak).toBeUndefined();
});
it('leaves replayGain fields undefined when the song has no replayGain block', () => {
const song = makeSubsonicSong({ replayGain: undefined });
const t = songToTrack(song);
expect(t.replayGainTrackDb).toBeUndefined();
expect(t.replayGainAlbumDb).toBeUndefined();
expect(t.replayGainPeak).toBeUndefined();
});
it('copies OpenSubsonic artists when present', () => {
const song = makeSubsonicSong({
artists: [{ id: 'a1', name: 'Feat' }, { id: 'a2', name: 'Main' }],
});
const t = songToTrack(song);
expect(t.artists).toEqual(song.artists);
});
it('does not invent fields that the Subsonic song lacks', () => {
const song = makeSubsonicSong({});
const t = songToTrack(song);
// Internal queue-routing flags are added by enqueue paths, not by mapping.
expect(t.autoAdded).toBeUndefined();
expect(t.radioAdded).toBeUndefined();
expect(t.playNextAdded).toBeUndefined();
expect(t.artists).toBeUndefined();
});
});
@@ -1,36 +0,0 @@
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/lib/media/trackTypes';
import { coerceOpenArtistRefs } from '@/lib/api/openArtistRefs';
import { activeServerProfileId } from '@/features/playback/utils/playback/trackServerScope';
export function songToTrack(song: SubsonicSong): Track {
return {
id: song.id,
title: song.title,
artist: song.artist,
album: song.album,
albumId: song.albumId,
artistId: song.artistId,
artists: (() => {
const artists = coerceOpenArtistRefs(song.artists);
return artists.length > 0 ? artists : undefined;
})(),
duration: song.duration,
coverArt: song.coverArt,
discNumber: song.discNumber,
track: song.track,
year: song.year,
bitRate: song.bitRate,
suffix: song.suffix,
userRating: song.userRating,
replayGainTrackDb: song.replayGain?.trackGain,
replayGainAlbumDb: song.replayGain?.albumGain,
replayGainPeak: song.replayGain?.trackPeak,
starred: song.starred,
genre: song.genre,
samplingRate: song.samplingRate,
bitDepth: song.bitDepth,
size: song.size,
serverId: song.serverId ?? activeServerProfileId(),
};
}
@@ -2,12 +2,14 @@ import { beforeEach, describe, expect, it } from 'vitest';
import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { Track } from '@/lib/media/trackTypes';
import {
isMultiServerQueue,
stampTrackServerId,
} from '@/lib/media/trackServerScope';
import {
activeServerQueueTrackIds,
filterQueueRefsForActiveServer,
isMultiServerQueue,
queueItemRefAt,
stampTrackServerId,
} from '@/features/playback/utils/playback/trackServerScope';
const baseTrack = (): Track => ({
@@ -1,29 +1,13 @@
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import { useAuthStore } from '@/store/authStore';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { canonicalQueueServerKey } from '@/utils/server/serverIndexKey';
import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
/** Active saved-server profile id (auth UUID), when logged in. */
export function activeServerProfileId(): string | undefined {
return useAuthStore.getState().activeServerId ?? undefined;
}
import { activeServerProfileId, profileIdFromQueueRef } from '@/lib/media/trackServerScope';
/**
* Ensure every track carries an owning server before it enters the queue.
* Explicit `track.serverId` wins; otherwise `fallbackServerId`, then active server.
* Store-reading queue-scope helpers. These read the live player store (current
* queue + pinned server), so they stay in the playback feature; the pure stamp
* / classify helpers they build on live in `@/lib/media/trackServerScope`.
*/
export function stampTrackServerId(track: Track, fallbackServerId?: string): Track {
const serverId = track.serverId ?? fallbackServerId ?? activeServerProfileId();
if (!serverId || track.serverId === serverId) {
return serverId && !track.serverId ? { ...track, serverId } : track;
}
return { ...track, serverId };
}
export function stampTrackServerIds(tracks: Track[], fallbackServerId?: string): Track[] {
return tracks.map(t => stampTrackServerId(t, fallbackServerId));
}
/** Canonical queue ref at `index`, or the currently playing slot. */
export function queueItemRefAt(index?: number): QueueItemRef | null {
@@ -34,22 +18,6 @@ export function queueItemRefAt(index?: number): QueueItemRef | null {
return queueItems[idx] ?? null;
}
/** True when queue refs resolve to more than one server bucket. */
export function isMultiServerQueue(refs: QueueItemRef[]): boolean {
const keys = new Set<string>();
for (const ref of refs) {
if (!ref.serverId) continue;
keys.add(canonicalQueueServerKey(ref.serverId) || ref.serverId);
if (keys.size > 1) return true;
}
return false;
}
export function profileIdFromQueueRef(ref: QueueItemRef | null | undefined): string {
if (!ref?.serverId) return '';
return resolveServerIdForIndexKey(ref.serverId) || ref.serverId;
}
function refsForServerProfile(refs: QueueItemRef[], profileId: string): QueueItemRef[] {
if (!profileId) return [];
return refs.filter(ref => queueRefProfileIdForTarget(ref, profileId));
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Check, ListPlus, X } from 'lucide-react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { formatTrackTime } from '@/lib/format/formatDuration';
import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes';
@@ -10,7 +10,7 @@ import StarRating from '@/components/StarRating';
import { PlaylistArtistCell } from '@/features/playlist/components/PlaylistArtistCell';
import { useThemeStore } from '@/store/themeStore';
import { usePlaylistLayoutStore } from '@/features/playlist/store/playlistLayoutStore';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { getQueueTracksView } from '@/features/playback/store/queueTrackView';
import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
@@ -17,7 +17,7 @@ import { previewInputFromSong, usePreviewStore } from '@/features/playback/store
import { useThemeStore } from '@/store/themeStore';
import { useDragDrop } from '@/lib/dnd/DragDropContext';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import type { PlaylistSortKey, PlaylistSortDir } from '@/features/playlist/utils/playlistDisplayedSongs';
import { AddToPlaylistSubmenu } from '@/components/ContextMenu';
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/lib/media/trackTypes';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { getDisplayedSongs, type PlaylistSortDir, type PlaylistSortKey } from '@/features/playlist/utils/playlistDisplayedSongs';
export interface PlaylistDerivedOptions {
+1 -1
View File
@@ -2,7 +2,7 @@ import { resolveMediaServerId, resolvePlaylist } from '@/features/offline';
import { getGenres } from '@/lib/api/subsonicGenres';
import { filterSongsToActiveLibrary } from '@/lib/api/subsonicLibrary';
import type { SubsonicPlaylist, SubsonicGenre } from '@/lib/api/subsonicTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { usePlaylistStore } from '@/features/playlist/store/playlistStore';
@@ -1,5 +1,5 @@
import { getPlaylist } from '@/lib/api/subsonicPlaylists';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { playPlaylistAll } from '@/features/playlist/utils/playlistBulkPlayActions';
@@ -1,6 +1,6 @@
import type React from 'react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
export interface StartPlaylistRowDragDeps {
e: React.MouseEvent;
+1 -1
View File
@@ -1,7 +1,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 { songToTrack } from '@/lib/media/songToTrack';
import type { Track } from '@/lib/media/trackTypes';
import { useState, useRef, useMemo } from 'react';
import { usePlayerStore } from '@/features/playback/store/playerStore';
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { resolveAlbum, resolveMediaServerId } from '@/features/offline';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useDragDrop, registerQueueDragHitTest } from '@/lib/dnd/DragDropContext';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { Track } from '@/lib/media/trackTypes';
@@ -1,6 +1,6 @@
import { subscribeLibrarySyncIdle, subscribeLibrarySyncProgress } from '@/lib/api/library';
import type { SearchResults, SubsonicArtist } from '@/lib/api/subsonicTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import {
LIVE_SEARCH_DEBOUNCE_NETWORK_MS,
LIVE_SEARCH_DEBOUNCE_RACE_MS,
@@ -1,6 +1,6 @@
import { search } from '@/lib/api/subsonicSearch';
import type { SearchResults, SubsonicArtist } from '@/lib/api/subsonicTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
import type { ServerProfile } from '@/store/authStoreTypes';
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
import { songToTrack } from '@/lib/media/songToTrack';
import { activateShareSearchServer } from '@/utils/share/enqueueShareSearchPayload';
import { sharePayloadTotal, type ShareSearchMatch } from '@/utils/share/shareSearch';
import type { ShareSearchPreviewState } from '@/features/search/hooks/useShareSearchPreview';