mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
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:
@@ -1,110 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||
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() });
|
||||
const all = data.playlists?.playlist ?? [];
|
||||
// Orbit session + outbox playlists are technical internals. They're `public`
|
||||
// so guests can reach them, which means they leak into every UI picker and
|
||||
// even into the Navidrome web client. Filter them out of every UI call;
|
||||
// orbit's own sweep passes `includeOrbit=true`.
|
||||
return includeOrbit ? all : all.filter(p => !p.name.startsWith('__psyorbit_'));
|
||||
}
|
||||
|
||||
export async function getPlaylist(id: string): Promise<{ playlist: SubsonicPlaylist; songs: SubsonicSong[] }> {
|
||||
const data = await api<{ playlist: SubsonicPlaylist & { entry: SubsonicSong[] } }>('getPlaylist.view', { id });
|
||||
const { entry, ...playlist } = data.playlist;
|
||||
return { playlist, songs: entry ?? [] };
|
||||
}
|
||||
|
||||
export async function getPlaylistForServer(
|
||||
serverId: string,
|
||||
id: string,
|
||||
): Promise<{ playlist: SubsonicPlaylist; songs: SubsonicSong[] }> {
|
||||
if (!shouldAttemptSubsonicForServer(serverId)) {
|
||||
throw new Error('Subsonic unavailable');
|
||||
}
|
||||
const data = await apiForServer<{ playlist: SubsonicPlaylist & { entry: SubsonicSong[] } }>(
|
||||
serverId,
|
||||
'getPlaylist.view',
|
||||
{ id },
|
||||
);
|
||||
const { entry, ...playlist } = data.playlist;
|
||||
return { playlist, songs: entry ?? [] };
|
||||
}
|
||||
|
||||
export async function createPlaylist(name: string, songIds?: string[]): Promise<SubsonicPlaylist> {
|
||||
const params: Record<string, unknown> = { name };
|
||||
if (songIds && songIds.length > 0) {
|
||||
params.songId = songIds;
|
||||
}
|
||||
const data = await api<{ playlist: SubsonicPlaylist }>('createPlaylist.view', params);
|
||||
return data.playlist;
|
||||
}
|
||||
|
||||
export async function updatePlaylist(id: string, songIds: string[], prevCount = 0): Promise<void> {
|
||||
if (songIds.length > 0) {
|
||||
// createPlaylist with playlistId replaces the existing playlist's songs (Subsonic API 1.14+)
|
||||
await api('createPlaylist.view', { playlistId: id, songId: songIds });
|
||||
} else if (prevCount > 0) {
|
||||
// Axios serialises empty arrays as no params — createPlaylist.view would leave songs unchanged.
|
||||
// Use updatePlaylist.view with explicit index removal to clear the list instead.
|
||||
await api('updatePlaylist.view', {
|
||||
playlistId: id,
|
||||
songIndexToRemove: Array.from({ length: prevCount }, (_, i) => i),
|
||||
});
|
||||
}
|
||||
void import('@/features/offline')
|
||||
.then(m => m.schedulePinnedPlaylistSync(id))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
export async function updatePlaylistMeta(
|
||||
id: string,
|
||||
name: string,
|
||||
comment: string,
|
||||
isPublic: boolean,
|
||||
): Promise<void> {
|
||||
await api('updatePlaylist.view', { playlistId: id, name, comment, public: isPublic });
|
||||
}
|
||||
|
||||
export async function uploadPlaylistCoverArt(id: string, file: File): Promise<void> {
|
||||
// Navidrome-specific endpoint — handled in Rust to bypass browser CORS restrictions.
|
||||
const { getBaseUrl, getActiveServer } = useAuthStore.getState();
|
||||
const server = getActiveServer();
|
||||
const baseUrl = getBaseUrl();
|
||||
const buffer = await file.arrayBuffer();
|
||||
const fileBytes = Array.from(new Uint8Array(buffer));
|
||||
await invoke('upload_playlist_cover', {
|
||||
serverUrl: baseUrl,
|
||||
playlistId: id,
|
||||
username: server?.username ?? '',
|
||||
password: server?.password ?? '',
|
||||
fileBytes,
|
||||
mimeType: file.type || 'image/jpeg',
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadArtistImage(id: string, file: File): Promise<void> {
|
||||
// Navidrome-specific endpoint — handled in Rust to bypass browser CORS restrictions.
|
||||
const { getBaseUrl, getActiveServer } = useAuthStore.getState();
|
||||
const server = getActiveServer();
|
||||
const baseUrl = getBaseUrl();
|
||||
const buffer = await file.arrayBuffer();
|
||||
const fileBytes = Array.from(new Uint8Array(buffer));
|
||||
await invoke('upload_artist_image', {
|
||||
serverUrl: baseUrl,
|
||||
artistId: id,
|
||||
username: server?.username ?? '',
|
||||
password: server?.password ?? '',
|
||||
fileBytes,
|
||||
mimeType: file.type || 'image/jpeg',
|
||||
});
|
||||
}
|
||||
|
||||
export async function deletePlaylist(id: string): Promise<void> {
|
||||
await api('deletePlaylist.view', { id });
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
* `playlistDetailHelpers` (shared with offline + favorites; keeping it here
|
||||
* would create a playlist⟷offline 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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user