refactor(api): pull feature-resident subsonic clients into lib/api

Move the 5 subsonic api modules that M3 had co-located into features
(subsonicAlbumInfo/Artists/Playlists/Radio/Statistics) into src/lib/api/, the
feature-free infra layer, and drop their feature-barrel re-exports. Consumers
now import these protocol calls directly from @/lib/api/subsonic*; the feature
barrels export only domain UI/hooks/state.

This is the consistent api placement (plan §10.3, revised: ALL subsonic protocol
clients are feature-free infra, not per-feature) and it kills the documented
api-induced feature<->feature cycles: offline->artist/playlist (getArtist/
getPlaylist*), album->artist (getArtistInfo), deviceSync/nowPlaying/orbit->*.
Remaining artist<->album refs are UI-only (OpenArtistRefInline/coerceOpenArtist
Refs) and offline->playlist is store-level (usePlaylistStore) — both deeper,
tracked for M5, not api placement.

Pure move: import specifiers + vi.mock/spy targets repointed; no behaviour
change. tsc 0, lint 0/0, full suite 319 files / 2353 tests green.

Tooling note: barrel-imported and dynamic-import api symbols were split out of
@/features/* to @/lib/api/* (tsc-driven); 12 vi.mock barrels retargeted to the
deep lib module (mock-collapse sweep), AlbumHeader's UI mock left untouched.
This commit is contained in:
Psychotoxical
2026-06-30 08:02:19 +02:00
parent 7e9cb60763
commit 8cc022581f
76 changed files with 89 additions and 93 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { getArtist, getArtistInfo } from '@/features/artist';
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';
+1 -1
View File
@@ -1,5 +1,5 @@
import { Play } from 'lucide-react';
import { updatePlaylist } from '@/features/playlist';
import { updatePlaylist } from '@/lib/api/subsonicPlaylists';
import { resolvePlaylist, resolveMediaServerId } from '@/features/offline';
import { songToTrack } from '../utils/playback/songToTrack';
import type { Track } from '../store/playerStoreTypes';
@@ -1,7 +1,7 @@
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ListMusic, Plus } from 'lucide-react';
import { getPlaylist, updatePlaylist } from '@/features/playlist';
import { getPlaylist, updatePlaylist } from '@/lib/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { usePlaylistStore } from '@/features/playlist';
import { showToast } from '../../utils/ui/toast';
@@ -39,7 +39,7 @@ export function MultiAlbumToPlaylistSubmenu({ albumIds, onDone, triggerId: _trig
}, [albumIds]);
const handleAddWithToast = async (pl: SubsonicPlaylist, songIds: string[]) => {
const { updatePlaylist } = await import('@/features/playlist');
const { updatePlaylist } = await import('@/lib/api/subsonicPlaylists');
const touchPlaylist = usePlaylistStore.getState().touchPlaylist;
try {
@@ -125,7 +125,7 @@ export function MultiAlbumToPlaylistSubmenu({ albumIds, onDone, triggerId: _trig
const handleCreate = async () => {
const name = newName.trim() || t('playlists.unnamed');
try {
const { createPlaylist } = await import('@/features/playlist');
const { createPlaylist } = await import('@/lib/api/subsonicPlaylists');
const pl = await createPlaylist(name, songIds);
if (pl?.id) {
usePlaylistStore.getState().touchPlaylist(pl.id);
@@ -2,7 +2,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ListMusic, Plus } from 'lucide-react';
import { resolveAlbum, resolveArtist, resolveMediaServerId, resolvePlaylist } from '@/features/offline';
import { getPlaylists } from '@/features/playlist';
import { getPlaylists } from '@/lib/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { usePlaylistStore } from '@/features/playlist';
import { showToast } from '../../utils/ui/toast';
@@ -53,7 +53,7 @@ export function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId: _tr
}, [artistIds]);
const handleAddWithToast = async (pl: SubsonicPlaylist, songIds: string[]) => {
const { updatePlaylist } = await import('@/features/playlist');
const { updatePlaylist } = await import('@/lib/api/subsonicPlaylists');
const touchPlaylist = usePlaylistStore.getState().touchPlaylist;
try {
@@ -139,7 +139,7 @@ export function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId: _tr
const handleCreate = async () => {
const name = newName.trim() || t('playlists.unnamed');
try {
const { createPlaylist } = await import('@/features/playlist');
const { createPlaylist } = await import('@/lib/api/subsonicPlaylists');
const pl = await createPlaylist(name, songIds);
if (pl?.id) {
usePlaylistStore.getState().touchPlaylist(pl.id);
@@ -65,7 +65,7 @@ export default function PlaylistContextItems(props: ContextMenuItemsProps) {
<div className="context-menu-divider" />
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => handleAction(async () => {
const { showToast } = await import('../../utils/ui/toast');
const { deletePlaylist } = await import('@/features/playlist');
const { deletePlaylist } = await import('@/lib/api/subsonicPlaylists');
const { removeId } = usePlaylistStore.getState();
try {
await deletePlaylist(playlist.id);
@@ -113,7 +113,7 @@ export default function PlaylistContextItems(props: ContextMenuItemsProps) {
{offlinePolicy.canEditPlaylist && (
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => handleAction(async () => {
const { showToast } = await import('../../utils/ui/toast');
const { deletePlaylist } = await import('@/features/playlist');
const { deletePlaylist } = await import('@/lib/api/subsonicPlaylists');
const { removeId } = usePlaylistStore.getState();
const deletedIds: string[] = [];
for (const pl of selectedPlaylists) {
@@ -41,7 +41,7 @@ export function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }:
const handleCreate = async () => {
if (!newName.trim()) return;
const { createPlaylist } = await import('@/features/playlist');
const { createPlaylist } = await import('@/lib/api/subsonicPlaylists');
try {
const newPl = await createPlaylist(newName.trim(), []);
if (newPl?.id) {
@@ -55,7 +55,7 @@ export function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }:
};
const handleAddToNewPlaylist = async (targetId: string, targetName: string) => {
const { getPlaylist, updatePlaylist } = await import('@/features/playlist');
const { getPlaylist, updatePlaylist } = await import('@/lib/api/subsonicPlaylists');
const touchPlaylist = usePlaylistStore.getState().touchPlaylist;
try {
@@ -73,7 +73,7 @@ export function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }:
};
const handleAdd = async (targetId: string, targetName: string) => {
const { getPlaylist, updatePlaylist } = await import('@/features/playlist');
const { getPlaylist, updatePlaylist } = await import('@/lib/api/subsonicPlaylists');
const touchPlaylist = usePlaylistStore.getState().touchPlaylist;
try {
@@ -180,7 +180,7 @@ export function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }:
const handleCreate = async () => {
if (!newName.trim()) return;
const { createPlaylist } = await import('@/features/playlist');
const { createPlaylist } = await import('@/lib/api/subsonicPlaylists');
try {
const newPl = await createPlaylist(newName.trim(), []);
if (newPl?.id) {
@@ -194,7 +194,7 @@ export function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }:
};
const handleMergeToNewPlaylist = async (targetId: string, targetName: string) => {
const { getPlaylist, updatePlaylist } = await import('@/features/playlist');
const { getPlaylist, updatePlaylist } = await import('@/lib/api/subsonicPlaylists');
const touchPlaylist = usePlaylistStore.getState().touchPlaylist;
try {
@@ -223,7 +223,7 @@ export function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }:
};
const handleMerge = async (targetId: string, targetName: string) => {
const { getPlaylist, updatePlaylist } = await import('@/features/playlist');
const { getPlaylist, updatePlaylist } = await import('@/lib/api/subsonicPlaylists');
const touchPlaylist = usePlaylistStore.getState().touchPlaylist;
try {
@@ -178,7 +178,7 @@ export default function SongContextItems(props: ContextMenuItemsProps) {
</div>
{offlinePolicy.canEditPlaylist && playlistId && playlistSongIndex !== undefined && (
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => handleAction(async () => {
const { updatePlaylist } = await import('@/features/playlist');
const { updatePlaylist } = await import('@/lib/api/subsonicPlaylists');
const { showToast } = await import('../../utils/ui/toast');
const touchPlaylist = usePlaylistStore.getState().touchPlaylist;
try {
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Play, X, Trash2, ListPlus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { getPlaylists, deletePlaylist } from '@/features/playlist';
import { getPlaylists, deletePlaylist } from '@/lib/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
interface Props {
-1
View File
@@ -11,7 +11,6 @@
* sync, core), the album context-menu items (context-menu subsystem), the
* shared `TracklistColumnPicker` (also used by favorites), and `cover/*`.
*/
export * from './api/subsonicAlbumInfo';
export * from './hooks/useAlbumBrowseData';
export * from './hooks/useAlbumBrowseFilters';
export * from './hooks/useAlbumBrowseScrollReset';
+1 -1
View File
@@ -2,7 +2,7 @@ import { buildDownloadUrl } from '@/lib/api/subsonicStreamUrl';
import { setRating, star, unstar } from '@/lib/api/subsonicStarRating';
import { queueSongStar, queueSongRating } from '@/store/pendingStarSync';
import { getAlbumForServer } from '@/lib/api/subsonicLibrary';
import { getArtistInfo } from '@/features/artist';
import { getArtistInfo } from '@/lib/api/subsonicArtists';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { songToTrack } from '@/utils/playback/songToTrack';
import { shuffleArray } from '@/utils/playback/shuffleArray';
@@ -15,10 +15,10 @@ import { MemoryRouter } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { SubsonicArtistInfo } from '@/lib/api/subsonicTypes';
vi.mock('@/features/artist/api/subsonicArtists');
vi.mock('@/lib/api/subsonicArtists');
vi.mock('@/lib/api/subsonicSearch');
import { getArtist, getArtistInfo, getTopSongs } from '@/features/artist/api/subsonicArtists';
import { getArtist, getArtistInfo, getTopSongs } from '@/lib/api/subsonicArtists';
import { search } from '@/lib/api/subsonicSearch';
import { useArtistDetailData } from '@/features/artist/hooks/useArtistDetailData';
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { search } from '@/lib/api/subsonicSearch';
import { getArtist, getArtistForServer, getArtistInfo, getTopSongs } from '@/features/artist/api/subsonicArtists';
import { getArtist, getArtistForServer, getArtistInfo, getTopSongs } from '@/lib/api/subsonicArtists';
import type {
SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo, SubsonicSong,
} from '@/lib/api/subsonicTypes';
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { getArtistInfoForServer } from '@/features/artist/api/subsonicArtists';
import { getArtistInfoForServer } from '@/lib/api/subsonicArtists';
import type { SubsonicArtistInfo, SubsonicOpenArtistRef } from '@/lib/api/subsonicTypes';
import { makeCache } from '@/utils/cache/nowPlayingCache';
@@ -1,4 +1,4 @@
import { getArtists } from '@/features/artist/api/subsonicArtists';
import { getArtists } from '@/lib/api/subsonicArtists';
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
import { useCallback, useEffect, useRef, useState } from 'react';
import { dedupeById } from '@/utils/dedupeById';
-1
View File
@@ -11,7 +11,6 @@
* `*ContextItems`/`*ToPlaylistSubmenu` items (the cross-cutting context-menu
* subsystem, shared with album), and `PlaylistArtistCell` (playlist).
*/
export * from './api/subsonicArtists';
export * from './hooks/useArtistDetailData';
export * from './hooks/useArtistInfoBatch';
export * from './hooks/useArtistOfflineState';
@@ -1,6 +1,6 @@
import type React from 'react';
import type { TFunction } from 'i18next';
import { uploadArtistImage } from '@/features/playlist';
import { uploadArtistImage } from '@/lib/api/subsonicPlaylists';
import { setRating, star, unstar } from '@/lib/api/subsonicStarRating';
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
import { useAuthStore } from '@/store/authStore';
@@ -1,5 +1,5 @@
import type { TFunction } from 'i18next';
import { getSimilarSongs2, getTopSongs } from '@/features/artist/api/subsonicArtists';
import { getSimilarSongs2, getTopSongs } from '@/lib/api/subsonicArtists';
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/store/playerStoreTypes';
import { songToTrack } from '@/utils/playback/songToTrack';
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { getPlaylists } from '@/features/playlist';
import { getArtists, getArtist } from '@/features/artist';
import { getPlaylists } from '@/lib/api/subsonicPlaylists';
import { getArtists, getArtist } from '@/lib/api/subsonicArtists';
import { getAlbumList } from '@/lib/api/subsonicLibrary';
import { search as searchSubsonic } from '@/lib/api/subsonicSearch';
import type {
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { getInternetRadioStations } from '@/features/radio';
import { getInternetRadioStations } from '@/lib/api/subsonicRadio';
import { getStarred } from '@/lib/api/subsonicStarRating';
import type {
InternetRadioStation, SubsonicAlbum, SubsonicArtist, SubsonicSong,
@@ -1,5 +1,5 @@
import { getSongForServer } from '@/lib/api/subsonicLibrary';
import { getArtistInfoForServer } from '@/features/artist';
import { getArtistInfoForServer } from '@/lib/api/subsonicArtists';
import type { SubsonicArtistInfo, SubsonicSong } from '@/lib/api/subsonicTypes';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -13,7 +13,7 @@ import { renderHook, act, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { SubsonicArtistInfo, SubsonicSong, SubsonicAlbum, SubsonicArtist } from '@/lib/api/subsonicTypes';
vi.mock('@/features/artist');
vi.mock('@/lib/api/subsonicArtists');
vi.mock('@/lib/api/subsonicLibrary');
vi.mock('@/api/bandsintown');
vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
@@ -21,7 +21,7 @@ vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
}));
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
import { getArtistForServer, getArtistInfoForServer, getTopSongsForServer } from '@/features/artist';
import { getArtistForServer, getArtistInfoForServer, getTopSongsForServer } from '@/lib/api/subsonicArtists';
import { getAlbumForServer, getSongForServer } from '@/lib/api/subsonicLibrary';
import { fetchBandsintownEvents } from '@/api/bandsintown';
import { useNowPlayingFetchers, type NowPlayingFetchersDeps } from '@/features/nowPlaying/hooks/useNowPlayingFetchers';
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { getArtistInfoForServer } from '@/features/artist';
import { getArtistInfoForServer } from '@/lib/api/subsonicArtists';
import type { SubsonicAlbum, SubsonicArtistInfo, SubsonicSong } from '@/lib/api/subsonicTypes';
import { resolveNpAlbum, resolveNpDiscography, resolveNpSongMeta, resolveNpTopSongs } from '@/features/nowPlaying/utils/nowPlayingMetadataResolve';
import { fetchBandsintownEvents, type BandsintownEvent } from '@/api/bandsintown';
@@ -9,7 +9,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
import { onInvoke } from '@/test/mocks/tauri';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import type { LibraryAdvancedSearchResponse } from '@/lib/api/library';
import * as subsonicArtists from '@/features/artist';
import * as subsonicArtists from '@/lib/api/subsonicArtists';
import * as subsonicLibrary from '@/lib/api/subsonicLibrary';
// Network reachability is decided by the guard; mock it so we can test both arms.
@@ -17,7 +17,7 @@
* is intentionally absent here.
*/
import { libraryGetTrack, libraryGetTracksByAlbum } from '@/lib/api/library';
import { getArtistForServer, getTopSongsForServer } from '@/features/artist';
import { getArtistForServer, getTopSongsForServer } from '@/lib/api/subsonicArtists';
import { getAlbumForServer, getSongForServer } from '@/lib/api/subsonicLibrary';
import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes';
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
+1 -1
View File
@@ -1,7 +1,7 @@
import { libraryUpsertSongsFromApi } from '@/lib/api/library';
import { buildStreamUrl } from '@/lib/api/subsonicStreamUrl';
import { getAlbum } from '@/lib/api/subsonicLibrary';
import { getArtist } from '@/features/artist';
import { getArtist } from '@/lib/api/subsonicArtists';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
@@ -28,7 +28,7 @@ vi.mock('@/lib/api/subsonicLibrary', () => ({
getAlbumForServer: vi.fn(async () => ({ songs: [] })),
}));
vi.mock('@/features/artist', () => ({
vi.mock('@/lib/api/subsonicArtists', () => ({
getArtistForServer: vi.fn(async () => ({ albums: [] })),
}));
@@ -1,7 +1,7 @@
import { libraryUpsertSongsFromApi } from '@/lib/api/library';
import { librarySqlServerId } from '@/api/coverCache';
import { getAlbumForServer } from '@/lib/api/subsonicLibrary';
import { getArtistForServer } from '@/features/artist';
import { getArtistForServer } from '@/lib/api/subsonicArtists';
import { getStarredForServer } from '@/lib/api/subsonicStarRating';
import { buildStreamUrlForServer } from '@/lib/api/subsonicStreamUrl';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
@@ -53,11 +53,11 @@ vi.mock('@/lib/api/subsonicLibrary', () => ({
getAlbumForServer: (serverId: string, albumId: string) => getAlbumForServerMock(serverId, albumId),
}));
vi.mock('@/features/artist', () => ({
vi.mock('@/lib/api/subsonicArtists', () => ({
getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId),
}));
vi.mock('@/features/playlist', () => ({
vi.mock('@/lib/api/subsonicPlaylists', () => ({
getPlaylistForServer: (serverId: string, playlistId: string) =>
getPlaylistForServerMock(serverId, playlistId),
}));
@@ -1,6 +1,6 @@
import { getAlbumForServer } from '@/lib/api/subsonicLibrary';
import { getArtistForServer } from '@/features/artist';
import { getPlaylistForServer } from '@/features/playlist';
import { getArtistForServer } from '@/lib/api/subsonicArtists';
import { getPlaylistForServer } from '@/lib/api/subsonicPlaylists';
import type {
SubsonicAlbum,
SubsonicArtist,
@@ -29,7 +29,7 @@ vi.mock('@/utils/network/activeServerReachability', () => ({
onActiveServerBecameReachable: () => () => {},
}));
vi.mock('@/features/playlist/api/subsonicPlaylists', () => ({
vi.mock('@/lib/api/subsonicPlaylists', () => ({
getPlaylistForServer: (serverId: string, id: string) => getPlaylistMock(serverId, id),
}));
@@ -38,7 +38,7 @@ vi.mock('@/lib/api/subsonicLibrary', () => ({
filterSongsToServerLibrary: (songs: SubsonicSong[]) => filterSongsMock(songs),
}));
vi.mock('@/features/artist', () => ({
vi.mock('@/lib/api/subsonicArtists', () => ({
getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId),
}));
@@ -1,7 +1,7 @@
import { libraryGetTracksByAlbum, subscribeLibrarySyncIdle } from '@/lib/api/library';
import { getAlbumForServer, filterSongsToServerLibrary } from '@/lib/api/subsonicLibrary';
import { getPlaylistForServer } from '@/features/playlist';
import { getArtistForServer } from '@/features/artist';
import { getPlaylistForServer } from '@/lib/api/subsonicPlaylists';
import { getArtistForServer } from '@/lib/api/subsonicArtists';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '@/store/authStore';
@@ -1,5 +1,5 @@
import { libraryGetTracksBatchChunked } from '@/lib/api/library';
import { getPlaylist } from '@/features/playlist';
import { getPlaylist } from '@/lib/api/subsonicPlaylists';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { useAuthStore } from '@/store/authStore';
import type { OfflineAlbumMeta } from '@/features/offline/store/offlineStore';
+1 -1
View File
@@ -19,7 +19,7 @@ const { authState, orbitState } = vi.hoisted(() => ({
orbitState: { sessionId: null as string | null },
}));
vi.mock('@/features/playlist', () => ({ getPlaylists, deletePlaylist }));
vi.mock('@/lib/api/subsonicPlaylists', () => ({ getPlaylists, deletePlaylist }));
vi.mock('@/store/authStore', () => ({
useAuthStore: {
getState: () => ({
+1 -1
View File
@@ -1,4 +1,4 @@
import { deletePlaylist, getPlaylists } from '@/features/playlist';
import { deletePlaylist, getPlaylists } from '@/lib/api/subsonicPlaylists';
import { useAuthStore } from '@/store/authStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { ORBIT_PLAYLIST_PREFIX, parseOrbitState } from '@/features/orbit/api/orbit';
+1 -1
View File
@@ -1,4 +1,4 @@
import { createPlaylist, deletePlaylist, getPlaylist, getPlaylists, updatePlaylist } from '@/features/playlist';
import { createPlaylist, deletePlaylist, getPlaylist, getPlaylists, updatePlaylist } from '@/lib/api/subsonicPlaylists';
import { getSong } from '@/lib/api/subsonicLibrary';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useAuthStore } from '@/store/authStore';
+1 -1
View File
@@ -24,7 +24,7 @@ vi.mock('@/features/orbit/utils/remote', () => ({
vi.mock('@/features/orbit/store/orbitStore', () => ({ useOrbitStore: { getState: () => orbitStore } }));
vi.mock('@/store/authStore', () => ({ useAuthStore: { getState: () => ({}) } }));
vi.mock('@/store/playerStore', () => ({ usePlayerStore: { getState: () => ({ enqueue: vi.fn() }) } }));
vi.mock('@/features/playlist', () => ({ createPlaylist: vi.fn(), deletePlaylist: vi.fn() }));
vi.mock('@/lib/api/subsonicPlaylists', () => ({ createPlaylist: vi.fn(), deletePlaylist: vi.fn() }));
vi.mock('@/lib/api/subsonicLibrary', () => ({ getSong: vi.fn() }));
vi.mock('@/utils/playback/songToTrack', () => ({ songToTrack: vi.fn() }));
+1 -1
View File
@@ -1,4 +1,4 @@
import { createPlaylist, deletePlaylist } from '@/features/playlist';
import { createPlaylist, deletePlaylist } from '@/lib/api/subsonicPlaylists';
import { getSong } from '@/lib/api/subsonicLibrary';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useAuthStore } from '@/store/authStore';
+1 -1
View File
@@ -1,4 +1,4 @@
import { deletePlaylist, getPlaylists } from '@/features/playlist';
import { deletePlaylist, getPlaylists } from '@/lib/api/subsonicPlaylists';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { orbitOutboxPlaylistName, type OrbitState } from '@/features/orbit/api/orbit';
import { writeOrbitState } from '@/features/orbit/utils/remote';
+1 -1
View File
@@ -1,4 +1,4 @@
import { getPlaylist, getPlaylists, updatePlaylistMeta } from '@/features/playlist';
import { getPlaylist, getPlaylists, updatePlaylistMeta } from '@/lib/api/subsonicPlaylists';
import {
orbitSessionPlaylistName,
parseOrbitState,
+1 -1
View File
@@ -1,4 +1,4 @@
import { getPlaylist, getPlaylists, updatePlaylist } from '@/features/playlist';
import { getPlaylist, getPlaylists, updatePlaylist } from '@/lib/api/subsonicPlaylists';
import { type OrbitOutboxMeta } from '@/features/orbit/api/orbit';
import { parseOutboxPlaylistName } from '@/features/orbit/utils/helpers';
import { type OutboxSnapshot } from '@/features/orbit/utils/stateMath';
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { filterSongsToActiveLibrary } from '@/lib/api/subsonicLibrary';
import { getPlaylist } from '@/features/playlist/api/subsonicPlaylists';
import { getPlaylist } from '@/lib/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { useOfflineBrowseContext } from '@/features/offline';
-1
View File
@@ -10,7 +10,6 @@
* `playlistDetailHelpers` (shared with offline + favorites; keeping it here
* would create a playlistoffline barrel cycle lib/shared in M4).
*/
export * from './api/subsonicPlaylists';
export * from './hooks/usePlaylistBulkPlayCallbacks';
export * from './hooks/usePlaylistCovers';
export * from './hooks/usePlaylistDerived';
@@ -1,4 +1,4 @@
import { updatePlaylist } from '@/features/playlist/api/subsonicPlaylists';
import { updatePlaylist } from '@/lib/api/subsonicPlaylists';
import type { SubsonicPlaylist, SubsonicSong } from '@/lib/api/subsonicTypes';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
+2 -2
View File
@@ -1,8 +1,8 @@
import { getPlaylists } from '@/features/playlist/api/subsonicPlaylists';
import { getPlaylists } from '@/lib/api/subsonicPlaylists';
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';
import { createPlaylist as apiCreatePlaylist } from '@/lib/api/subsonicPlaylists';
import { useAuthStore } from '@/store/authStore';
import { isOfflineBrowseActive } from '@/features/offline';
import { fetchOfflineBrowsablePlaylists } from '@/features/offline';
@@ -1,4 +1,4 @@
import { getPlaylist } from '@/features/playlist/api/subsonicPlaylists';
import { getPlaylist } from '@/lib/api/subsonicPlaylists';
import { songToTrack } from '@/utils/playback/songToTrack';
import { usePlayerStore } from '@/store/playerStore';
import { playPlaylistAll } from '@/features/playlist/utils/playlistBulkPlayActions';
@@ -1,5 +1,5 @@
import type React from 'react';
import { getPlaylist } from '@/features/playlist/api/subsonicPlaylists';
import { getPlaylist } from '@/lib/api/subsonicPlaylists';
import { filterSongsToActiveLibrary } from '@/lib/api/subsonicLibrary';
import type { SubsonicPlaylist, SubsonicSong } from '@/lib/api/subsonicTypes';
import { useAuthStore } from '@/store/authStore';
@@ -1,5 +1,5 @@
import type { TFunction } from 'i18next';
import { getPlaylist, updatePlaylistMeta, uploadPlaylistCoverArt } from '@/features/playlist/api/subsonicPlaylists';
import { getPlaylist, updatePlaylistMeta, uploadPlaylistCoverArt } from '@/lib/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { showToast } from '@/utils/ui/toast';
@@ -1,6 +1,6 @@
import type React from 'react';
import type { TFunction } from 'i18next';
import { deletePlaylist, getPlaylist, updatePlaylist } from '@/features/playlist/api/subsonicPlaylists';
import { deletePlaylist, getPlaylist, updatePlaylist } from '@/lib/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { usePlaylistStore } from '@/features/playlist/store/playlistStore';
import { showToast } from '@/utils/ui/toast';
@@ -5,7 +5,7 @@ import { Cast, Check, Loader2, Plus, X } from 'lucide-react';
import {
createInternetRadioStation, fetchUrlBytes, getInternetRadioStations,
getTopRadioStations, searchRadioBrowser, uploadRadioCoverArtBytes,
} from '@/features/radio/api/subsonicRadio';
} from '@/lib/api/subsonicRadio';
import {
type InternetRadioStation, type RadioBrowserStation, RADIO_PAGE_SIZE,
} from '@/lib/api/subsonicTypes';
-1
View File
@@ -8,7 +8,6 @@
* playback/audio core the player core drives them, so they are not part of
* this UI feature.
*/
export { getInternetRadioStations } from './api/subsonicRadio';
export { useRadioMetadata } from './hooks/useRadioMetadata';
export type { RadioMetadata } from './hooks/useRadioMetadata';
export { useRadioMprisSync } from './hooks/useRadioMprisSync';
+1 -1
View File
@@ -1,4 +1,4 @@
import { getInternetRadioStations, createInternetRadioStation, updateInternetRadioStation, deleteInternetRadioStation, uploadRadioCoverArt, deleteRadioCoverArt } from '@/features/radio/api/subsonicRadio';
import { getInternetRadioStations, createInternetRadioStation, updateInternetRadioStation, deleteInternetRadioStation, uploadRadioCoverArt, deleteRadioCoverArt } from '@/lib/api/subsonicRadio';
import { type InternetRadioStation } from '@/lib/api/subsonicTypes';
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { Plus, Search } from 'lucide-react';
+1 -1
View File
@@ -1,4 +1,4 @@
import { fetchStatisticsFormatSample, fetchStatisticsLibraryAggregates, fetchStatisticsOverview } from '@/features/stats/api/subsonicStatistics';
import { fetchStatisticsFormatSample, fetchStatisticsLibraryAggregates, fetchStatisticsOverview } from '@/lib/api/subsonicStatistics';
import { getAlbumList } from '@/lib/api/subsonicLibrary';
import type { SubsonicAlbum, SubsonicGenre } from '@/lib/api/subsonicTypes';
import React, { useEffect, useState } from 'react';
+1 -1
View File
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';
import type { NavigateFunction } from 'react-router-dom';
import { getSimilarSongs } from '@/features/artist';
import { getSimilarSongs } from '@/lib/api/subsonicArtists';
import { getMusicFolders } from '@/lib/api/subsonicLibrary';
import { search as subsonicSearch } from '@/lib/api/subsonicSearch';
import { filterSongsForLuckyMixRatings, getMixMinRatingsConfigFromAuth } from '../../utils/mix/mixRatingFilter';
+1 -1
View File
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import type React from 'react';
import { getPlaylist } from '@/features/playlist';
import { getPlaylist } from '@/lib/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { usePlaylistStore } from '@/features/playlist';
import type { PendingSmartPlaylist } from '@/features/playlist';
+1 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { filterSongsToActiveLibrary } from '@/lib/api/subsonicLibrary';
import { getPlaylist } from '@/features/playlist';
import { getPlaylist } from '@/lib/api/subsonicPlaylists';
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
import { isSmartPlaylistName } from '@/features/playlist';
+2 -2
View File
@@ -27,11 +27,11 @@ vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
import axios from 'axios';
import { pingWithCredentials, pingWithCredentialsForProfile, ping } from '@/lib/api/subsonic';
import { getAlbumInfo2 } from '@/features/album';
import { getAlbumInfo2 } from '@/lib/api/subsonicAlbumInfo';
import { getStarred } from '@/lib/api/subsonicStarRating';
import { search } from '@/lib/api/subsonicSearch';
import { getAlbum, getMusicDirectory, getMusicFolders, getMusicIndexes, getRandomSongs, getSong } from '@/lib/api/subsonicLibrary';
import { getArtists, getTopSongs } from '@/features/artist';
import { getArtists, getTopSongs } from '@/lib/api/subsonicArtists';
import { useAuthStore } from '@/store/authStore';
import { resetAuthStore } from '@/test/helpers/storeReset';
@@ -14,7 +14,7 @@ vi.mock('@/lib/api/subsonicLibrary', () => ({
}));
import { api } from '@/lib/api/subsonicClient';
import { fetchSimilarTracksRouted } from '@/features/artist/api/subsonicArtists';
import { fetchSimilarTracksRouted } from '@/lib/api/subsonicArtists';
import { useAuthStore } from '@/store/authStore';
const SID = 'srv-router';
+2 -2
View File
@@ -1,12 +1,12 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@/features/artist', () => ({ getArtist: vi.fn() }));
vi.mock('@/lib/api/subsonicArtists', () => ({ getArtist: vi.fn() }));
vi.mock('@/lib/api/subsonicLibrary', () => ({ getAlbum: vi.fn() }));
vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
shouldAttemptSubsonicForActiveServer: vi.fn(() => true),
}));
import { getArtist } from '@/features/artist';
import { getArtist } from '@/lib/api/subsonicArtists';
import { invalidateEntityUserRatingCaches, prefetchArtistUserRatings } from '@/lib/api/subsonicRatings';
beforeEach(() => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { getArtist } from '@/features/artist';
import { getArtist } from '@/lib/api/subsonicArtists';
import { getAlbum } from '@/lib/api/subsonicLibrary';
import { shouldAttemptSubsonicForActiveServer } from '@/utils/network/subsonicNetworkGuard';
@@ -1,6 +1,6 @@
import { useAuthStore } from '@/store/authStore';
import { genreTagsFor } from '@/utils/library/genreTags';
import { getArtists } from '@/features/artist';
import { getArtists } from '@/lib/api/subsonicArtists';
import { getAlbumList, getRandomSongs } from '@/lib/api/subsonicLibrary';
import type {
StatisticsFormatSample,
+1 -1
View File
@@ -1,5 +1,5 @@
import { star, unstar } from '@/lib/api/subsonicStarRating';
import { getArtist, getArtistInfo } from '@/features/artist';
import { getArtist, getArtistInfo } from '@/lib/api/subsonicArtists';
import type { SubsonicArtist, SubsonicAlbum, SubsonicArtistInfo } from '@/lib/api/subsonicTypes';
import { useEffect, useState, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
+1 -1
View File
@@ -1,4 +1,4 @@
import { getArtists } from '@/features/artist';
import { getArtists } from '@/lib/api/subsonicArtists';
import { getAlbumList, getRandomSongs } from '@/lib/api/subsonicLibrary';
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/lib/api/subsonicTypes';
import { runLocalRandomSongs } from '../utils/library/browseTextSearch';
+1 -1
View File
@@ -1,4 +1,4 @@
import { getSimilarSongs2, getTopSongs } from '@/features/artist';
import { getSimilarSongs2, getTopSongs } from '@/lib/api/subsonicArtists';
import { invoke } from '@tauri-apps/api/core';
import { buildInfiniteQueueCandidates } from '../utils/playback/buildInfiniteQueueCandidates';
import { songToTrack } from '../utils/playback/songToTrack';
+1 -1
View File
@@ -7,7 +7,7 @@ const { inOrbit, getSimilarSongs2, getTopSongs } = vi.hoisted(() => ({
getTopSongs: vi.fn(() => Promise.resolve([])),
}));
vi.mock('@/features/artist', () => ({ getSimilarSongs2, getTopSongs }));
vi.mock('@/lib/api/subsonicArtists', () => ({ getSimilarSongs2, getTopSongs }));
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(() => Promise.resolve()) }));
vi.mock('@/features/orbit', () => ({ isInOrbitSession: () => inOrbit.value }));
vi.mock('./authStore', () => ({
+2 -2
View File
@@ -43,11 +43,11 @@ vi.mock('@/lib/api/subsonicLibrary', () => ({
getSong: vi.fn(async () => null),
getRandomSongs: vi.fn(async () => []),
}));
vi.mock('@/features/artist', () => ({
vi.mock('@/lib/api/subsonicArtists', () => ({
getSimilarSongs2: vi.fn(async () => []),
getTopSongs: vi.fn(async () => []),
}));
vi.mock('@/features/album', () => ({
vi.mock('@/lib/api/subsonicAlbumInfo', () => ({
getAlbumInfo2: vi.fn(async () => null),
}));
vi.mock('@/lib/api/subsonicScrobble', () => ({
@@ -1,6 +1,6 @@
import { join } from '@tauri-apps/api/path';
import { invoke } from '@tauri-apps/api/core';
import { getSimilarSongs2, fetchSimilarTracksRouted, getTopSongs } from '@/features/artist';
import { getSimilarSongs2, fetchSimilarTracksRouted, getTopSongs } from '@/lib/api/subsonicArtists';
import { filterSongsForLuckyMixRatings, getMixMinRatingsConfigFromAuth } from '../mix/mixRatingFilter';
import { buildDownloadUrl } from '@/lib/api/subsonicStreamUrl';
import { useAuthStore } from '../../store/authStore';
+1 -1
View File
@@ -1,4 +1,4 @@
import { fetchSimilarTracksRouted } from '@/features/artist';
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 '../../store/playerStoreTypes';
+1 -1
View File
@@ -1,4 +1,4 @@
import { getTopSongs } from '@/features/artist';
import { getTopSongs } from '@/lib/api/subsonicArtists';
import { filterSongsToActiveLibrary, getAlbumList, getRandomSongs } from '@/lib/api/subsonicLibrary';
import { resolveAlbumForActiveServer } from '@/features/offline';
import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes';
@@ -6,7 +6,7 @@
* refactor (2026-05-12). This test pins the artist-first / random-fallback
* order, the dedup contract against existingIds, and the autoAdded flag.
*/
import { getSimilarSongs2, getTopSongs } from '@/features/artist';
import { getSimilarSongs2, getTopSongs } from '@/lib/api/subsonicArtists';
import { getRandomSongs } from '@/lib/api/subsonicLibrary';
import type { Track } from '../../store/playerStoreTypes';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -14,7 +14,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Mock only the artist Subsonic API submodule (the pre-move target was
// `api/subsonicArtists`); the barrel re-exports it, so consumers still get the
// stubs while `coerceOpenArtistRefs` (used by songToTrack) stays real.
vi.mock('@/features/artist/api/subsonicArtists', () => ({
vi.mock('@/lib/api/subsonicArtists', () => ({
getSimilarSongs2: vi.fn(),
getTopSongs: vi.fn(),
}));
@@ -1,4 +1,4 @@
import { getSimilarSongs2, getTopSongs } from '@/features/artist';
import { getSimilarSongs2, getTopSongs } from '@/lib/api/subsonicArtists';
import { getRandomSongs } from '@/lib/api/subsonicLibrary';
import type { Track } from '../../store/playerStoreTypes';
import {
@@ -27,7 +27,7 @@ vi.mock('@/lib/api/subsonicLibrary', () => ({
getSong: mocks.getSong,
}));
vi.mock('@/features/artist', () => ({
vi.mock('@/lib/api/subsonicArtists', () => ({
getArtist: mocks.getArtist,
}));