From 572dce470391d619e533901b5c439bb55b8a10b0 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:24:29 +0200 Subject: [PATCH] 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. --- src/components/BecauseYouLikeRail.tsx | 2 +- src/components/Hero.tsx | 2 +- src/components/SongCard.tsx | 2 +- src/components/SongRow.tsx | 2 +- .../contextMenu/AlbumContextItems.tsx | 2 +- .../contextMenu/SongContextItems.tsx | 2 +- src/components/tracks/TracksPageChrome.tsx | 2 +- src/config/shortcutActionRegistry.ts | 2 +- .../album/components/AlbumTrackListMobile.tsx | 2 +- src/features/album/components/TrackRow.tsx | 2 +- .../album/hooks/useAlbumTrackListSelection.ts | 2 +- src/features/album/pages/AlbumDetail.tsx | 2 +- src/features/album/pages/Albums.tsx | 2 +- src/features/album/pages/LosslessAlbums.tsx | 2 +- .../components/ArtistDetailTopTracks.tsx | 2 +- .../artist/utils/runArtistDetailPlay.ts | 2 +- .../FavoritesSongsSectionHeader.tsx | 2 +- .../components/FavoritesSongsTracklist.tsx | 2 +- src/features/nowPlaying/pages/NowPlaying.tsx | 2 +- .../orbit/components/OrbitSessionBar.tsx | 2 +- src/features/orbit/hooks/useOrbitGuest.ts | 2 +- src/features/orbit/hooks/useOrbitHost.ts | 2 +- src/features/orbit/utils/guest.ts | 2 +- src/features/orbit/utils/host.test.ts | 2 +- src/features/orbit/utils/host.ts | 2 +- .../store/applyServerPlayQueue.idle.test.ts | 2 +- .../playback/store/applyServerPlayQueue.ts | 2 +- src/features/playback/store/nextAction.ts | 2 +- .../store/nextActionOrbitRadio.test.ts | 2 +- .../playback/store/playTrackAction.ts | 2 +- src/features/playback/store/queueItemRef.ts | 2 +- .../playback/store/queueTrackResolver.ts | 2 +- src/features/playback/store/resumeAction.ts | 2 +- .../playback/buildInfiniteQueueCandidates.ts | 2 +- .../utils/playback/genreBrowsePlayback.ts | 2 +- .../playback/utils/playback/playAlbum.ts | 2 +- .../utils/playback/playArtistShuffled.ts | 2 +- .../playback/utils/playback/playByOpaqueId.ts | 2 +- .../playback/utils/playback/playSong.ts | 2 +- .../playback/utils/playback/playbackServer.ts | 6 ++- .../utils/playback/trackServerScope.test.ts | 6 ++- .../utils/playback/trackServerScope.ts | 42 ++-------------- .../components/PlaylistSongSearchPanel.tsx | 2 +- .../components/PlaylistSuggestions.tsx | 2 +- .../playlist/components/PlaylistTracklist.tsx | 2 +- .../playlist/hooks/usePlaylistDerived.ts | 2 +- src/features/playlist/pages/Playlists.tsx | 2 +- .../playlist/utils/playPlaylistById.ts | 2 +- .../playlist/utils/startPlaylistRowDrag.ts | 2 +- src/features/queue/components/QueuePanel.tsx | 2 +- src/features/queue/hooks/useQueuePanelDrag.ts | 2 +- src/features/search/components/LiveSearch.tsx | 2 +- .../search/components/MobileSearchOverlay.tsx | 2 +- .../search/components/ShareSearchResults.tsx | 2 +- src/hooks/tauriBridge/useCliBridge.ts | 2 +- .../media}/songToTrack.test.ts | 2 +- .../playback => lib/media}/songToTrack.ts | 2 +- src/lib/media/trackServerScope.ts | 49 +++++++++++++++++++ src/pages/MostPlayed.tsx | 2 +- src/pages/RandomMix.tsx | 2 +- .../componentHelpers/contextMenuActions.ts | 2 +- src/utils/mix/luckyMix.ts | 2 +- src/utils/share/applySharePaste.ts | 2 +- .../share/enqueueShareSearchPayload.test.ts | 2 +- src/utils/share/enqueueShareSearchPayload.ts | 2 +- 65 files changed, 123 insertions(+), 102 deletions(-) rename src/{features/playback/utils/playback => lib/media}/songToTrack.test.ts (97%) rename src/{features/playback/utils/playback => lib/media}/songToTrack.ts (92%) create mode 100644 src/lib/media/trackServerScope.ts diff --git a/src/components/BecauseYouLikeRail.tsx b/src/components/BecauseYouLikeRail.tsx index 93031729..f2578ece 100644 --- a/src/components/BecauseYouLikeRail.tsx +++ b/src/components/BecauseYouLikeRail.tsx @@ -2,7 +2,7 @@ import { getArtist, getArtistInfo } from '@/lib/api/subsonicArtists'; import { filterAlbumsToActiveLibrary } from '@/lib/api/subsonicLibrary'; import { resolveAlbum, resolveMediaServerId } 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 { shuffleArray } from '@/lib/util/shuffleArray'; import React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 878fe345..da5498c5 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -1,7 +1,7 @@ import { getRandomAlbums } from '@/lib/api/subsonicLibrary'; import { resolveAlbum, resolveMediaServerId } 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 React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { useNavigateToAlbum } from '@/features/album'; import { Play, ListPlus, ChevronLeft, ChevronRight } from 'lucide-react'; diff --git a/src/components/SongCard.tsx b/src/components/SongCard.tsx index 05fb183d..415482cb 100644 --- a/src/components/SongCard.tsx +++ b/src/components/SongCard.tsx @@ -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 React, { memo, useMemo } from 'react'; import { Play, ListPlus, Star, Disc3 } from 'lucide-react'; import { useTranslation } from 'react-i18next'; diff --git a/src/components/SongRow.tsx b/src/components/SongRow.tsx index c7fe3545..95d16398 100644 --- a/src/components/SongRow.tsx +++ b/src/components/SongRow.tsx @@ -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 React, { memo } from 'react'; import { useNavigateToAlbum } from '@/features/album'; import { useNavigateToArtist } from '@/features/artist'; diff --git a/src/components/contextMenu/AlbumContextItems.tsx b/src/components/contextMenu/AlbumContextItems.tsx index a25aa362..1456b77d 100644 --- a/src/components/contextMenu/AlbumContextItems.tsx +++ b/src/components/contextMenu/AlbumContextItems.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { resolveAlbum, resolveMediaServerId } from '@/features/offline'; import { star, unstar } from '@/lib/api/subsonicStarRating'; import type { SubsonicAlbum } from '@/lib/api/subsonicTypes'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import StarRating from '../StarRating'; import { AlbumToPlaylistSubmenu } from './AlbumArtistToPlaylistSubmenu'; import { MultiAlbumToPlaylistSubmenu } from './MultiAlbumToPlaylistSubmenu'; diff --git a/src/components/contextMenu/SongContextItems.tsx b/src/components/contextMenu/SongContextItems.tsx index 845474a3..32e3d521 100644 --- a/src/components/contextMenu/SongContextItems.tsx +++ b/src/components/contextMenu/SongContextItems.tsx @@ -8,7 +8,7 @@ import { getMusicNetworkRuntime, useEnrichmentPrimary } from '../../music-networ import type { Track } from '@/lib/media/trackTypes'; import { useAuthStore } from '../../store/authStore'; import { usePlaylistStore } from '@/features/playlist'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import { showToast } from '../../utils/ui/toast'; import { suggestOrbitTrack, hostEnqueueToOrbit, evaluateOrbitSuggestGate, OrbitSuggestBlockedError } from '@/features/orbit'; import { renderPresetIcon } from '../settings/musicNetwork/presetIcon'; diff --git a/src/components/tracks/TracksPageChrome.tsx b/src/components/tracks/TracksPageChrome.tsx index 56d63c59..a44b79fe 100644 --- a/src/components/tracks/TracksPageChrome.tsx +++ b/src/components/tracks/TracksPageChrome.tsx @@ -1,7 +1,7 @@ import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; import { getRandomSongs } from '@/lib/api/subsonicLibrary'; import type { SubsonicSong } from '@/lib/api/subsonicTypes'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Play, ListPlus, RefreshCw, Sparkles } from 'lucide-react'; import { useTranslation } from 'react-i18next'; diff --git a/src/config/shortcutActionRegistry.ts b/src/config/shortcutActionRegistry.ts index 46211e7c..5d865611 100644 --- a/src/config/shortcutActionRegistry.ts +++ b/src/config/shortcutActionRegistry.ts @@ -1,6 +1,6 @@ import { queueSongStar } from '@/features/playback/store/pendingStarSync'; import { getSong } from '@/lib/api/subsonicLibrary'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import { invoke } from '@tauri-apps/api/core'; import { getCurrentWindow } from '@tauri-apps/api/window'; import i18n from '@/lib/i18n'; diff --git a/src/features/album/components/AlbumTrackListMobile.tsx b/src/features/album/components/AlbumTrackListMobile.tsx index 72de6807..db5014b4 100644 --- a/src/features/album/components/AlbumTrackListMobile.tsx +++ b/src/features/album/components/AlbumTrackListMobile.tsx @@ -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 { diff --git a/src/features/album/components/TrackRow.tsx b/src/features/album/components/TrackRow.tsx index ce0c52fa..358b4060 100644 --- a/src/features/album/components/TrackRow.tsx +++ b/src/features/album/components/TrackRow.tsx @@ -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'; diff --git a/src/features/album/hooks/useAlbumTrackListSelection.ts b/src/features/album/hooks/useAlbumTrackListSelection.ts index fbd2fc75..e8400db8 100644 --- a/src/features/album/hooks/useAlbumTrackListSelection.ts +++ b/src/features/album/hooks/useAlbumTrackListSelection.ts @@ -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[]; diff --git a/src/features/album/pages/AlbumDetail.tsx b/src/features/album/pages/AlbumDetail.tsx index 37f55d2c..bdb402a0 100644 --- a/src/features/album/pages/AlbumDetail.tsx +++ b/src/features/album/pages/AlbumDetail.tsx @@ -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'; diff --git a/src/features/album/pages/Albums.tsx b/src/features/album/pages/Albums.tsx index 2e648d97..bf26f54b 100644 --- a/src/features/album/pages/Albums.tsx +++ b/src/features/album/pages/Albums.tsx @@ -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'; diff --git a/src/features/album/pages/LosslessAlbums.tsx b/src/features/album/pages/LosslessAlbums.tsx index ffef7b57..c74111dd 100644 --- a/src/features/album/pages/LosslessAlbums.tsx +++ b/src/features/album/pages/LosslessAlbums.tsx @@ -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'; diff --git a/src/features/artist/components/ArtistDetailTopTracks.tsx b/src/features/artist/components/ArtistDetailTopTracks.tsx index 806bcd4d..d490b70d 100644 --- a/src/features/artist/components/ArtistDetailTopTracks.tsx +++ b/src/features/artist/components/ArtistDetailTopTracks.tsx @@ -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'; diff --git a/src/features/artist/utils/runArtistDetailPlay.ts b/src/features/artist/utils/runArtistDetailPlay.ts index ca8c15ec..bbac7101 100644 --- a/src/features/artist/utils/runArtistDetailPlay.ts +++ b/src/features/artist/utils/runArtistDetailPlay.ts @@ -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'; diff --git a/src/features/favorites/components/FavoritesSongsSectionHeader.tsx b/src/features/favorites/components/FavoritesSongsSectionHeader.tsx index f2b5535a..df9179f8 100644 --- a/src/features/favorites/components/FavoritesSongsSectionHeader.tsx +++ b/src/features/favorites/components/FavoritesSongsSectionHeader.tsx @@ -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'; diff --git a/src/features/favorites/components/FavoritesSongsTracklist.tsx b/src/features/favorites/components/FavoritesSongsTracklist.tsx index 8d889ede..c444c3be 100644 --- a/src/features/favorites/components/FavoritesSongsTracklist.tsx +++ b/src/features/favorites/components/FavoritesSongsTracklist.tsx @@ -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'; diff --git a/src/features/nowPlaying/pages/NowPlaying.tsx b/src/features/nowPlaying/pages/NowPlaying.tsx index ede611b7..93786204 100644 --- a/src/features/nowPlaying/pages/NowPlaying.tsx +++ b/src/features/nowPlaying/pages/NowPlaying.tsx @@ -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'; diff --git a/src/features/orbit/components/OrbitSessionBar.tsx b/src/features/orbit/components/OrbitSessionBar.tsx index 4cd6d08e..ff778a75 100644 --- a/src/features/orbit/components/OrbitSessionBar.tsx +++ b/src/features/orbit/components/OrbitSessionBar.tsx @@ -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'; diff --git a/src/features/orbit/hooks/useOrbitGuest.ts b/src/features/orbit/hooks/useOrbitGuest.ts index 1cf40cee..fd9ce31b 100644 --- a/src/features/orbit/hooks/useOrbitGuest.ts +++ b/src/features/orbit/hooks/useOrbitGuest.ts @@ -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'; diff --git a/src/features/orbit/hooks/useOrbitHost.ts b/src/features/orbit/hooks/useOrbitHost.ts index 5023e78b..4d984eb6 100644 --- a/src/features/orbit/hooks/useOrbitHost.ts +++ b/src/features/orbit/hooks/useOrbitHost.ts @@ -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'; diff --git a/src/features/orbit/utils/guest.ts b/src/features/orbit/utils/guest.ts index ffe85b6d..f1770c4f 100644 --- a/src/features/orbit/utils/guest.ts +++ b/src/features/orbit/utils/guest.ts @@ -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'; diff --git a/src/features/orbit/utils/host.test.ts b/src/features/orbit/utils/host.test.ts index deba5a5f..34b663e4 100644 --- a/src/features/orbit/utils/host.test.ts +++ b/src/features/orbit/utils/host.test.ts @@ -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'; diff --git a/src/features/orbit/utils/host.ts b/src/features/orbit/utils/host.ts index 39985e58..b9c095dd 100644 --- a/src/features/orbit/utils/host.ts +++ b/src/features/orbit/utils/host.ts @@ -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'; diff --git a/src/features/playback/store/applyServerPlayQueue.idle.test.ts b/src/features/playback/store/applyServerPlayQueue.idle.test.ts index dbf404ab..83462bc2 100644 --- a/src/features/playback/store/applyServerPlayQueue.idle.test.ts +++ b/src/features/playback/store/applyServerPlayQueue.idle.test.ts @@ -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, diff --git a/src/features/playback/store/applyServerPlayQueue.ts b/src/features/playback/store/applyServerPlayQueue.ts index b995259d..8bca47bc 100644 --- a/src/features/playback/store/applyServerPlayQueue.ts +++ b/src/features/playback/store/applyServerPlayQueue.ts @@ -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'; diff --git a/src/features/playback/store/nextAction.ts b/src/features/playback/store/nextAction.ts index c53f34ab..15142d66 100644 --- a/src/features/playback/store/nextAction.ts +++ b/src/features/playback/store/nextAction.ts @@ -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'; diff --git a/src/features/playback/store/nextActionOrbitRadio.test.ts b/src/features/playback/store/nextActionOrbitRadio.test.ts index 8dbcb705..26cf2f67 100644 --- a/src/features/playback/store/nextActionOrbitRadio.test.ts +++ b/src/features/playback/store/nextActionOrbitRadio.test.ts @@ -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: () => [] })); diff --git a/src/features/playback/store/playTrackAction.ts b/src/features/playback/store/playTrackAction.ts index d923caff..42fe2a9f 100644 --- a/src/features/playback/store/playTrackAction.ts +++ b/src/features/playback/store/playTrackAction.ts @@ -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, diff --git a/src/features/playback/store/queueItemRef.ts b/src/features/playback/store/queueItemRef.ts index c2fd7c3c..57f70d18 100644 --- a/src/features/playback/store/queueItemRef.ts +++ b/src/features/playback/store/queueItemRef.ts @@ -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'; /** diff --git a/src/features/playback/store/queueTrackResolver.ts b/src/features/playback/store/queueTrackResolver.ts index d382f5de..01fa7d62 100644 --- a/src/features/playback/store/queueTrackResolver.ts +++ b/src/features/playback/store/queueTrackResolver.ts @@ -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'; diff --git a/src/features/playback/store/resumeAction.ts b/src/features/playback/store/resumeAction.ts index 5ac2b575..4355f279 100644 --- a/src/features/playback/store/resumeAction.ts +++ b/src/features/playback/store/resumeAction.ts @@ -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, diff --git a/src/features/playback/utils/playback/buildInfiniteQueueCandidates.ts b/src/features/playback/utils/playback/buildInfiniteQueueCandidates.ts index c7d9dba1..3624c4f0 100644 --- a/src/features/playback/utils/playback/buildInfiniteQueueCandidates.ts +++ b/src/features/playback/utils/playback/buildInfiniteQueueCandidates.ts @@ -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. diff --git a/src/features/playback/utils/playback/genreBrowsePlayback.ts b/src/features/playback/utils/playback/genreBrowsePlayback.ts index fddcde0a..fd4f15f6 100644 --- a/src/features/playback/utils/playback/genreBrowsePlayback.ts +++ b/src/features/playback/utils/playback/genreBrowsePlayback.ts @@ -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'; diff --git a/src/features/playback/utils/playback/playAlbum.ts b/src/features/playback/utils/playback/playAlbum.ts index fe197c46..4dd3cca9 100644 --- a/src/features/playback/utils/playback/playAlbum.ts +++ b/src/features/playback/utils/playback/playAlbum.ts @@ -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'; diff --git a/src/features/playback/utils/playback/playArtistShuffled.ts b/src/features/playback/utils/playback/playArtistShuffled.ts index f74dee83..7ed44872 100644 --- a/src/features/playback/utils/playback/playArtistShuffled.ts +++ b/src/features/playback/utils/playback/playArtistShuffled.ts @@ -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'; /** diff --git a/src/features/playback/utils/playback/playByOpaqueId.ts b/src/features/playback/utils/playback/playByOpaqueId.ts index 578bbf3b..60f4bddd 100644 --- a/src/features/playback/utils/playback/playByOpaqueId.ts +++ b/src/features/playback/utils/playback/playByOpaqueId.ts @@ -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'; diff --git a/src/features/playback/utils/playback/playSong.ts b/src/features/playback/utils/playback/playSong.ts index 3dbed440..5acf997d 100644 --- a/src/features/playback/utils/playback/playSong.ts +++ b/src/features/playback/utils/playback/playSong.ts @@ -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'; diff --git a/src/features/playback/utils/playback/playbackServer.ts b/src/features/playback/utils/playback/playbackServer.ts index c9092c5e..72cf9730 100644 --- a/src/features/playback/utils/playback/playbackServer.ts +++ b/src/features/playback/utils/playback/playbackServer.ts @@ -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 { diff --git a/src/features/playback/utils/playback/trackServerScope.test.ts b/src/features/playback/utils/playback/trackServerScope.test.ts index 5fbbdf95..19193cd1 100644 --- a/src/features/playback/utils/playback/trackServerScope.test.ts +++ b/src/features/playback/utils/playback/trackServerScope.test.ts @@ -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 => ({ diff --git a/src/features/playback/utils/playback/trackServerScope.ts b/src/features/playback/utils/playback/trackServerScope.ts index f718b221..3e4ee520 100644 --- a/src/features/playback/utils/playback/trackServerScope.ts +++ b/src/features/playback/utils/playback/trackServerScope.ts @@ -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(); - 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)); diff --git a/src/features/playlist/components/PlaylistSongSearchPanel.tsx b/src/features/playlist/components/PlaylistSongSearchPanel.tsx index ee6a851b..70a2b33d 100644 --- a/src/features/playlist/components/PlaylistSongSearchPanel.tsx +++ b/src/features/playlist/components/PlaylistSongSearchPanel.tsx @@ -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'; diff --git a/src/features/playlist/components/PlaylistSuggestions.tsx b/src/features/playlist/components/PlaylistSuggestions.tsx index a2b51c9c..c1ad0630 100644 --- a/src/features/playlist/components/PlaylistSuggestions.tsx +++ b/src/features/playlist/components/PlaylistSuggestions.tsx @@ -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'; diff --git a/src/features/playlist/components/PlaylistTracklist.tsx b/src/features/playlist/components/PlaylistTracklist.tsx index 04138b06..adff5f58 100644 --- a/src/features/playlist/components/PlaylistTracklist.tsx +++ b/src/features/playlist/components/PlaylistTracklist.tsx @@ -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'; diff --git a/src/features/playlist/hooks/usePlaylistDerived.ts b/src/features/playlist/hooks/usePlaylistDerived.ts index b2cde553..d3f355b5 100644 --- a/src/features/playlist/hooks/usePlaylistDerived.ts +++ b/src/features/playlist/hooks/usePlaylistDerived.ts @@ -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 { diff --git a/src/features/playlist/pages/Playlists.tsx b/src/features/playlist/pages/Playlists.tsx index 0df2d9f3..372cab4d 100644 --- a/src/features/playlist/pages/Playlists.tsx +++ b/src/features/playlist/pages/Playlists.tsx @@ -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'; diff --git a/src/features/playlist/utils/playPlaylistById.ts b/src/features/playlist/utils/playPlaylistById.ts index 7d7a146a..616e545a 100644 --- a/src/features/playlist/utils/playPlaylistById.ts +++ b/src/features/playlist/utils/playPlaylistById.ts @@ -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'; diff --git a/src/features/playlist/utils/startPlaylistRowDrag.ts b/src/features/playlist/utils/startPlaylistRowDrag.ts index 58bc546a..13e45987 100644 --- a/src/features/playlist/utils/startPlaylistRowDrag.ts +++ b/src/features/playlist/utils/startPlaylistRowDrag.ts @@ -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; diff --git a/src/features/queue/components/QueuePanel.tsx b/src/features/queue/components/QueuePanel.tsx index 8851a3da..07316254 100644 --- a/src/features/queue/components/QueuePanel.tsx +++ b/src/features/queue/components/QueuePanel.tsx @@ -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'; diff --git a/src/features/queue/hooks/useQueuePanelDrag.ts b/src/features/queue/hooks/useQueuePanelDrag.ts index 26eebedb..aedc30b3 100644 --- a/src/features/queue/hooks/useQueuePanelDrag.ts +++ b/src/features/queue/hooks/useQueuePanelDrag.ts @@ -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'; diff --git a/src/features/search/components/LiveSearch.tsx b/src/features/search/components/LiveSearch.tsx index ad45b70c..841c6cd9 100644 --- a/src/features/search/components/LiveSearch.tsx +++ b/src/features/search/components/LiveSearch.tsx @@ -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, diff --git a/src/features/search/components/MobileSearchOverlay.tsx b/src/features/search/components/MobileSearchOverlay.tsx index 0b76ba03..114190ae 100644 --- a/src/features/search/components/MobileSearchOverlay.tsx +++ b/src/features/search/components/MobileSearchOverlay.tsx @@ -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'; diff --git a/src/features/search/components/ShareSearchResults.tsx b/src/features/search/components/ShareSearchResults.tsx index 191859d3..85e8146b 100644 --- a/src/features/search/components/ShareSearchResults.tsx +++ b/src/features/search/components/ShareSearchResults.tsx @@ -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'; diff --git a/src/hooks/tauriBridge/useCliBridge.ts b/src/hooks/tauriBridge/useCliBridge.ts index 12824a02..806a0ce8 100644 --- a/src/hooks/tauriBridge/useCliBridge.ts +++ b/src/hooks/tauriBridge/useCliBridge.ts @@ -7,7 +7,7 @@ import { getMusicFolders } from '@/lib/api/subsonicLibrary'; import { search as subsonicSearch } from '@/lib/api/subsonicSearch'; import { filterSongsForLuckyMixRatings, getMixMinRatingsConfigFromAuth } from '../../utils/mix/mixRatingFilter'; import { shuffleArray } from '@/lib/util/shuffleArray'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import { showToast } from '../../utils/ui/toast'; import { switchActiveServer } from '../../utils/server/switchActiveServer'; import i18n from '@/lib/i18n'; diff --git a/src/features/playback/utils/playback/songToTrack.test.ts b/src/lib/media/songToTrack.test.ts similarity index 97% rename from src/features/playback/utils/playback/songToTrack.test.ts rename to src/lib/media/songToTrack.test.ts index 3cf503a0..1460de30 100644 --- a/src/features/playback/utils/playback/songToTrack.test.ts +++ b/src/lib/media/songToTrack.test.ts @@ -7,7 +7,7 @@ */ import type { Track } from '@/lib/media/trackTypes'; import { describe, expect, it } from 'vitest'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import { makeSubsonicSong } from '@/test/helpers/factories'; describe('songToTrack', () => { diff --git a/src/features/playback/utils/playback/songToTrack.ts b/src/lib/media/songToTrack.ts similarity index 92% rename from src/features/playback/utils/playback/songToTrack.ts rename to src/lib/media/songToTrack.ts index cdf541ec..61d93c27 100644 --- a/src/features/playback/utils/playback/songToTrack.ts +++ b/src/lib/media/songToTrack.ts @@ -1,7 +1,7 @@ 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'; +import { activeServerProfileId } from '@/lib/media/trackServerScope'; export function songToTrack(song: SubsonicSong): Track { return { diff --git a/src/lib/media/trackServerScope.ts b/src/lib/media/trackServerScope.ts new file mode 100644 index 00000000..81b5d9c8 --- /dev/null +++ b/src/lib/media/trackServerScope.ts @@ -0,0 +1,49 @@ +import type { Track, QueueItemRef } from '@/lib/media/trackTypes'; +import { useAuthStore } from '@/store/authStore'; +import { canonicalQueueServerKey } from '@/utils/server/serverIndexKey'; +import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup'; + +/** + * Pure server-scope helpers for the shared media model: stamp the owning server + * onto a Track, and classify queue refs by server profile. No playback-store + * read, so they live in lib/media next to the Track model. The store-reading + * queue helpers (queueItemRefAt, filterQueueRefs*) stay in + * features/playback/utils/playback/trackServerScope and build on these. + */ + +/** Active saved-server profile id (auth UUID), when logged in. */ +export function activeServerProfileId(): string | undefined { + return useAuthStore.getState().activeServerId ?? undefined; +} + +/** + * Ensure every track carries an owning server before it enters the queue. + * Explicit `track.serverId` wins; otherwise `fallbackServerId`, then active server. + */ +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)); +} + +/** True when queue refs resolve to more than one server bucket. */ +export function isMultiServerQueue(refs: QueueItemRef[]): boolean { + const keys = new Set(); + 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; +} diff --git a/src/pages/MostPlayed.tsx b/src/pages/MostPlayed.tsx index 2981d911..3e622d0f 100644 --- a/src/pages/MostPlayed.tsx +++ b/src/pages/MostPlayed.tsx @@ -1,7 +1,7 @@ import { getAlbumList } from '@/lib/api/subsonicLibrary'; 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 React, { useEffect, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { ArrowUpDown, ArrowDown, ArrowUp, TrendingUp, UsersRound, Play, ListPlus } from 'lucide-react'; diff --git a/src/pages/RandomMix.tsx b/src/pages/RandomMix.tsx index e750c055..b96c4874 100644 --- a/src/pages/RandomMix.tsx +++ b/src/pages/RandomMix.tsx @@ -1,6 +1,6 @@ import { queueSongStar } from '@/features/playback/store/pendingStarSync'; import type { SubsonicSong, SubsonicGenre } from '@/lib/api/subsonicTypes'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import React, { useEffect, useMemo, useState } from 'react'; import { usePlayerStore } from '@/features/playback/store/playerStore'; import { usePreviewStore } from '@/features/playback/store/previewStore'; diff --git a/src/utils/componentHelpers/contextMenuActions.ts b/src/utils/componentHelpers/contextMenuActions.ts index c2b16962..6785663b 100644 --- a/src/utils/componentHelpers/contextMenuActions.ts +++ b/src/utils/componentHelpers/contextMenuActions.ts @@ -12,7 +12,7 @@ import { useDownloadModalStore } from '@/features/offline'; import type { EntityShareKind } from '../share/shareLink'; import { copyEntityShareLink } from '../share/copyEntityShareLink'; import { sanitizeFilename, shuffleArray } from './contextMenuHelpers'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import { showToast } from '../ui/toast'; export async function copyShareLink( diff --git a/src/utils/mix/luckyMix.ts b/src/utils/mix/luckyMix.ts index 33b64134..2d29f666 100644 --- a/src/utils/mix/luckyMix.ts +++ b/src/utils/mix/luckyMix.ts @@ -2,7 +2,7 @@ import { fetchSimilarTracksRouted } from '@/lib/api/subsonicArtists'; import { filterSongsToActiveLibrary, getRandomSongs } from '@/lib/api/subsonicLibrary'; import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes'; import type { QueueItemRef } from '@/lib/media/trackTypes'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import { invoke } from '@tauri-apps/api/core'; import i18n from '@/lib/i18n'; import { useAuthStore } from '../../store/authStore'; diff --git a/src/utils/share/applySharePaste.ts b/src/utils/share/applySharePaste.ts index 379de34b..3f2477d5 100644 --- a/src/utils/share/applySharePaste.ts +++ b/src/utils/share/applySharePaste.ts @@ -1,7 +1,7 @@ import { getSong } from '@/lib/api/subsonicLibrary'; import { resolveAlbum, resolveArtist } from '@/features/offline'; import type { SubsonicSong } from '@/lib/api/subsonicTypes'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import type { Location, NavigateFunction } from 'react-router-dom'; import type { TFunction } from 'i18next'; import { useAuthStore } from '../../store/authStore'; diff --git a/src/utils/share/enqueueShareSearchPayload.test.ts b/src/utils/share/enqueueShareSearchPayload.test.ts index 2fb73670..d033862b 100644 --- a/src/utils/share/enqueueShareSearchPayload.test.ts +++ b/src/utils/share/enqueueShareSearchPayload.test.ts @@ -49,7 +49,7 @@ vi.mock('@/features/playback/store/playerStore', () => ({ }, })); -vi.mock('@/features/playback/utils/playback/songToTrack', () => ({ +vi.mock('@/lib/media/songToTrack', () => ({ songToTrack: mocks.songToTrack, })); diff --git a/src/utils/share/enqueueShareSearchPayload.ts b/src/utils/share/enqueueShareSearchPayload.ts index 4e0042f7..b2a76a64 100644 --- a/src/utils/share/enqueueShareSearchPayload.ts +++ b/src/utils/share/enqueueShareSearchPayload.ts @@ -10,7 +10,7 @@ import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/lib/api/subs import { useAuthStore } from '../../store/authStore'; import type { ServerProfile } from '../../store/authStoreTypes'; import { usePlayerStore } from '@/features/playback/store/playerStore'; -import { songToTrack } from '@/features/playback/utils/playback/songToTrack'; +import { songToTrack } from '@/lib/media/songToTrack'; import type { Track } from '@/lib/media/trackTypes'; import { orbitBulkGuard } from '@/features/orbit'; import { findServerIdForShareUrl } from './shareLink';