mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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,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,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';
|
||||
|
||||
@@ -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,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,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,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';
|
||||
|
||||
@@ -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,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,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,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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
Reference in New Issue
Block a user