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
+1 -2
View File
@@ -1,8 +1,7 @@
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from './subsonicTypes';
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '../store/authStore';
import { ndLogin } from './navidromeAdmin';
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from './subsonic';
/** Server-keyed Bearer token cache. Cheap to keep — Navidrome tokens are long-lived. */
let cachedToken: { serverUrl: string; token: string } | null = null;
+2 -9
View File
@@ -11,15 +11,8 @@
* mocking and are not in this PR.
*/
import { beforeEach, describe, expect, it } from 'vitest';
import {
buildCoverArtUrl,
buildDownloadUrl,
buildStreamUrl,
coverArtCacheKey,
getClient,
libraryFilterParams,
parseSubsonicEntityStarRating,
} from './subsonic';
import { buildCoverArtUrl, buildDownloadUrl, buildStreamUrl, coverArtCacheKey, parseSubsonicEntityStarRating } from './subsonic';
import { getClient, libraryFilterParams } from './subsonicClient';
import { useAuthStore } from '@/store/authStore';
import { resetAuthStore } from '@/test/helpers/storeReset';
+33 -288
View File
@@ -2,236 +2,48 @@ import axios from 'axios';
import md5 from 'md5';
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '../store/authStore';
import { version } from '../../package.json';
import {
isNavidromeAudiomuseSoftwareEligible,
type InstantMixProbeResult,
type SubsonicServerIdentity,
} from '../utils/subsonicServerIdentity';
import {
SUBSONIC_CLIENT,
api,
getAuthParams,
getClient,
libraryFilterParams,
secureRandomSalt,
} from './subsonicClient';
import type {
AlbumInfo,
EntityRatingSupportLevel,
InternetRadioStation,
PingWithCredentialsResult,
RadioBrowserStation,
RandomSongsFilters,
SearchResults,
StarredResults,
StatisticsFormatSample,
StatisticsLibraryAggregates,
StatisticsOverviewData,
SubsonicAlbum,
SubsonicArtist,
SubsonicArtistInfo,
SubsonicDirectory,
SubsonicDirectoryEntry,
SubsonicGenre,
SubsonicMusicFolder,
SubsonicNowPlaying,
SubsonicOpenArtistRef,
SubsonicPlaylist,
SubsonicSong,
SubsonicStructuredLyrics,
} from './subsonicTypes';
const SUBSONIC_CLIENT = `psysonic/${version}`;
// ─── Secure random salt ────────────────────────────────────────
function secureRandomSalt(): string {
const buf = new Uint8Array(8);
crypto.getRandomValues(buf);
return Array.from(buf, b => b.toString(16).padStart(2, '0')).join('');
}
// ─── Token Auth ───────────────────────────────────────────────
function getAuthParams(username: string, password: string) {
const salt = secureRandomSalt();
const token = md5(password + salt);
return { u: username, t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json' };
}
export function getClient() {
const { getBaseUrl, getActiveServer } = useAuthStore.getState();
const server = getActiveServer();
const baseUrl = getBaseUrl();
if (!baseUrl) throw new Error('No server configured');
const params = getAuthParams(server?.username ?? '', server?.password ?? '');
return { baseUrl: `${baseUrl}/rest`, params };
}
async function api<T>(endpoint: string, extra: Record<string, unknown> = {}, timeout = 15000): Promise<T> {
const { baseUrl, params } = getClient();
const resp = await axios.get(`${baseUrl}/${endpoint}`, {
params: { ...params, ...extra },
paramsSerializer: { indexes: null },
timeout,
});
const data = resp.data?.['subsonic-response'];
if (!data) throw new Error('Invalid response from server (possibly not a Subsonic server)');
if (data.status !== 'ok') throw new Error(data.error?.message ?? 'Subsonic API error');
return data as T;
}
/** Optional `musicFolderId` when the user narrowed browsing to one Subsonic library (see `getMusicFolders`). */
export function libraryFilterParams(): Record<string, string | number> {
const { activeServerId, musicLibraryFilterByServer } = useAuthStore.getState();
if (!activeServerId) return {};
const f = musicLibraryFilterByServer[activeServerId];
if (f === undefined || f === 'all') return {};
return { musicFolderId: f };
}
// ─── Types ────────────────────────────────────────────────────
export interface SubsonicAlbum {
id: string;
name: string;
artist: string;
artistId: string;
coverArt?: string;
songCount: number;
duration: number;
playCount?: number;
year?: number;
genre?: string;
starred?: string;
recordLabel?: string;
created?: string;
/** Present on some servers (e.g. OpenSubsonic) for album-level rating. */
userRating?: number;
/** OpenSubsonic: true when the album is tagged as a compilation. */
isCompilation?: boolean;
/** OpenSubsonic: release types from MusicBrainz tags (e.g. "Album", "EP", "Single", "Compilation", "Live"). */
releaseTypes?: string[];
}
/** OpenSubsonic `artists` / `albumArtists` entries on a child song (may include `userRating`). */
export interface SubsonicOpenArtistRef {
id?: string;
name?: string;
userRating?: number;
/** Navidrome / alternate OpenSubsonic payloads (same meaning as `userRating`). */
rating?: number;
}
export interface SubsonicSong {
id: string;
title: string;
artist: string;
album: string;
albumId: string;
artistId?: string;
duration: number;
track?: number;
discNumber?: number;
coverArt?: string;
year?: number;
userRating?: number;
/** Some OpenSubsonic responses attach parent ratings on child songs. */
albumUserRating?: number;
artistUserRating?: number;
artists?: SubsonicOpenArtistRef[];
albumArtists?: SubsonicOpenArtistRef[];
// Audio technical info
bitRate?: number;
suffix?: string;
contentType?: string;
size?: number;
samplingRate?: number;
bitDepth?: number;
channelCount?: number;
starred?: string;
genre?: string;
path?: string;
albumArtist?: string;
/** ISRC code when available (e.g., Navidrome) */
isrc?: string;
replayGain?: {
trackGain?: number;
albumGain?: number;
trackPeak?: number;
albumPeak?: number;
};
/** OpenSubsonic: structured composer credit (string for back-compat). */
displayComposer?: string;
/** OpenSubsonic: structured contributors list — Navidrome ≥ 0.55. */
contributors?: Array<{
role: string;
subRole?: string;
artist: { id?: string; name: string };
}>;
}
export interface InternetRadioStation {
id: string;
name: string;
streamUrl: string;
homepageUrl?: string;
coverArt?: string; // Navidrome v0.61.0+
}
export interface RadioBrowserStation {
stationuuid: string;
name: string;
url: string;
favicon: string;
tags: string;
}
export interface SubsonicPlaylist {
id: string;
name: string;
songCount: number;
duration: number;
created: string;
changed: string;
owner?: string;
public?: boolean;
comment?: string;
coverArt?: string;
}
export interface SubsonicNowPlaying extends SubsonicSong {
username: string;
minutesAgo: number;
playerId: number;
playerName: string;
}
export interface SubsonicArtist {
id: string;
name: string;
albumCount?: number;
coverArt?: string;
starred?: string;
/** Present on some servers (e.g. OpenSubsonic) for artist-level rating. */
userRating?: number;
}
export interface SubsonicGenre {
value: string;
songCount: number;
albumCount: number;
}
export interface SubsonicMusicFolder {
id: string;
name: string;
}
export interface SubsonicArtistInfo {
biography?: string;
musicBrainzId?: string;
lastFmUrl?: string;
smallImageUrl?: string;
mediumImageUrl?: string;
largeImageUrl?: string;
similarArtist?: Array<{ id: string; name: string; albumCount?: number }>;
}
// ─── API Methods ──────────────────────────────────────────────
export interface SubsonicDirectoryEntry {
id: string;
parent?: string;
title: string;
isDir: boolean;
album?: string;
artist?: string;
albumId?: string;
artistId?: string;
coverArt?: string;
duration?: number;
track?: number;
year?: number;
bitRate?: number;
suffix?: string;
size?: number;
genre?: string;
starred?: string;
userRating?: number;
}
export interface SubsonicDirectory {
id: string;
parent?: string;
name: string;
child: SubsonicDirectoryEntry[];
}
export async function getMusicDirectory(id: string): Promise<SubsonicDirectory> {
const data = await api<{ directory: { id: string; parent?: string; name: string; child?: SubsonicDirectoryEntry | SubsonicDirectoryEntry[] } }>(
'getMusicDirectory.view',
@@ -288,7 +100,6 @@ export async function ping(): Promise<boolean> {
}
}
export type PingWithCredentialsResult = SubsonicServerIdentity & { ok: boolean };
/** Test a connection with explicit credentials — does NOT depend on store state. */
export async function pingWithCredentials(
@@ -504,13 +315,6 @@ export async function getRandomSongs(size = 50, genre?: string, timeout = 15000)
return data.randomSongs?.song ?? [];
}
export interface RandomSongsFilters {
size?: number;
genre?: string;
fromYear?: number;
toYear?: number;
}
/** Extended random song fetch with server-side year/genre filtering. */
export async function getRandomSongsFiltered(
filters: RandomSongsFilters,
@@ -649,14 +453,6 @@ export async function prefetchAlbumUserRatings(
}
/** Paginated album stats for Statistics (playtime, counts, genre breakdown). Same TTL as rating prefetch. */
export interface StatisticsLibraryAggregates {
playtimeSec: number;
albumsCounted: number;
songsCounted: number;
capped: boolean;
genres: SubsonicGenre[];
}
/** Key `prefix:serverId:folder` — Statistics caches share scope with `libraryFilterParams()`. */
function statisticsPageCacheKey(prefix: string): string | null {
const { activeServerId, musicLibraryFilterByServer } = useAuthStore.getState();
@@ -731,13 +527,6 @@ export async function fetchStatisticsLibraryAggregates(): Promise<StatisticsLibr
}
/** Recent / frequent / highest album strips + artist count for Statistics. */
export interface StatisticsOverviewData {
recent: SubsonicAlbum[];
frequent: SubsonicAlbum[];
highest: SubsonicAlbum[];
artistCount: number;
}
const statisticsOverviewCache = new Map<string, { value: StatisticsOverviewData; expiresAt: number }>();
export async function fetchStatisticsOverview(): Promise<StatisticsOverviewData> {
@@ -765,11 +554,6 @@ export async function fetchStatisticsOverview(): Promise<StatisticsOverviewData>
}
/** Format (suffix) histogram from a random sample for Statistics. */
export interface StatisticsFormatSample {
rows: { format: string; count: number }[];
sampleSize: number;
}
const statisticsFormatCache = new Map<string, { value: StatisticsFormatSample; expiresAt: number }>();
export async function fetchStatisticsFormatSample(): Promise<StatisticsFormatSample> {
@@ -883,18 +667,6 @@ export async function getAlbumsByGenre(genre: string, size = 50, offset = 0): Pr
return Array.isArray(raw) ? raw : [raw];
}
export interface SearchResults {
artists: SubsonicArtist[];
albums: SubsonicAlbum[];
songs: SubsonicSong[];
}
export interface StarredResults {
artists: SubsonicArtist[];
albums: SubsonicAlbum[];
songs: SubsonicSong[];
}
export async function getStarred(): Promise<StarredResults> {
const data = await api<{
starred2: {
@@ -969,7 +741,6 @@ export async function setRating(id: string, rating: number): Promise<void> {
}
/** How aggressively we assume `setRating` accepts album/artist ids (OpenSubsonic-style). */
export type EntityRatingSupportLevel = 'track_only' | 'full';
/**
* Probe server for OpenSubsonic extensions. When `openSubsonic: true`, we treat album/artist
@@ -1056,13 +827,6 @@ export function buildDownloadUrl(id: string): string {
}
// ─── Album Info (public image URLs from Last.fm/MusicBrainz) ──
export interface AlbumInfo {
largeImageUrl?: string;
mediumImageUrl?: string;
smallImageUrl?: string;
notes?: string;
}
export async function getAlbumInfo2(albumId: string): Promise<AlbumInfo | null> {
try {
const data = await api<{ albumInfo: AlbumInfo }>('getAlbumInfo2.view', { id: albumId });
@@ -1276,7 +1040,6 @@ function parseRadioBrowserStations(raw: Array<Record<string, string>>): RadioBro
}));
}
export const RADIO_PAGE_SIZE = 25;
export async function searchRadioBrowser(query: string, offset = 0): Promise<RadioBrowserStation[]> {
const raw = await invoke<Array<Record<string, string>>>('search_radio_browser', { query, offset });
@@ -1292,24 +1055,6 @@ export async function fetchUrlBytes(url: string): Promise<[number[], string]> {
return invoke<[number[], string]>('fetch_url_bytes', { url });
}
// ─── Structured Lyrics (OpenSubsonic / getLyricsBySongId) ────────────────────
export interface SubsonicLyricLine {
start?: number; // milliseconds — absent when unsynced
value: string;
}
export interface SubsonicStructuredLyrics {
/** OpenSubsonic spec field name (Navidrome ≥ 0.50.0 / any OpenSubsonic server). */
synced?: boolean;
/** Legacy / alternate casing used by some older Subsonic-compatible servers. */
issynced?: boolean;
lang?: string;
offset?: number;
displayArtist?: string;
displayTitle?: string;
line: SubsonicLyricLine[];
}
/**
* Fetches structured lyrics from the server's embedded tags via the
+49
View File
@@ -0,0 +1,49 @@
import axios from 'axios';
import md5 from 'md5';
import { version } from '../../package.json';
import { useAuthStore } from '../store/authStore';
export const SUBSONIC_CLIENT = `psysonic/${version}`;
export function secureRandomSalt(): string {
const buf = new Uint8Array(8);
crypto.getRandomValues(buf);
return Array.from(buf, b => b.toString(16).padStart(2, '0')).join('');
}
export function getAuthParams(username: string, password: string) {
const salt = secureRandomSalt();
const token = md5(password + salt);
return { u: username, t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json' };
}
export function getClient() {
const { getBaseUrl, getActiveServer } = useAuthStore.getState();
const server = getActiveServer();
const baseUrl = getBaseUrl();
if (!baseUrl) throw new Error('No server configured');
const params = getAuthParams(server?.username ?? '', server?.password ?? '');
return { baseUrl: `${baseUrl}/rest`, params };
}
export async function api<T>(endpoint: string, extra: Record<string, unknown> = {}, timeout = 15000): Promise<T> {
const { baseUrl, params } = getClient();
const resp = await axios.get(`${baseUrl}/${endpoint}`, {
params: { ...params, ...extra },
paramsSerializer: { indexes: null },
timeout,
});
const data = resp.data?.['subsonic-response'];
if (!data) throw new Error('Invalid response from server (possibly not a Subsonic server)');
if (data.status !== 'ok') throw new Error(data.error?.message ?? 'Subsonic API error');
return data as T;
}
/** Optional `musicFolderId` when the user narrowed browsing to one Subsonic library (see `getMusicFolders`). */
export function libraryFilterParams(): Record<string, string | number> {
const { activeServerId, musicLibraryFilterByServer } = useAuthStore.getState();
if (!activeServerId) return {};
const f = musicLibraryFilterByServer[activeServerId];
if (f === undefined || f === 'all') return {};
return { musicFolderId: f };
}
+244
View File
@@ -0,0 +1,244 @@
import type { SubsonicServerIdentity } from '../utils/subsonicServerIdentity';
export interface SubsonicAlbum {
id: string;
name: string;
artist: string;
artistId: string;
coverArt?: string;
songCount: number;
duration: number;
playCount?: number;
year?: number;
genre?: string;
starred?: string;
recordLabel?: string;
created?: string;
/** Present on some servers (e.g. OpenSubsonic) for album-level rating. */
userRating?: number;
/** OpenSubsonic: true when the album is tagged as a compilation. */
isCompilation?: boolean;
/** OpenSubsonic: release types from MusicBrainz tags (e.g. "Album", "EP", "Single", "Compilation", "Live"). */
releaseTypes?: string[];
}
/** OpenSubsonic `artists` / `albumArtists` entries on a child song (may include `userRating`). */
export interface SubsonicOpenArtistRef {
id?: string;
name?: string;
userRating?: number;
/** Navidrome / alternate OpenSubsonic payloads (same meaning as `userRating`). */
rating?: number;
}
export interface SubsonicSong {
id: string;
title: string;
artist: string;
album: string;
albumId: string;
artistId?: string;
duration: number;
track?: number;
discNumber?: number;
coverArt?: string;
year?: number;
userRating?: number;
/** Some OpenSubsonic responses attach parent ratings on child songs. */
albumUserRating?: number;
artistUserRating?: number;
artists?: SubsonicOpenArtistRef[];
albumArtists?: SubsonicOpenArtistRef[];
// Audio technical info
bitRate?: number;
suffix?: string;
contentType?: string;
size?: number;
samplingRate?: number;
bitDepth?: number;
channelCount?: number;
starred?: string;
genre?: string;
path?: string;
albumArtist?: string;
/** ISRC code when available (e.g., Navidrome) */
isrc?: string;
replayGain?: {
trackGain?: number;
albumGain?: number;
trackPeak?: number;
albumPeak?: number;
};
/** OpenSubsonic: structured composer credit (string for back-compat). */
displayComposer?: string;
/** OpenSubsonic: structured contributors list — Navidrome ≥ 0.55. */
contributors?: Array<{
role: string;
subRole?: string;
artist: { id?: string; name: string };
}>;
}
export interface InternetRadioStation {
id: string;
name: string;
streamUrl: string;
homepageUrl?: string;
coverArt?: string; // Navidrome v0.61.0+
}
export interface RadioBrowserStation {
stationuuid: string;
name: string;
url: string;
favicon: string;
tags: string;
}
export interface SubsonicPlaylist {
id: string;
name: string;
songCount: number;
duration: number;
created: string;
changed: string;
owner?: string;
public?: boolean;
comment?: string;
coverArt?: string;
}
export interface SubsonicNowPlaying extends SubsonicSong {
username: string;
minutesAgo: number;
playerId: number;
playerName: string;
}
export interface SubsonicArtist {
id: string;
name: string;
albumCount?: number;
coverArt?: string;
starred?: string;
/** Present on some servers (e.g. OpenSubsonic) for artist-level rating. */
userRating?: number;
}
export interface SubsonicGenre {
value: string;
songCount: number;
albumCount: number;
}
export interface SubsonicMusicFolder {
id: string;
name: string;
}
export interface SubsonicArtistInfo {
biography?: string;
musicBrainzId?: string;
lastFmUrl?: string;
smallImageUrl?: string;
mediumImageUrl?: string;
largeImageUrl?: string;
similarArtist?: Array<{ id: string; name: string; albumCount?: number }>;
}
export interface SubsonicDirectoryEntry {
id: string;
parent?: string;
title: string;
isDir: boolean;
album?: string;
artist?: string;
albumId?: string;
artistId?: string;
coverArt?: string;
duration?: number;
track?: number;
year?: number;
bitRate?: number;
suffix?: string;
size?: number;
genre?: string;
starred?: string;
userRating?: number;
}
export interface SubsonicDirectory {
id: string;
parent?: string;
name: string;
child: SubsonicDirectoryEntry[];
}
export type PingWithCredentialsResult = SubsonicServerIdentity & { ok: boolean };
export interface RandomSongsFilters {
size?: number;
genre?: string;
fromYear?: number;
toYear?: number;
}
export interface StatisticsLibraryAggregates {
playtimeSec: number;
albumsCounted: number;
songsCounted: number;
capped: boolean;
genres: SubsonicGenre[];
}
export interface StatisticsOverviewData {
recent: SubsonicAlbum[];
frequent: SubsonicAlbum[];
highest: SubsonicAlbum[];
artistCount: number;
}
export interface StatisticsFormatSample {
rows: { format: string; count: number }[];
sampleSize: number;
}
export interface SearchResults {
artists: SubsonicArtist[];
albums: SubsonicAlbum[];
songs: SubsonicSong[];
}
export interface StarredResults {
artists: SubsonicArtist[];
albums: SubsonicAlbum[];
songs: SubsonicSong[];
}
export type EntityRatingSupportLevel = 'track_only' | 'full';
export interface AlbumInfo {
largeImageUrl?: string;
mediumImageUrl?: string;
smallImageUrl?: string;
notes?: string;
}
export const RADIO_PAGE_SIZE = 25;
export interface SubsonicLyricLine {
start?: number; // milliseconds — absent when unsynced
value: string;
}
export interface SubsonicStructuredLyrics {
/** OpenSubsonic spec field name (Navidrome ≥ 0.50.0 / any OpenSubsonic server). */
synced?: boolean;
/** Legacy / alternate casing used by some older Subsonic-compatible servers. */
issynced?: boolean;
lang?: string;
offset?: number;
displayArtist?: string;
displayTitle?: string;
line: SubsonicLyricLine[];
}
+2 -1
View File
@@ -1,9 +1,10 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { memo, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus, HardDriveDownload, Check } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey, getAlbum } from '../api/subsonic';
import { buildCoverArtUrl, coverArtCacheKey, getAlbum } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useOfflineStore } from '../store/offlineStore';
import { useAuthStore } from '../store/authStore';
+3 -3
View File
@@ -1,14 +1,14 @@
import type { EntityRatingSupportLevel, SubsonicSong } from '../api/subsonicTypes';
import React, { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, Heart, ExternalLink, X, ChevronLeft, Download, ListPlus, HardDriveDownload, Loader2, Highlighter, Shuffle, Share2 } from 'lucide-react';
import { SubsonicSong, buildCoverArtUrl } from '../api/subsonic';
import { Play, Heart, ExternalLink, X, ChevronLeft, Download, ListPlus, HardDriveDownload, Share2, Highlighter, Loader2, Shuffle } from 'lucide-react';
import { buildCoverArtUrl } from '../api/subsonic';
import CachedImage from './CachedImage';
import CoverLightbox from './CoverLightbox';
import { useTranslation } from 'react-i18next';
import { useIsMobile } from '../hooks/useIsMobile';
import { useThemeStore } from '../store/themeStore';
import StarRating from './StarRating';
import type { EntityRatingSupportLevel } from '../api/subsonic';
import { copyEntityShareLink } from '../utils/copyEntityShareLink';
import { showToast } from '../utils/toast';
import { isAlbumRecentlyAdded } from '../utils/albumRecency';
+1 -1
View File
@@ -1,5 +1,5 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import React, { useRef, useState, useEffect, useMemo } from 'react';
import { SubsonicAlbum } from '../api/subsonic';
import AlbumCard from './AlbumCard';
import { ChevronLeft, ChevronRight, ArrowRight } from 'lucide-react';
import { NavLink, useNavigate } from 'react-router-dom';
+1 -1
View File
@@ -1,9 +1,9 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import type { Track } from '../store/playerStoreTypes';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Play, ChevronRight, Heart, ChevronDown, Check, RotateCcw, Square, AudioLines } from 'lucide-react';
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
import { SubsonicSong } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
+2 -1
View File
@@ -1,5 +1,6 @@
import type { SubsonicArtist } from '../api/subsonicTypes';
import React, { useMemo } from 'react';
import { SubsonicArtist, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { useNavigate } from 'react-router-dom';
import { Users } from 'lucide-react';
import { useTranslation } from 'react-i18next';
+1 -1
View File
@@ -1,5 +1,5 @@
import type { SubsonicArtist } from '../api/subsonicTypes';
import React, { useRef, useState, useEffect } from 'react';
import { SubsonicArtist } from '../api/subsonic';
import ArtistCardLocal from './ArtistCardLocal';
import { ChevronLeft, ChevronRight, ArrowRight } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
+2 -8
View File
@@ -1,16 +1,10 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Play, ListPlus, Music } from 'lucide-react';
import {
SubsonicAlbum,
buildCoverArtUrl,
coverArtCacheKey,
getAlbum,
getArtist,
getArtistInfo,
} from '../api/subsonic';
import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getArtist, getArtistInfo } from '../api/subsonic';
import CachedImage, { useCachedUrl } from './CachedImage';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
+2 -1
View File
@@ -1,3 +1,4 @@
import type { SubsonicAlbum, SubsonicArtist, SubsonicPlaylist } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import type { Track } from '../store/playerStoreTypes';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
@@ -14,7 +15,7 @@ import StarRating from './StarRating';
import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm';
import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { SubsonicAlbum, SubsonicArtist, star, unstar, getSimilarSongs2, getSimilarSongs, getTopSongs, buildDownloadUrl, getAlbum, getArtist, getPlaylists, getPlaylist, updatePlaylist, SubsonicPlaylist, setRating } from '../api/subsonic';
import { star, unstar, getSimilarSongs2, getSimilarSongs, getTopSongs, buildDownloadUrl, getAlbum, getArtist, getPlaylists, getPlaylist, updatePlaylist, setRating } from '../api/subsonic';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { useDownloadModalStore } from '../store/downloadModalStore';
+2 -1
View File
@@ -1,8 +1,9 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus } from 'lucide-react';
import { getRandomAlbums, SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey, getAlbum } from '../api/subsonic';
import { getRandomAlbums, buildCoverArtUrl, coverArtCacheKey, getAlbum } from '../api/subsonic';
import CachedImage, { useCachedUrl } from './CachedImage';
import { usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
+2 -6
View File
@@ -1,3 +1,4 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import { useEffect, useMemo, useState } from 'react';
import { Check, X, Inbox } from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -7,12 +8,7 @@ import {
declineOrbitSuggestion,
suggestionKey,
} from '../utils/orbit';
import {
getSong,
buildCoverArtUrl,
coverArtCacheKey,
type SubsonicSong,
} from '../api/subsonic';
import { getSong, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import CachedImage from './CachedImage';
import { ORBIT_DEFAULT_SETTINGS } from '../api/orbit';
+2 -1
View File
@@ -1,8 +1,9 @@
import type { SearchResults, SubsonicArtist } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Search, Disc3, Users, Music, TextSearch } from 'lucide-react';
import { search, SearchResults, buildCoverArtUrl, coverArtCacheKey, type SubsonicArtist } from '../api/subsonic';
import { search, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useTranslation } from 'react-i18next';
+1 -1
View File
@@ -1,8 +1,8 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ndListLosslessAlbumsPage } from '../api/navidromeBrowse';
import AlbumRow from './AlbumRow';
import type { SubsonicAlbum } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
interface Props {
+2 -1
View File
@@ -1,9 +1,10 @@
import type { SearchResults, SubsonicArtist } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { X, Search, Disc3, Users, Music, Music2, Clock, ChevronRight } from 'lucide-react';
import { search, SearchResults, buildCoverArtUrl, coverArtCacheKey, type SubsonicArtist } from '../api/subsonic';
import { search, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useTranslation } from 'react-i18next';
+2 -1
View File
@@ -1,7 +1,8 @@
import type { SubsonicNowPlaying } from '../api/subsonicTypes';
import React, { useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom';
import { PlayCircle, User, Radio, RefreshCw } from 'lucide-react';
import { getNowPlaying, SubsonicNowPlaying, buildCoverArtUrl } from '../api/subsonic';
import { getNowPlaying, buildCoverArtUrl } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
+2 -1
View File
@@ -1,10 +1,11 @@
import type { SubsonicArtistInfo, SubsonicSong } from '../api/subsonicTypes';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Info } from 'lucide-react';
import { open as shellOpen } from '@tauri-apps/plugin-shell';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { getArtistInfo, getSong, type SubsonicArtistInfo, type SubsonicSong } from '../api/subsonic';
import { getArtistInfo, getSong } from '../api/subsonic';
import { fetchBandsintownEvents, type BandsintownEvent } from '../api/bandsintown';
import CachedImage from './CachedImage';
import OverlayScrollArea from './OverlayScrollArea';
+2 -6
View File
@@ -1,13 +1,9 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import { useEffect, useMemo, useState } from 'react';
import { Radio, Clock } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import {
getSong,
buildCoverArtUrl,
coverArtCacheKey,
type SubsonicSong,
} from '../api/subsonic';
import { getSong, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import CachedImage from './CachedImage';
import OrbitQueueHead from './OrbitQueueHead';
+2 -1
View File
@@ -1,3 +1,4 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
@@ -11,7 +12,7 @@ import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { useAuthStore } from '../store/authStore';
import { useThemeStore } from '../store/themeStore';
import { buildCoverArtUrl, coverArtCacheKey, star, unstar, setRating, SubsonicAlbum } from '../api/subsonic';
import { buildCoverArtUrl, coverArtCacheKey, star, unstar, setRating } from '../api/subsonic';
import CachedImage from './CachedImage';
import WaveformSeek from './WaveformSeek';
import Equalizer from './Equalizer';
+2 -1
View File
@@ -1,3 +1,4 @@
import type { SubsonicPlaylist } from '../api/subsonicTypes';
import { registerQueueListScrollTopReader, consumePendingQueueListScrollTop } from '../store/queueUndo';
import { songToTrack } from '../utils/songToTrack';
import type { Track } from '../store/playerStoreTypes';
@@ -9,7 +10,7 @@ import OrbitGuestQueue from './OrbitGuestQueue';
import OrbitQueueHead from './OrbitQueueHead';
import HostApprovalQueue from './HostApprovalQueue';
import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, MoveRight, Radio, HardDrive, ChevronDown, Info, Share2 } from 'lucide-react';
import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist, SubsonicPlaylist } from '../api/subsonic';
import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist } from '../api/subsonic';
import { usePlaylistStore } from '../store/playlistStore';
import { useCachedUrl } from './CachedImage';
import { useTranslation } from 'react-i18next';
+2 -1
View File
@@ -1,9 +1,10 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { memo, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus, Star } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { SubsonicSong, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import CachedImage from './CachedImage';
import { enqueueAndPlay } from '../utils/playSong';
+2 -1
View File
@@ -1,9 +1,10 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { X } from 'lucide-react';
import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { getSong, SubsonicSong } from '../api/subsonic';
import { getSong } from '../api/subsonic';
import { ndGetSongPath } from '../api/navidromeAdmin';
import { useAuthStore } from '../store/authStore';
import { useTranslation } from 'react-i18next';
+1 -1
View File
@@ -1,6 +1,6 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import React, { useRef, useState, useEffect, useMemo } from 'react';
import { ChevronLeft, ChevronRight, RefreshCw } from 'lucide-react';
import { SubsonicSong } from '../api/subsonic';
import SongCard from './SongCard';
import { usePerfProbeFlags } from '../utils/perfFlags';
import { dedupeById } from '../utils/dedupeById';
+1 -1
View File
@@ -1,9 +1,9 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { memo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { SubsonicSong } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { enqueueAndPlay } from '../utils/playSong';
import { useDragDrop } from '../contexts/DragDropContext';
+2 -1
View File
@@ -1,10 +1,11 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { X } from 'lucide-react';
import { save } from '@tauri-apps/plugin-dialog';
import { writeFile } from '@tauri-apps/plugin-fs';
import { getAlbumList, SubsonicAlbum } from '../api/subsonic';
import { getAlbumList } from '../api/subsonic';
import { showToast } from '../utils/toast';
import {
exportAlbumCardBlob,
+2 -1
View File
@@ -1,9 +1,10 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useRefElementClientHeight } from '../hooks/useResizeClientHeight';
import { Search as SearchIcon, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useVirtualizer } from '@tanstack/react-virtual';
import { SubsonicSong, searchSongsPaged } from '../api/subsonic';
import { searchSongsPaged } from '../api/subsonic';
import { ndListSongs } from '../api/navidromeBrowse';
import SongRow, { SongListHeader } from './SongRow';
+2 -1
View File
@@ -1,10 +1,11 @@
import type { SubsonicStructuredLyrics } from '../api/subsonicTypes';
import type { Track } from '../store/playerStoreTypes';
import { useEffect, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { invoke } from '@tauri-apps/api/core';
import { fetchLyrics, parseLrc, LrcLine } from '../api/lrclib';
import { fetchNeteaselyrics } from '../api/netease';
import { getLyricsBySongId, SubsonicStructuredLyrics } from '../api/subsonic';
import { getLyricsBySongId } from '../api/subsonic';
import { fetchLyricsPlus, hasWordSync } from '../api/lyricsplus';
import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
+1 -1
View File
@@ -1,6 +1,6 @@
import type { InternetRadioStation } from '../api/subsonicTypes';
import { useEffect, useRef, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import type { InternetRadioStation } from '../api/subsonic';
import { usePerfProbeFlags } from '../utils/perfFlags';
import {
guessAzuraCastApiUrl,
+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';
+1 -1
View File
@@ -1,4 +1,4 @@
import type { EntityRatingSupportLevel } from '../api/subsonic';
import type { EntityRatingSupportLevel } from '../api/subsonicTypes';
import type {
InstantMixProbeResult,
SubsonicServerIdentity,
+1 -1
View File
@@ -1,8 +1,8 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { invoke } from '@tauri-apps/api/core';
import { buildStreamUrl, getArtist, getAlbum } from '../api/subsonic';
import type { SubsonicSong } from '../api/subsonic';
import { useAuthStore } from './authStore';
import { showToast } from '../utils/toast';
import { useOfflineJobStore, cancelledDownloads } from './offlineJobStore';
+1 -1
View File
@@ -1,4 +1,4 @@
import type { InternetRadioStation } from '../api/subsonic';
import type { InternetRadioStation } from '../api/subsonicTypes';
import type { PlaybackSourceKind } from '../utils/resolvePlaybackUrl';
export interface Track {
+2 -2
View File
@@ -1,7 +1,7 @@
import type { SubsonicPlaylist } from '../api/subsonicTypes';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { getPlaylists, createPlaylist as apiCreatePlaylist, SubsonicPlaylist } from '../api/subsonic';
import { getPlaylists, createPlaylist as apiCreatePlaylist } from '../api/subsonic';
interface PlaylistStore {
recentIds: string[];
playlists: SubsonicPlaylist[];
+1 -2
View File
@@ -5,10 +5,9 @@
* the fields the test cares about. Keeps tests focused on behaviour rather
* than on assembling boilerplate.
*/
import type { SubsonicSong } from '@/api/subsonicTypes';
import type { ServerProfile } from '@/store/authStoreTypes';
import type { Track } from '@/store/playerStoreTypes';
import type { SubsonicSong } from '@/api/subsonic';
let trackCounter = 0;
let songCounter = 0;
let serverCounter = 0;
+1 -1
View File
@@ -18,7 +18,7 @@
* Realistic shape matters more than perfect coverage these fixtures
* mirror what Navidrome actually returns for common queries.
*/
import type { SubsonicSong, SubsonicAlbum, SubsonicPlaylist } from '@/api/subsonic';
import type { SubsonicSong, SubsonicAlbum, SubsonicPlaylist } from '@/api/subsonicTypes';
import { makeSubsonicSong } from '@/test/helpers/factories';
export const sampleSubsonicSong: SubsonicSong = makeSubsonicSong({
+2 -1
View File
@@ -1,7 +1,8 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import type { NavigateFunction } from 'react-router-dom';
import type { TFunction } from 'i18next';
import { getAlbum, getArtist, getSong, type SubsonicSong } from '../api/subsonic';
import { getAlbum, getArtist, getSong } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import { findServerIdForShareUrl, type EntitySharePayloadV1 } from './shareLink';
+2 -1
View File
@@ -1,6 +1,7 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { buildCoverArtUrl, coverArtCacheKey, SubsonicAlbum } from '../api/subsonic';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { getCachedBlob } from './imageCache';
import PsysonicLogo from '../components/PsysonicLogo';
+1 -2
View File
@@ -1,9 +1,8 @@
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { writeFile } from '@tauri-apps/plugin-fs';
import { downloadDir, join } from '@tauri-apps/api/path';
import { getAlbumList, buildCoverArtUrl } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import type { SubsonicAlbum } from '../api/subsonic';
// Catppuccin Macchiato palette
const M = {
crust: '#181926',
+2 -10
View File
@@ -1,15 +1,7 @@
import type { SubsonicAlbum, SubsonicSong } from '../api/subsonicTypes';
import type { Track } from '../store/playerStoreTypes';
import { songToTrack } from '../utils/songToTrack';
import {
filterSongsToActiveLibrary,
getAlbum,
getAlbumList,
getRandomSongs,
getSimilarSongs,
getTopSongs,
type SubsonicAlbum,
type SubsonicSong,
} from '../api/subsonic';
import { filterSongsToActiveLibrary, getAlbum, getAlbumList, getRandomSongs, getSimilarSongs, getTopSongs } from '../api/subsonic';
import { invoke } from '@tauri-apps/api/core';
import i18n from '../i18n';
import { useAuthStore } from '../store/authStore';
+2 -8
View File
@@ -1,11 +1,5 @@
import {
getRandomSongs,
parseSubsonicEntityStarRating,
prefetchAlbumUserRatings,
prefetchArtistUserRatings,
type SubsonicAlbum,
type SubsonicSong,
} from '../api/subsonic';
import type { SubsonicAlbum, SubsonicSong } from '../api/subsonicTypes';
import { getRandomSongs, parseSubsonicEntityStarRating, prefetchAlbumUserRatings, prefetchArtistUserRatings } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
/** Default target list size for Random Mix; per-call override via `fetchRandomMixSongsUntilFull(c, { targetSize })`. */
+1 -2
View File
@@ -1,7 +1,6 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import { usePlayerStore } from '../store/playerStore';
import type { SubsonicSong } from '../api/subsonic';
function fadeOut(setVolume: (v: number) => void, from: number, durationMs: number): Promise<void> {
return new Promise(resolve => {
const steps = 16;
+1 -1
View File
@@ -1,5 +1,5 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import type { Track } from '../store/playerStoreTypes';
import type { SubsonicSong } from '../api/subsonic';
export function songToTrack(song: SubsonicSong): Track {
return {
id: song.id,