refactor(api): co-locate server-protocol clients into lib/api

Move the Subsonic + Navidrome protocol clients and the library IPC facade
(subsonic*, navidrome*, library.ts + tests) from src/api/ into src/lib/api/,
the feature-free infra layer (plan M4, decision §10.3: shared client core → lib/).
Pure move: deep-path import specifiers @/api/* -> @/lib/api/* across ~150
consumers; no behaviour change. Domain REST (lyrics/events) and cover/analysis
infra stay in src/api/ pending their own M4 placement.

tsc 0, lint 0/0, full suite 319 files / 2353 tests green.
This commit is contained in:
Psychotoxical
2026-06-30 07:53:25 +02:00
parent 429e632598
commit 7e9cb60763
392 changed files with 652 additions and 652 deletions
@@ -1,8 +1,8 @@
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '@/store/authStore';
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
import { api, apiForServer } from '@/api/subsonicClient';
import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes';
import { api, apiForServer } from '@/lib/api/subsonicClient';
import type { SubsonicPlaylist, SubsonicSong } from '@/lib/api/subsonicTypes';
export async function getPlaylists(includeOrbit = false): Promise<SubsonicPlaylist[]> {
const data = await api<{ playlists: { playlist: SubsonicPlaylist[] } }>('getPlaylists.view', { _t: Date.now() });
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
import { screen } from '@testing-library/react';
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import { PlaylistArtistCell } from '@/features/playlist/components/PlaylistArtistCell';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
function song(overrides: Partial<SubsonicSong>): SubsonicSong {
return {
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
/**
@@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Check, Clock3, ListMusic, Pencil, Play, Sparkles, Trash2 } from 'lucide-react';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { usePlayerStore } from '@/store/playerStore';
import {
displayPlaylistName, isSmartPlaylistName, type PendingSmartPlaylist,
@@ -1,7 +1,7 @@
import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Camera, Loader2, X } from 'lucide-react';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import type { CoverArtId } from '@/cover/types';
import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
import { PLAYLIST_MAIN_COVER_CSS_PX } from '@/features/playlist/hooks/usePlaylistCovers';
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronRight, Folder, Pencil, Trash2 } from 'lucide-react';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { usePlaylistFolderStore } from '@/features/playlist/store/playlistFolderStore';
import type { PlaylistFolder } from '@/features/playlist/utils/playlistFolders';
import { useDragDrop } from '@/contexts/DragDropContext';
@@ -5,7 +5,7 @@ import {
Camera, ChevronLeft, Download, FileUp, Globe, HardDriveDownload, ListPlus,
Loader2, Lock, Pencil, Play, Search, Shuffle, Sparkles, Trash2,
} from 'lucide-react';
import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicPlaylist, SubsonicSong } from '@/lib/api/subsonicTypes';
import type { ZipDownload } from '@/features/offline';
import type { AlbumOfflineStatus } from '@/features/album';
import { dequeueOfflinePin } from '@/features/offline';
@@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { AudioLines, ChevronRight, Heart, Play, Square, Trash2 } from 'lucide-react';
import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
import i18n from '@/i18n';
@@ -1,7 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Check, ListPlus, X } from 'lucide-react';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { usePlayerStore } from '@/store/playerStore';
import { songToTrack } from '@/utils/playback/songToTrack';
import { formatTrackTime } from '@/utils/format/formatDuration';
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { ChevronRight, Heart, Play, Plus, RefreshCw, Square } from 'lucide-react';
import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { usePlayerStore } from '@/store/playerStore';
import { usePreviewStore } from '@/store/previewStore';
import StarRating from '@/components/StarRating';
@@ -10,7 +10,7 @@ import {
ListPlus, Search, Trash2, X,
} from 'lucide-react';
import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/store/playerStoreTypes';
import { usePlayerStore } from '@/store/playerStore';
import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
@@ -1,5 +1,5 @@
import React from 'react';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { EMPTY_SERVER_FOLDERS, usePlaylistFolderStore } from '@/features/playlist/store/playlistFolderStore';
import { groupPlaylistsByFolder } from '@/features/playlist/utils/playlistFolders';
import { useDragDrop } from '@/contexts/DragDropContext';
@@ -1,7 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CheckSquare2, Plus, Sparkles, Trash2 } from 'lucide-react';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import {
defaultSmartFilters, type SmartFilters,
} from '@/features/playlist/utils/playlistsSmart';
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { CoverArtId, CoverArtRef } from '@/cover/types';
import { coverPrefetchRegister } from '@/cover/prefetchRegistry';
import { resolveAlbumCoverRefFromLibrary } from '@/cover/resolveEntryLibrary';
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/store/playerStoreTypes';
import { usePlayerStore } from '@/store/playerStore';
import { songToTrack } from '@/utils/playback/songToTrack';
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { useDragDrop } from '@/contexts/DragDropContext';
import { runPlaylistReorderDrop } from '@/features/playlist/utils/runPlaylistReorderDrop';
@@ -1,6 +1,6 @@
import { useCallback, useEffect } from 'react';
import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
export function usePlaylistPreview(): {
startPreview: (song: SubsonicSong) => void;
@@ -1,6 +1,6 @@
import { useState } from 'react';
import type React from 'react';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
export interface PlaylistSelection {
selectedIds: Set<string>;
@@ -1,6 +1,6 @@
import type React from 'react';
import type { TFunction } from 'i18next';
import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicPlaylist, SubsonicSong } from '@/lib/api/subsonicTypes';
import { showToast } from '@/utils/ui/toast';
export interface PlaylistSongMutationsDeps {
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import type React from 'react';
import { search } from '@/api/subsonicSearch';
import type { SubsonicSong } from '@/api/subsonicTypes';
import { search } from '@/lib/api/subsonicSearch';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
export interface PlaylistSongSearchResult {
searchResults: SubsonicSong[];
@@ -1,6 +1,6 @@
import React from 'react';
import { queueSongStar, queueSongRating } from '@/store/pendingStarSync';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { usePlayerStore } from '@/store/playerStore';
export interface PlaylistStarRatingDeps {
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import type React from 'react';
import { getRandomSongs } from '@/api/subsonicLibrary';
import type { SubsonicSong } from '@/api/subsonicTypes';
import { getRandomSongs } from '@/lib/api/subsonicLibrary';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
export interface PlaylistSuggestionsResult {
suggestions: SubsonicSong[];
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { filterSongsToActiveLibrary } from '@/api/subsonicLibrary';
import { filterSongsToActiveLibrary } from '@/lib/api/subsonicLibrary';
import { getPlaylist } from '@/features/playlist/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { useOfflineBrowseContext } from '@/features/offline';
export interface PlaylistsLibraryScopeCountsResult {
@@ -1,5 +1,5 @@
import { updatePlaylist } from '@/features/playlist/api/subsonicPlaylists';
import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicPlaylist, SubsonicSong } from '@/lib/api/subsonicTypes';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { useTracklistColumns, type ColDef } from '@/utils/useTracklistColumns';
+3 -3
View File
@@ -1,7 +1,7 @@
import { resolveMediaServerId, resolvePlaylist } from '@/features/offline';
import { getGenres } from '@/api/subsonicGenres';
import { filterSongsToActiveLibrary } from '@/api/subsonicLibrary';
import type { SubsonicPlaylist, SubsonicGenre } from '@/api/subsonicTypes';
import { getGenres } from '@/lib/api/subsonicGenres';
import { filterSongsToActiveLibrary } from '@/lib/api/subsonicLibrary';
import type { SubsonicPlaylist, SubsonicGenre } from '@/lib/api/subsonicTypes';
import { songToTrack } from '@/utils/playback/songToTrack';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { usePlayerStore } from '@/store/playerStore';
+1 -1
View File
@@ -1,5 +1,5 @@
import { getPlaylists } from '@/features/playlist/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { createPlaylist as apiCreatePlaylist } from '@/features/playlist/api/subsonicPlaylists';
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import {
getDisplayedSongs,
type DisplayedSongsOptions,
@@ -1,4 +1,4 @@
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
export type PlaylistSortKey = 'natural' | 'position' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration' | 'playCount' | 'lastPlayed' | 'bpm';
export type PlaylistSortDir = 'asc' | 'desc';
@@ -1,8 +1,8 @@
import type { TFunction } from 'i18next';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
import { readTextFile } from '@tauri-apps/plugin-fs';
import { search } from '@/api/subsonicSearch';
import type { SubsonicSong } from '@/api/subsonicTypes';
import { search } from '@/lib/api/subsonicSearch';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { showToast } from '@/utils/ui/toast';
import { parseSpotifyCsv, type SpotifyCsvTrack } from '@/features/playlist/utils/spotifyCsvImport';
import {
@@ -1,7 +1,7 @@
import type React from 'react';
import { getPlaylist } from '@/features/playlist/api/subsonicPlaylists';
import { filterSongsToActiveLibrary } from '@/api/subsonicLibrary';
import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes';
import { filterSongsToActiveLibrary } from '@/lib/api/subsonicLibrary';
import type { SubsonicPlaylist, SubsonicSong } from '@/lib/api/subsonicTypes';
import { useAuthStore } from '@/store/authStore';
import { usePlaylistStore } from '@/features/playlist/store/playlistStore';
import { isOfflineBrowseActive } from '@/features/offline';
@@ -1,5 +1,5 @@
import type React from 'react';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
export interface RunPlaylistReorderDropDeps {
e: Event;
@@ -1,6 +1,6 @@
import type { TFunction } from 'i18next';
import { getPlaylist, updatePlaylistMeta, uploadPlaylistCoverArt } from '@/features/playlist/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { showToast } from '@/utils/ui/toast';
export interface RunPlaylistSaveMetaDeps {
@@ -1,7 +1,7 @@
import { invoke } from '@tauri-apps/api/core';
import { join } from '@tauri-apps/api/path';
import { buildDownloadUrl } from '@/api/subsonicStreamUrl';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import { buildDownloadUrl } from '@/lib/api/subsonicStreamUrl';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { useZipDownloadStore } from '@/features/offline';
import { sanitizeFilename } from '@/utils/componentHelpers/playlistDetailHelpers';
@@ -1,7 +1,7 @@
import type React from 'react';
import type { TFunction } from 'i18next';
import { deletePlaylist, getPlaylist, updatePlaylist } from '@/features/playlist/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { usePlaylistStore } from '@/features/playlist/store/playlistStore';
import { showToast } from '@/utils/ui/toast';
@@ -1,7 +1,7 @@
import type React from 'react';
import type { TFunction } from 'i18next';
import { ndGetSmartPlaylist, ndListSmartPlaylists } from '@/api/navidromeSmart';
import type { SubsonicGenre, SubsonicPlaylist } from '@/api/subsonicTypes';
import { ndGetSmartPlaylist, ndListSmartPlaylists } from '@/lib/api/navidromeSmart';
import type { SubsonicGenre, SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import {
defaultSmartFilters, displayPlaylistName, isSmartPlaylistName,
parseSmartRulesToFilters, type SmartFilters,
@@ -1,7 +1,7 @@
import type React from 'react';
import type { TFunction } from 'i18next';
import { ndCreateSmartPlaylist, ndUpdateSmartPlaylist } from '@/api/navidromeSmart';
import type { SubsonicPlaylist } from '@/api/subsonicTypes';
import { ndCreateSmartPlaylist, ndUpdateSmartPlaylist } from '@/lib/api/navidromeSmart';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { usePlaylistStore } from '@/features/playlist/store/playlistStore';
import {
buildSmartRulesPayload, defaultSmartFilters, SMART_PREFIX,
@@ -1,5 +1,5 @@
import type React from 'react';
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { songToTrack } from '@/utils/playback/songToTrack';
export interface StartPlaylistRowDragDeps {