refactor(api): F.48 — extract subsonic types + client primitives (#613)

First Phase F slice. Splits the 1333-LOC `api/subsonic.ts` along its
two most obvious axes:

- `subsonicTypes.ts` — all ~24 exported interfaces + type aliases
  (album/song/artist/playlist/directory/genre/now-playing/radio,
  random-songs filters, three statistics shapes, search + starred
  results, AlbumInfo, structured-lyrics types, etc.) plus the
  `RADIO_PAGE_SIZE` constant.
- `subsonicClient.ts` — token-auth + `getClient` + `api<T>()` +
  `libraryFilterParams` + `secureRandomSalt` / `getAuthParams` /
  `SUBSONIC_CLIENT`. The credential-bearing API helpers
  (`pingWithCredentials`, `apiWithCredentials`, `restBaseFromUrl`,
  `probeInstantMixWithCredentials`) stay in `subsonic.ts` for now —
  they could move into the client module in a follow-up.

66 external call sites migrated to direct imports from the new
modules (no re-export shims in `subsonic.ts`). Pure code-move;
contract test stays green.

subsonic.ts: 1333 → 1078 LOC (−255).
This commit is contained in:
Frank Stellmacher
2026-05-13 00:05:58 +02:00
committed by GitHub
parent acdb0ae795
commit 72030f17fd
70 changed files with 445 additions and 452 deletions
+2 -4
View File
@@ -1,10 +1,8 @@
import type { SubsonicGenre, SubsonicArtist, SubsonicAlbum, SubsonicSong } from '../api/subsonicTypes';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { SlidersVertical } from 'lucide-react';
import {
search, searchSongsPaged, getGenres, getAlbumsByGenre, getAlbumList, getRandomSongs,
SubsonicGenre, SubsonicArtist, SubsonicAlbum, SubsonicSong,
} from '../api/subsonic';
import { search, searchSongsPaged, getGenres, getAlbumsByGenre, getAlbumList, getRandomSongs } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import AlbumRow from '../components/AlbumRow';
import ArtistRow from '../components/ArtistRow';
+2 -1
View File
@@ -1,9 +1,10 @@
import type { SubsonicSong, SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Search, X, ListPlus } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { getAlbum, getArtist, getArtistInfo, setRating, buildCoverArtUrl, coverArtCacheKey, buildDownloadUrl, star, unstar, SubsonicSong, SubsonicAlbum } from '../api/subsonic';
import { getAlbum, getArtist, getArtistInfo, setRating, buildCoverArtUrl, coverArtCacheKey, buildDownloadUrl, star, unstar } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
+2 -1
View File
@@ -1,3 +1,4 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useState, useEffect, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
import AlbumCard from '../components/AlbumCard';
@@ -5,7 +6,7 @@ import GenreFilterBar from '../components/GenreFilterBar';
import YearFilterButton from '../components/YearFilterButton';
import StarFilterButton from '../components/StarFilterButton';
import SortDropdown from '../components/SortDropdown';
import { getAlbumList, getAlbumsByGenre, getAlbum, SubsonicAlbum, buildDownloadUrl } from '../api/subsonic';
import { getAlbumList, getAlbumsByGenre, getAlbum, buildDownloadUrl } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
+2 -1
View File
@@ -1,7 +1,8 @@
import type { SubsonicArtist, SubsonicAlbum, SubsonicSong, SubsonicArtistInfo } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import { useEffect, useState, useRef, Fragment, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { getArtist, getArtistInfo, getTopSongs, getSimilarSongs2, getAlbum, search, setRating, SubsonicArtist, SubsonicAlbum, SubsonicSong, SubsonicArtistInfo, buildCoverArtUrl, coverArtCacheKey, star, unstar, uploadArtistImage } from '../api/subsonic';
import { getArtist, getArtistInfo, getTopSongs, getSimilarSongs2, getAlbum, search, setRating, buildCoverArtUrl, coverArtCacheKey, star, unstar, uploadArtistImage } from '../api/subsonic';
import AlbumCard from '../components/AlbumCard';
import CachedImage from '../components/CachedImage';
import CoverLightbox from '../components/CoverLightbox';
+2 -1
View File
@@ -1,6 +1,7 @@
import type { SubsonicArtist } from '../api/subsonicTypes';
import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { getArtists, SubsonicArtist, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { getArtists, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { LayoutGrid, List, Images, CheckSquare2, ListMusic, Check } from 'lucide-react';
import StarFilterButton from '../components/StarFilterButton';
import { usePlayerStore } from '../store/playerStore';
+2 -5
View File
@@ -1,10 +1,7 @@
import type { SubsonicArtist, SubsonicAlbum, SubsonicArtistInfo } from '../api/subsonicTypes';
import { useEffect, useState, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
getArtist, getArtistInfo, star, unstar,
SubsonicArtist, SubsonicAlbum, SubsonicArtistInfo,
buildCoverArtUrl, coverArtCacheKey,
} from '../api/subsonic';
import { getArtist, getArtistInfo, star, unstar, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { ndListAlbumsByArtistRole } from '../api/navidromeBrowse';
import AlbumCard from '../components/AlbumCard';
import CachedImage from '../components/CachedImage';
+1 -1
View File
@@ -1,6 +1,6 @@
import type { SubsonicArtist } from '../api/subsonicTypes';
import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { SubsonicArtist } from '../api/subsonic';
import { ndListArtistsByRole } from '../api/navidromeBrowse';
import { LayoutGrid, List } from 'lucide-react';
import StarFilterButton from '../components/StarFilterButton';
+3 -6
View File
@@ -1,3 +1,4 @@
import type { SubsonicSong, SubsonicAlbum, SubsonicPlaylist, SubsonicArtist } from '../api/subsonicTypes';
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
@@ -11,11 +12,7 @@ import CustomSelect from '../components/CustomSelect';
import { useTranslation } from 'react-i18next';
import { useDeviceSyncStore, DeviceSyncSource } from '../store/deviceSyncStore';
import { useDeviceSyncJobStore } from '../store/deviceSyncJobStore';
import {
getPlaylists, getAlbumList, getArtists, getAlbum, getPlaylist, getArtist,
buildDownloadUrl, search as searchSubsonic,
SubsonicSong, SubsonicAlbum, SubsonicPlaylist, SubsonicArtist,
} from '../api/subsonic';
import { getPlaylists, getAlbumList, getArtists, getAlbum, getPlaylist, getArtist, buildDownloadUrl, search as searchSubsonic } from '../api/subsonic';
import { showToast } from '../utils/toast';
import { IS_WINDOWS } from '../utils/platform';
@@ -598,7 +595,7 @@ export default function DeviceSync() {
setPreSyncOpen(true);
try {
const { getClient } = await import('../api/subsonic');
const { getClient } = await import('../api/subsonicClient');
const { baseUrl, params } = getClient();
const payload = await invoke<{
addBytes: number; addCount: number; delBytes: number; delCount: number; availableBytes: number; tracks: SubsonicSong[];
+2 -5
View File
@@ -1,14 +1,11 @@
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong, InternetRadioStation } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
import AlbumRow from '../components/AlbumRow';
import ArtistRow from '../components/ArtistRow';
import CachedImage from '../components/CachedImage';
import {
getStarred, getInternetRadioStations, setRating,
SubsonicAlbum, SubsonicArtist, SubsonicSong, InternetRadioStation,
buildCoverArtUrl, coverArtCacheKey,
} from '../api/subsonic';
import { getStarred, getInternetRadioStations, setRating, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import StarRating from '../components/StarRating';
+2 -8
View File
@@ -1,13 +1,7 @@
import type { SubsonicDirectoryEntry, SubsonicArtist, SubsonicAlbum } from '../api/subsonicTypes';
import type { Track } from '../store/playerStoreTypes';
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import {
getMusicFolders,
getMusicDirectory,
getMusicIndexes,
SubsonicDirectoryEntry,
SubsonicArtist,
SubsonicAlbum,
} from '../api/subsonic';
import { getMusicFolders, getMusicDirectory, getMusicIndexes } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
import { Folder, FolderOpen, Music, ChevronRight } from 'lucide-react';
+2 -1
View File
@@ -1,8 +1,9 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import React, { useEffect, useState, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ArrowLeft, Disc3 } from 'lucide-react';
import { getAlbumsByGenre, SubsonicAlbum } from '../api/subsonic';
import { getAlbumsByGenre } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import AlbumCard from '../components/AlbumCard';
+2 -1
View File
@@ -1,8 +1,9 @@
import type { SubsonicGenre } from '../api/subsonicTypes';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Tags } from 'lucide-react';
import { getGenres, SubsonicGenre } from '../api/subsonic';
import { getGenres } from '../api/subsonic';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
const CTP_COLORS = [
+2 -1
View File
@@ -1,10 +1,11 @@
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../api/subsonicTypes';
import React, { useEffect, useState } from 'react';
import Hero from '../components/Hero';
import AlbumRow from '../components/AlbumRow';
import SongRail from '../components/SongRail';
import BecauseYouLikeRail from '../components/BecauseYouLikeRail';
import LosslessAlbumsRail from '../components/LosslessAlbumsRail';
import { getAlbumList, getArtists, getRandomSongs, SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../api/subsonic';
import { getAlbumList, getArtists, getRandomSongs } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import { NavLink, useNavigate } from 'react-router-dom';
import { ChevronRight } from 'lucide-react';
+2 -7
View File
@@ -1,14 +1,9 @@
import { type InternetRadioStation, type RadioBrowserStation, RADIO_PAGE_SIZE } from '../api/subsonicTypes';
import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { Cast, Plus, Trash2, X, Globe, Camera, Loader2, Search, Heart, Check } from 'lucide-react';
import { useDragSource, useDragDrop } from '../contexts/DragDropContext';
import {
getInternetRadioStations, createInternetRadioStation,
updateInternetRadioStation, deleteInternetRadioStation,
uploadRadioCoverArt, deleteRadioCoverArt,
uploadRadioCoverArtBytes, searchRadioBrowser, getTopRadioStations, fetchUrlBytes,
InternetRadioStation, RadioBrowserStation, buildCoverArtUrl, coverArtCacheKey, RADIO_PAGE_SIZE,
} from '../api/subsonic';
import { getInternetRadioStations, createInternetRadioStation, updateInternetRadioStation, deleteInternetRadioStation, uploadRadioCoverArt, deleteRadioCoverArt, uploadRadioCoverArtBytes, searchRadioBrowser, getTopRadioStations, fetchUrlBytes, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import CachedImage from '../components/CachedImage';
import { invalidateCoverArt } from '../utils/imageCache';
+2 -1
View File
@@ -1,8 +1,9 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ChevronLeft } from 'lucide-react';
import AlbumCard from '../components/AlbumCard';
import { search, SubsonicAlbum } from '../api/subsonic';
import { search } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
+2 -1
View File
@@ -1,8 +1,9 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import AlbumCard from '../components/AlbumCard';
import { ndListLosslessAlbumsPage } from '../api/navidromeBrowse';
import { getAlbum, type SubsonicAlbum, buildDownloadUrl } from '../api/subsonic';
import { getAlbum, buildDownloadUrl } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
+2 -1
View File
@@ -1,8 +1,9 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowUpDown, ArrowDown, ArrowUp, TrendingUp, UsersRound, Play, ListPlus } from 'lucide-react';
import { getAlbumList, getAlbum, SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { getAlbumList, getAlbum, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import CachedImage from '../components/CachedImage';
+2 -1
View File
@@ -1,8 +1,9 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { CheckSquare2, Download, HardDriveDownload, ListMusic } from 'lucide-react';
import AlbumCard from '../components/AlbumCard';
import GenreFilterBar from '../components/GenreFilterBar';
import { getAlbumList, getAlbumsByGenre, getAlbum, SubsonicAlbum, buildDownloadUrl } from '../api/subsonic';
import { getAlbumList, getAlbumsByGenre, getAlbum, buildDownloadUrl } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
+2 -5
View File
@@ -1,3 +1,4 @@
import type { SubsonicSong, SubsonicArtistInfo, SubsonicAlbum } from '../api/subsonicTypes';
import React, { useState, useRef, useEffect, useCallback, useLayoutEffect, useMemo, memo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -6,11 +7,7 @@ import { open as shellOpen } from '@tauri-apps/plugin-shell';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useLyricsStore } from '../store/lyricsStore';
import {
buildCoverArtUrl, coverArtCacheKey, getSong, star, unstar,
getAlbum, getArtist, getArtistInfo, getTopSongs,
SubsonicSong, SubsonicArtistInfo, SubsonicAlbum,
} from '../api/subsonic';
import { buildCoverArtUrl, coverArtCacheKey, getSong, star, unstar, getAlbum, getArtist, getArtistInfo, getTopSongs } from '../api/subsonic';
import { songToTrack } from '../utils/songToTrack';
import {
lastfmIsConfigured,
+2 -5
View File
@@ -1,3 +1,4 @@
import type { SubsonicPlaylist, SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
@@ -5,11 +6,7 @@ import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { ChevronDown, ChevronLeft, ChevronRight, Play, ListPlus, Trash2, Search, X, Loader2, Plus, GripVertical, Star, RefreshCw, Shuffle, Heart, HardDriveDownload, Check, Pencil, Globe, Lock, Camera, Download, FileUp, RotateCcw, Sparkles, Square, AudioLines } from 'lucide-react';
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
import { AddToPlaylistSubmenu } from '../components/ContextMenu';
import {
getPlaylist, updatePlaylist, updatePlaylistMeta, uploadPlaylistCoverArt,
search, setRating, star, unstar,
getRandomSongs, buildDownloadUrl, filterSongsToActiveLibrary, SubsonicPlaylist, SubsonicSong,
} from '../api/subsonic';
import { getPlaylist, updatePlaylist, updatePlaylistMeta, uploadPlaylistCoverArt, search, setRating, star, unstar, getRandomSongs, buildDownloadUrl, filterSongsToActiveLibrary } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { usePlaylistStore } from '../store/playlistStore';
+2 -1
View File
@@ -1,8 +1,9 @@
import type { SubsonicPlaylist, SubsonicGenre } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ListMusic, Play, Plus, Trash2, CheckSquare2, Check, Clock3, Sparkles, Pencil } from 'lucide-react';
import { deletePlaylist, SubsonicPlaylist, getPlaylist, buildCoverArtUrl, coverArtCacheKey, updatePlaylist, getGenres, SubsonicGenre, filterSongsToActiveLibrary } from '../api/subsonic';
import { deletePlaylist, getPlaylist, buildCoverArtUrl, coverArtCacheKey, updatePlaylist, getGenres, filterSongsToActiveLibrary } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { usePlaylistStore } from '../store/playlistStore';
import { useAuthStore } from '../store/authStore';
+2 -1
View File
@@ -1,6 +1,7 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { RefreshCw, CheckSquare2, Download, HardDriveDownload } from 'lucide-react';
import { getAlbumList, getAlbumsByGenre, getAlbum, SubsonicAlbum, buildDownloadUrl } from '../api/subsonic';
import { getAlbumList, getAlbumsByGenre, getAlbum, buildDownloadUrl } from '../api/subsonic';
import AlbumCard from '../components/AlbumCard';
import GenreFilterBar from '../components/GenreFilterBar';
import { useTranslation } from 'react-i18next';
+2 -1
View File
@@ -1,7 +1,8 @@
import type { SubsonicSong, SubsonicGenre } from '../api/subsonicTypes';
import { RANDOM_MIX_SIZE_OPTIONS } from '../store/authStoreDefaults';
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useMemo, useState } from 'react';
import { getGenres, SubsonicSong, SubsonicGenre, star, unstar } from '../api/subsonic';
import { getGenres, star, unstar } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import { useAuthStore } from '../store/authStore';
+2 -1
View File
@@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Search } from 'lucide-react';
import { search, searchSongsPaged, SearchResults as ISearchResults } from '../api/subsonic';
import { search, searchSongsPaged } from '../api/subsonic';
import type { SearchResults as ISearchResults } from '../api/subsonicTypes';
import AlbumRow from '../components/AlbumRow';
import ArtistRow from '../components/ArtistRow';
import SongRow, { SongListHeader } from '../components/SongRow';
+2 -8
View File
@@ -1,13 +1,7 @@
import type { SubsonicAlbum, SubsonicGenre } from '../api/subsonicTypes';
import React, { useEffect, useState } from 'react';
import { Share2 } from 'lucide-react';
import {
fetchStatisticsFormatSample,
fetchStatisticsLibraryAggregates,
fetchStatisticsOverview,
getAlbumList,
SubsonicAlbum,
SubsonicGenre,
} from '../api/subsonic';
import { fetchStatisticsFormatSample, fetchStatisticsLibraryAggregates, fetchStatisticsOverview, getAlbumList } from '../api/subsonic';
import { formatHumanHoursMinutes } from '../utils/formatHumanDuration';
import AlbumRow from '../components/AlbumRow';
import StatsExportModal from '../components/StatsExportModal';
+2 -6
View File
@@ -1,14 +1,10 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus, RefreshCw, Sparkles } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import {
SubsonicSong,
getRandomSongs,
buildCoverArtUrl,
coverArtCacheKey,
} from '../api/subsonic';
import { getRandomSongs, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import CachedImage from '../components/CachedImage';