refactor(api): F.50 — extract 7 small domain modules (#615)

Seven domain-eng splits peel ~200 LOC of read endpoints out of
`api/subsonic.ts`:

- `subsonicStreamUrl.ts` — `buildStreamUrl`, `coverArtCacheKey`,
  `buildCoverArtUrl`, `buildDownloadUrl` (token-signed URL builders
  for the four /rest endpoints we hand to the browser).
- `subsonicStarRating.ts` — `getStarred`, `star`, `unstar`,
  `setRating`, `probeEntityRatingSupport`. `setRating` still triggers
  the lazy `navidromeBrowse` cache invalidation; the same-folder
  lazy import path is preserved.
- `subsonicSearch.ts` — `search`, `searchSongsPaged`.
- `subsonicScrobble.ts` — `scrobbleSong`, `reportNowPlaying`,
  `getNowPlaying`.
- `subsonicAlbumInfo.ts` — `getAlbumInfo2`.
- `subsonicLyrics.ts` — `getLyricsBySongId`.
- `subsonicGenres.ts` — `getGenres`, `getAlbumsByGenre`.

63 external call sites migrated to direct imports. Four `vi.mock`
targets in the store-level tests pointed at `../api/subsonic` and
were updated to the new module paths.

Pure code-move. subsonic.ts: 762 → 561 LOC (−201).
This commit is contained in:
Frank Stellmacher
2026-05-13 00:46:13 +02:00
committed by GitHub
parent 006635de4b
commit 9606a99efb
76 changed files with 347 additions and 275 deletions
+4 -1
View File
@@ -21,7 +21,10 @@ vi.mock('axios', () => ({
}));
import axios from 'axios';
import { getAlbumInfo2, getStarred, pingWithCredentials, ping, search } from './subsonic';
import { pingWithCredentials, ping } from './subsonic';
import { getAlbumInfo2 } from './subsonicAlbumInfo';
import { getStarred } from './subsonicStarRating';
import { search } from './subsonicSearch';
import { getAlbum, getMusicDirectory, getMusicFolders, getMusicIndexes, getRandomSongs, getSong } from './subsonicLibrary';
import { getArtists, getTopSongs } from './subsonicArtists';
import { useAuthStore } from '@/store/authStore';
+1 -1
View File
@@ -10,8 +10,8 @@
* Network-bound endpoints (`getAlbum`, `search`, etc.) require axios
* mocking and are not in this PR.
*/
import { buildCoverArtUrl, buildDownloadUrl, buildStreamUrl, coverArtCacheKey } from './subsonicStreamUrl';
import { beforeEach, describe, expect, it } from 'vitest';
import { buildCoverArtUrl, buildDownloadUrl, buildStreamUrl, coverArtCacheKey } from './subsonic';
import { parseSubsonicEntityStarRating } from './subsonicRatings';
import { getClient, libraryFilterParams } from './subsonicClient';
import { useAuthStore } from '@/store/authStore';
-201
View File
@@ -330,195 +330,23 @@ export async function fetchStatisticsFormatSample(): Promise<StatisticsFormatSam
export async function getGenres(): Promise<SubsonicGenre[]> {
const data = await api<{ genres: { genre: SubsonicGenre | SubsonicGenre[] } }>('getGenres.view');
const raw = data.genres?.genre;
if (!raw) return [];
return Array.isArray(raw) ? raw : [raw];
}
export async function getAlbumsByGenre(genre: string, size = 50, offset = 0): Promise<SubsonicAlbum[]> {
const data = await api<{ albumList2: { album: SubsonicAlbum | SubsonicAlbum[] } }>('getAlbumList2.view', {
type: 'byGenre',
genre,
size,
offset,
_t: Date.now(),
...libraryFilterParams(),
});
const raw = data.albumList2?.album;
if (!raw) return [];
return Array.isArray(raw) ? raw : [raw];
}
export async function getStarred(): Promise<StarredResults> {
const data = await api<{
starred2: {
artist?: SubsonicArtist[];
album?: SubsonicAlbum[];
song?: SubsonicSong[];
}
}>('getStarred2.view', { ...libraryFilterParams() });
const r = data.starred2 ?? {};
return { artists: r.artist ?? [], albums: r.album ?? [], songs: r.song ?? [] };
}
export async function star(id: string, type: 'song' | 'album' | 'artist' = 'album'): Promise<void> {
const params: Record<string, string> = {};
if (type === 'song') params.id = id;
if (type === 'album') params.albumId = id;
if (type === 'artist') params.artistId = id;
await api('star.view', params);
}
export async function unstar(id: string, type: 'song' | 'album' | 'artist' = 'album'): Promise<void> {
const params: Record<string, string> = {};
if (type === 'song') params.id = id;
if (type === 'album') params.albumId = id;
if (type === 'artist') params.artistId = id;
await api('unstar.view', params);
}
export async function search(query: string, options?: { albumCount?: number; artistCount?: number; songCount?: number }): Promise<SearchResults> {
if (!query.trim()) return { artists: [], albums: [], songs: [] };
const data = await api<{
searchResult3: {
artist?: SubsonicArtist[];
album?: SubsonicAlbum[];
song?: SubsonicSong[];
};
}>('search3.view', {
query,
artistCount: options?.artistCount ?? 5,
albumCount: options?.albumCount ?? 5,
songCount: options?.songCount ?? 10,
...libraryFilterParams(),
});
const r = data.searchResult3 ?? {};
return { artists: r.artist ?? [], albums: r.album ?? [], songs: r.song ?? [] };
}
/**
* Song-only paginated search3. Tolerates empty query — Navidrome returns all songs
* ordered by title in that case; strict Subsonic implementations may return nothing.
* Caller handles empty results gracefully (Tracks page falls back to its random pool).
*/
export async function searchSongsPaged(query: string, songCount: number, songOffset: number): Promise<SubsonicSong[]> {
const data = await api<{ searchResult3: { song?: SubsonicSong[] } }>('search3.view', {
query,
artistCount: 0,
albumCount: 0,
songCount,
songOffset,
...libraryFilterParams(),
});
return data.searchResult3?.song ?? [];
}
export async function setRating(id: string, rating: number): Promise<void> {
await api('setRating.view', { id, rating });
// Cached song lists keyed by rating (e.g. Tracks → Highly Rated rail) become
// stale immediately. Lazy-import to keep the module dep direction
// subsonic ← navidromeBrowse and avoid pulling Tauri internals into shared
// type-only consumers.
void import('./navidromeBrowse').then(m => m.ndInvalidateSongsCache()).catch(() => {});
}
/** How aggressively we assume `setRating` accepts album/artist ids (OpenSubsonic-style). */
/**
* Probe server for OpenSubsonic extensions. When `openSubsonic: true`, we treat album/artist
* rating as supported (same `setRating.view` + entity id); otherwise track-only.
*/
export async function probeEntityRatingSupport(): Promise<EntityRatingSupportLevel> {
try {
const data = await api<{ openSubsonic?: boolean; openSubsonicExtensions?: unknown[] }>(
'getOpenSubsonicExtensions.view',
{},
8000,
);
if (data.openSubsonic === true) return 'full';
if (Array.isArray(data.openSubsonicExtensions)) return 'full';
return 'track_only';
} catch {
return 'track_only';
}
}
export async function scrobbleSong(id: string, time: number): Promise<void> {
try {
await api('scrobble.view', { id, time, submission: true });
} catch {
// best effort
}
}
export async function reportNowPlaying(id: string): Promise<void> {
try {
await api('scrobble.view', { id, submission: false });
} catch {
// best effort
}
}
// ─── Stream URL ───────────────────────────────────────────────
export function buildStreamUrl(id: string): string {
const { getBaseUrl, getActiveServer } = useAuthStore.getState();
const server = getActiveServer();
const baseUrl = getBaseUrl();
const salt = secureRandomSalt();
const token = md5((server?.password ?? '') + salt);
const p = new URLSearchParams({
id,
u: server?.username ?? '',
t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json',
});
return `${baseUrl}/rest/stream.view?${p.toString()}`;
}
/** Stable cache key for cover art — does not include ephemeral auth params. */
export function coverArtCacheKey(id: string, size = 256): string {
const server = useAuthStore.getState().getActiveServer();
return `${server?.id ?? '_'}:cover:${id}:${size}`;
}
export function buildCoverArtUrl(id: string, size = 256): string {
const { getBaseUrl, getActiveServer } = useAuthStore.getState();
const server = getActiveServer();
const baseUrl = getBaseUrl();
const salt = secureRandomSalt();
const token = md5((server?.password ?? '') + salt);
const p = new URLSearchParams({
id, size: String(size),
u: server?.username ?? '',
t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json',
});
return `${baseUrl}/rest/getCoverArt.view?${p.toString()}`;
}
export function buildDownloadUrl(id: string): string {
const { getBaseUrl, getActiveServer } = useAuthStore.getState();
const server = getActiveServer();
const baseUrl = getBaseUrl();
const salt = secureRandomSalt();
const token = md5((server?.password ?? '') + salt);
const p = new URLSearchParams({
id,
u: server?.username ?? '',
t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json',
});
return `${baseUrl}/rest/download.view?${p.toString()}`;
}
// ─── Album Info (public image URLs from Last.fm/MusicBrainz) ──
export async function getAlbumInfo2(albumId: string): Promise<AlbumInfo | null> {
try {
const data = await api<{ albumInfo: AlbumInfo }>('getAlbumInfo2.view', { id: albumId });
return data.albumInfo ?? null;
} catch {
return null;
}
}
// ─── Playlists ────────────────────────────────────────────────
export async function getPlaylists(includeOrbit = false): Promise<SubsonicPlaylist[]> {
@@ -627,15 +455,6 @@ export async function savePlayQueue(songIds: string[], current?: string, positio
}
// ─── Now Playing ──────────────────────────────────────────────
export async function getNowPlaying(): Promise<SubsonicNowPlaying[]> {
try {
const data = await api<{ nowPlaying: { entry?: SubsonicNowPlaying[] } | '' }>('getNowPlaying.view');
if (!data.nowPlaying || typeof data.nowPlaying === 'string') return [];
return data.nowPlaying.entry ?? [];
} catch {
return [];
}
}
// ─── Internet Radio ───────────────────────────────────────────
@@ -740,23 +559,3 @@ export async function fetchUrlBytes(url: string): Promise<[number[], string]> {
}
/**
* Fetches structured lyrics from the server's embedded tags via the
* OpenSubsonic `getLyricsBySongId` endpoint. Returns null when the
* server doesn't support the endpoint or the track has no embedded lyrics.
* Prefers synced lyrics over plain when both are present.
*/
export async function getLyricsBySongId(id: string): Promise<SubsonicStructuredLyrics | null> {
try {
const data = await api<{ lyricsList: { structuredLyrics?: SubsonicStructuredLyrics[] } }>(
'getLyricsBySongId.view',
{ id },
);
const list = data.lyricsList?.structuredLyrics;
if (!list || list.length === 0) return null;
return list.find(l => l.synced || l.issynced) ?? list[0];
} catch {
// Server doesn't support the endpoint or track has no embedded lyrics
return null;
}
}
+11
View File
@@ -0,0 +1,11 @@
import { api } from './subsonicClient';
import type { AlbumInfo } from './subsonicTypes';
export async function getAlbumInfo2(albumId: string): Promise<AlbumInfo | null> {
try {
const data = await api<{ albumInfo: AlbumInfo }>('getAlbumInfo2.view', { id: albumId });
return data.albumInfo ?? null;
} catch {
return null;
}
}
+23
View File
@@ -0,0 +1,23 @@
import { api, libraryFilterParams } from './subsonicClient';
import type { SubsonicAlbum, SubsonicGenre } from './subsonicTypes';
export async function getGenres(): Promise<SubsonicGenre[]> {
const data = await api<{ genres: { genre: SubsonicGenre | SubsonicGenre[] } }>('getGenres.view');
const raw = data.genres?.genre;
if (!raw) return [];
return Array.isArray(raw) ? raw : [raw];
}
export async function getAlbumsByGenre(genre: string, size = 50, offset = 0): Promise<SubsonicAlbum[]> {
const data = await api<{ albumList2: { album: SubsonicAlbum | SubsonicAlbum[] } }>('getAlbumList2.view', {
type: 'byGenre',
genre,
size,
offset,
_t: Date.now(),
...libraryFilterParams(),
});
const raw = data.albumList2?.album;
if (!raw) return [];
return Array.isArray(raw) ? raw : [raw];
}
+23
View File
@@ -0,0 +1,23 @@
import { api } from './subsonicClient';
import type { SubsonicStructuredLyrics } from './subsonicTypes';
/**
* Fetches structured lyrics from the server's embedded tags via the
* OpenSubsonic `getLyricsBySongId` endpoint. Returns null when the
* server doesn't support the endpoint or the track has no embedded lyrics.
* Prefers synced lyrics over plain when both are present.
*/
export async function getLyricsBySongId(id: string): Promise<SubsonicStructuredLyrics | null> {
try {
const data = await api<{ lyricsList: { structuredLyrics?: SubsonicStructuredLyrics[] } }>(
'getLyricsBySongId.view',
{ id },
);
const list = data.lyricsList?.structuredLyrics;
if (!list || list.length === 0) return null;
return list.find(l => l.synced || l.issynced) ?? list[0];
} catch {
// Server doesn't support the endpoint or track has no embedded lyrics
return null;
}
}
+29
View File
@@ -0,0 +1,29 @@
import { api } from './subsonicClient';
import type { SubsonicNowPlaying } from './subsonicTypes';
export async function scrobbleSong(id: string, time: number): Promise<void> {
try {
await api('scrobble.view', { id, time, submission: true });
} catch {
// best effort
}
}
export async function reportNowPlaying(id: string): Promise<void> {
try {
await api('scrobble.view', { id, submission: false });
} catch {
// best effort
}
}
export async function getNowPlaying(): Promise<SubsonicNowPlaying[]> {
try {
const data = await api<{ nowPlaying: { entry?: SubsonicNowPlaying | SubsonicNowPlaying[] } }>('getNowPlaying.view', { _t: Date.now() });
const raw = data.nowPlaying?.entry;
if (!raw) return [];
return Array.isArray(raw) ? raw : [raw];
} catch {
return [];
}
}
+43
View File
@@ -0,0 +1,43 @@
import { api, libraryFilterParams } from './subsonicClient';
import type {
SearchResults,
SubsonicAlbum,
SubsonicArtist,
SubsonicSong,
} from './subsonicTypes';
export async function search(query: string, options?: { albumCount?: number; artistCount?: number; songCount?: number }): Promise<SearchResults> {
if (!query.trim()) return { artists: [], albums: [], songs: [] };
const data = await api<{
searchResult3: {
artist?: SubsonicArtist[];
album?: SubsonicAlbum[];
song?: SubsonicSong[];
};
}>('search3.view', {
query,
artistCount: options?.artistCount ?? 5,
albumCount: options?.albumCount ?? 5,
songCount: options?.songCount ?? 10,
...libraryFilterParams(),
});
const r = data.searchResult3 ?? {};
return { artists: r.artist ?? [], albums: r.album ?? [], songs: r.song ?? [] };
}
/**
* Song-only paginated search3. Tolerates empty query — Navidrome returns all songs
* ordered by title in that case; strict Subsonic implementations may return nothing.
* Caller handles empty results gracefully (Tracks page falls back to its random pool).
*/
export async function searchSongsPaged(query: string, songCount: number, songOffset: number): Promise<SubsonicSong[]> {
const data = await api<{ searchResult3: { song?: SubsonicSong[] } }>('search3.view', {
query,
artistCount: 0,
albumCount: 0,
songCount,
songOffset,
...libraryFilterParams(),
});
return data.searchResult3?.song ?? [];
}
+64
View File
@@ -0,0 +1,64 @@
import { api, libraryFilterParams } from './subsonicClient';
import type {
EntityRatingSupportLevel,
StarredResults,
SubsonicAlbum,
SubsonicArtist,
SubsonicSong,
} from './subsonicTypes';
export async function getStarred(): Promise<StarredResults> {
const data = await api<{
starred2: {
artist?: SubsonicArtist[];
album?: SubsonicAlbum[];
song?: SubsonicSong[];
}
}>('getStarred2.view', { ...libraryFilterParams() });
const r = data.starred2 ?? {};
return { artists: r.artist ?? [], albums: r.album ?? [], songs: r.song ?? [] };
}
export async function star(id: string, type: 'song' | 'album' | 'artist' = 'album'): Promise<void> {
const params: Record<string, string> = {};
if (type === 'song') params.id = id;
if (type === 'album') params.albumId = id;
if (type === 'artist') params.artistId = id;
await api('star.view', params);
}
export async function unstar(id: string, type: 'song' | 'album' | 'artist' = 'album'): Promise<void> {
const params: Record<string, string> = {};
if (type === 'song') params.id = id;
if (type === 'album') params.albumId = id;
if (type === 'artist') params.artistId = id;
await api('unstar.view', params);
}
export async function setRating(id: string, rating: number): Promise<void> {
await api('setRating.view', { id, rating });
// Cached song lists keyed by rating (e.g. Tracks → Highly Rated rail) become
// stale immediately. Lazy-import to keep the module dep direction
// subsonic ← navidromeBrowse and avoid pulling Tauri internals into shared
// type-only consumers.
void import('./navidromeBrowse').then(m => m.ndInvalidateSongsCache()).catch(() => {});
}
/**
* Probe server for OpenSubsonic extensions. When `openSubsonic: true`, we treat album/artist
* rating as supported (same `setRating.view` + entity id); otherwise track-only.
*/
export async function probeEntityRatingSupport(): Promise<EntityRatingSupportLevel> {
try {
const data = await api<{ openSubsonic?: boolean; openSubsonicExtensions?: unknown[] }>(
'getOpenSubsonicExtensions.view',
{},
8000,
);
if (data.openSubsonic === true) return 'full';
if (Array.isArray(data.openSubsonicExtensions)) return 'full';
return 'track_only';
} catch {
return 'track_only';
}
}
+51
View File
@@ -0,0 +1,51 @@
import md5 from 'md5';
import { useAuthStore } from '../store/authStore';
import { SUBSONIC_CLIENT, secureRandomSalt } from './subsonicClient';
export function buildStreamUrl(id: string): string {
const { getBaseUrl, getActiveServer } = useAuthStore.getState();
const server = getActiveServer();
const baseUrl = getBaseUrl();
const salt = secureRandomSalt();
const token = md5((server?.password ?? '') + salt);
const p = new URLSearchParams({
id,
u: server?.username ?? '',
t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json',
});
return `${baseUrl}/rest/stream.view?${p.toString()}`;
}
/** Stable cache key for cover art — does not include ephemeral auth params. */
export function coverArtCacheKey(id: string, size = 256): string {
const server = useAuthStore.getState().getActiveServer();
return `${server?.id ?? '_'}:cover:${id}:${size}`;
}
export function buildCoverArtUrl(id: string, size = 256): string {
const { getBaseUrl, getActiveServer } = useAuthStore.getState();
const server = getActiveServer();
const baseUrl = getBaseUrl();
const salt = secureRandomSalt();
const token = md5((server?.password ?? '') + salt);
const p = new URLSearchParams({
id, size: String(size),
u: server?.username ?? '',
t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json',
});
return `${baseUrl}/rest/getCoverArt.view?${p.toString()}`;
}
export function buildDownloadUrl(id: string): string {
const { getBaseUrl, getActiveServer } = useAuthStore.getState();
const server = getActiveServer();
const baseUrl = getBaseUrl();
const salt = secureRandomSalt();
const token = md5((server?.password ?? '') + salt);
const p = new URLSearchParams({
id,
u: server?.username ?? '',
t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json',
});
return `${baseUrl}/rest/download.view?${p.toString()}`;
}
+1 -1
View File
@@ -1,3 +1,4 @@
import { probeEntityRatingSupport } from '../api/subsonicStarRating';
import { getMusicFolders } from '../api/subsonicLibrary';
import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
@@ -37,7 +38,6 @@ import { useOrbitStore } from '../store/orbitStore';
import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform';
import { useConnectionStatus } from '../hooks/useConnectionStatus';
import { useAuthStore } from '../store/authStore';
import { probeEntityRatingSupport } from '../api/subsonic';
import { useOfflineStore } from '../store/offlineStore';
import { usePlayerStore } from '../store/playerStore';
import { useThemeStore } from '../store/themeStore';
+1 -1
View File
@@ -13,7 +13,7 @@ import { showToast } from '../utils/toast';
import { endOrbitSession, leaveOrbitSession } from '../utils/orbit';
import { useOrbitStore } from '../store/orbitStore';
import { useAuthStore } from '../store/authStore';
import { search as subsonicSearch } from '../api/subsonic';
import { search as subsonicSearch } from '../api/subsonicSearch';
import i18n from '../i18n';
import { switchActiveServer } from '../utils/switchActiveServer';
import { usePlayerStore } from '../store/playerStore';
+1 -1
View File
@@ -1,3 +1,4 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getAlbum } from '../api/subsonicLibrary';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
@@ -5,7 +6,6 @@ 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 { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useOfflineStore } from '../store/offlineStore';
import { useAuthStore } from '../store/authStore';
+1 -1
View File
@@ -1,8 +1,8 @@
import { buildCoverArtUrl } from '../api/subsonicStreamUrl';
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, 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';
+1 -1
View File
@@ -1,6 +1,6 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import type { SubsonicArtist } from '../api/subsonicTypes';
import React, { useMemo } from 'react';
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,3 +1,4 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getArtist, getArtistInfo } from '../api/subsonicArtists';
import { getAlbum } from '../api/subsonicLibrary';
import type { SubsonicAlbum } from '../api/subsonicTypes';
@@ -6,7 +7,6 @@ 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 { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import CachedImage, { useCachedUrl } from './CachedImage';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
+3 -1
View File
@@ -1,3 +1,5 @@
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
import { star, unstar, setRating } from '../api/subsonicStarRating';
import { getAlbum } from '../api/subsonicLibrary';
import { getSimilarSongs2, getSimilarSongs, getTopSongs, getArtist } from '../api/subsonicArtists';
import type { SubsonicAlbum, SubsonicArtist, SubsonicPlaylist } from '../api/subsonicTypes';
@@ -17,7 +19,7 @@ import StarRating from './StarRating';
import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm';
import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { star, unstar, buildDownloadUrl, getPlaylists, getPlaylist, updatePlaylist, setRating } from '../api/subsonic';
import { getPlaylists, getPlaylist, updatePlaylist } from '../api/subsonic';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { useDownloadModalStore } from '../store/downloadModalStore';
+2 -1
View File
@@ -1,3 +1,5 @@
import { star, unstar } from '../api/subsonicStarRating';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getArtistInfo } from '../api/subsonicArtists';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import React, { useCallback, useEffect, useState, useRef, memo, useMemo } from 'react';
@@ -7,7 +9,6 @@ import {
Moon, Sunrise,
} from 'lucide-react';
import { usePlayerStore } from '../store/playerStore';
import { buildCoverArtUrl, coverArtCacheKey, star, unstar } from '../api/subsonic';
import { useCachedUrl } from './CachedImage';
import { getCachedBlob } from '../utils/imageCache';
import { extractCoverColors } from '../utils/dynamicColors';
+1 -1
View File
@@ -1,8 +1,8 @@
import { getGenres } from '../api/subsonicGenres';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Check, Filter, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { getGenres } from '../api/subsonic';
interface GenreFilterBarProps {
selected: string[];
+1 -1
View File
@@ -1,10 +1,10 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getRandomAlbums, getAlbum } from '../api/subsonicLibrary';
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 { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import CachedImage, { useCachedUrl } from './CachedImage';
import { usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
+1 -1
View File
@@ -1,3 +1,4 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getSong } from '../api/subsonicLibrary';
import type { SubsonicSong } from '../api/subsonicTypes';
import { useEffect, useMemo, useState } from 'react';
@@ -9,7 +10,6 @@ import {
declineOrbitSuggestion,
suggestionKey,
} from '../utils/orbit';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import CachedImage from './CachedImage';
import { ORBIT_DEFAULT_SETTINGS } from '../api/orbit';
+2 -1
View File
@@ -1,9 +1,10 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { search } from '../api/subsonicSearch';
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, 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,9 +1,9 @@
import { star, unstar } from '../api/subsonicStarRating';
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { emit } from '@tauri-apps/api/event';
import { useTranslation } from 'react-i18next';
import { Play, Trash2, Disc3, User, Heart, Info } from 'lucide-react';
import { star, unstar } from '../api/subsonic';
import type { MiniTrackInfo } from '../utils/miniPlayerBridge';
interface Props {
+1 -1
View File
@@ -1,3 +1,4 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { emit, listen } from '@tauri-apps/api/event';
@@ -5,7 +6,6 @@ import { invoke } from '@tauri-apps/api/core';
import { useTranslation } from 'react-i18next';
import { Play, Pause, SkipBack, SkipForward, Pin, PinOff, Maximize2, X, ListMusic, Volume2, VolumeX, Shuffle, Infinity as InfinityIcon, Waves, MoveRight } from 'lucide-react';
import CachedImage from './CachedImage';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useKeybindingsStore, matchInAppBinding } from '../store/keybindingsStore';
+2 -1
View File
@@ -1,3 +1,5 @@
import { star, unstar } from '../api/subsonicStarRating';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import type { Track } from '../store/playerStoreTypes';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import React, { useState, useCallback, useMemo, useRef, useEffect, useSyncExternalStore, CSSProperties } from 'react';
@@ -9,7 +11,6 @@ import {
Moon, Sunrise,
} from 'lucide-react';
import { usePlayerStore } from '../store/playerStore';
import { buildCoverArtUrl, coverArtCacheKey, star, unstar } from '../api/subsonic';
import { useCachedUrl } from './CachedImage';
import LyricsPane from './LyricsPane';
import { usePlaybackDelayPress } from '../hooks/usePlaybackDelayPress';
+2 -1
View File
@@ -1,10 +1,11 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { search } from '../api/subsonicSearch';
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, 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,8 +1,9 @@
import { buildCoverArtUrl } from '../api/subsonicStreamUrl';
import { getNowPlaying } from '../api/subsonicScrobble';
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, buildCoverArtUrl } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
+1 -1
View File
@@ -1,10 +1,10 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getSong } from '../api/subsonicLibrary';
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 { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import CachedImage from './CachedImage';
import OrbitQueueHead from './OrbitQueueHead';
+2 -1
View File
@@ -1,3 +1,5 @@
import { star, unstar, setRating } from '../api/subsonicStarRating';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -12,7 +14,6 @@ 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 } from '../api/subsonic';
import CachedImage from './CachedImage';
import WaveformSeek from './WaveformSeek';
import Equalizer from './Equalizer';
+2 -1
View File
@@ -1,3 +1,4 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getAlbum } from '../api/subsonicLibrary';
import type { SubsonicPlaylist } from '../api/subsonicTypes';
import { registerQueueListScrollTopReader, consumePendingQueueListScrollTop } from '../store/queueUndo';
@@ -11,7 +12,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, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist } from '../api/subsonic';
import { getPlaylists, getPlaylist, updatePlaylist, deletePlaylist } from '../api/subsonic';
import { usePlaylistStore } from '../store/playlistStore';
import { useCachedUrl } from './CachedImage';
import { useTranslation } from 'react-i18next';
+1 -1
View File
@@ -1,10 +1,10 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
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 { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import CachedImage from './CachedImage';
import { enqueueAndPlay } from '../utils/playSong';
+1 -1
View File
@@ -1,10 +1,10 @@
import { searchSongsPaged } from '../api/subsonicSearch';
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 { searchSongsPaged } from '../api/subsonic';
import { ndListSongs } from '../api/navidromeBrowse';
import SongRow, { SongListHeader } from './SongRow';
+1 -1
View File
@@ -1,9 +1,9 @@
import { setRating, star, unstar } from '../api/subsonicStarRating';
import { getSong } from '../api/subsonicLibrary';
import { songToTrack } from '../utils/songToTrack';
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';
import i18n from '../i18n';
import { setRating, star, unstar } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import { useLyricsStore } from '../store/lyricsStore';
+1 -1
View File
@@ -1,3 +1,4 @@
import { getLyricsBySongId } from '../api/subsonicLyrics';
import type { SubsonicStructuredLyrics } from '../api/subsonicTypes';
import type { Track } from '../store/playerStoreTypes';
import { useEffect, useState } from 'react';
@@ -5,7 +6,6 @@ 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 } 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 { buildStreamUrl } from './api/subsonicStreamUrl';
import type { Track } from './store/playerStoreTypes';
import { invoke } from '@tauri-apps/api/core';
import { buildStreamUrl } from './api/subsonic';
import { useAuthStore } from './store/authStore';
import { HOT_CACHE_PROTECT_AFTER_CURRENT, useHotCacheStore, type HotCacheEntry } from './store/hotCacheStore';
import { useOfflineStore } from './store/offlineStore';
+2 -1
View File
@@ -1,9 +1,10 @@
import { getGenres, getAlbumsByGenre } from '../api/subsonicGenres';
import { search, searchSongsPaged } from '../api/subsonicSearch';
import { getAlbumList, getRandomSongs } from '../api/subsonicLibrary';
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 } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import AlbumRow from '../components/AlbumRow';
import ArtistRow from '../components/ArtistRow';
+2 -1
View File
@@ -1,3 +1,5 @@
import { buildCoverArtUrl, coverArtCacheKey, buildDownloadUrl } from '../api/subsonicStreamUrl';
import { setRating, star, unstar } from '../api/subsonicStarRating';
import { getArtist, getArtistInfo } from '../api/subsonicArtists';
import { getAlbum } from '../api/subsonicLibrary';
import type { SubsonicSong, SubsonicAlbum } from '../api/subsonicTypes';
@@ -6,7 +8,6 @@ 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 { 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,5 @@
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
import { getAlbumsByGenre } from '../api/subsonicGenres';
import { getAlbumList, getAlbum } from '../api/subsonicLibrary';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
@@ -7,7 +9,6 @@ import GenreFilterBar from '../components/GenreFilterBar';
import YearFilterButton from '../components/YearFilterButton';
import StarFilterButton from '../components/StarFilterButton';
import SortDropdown from '../components/SortDropdown';
import { getAlbumsByGenre, buildDownloadUrl } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
+4 -1
View File
@@ -1,10 +1,13 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { setRating, star, unstar } from '../api/subsonicStarRating';
import { search } from '../api/subsonicSearch';
import { getAlbum } from '../api/subsonicLibrary';
import { getArtist, getArtistInfo, getTopSongs, getSimilarSongs2 } from '../api/subsonicArtists';
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 { search, setRating, buildCoverArtUrl, coverArtCacheKey, star, unstar, uploadArtistImage } from '../api/subsonic';
import { uploadArtistImage } from '../api/subsonic';
import AlbumCard from '../components/AlbumCard';
import CachedImage from '../components/CachedImage';
import CoverLightbox from '../components/CoverLightbox';
+1 -1
View File
@@ -1,8 +1,8 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getArtists } from '../api/subsonicArtists';
import type { SubsonicArtist } from '../api/subsonicTypes';
import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { 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 -1
View File
@@ -1,8 +1,9 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { star, unstar } from '../api/subsonicStarRating';
import { getArtist, getArtistInfo } from '../api/subsonicArtists';
import type { SubsonicArtist, SubsonicAlbum, SubsonicArtistInfo } from '../api/subsonicTypes';
import { useEffect, useState, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { star, unstar, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { ndListAlbumsByArtistRole } from '../api/navidromeBrowse';
import AlbumCard from '../components/AlbumCard';
import CachedImage from '../components/CachedImage';
+3 -1
View File
@@ -1,3 +1,4 @@
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
import { getArtists, getArtist } from '../api/subsonicArtists';
import { getAlbumList, getAlbum } from '../api/subsonicLibrary';
import type { SubsonicSong, SubsonicAlbum, SubsonicPlaylist, SubsonicArtist } from '../api/subsonicTypes';
@@ -14,7 +15,8 @@ import CustomSelect from '../components/CustomSelect';
import { useTranslation } from 'react-i18next';
import { useDeviceSyncStore, DeviceSyncSource } from '../store/deviceSyncStore';
import { useDeviceSyncJobStore } from '../store/deviceSyncJobStore';
import { getPlaylists, getPlaylist, buildDownloadUrl, search as searchSubsonic } from '../api/subsonic';
import { getPlaylists, getPlaylist } from '../api/subsonic';
import { search as searchSubsonic } from '../api/subsonicSearch';
import { showToast } from '../utils/toast';
import { IS_WINDOWS } from '../utils/platform';
+3 -2
View File
@@ -1,3 +1,5 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getStarred, setRating, unstar } from '../api/subsonicStarRating';
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong, InternetRadioStation } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -5,14 +7,13 @@ 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, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { getInternetRadioStations } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import StarRating from '../components/StarRating';
import { Cast, ChevronDown, ChevronLeft, ChevronRight, Check, Heart, ListPlus, Play, Square, Star, Users, X, SlidersHorizontal, ArrowUp, ArrowDown, RotateCcw, AudioLines } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { unstar } from '../api/subsonic';
import { useDragDrop } from '../contexts/DragDropContext';
import { useAuthStore } from '../store/authStore';
import { useSelectionStore } from '../store/selectionStore';
+1 -1
View File
@@ -1,9 +1,9 @@
import { getAlbumsByGenre } from '../api/subsonicGenres';
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 } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import AlbumCard from '../components/AlbumCard';
+1 -1
View File
@@ -1,9 +1,9 @@
import { getGenres } from '../api/subsonicGenres';
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 } from '../api/subsonic';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
const CTP_COLORS = [
+2 -1
View File
@@ -1,9 +1,10 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
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, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { getInternetRadioStations, createInternetRadioStation, updateInternetRadioStation, deleteInternetRadioStation, uploadRadioCoverArt, deleteRadioCoverArt, uploadRadioCoverArtBytes, searchRadioBrowser, getTopRadioStations, fetchUrlBytes } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import CachedImage from '../components/CachedImage';
import { invalidateCoverArt } from '../utils/imageCache';
+1 -1
View File
@@ -1,9 +1,9 @@
import { search } from '../api/subsonicSearch';
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 } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
+1 -1
View File
@@ -1,10 +1,10 @@
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
import { getAlbum } from '../api/subsonicLibrary';
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 { buildDownloadUrl } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
+1 -1
View File
@@ -1,10 +1,10 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getAlbumList, getAlbum } from '../api/subsonicLibrary';
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 { 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,10 +1,11 @@
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
import { getAlbumsByGenre } from '../api/subsonicGenres';
import { getAlbumList, getAlbum } from '../api/subsonicLibrary';
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 { getAlbumsByGenre, buildDownloadUrl } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
+2 -1
View File
@@ -1,3 +1,5 @@
import { star, unstar } from '../api/subsonicStarRating';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getArtist, getArtistInfo, getTopSongs } from '../api/subsonicArtists';
import { getSong, getAlbum } from '../api/subsonicLibrary';
import type { SubsonicSong, SubsonicArtistInfo, SubsonicAlbum } from '../api/subsonicTypes';
@@ -9,7 +11,6 @@ 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, star, unstar } from '../api/subsonic';
import { songToTrack } from '../utils/songToTrack';
import {
lastfmIsConfigured,
+1 -1
View File
@@ -1,10 +1,10 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import React, { useState } from 'react';
import { Play, HardDriveDownload, Trash2, ListPlus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOfflineStore } from '../store/offlineStore';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import CachedImage from '../components/CachedImage';
type FilterType = 'all' | 'album' | 'playlist' | 'artist';
+4 -2
View File
@@ -1,3 +1,6 @@
import { buildDownloadUrl, coverArtCacheKey, buildCoverArtUrl } from '../api/subsonicStreamUrl';
import { setRating, star, unstar } from '../api/subsonicStarRating';
import { search } from '../api/subsonicSearch';
import { getRandomSongs, filterSongsToActiveLibrary } from '../api/subsonicLibrary';
import type { SubsonicPlaylist, SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
@@ -7,7 +10,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, buildDownloadUrl } from '../api/subsonic';
import { getPlaylist, updatePlaylist, updatePlaylistMeta, uploadPlaylistCoverArt } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { usePlaylistStore } from '../store/playlistStore';
@@ -25,7 +28,6 @@ import { readTextFile } from '@tauri-apps/plugin-fs';
import { useZipDownloadStore } from '../store/zipDownloadStore';
import { useDragDrop } from '../contexts/DragDropContext';
import CachedImage, { useCachedUrl } from '../components/CachedImage';
import { coverArtCacheKey, buildCoverArtUrl } from '../api/subsonic';
import { useTranslation } from 'react-i18next';
import { showToast } from '../utils/toast';
import { formatHumanHoursMinutes } from '../utils/formatHumanDuration';
+3 -1
View File
@@ -1,10 +1,12 @@
import { getGenres } from '../api/subsonicGenres';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { filterSongsToActiveLibrary } from '../api/subsonicLibrary';
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, getPlaylist, buildCoverArtUrl, coverArtCacheKey, updatePlaylist, getGenres } from '../api/subsonic';
import { deletePlaylist, getPlaylist, updatePlaylist } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { usePlaylistStore } from '../store/playlistStore';
import { useAuthStore } from '../store/authStore';
+2 -1
View File
@@ -1,8 +1,9 @@
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
import { getAlbumsByGenre } from '../api/subsonicGenres';
import { getAlbumList, getAlbum } from '../api/subsonicLibrary';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { RefreshCw, CheckSquare2, Download, HardDriveDownload } from 'lucide-react';
import { getAlbumsByGenre, buildDownloadUrl } from '../api/subsonic';
import AlbumCard from '../components/AlbumCard';
import GenreFilterBar from '../components/GenreFilterBar';
import { useTranslation } from 'react-i18next';
+2 -1
View File
@@ -1,8 +1,9 @@
import { star, unstar } from '../api/subsonicStarRating';
import { getGenres } from '../api/subsonicGenres';
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, star, unstar } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import { useAuthStore } from '../store/authStore';
+1 -1
View File
@@ -1,7 +1,7 @@
import { search, searchSongsPaged } from '../api/subsonicSearch';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Search } from 'lucide-react';
import { search, searchSongsPaged } from '../api/subsonic';
import type { SearchResults as ISearchResults } from '../api/subsonicTypes';
import AlbumRow from '../components/AlbumRow';
import ArtistRow from '../components/ArtistRow';
+1 -1
View File
@@ -1,3 +1,4 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getRandomSongs } from '../api/subsonicLibrary';
import type { SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/songToTrack';
@@ -5,7 +6,6 @@ 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 { 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,5 +1,5 @@
import { reportNowPlaying } from '../api/subsonicScrobble';
import { invoke } from '@tauri-apps/api/core';
import { reportNowPlaying } from '../api/subsonic';
import { getPlaybackSourceKind } from '../utils/resolvePlaybackUrl';
import { useAuthStore } from './authStore';
import {
+1 -1
View File
@@ -1,7 +1,7 @@
import { reportNowPlaying, scrobbleSong } from '../api/subsonicScrobble';
import type { Track } from './playerStoreTypes';
import { invoke } from '@tauri-apps/api/core';
import { lastfmGetTrackLoved, lastfmScrobble, lastfmUpdateNowPlaying } from '../api/lastfm';
import { reportNowPlaying, scrobbleSong } from '../api/subsonic';
import { setDeferHotCachePrefetch } from '../utils/hotCacheGate';
import { getPerfProbeFlags } from '../utils/perfFlags';
import { bumpPerfCounter } from '../utils/perfTelemetry';
+2 -4
View File
@@ -1,9 +1,7 @@
import { getAlbumInfo2 } from '../api/subsonicAlbumInfo';
import { buildCoverArtUrl } from '../api/subsonicStreamUrl';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import {
buildCoverArtUrl,
getAlbumInfo2,
} from '../api/subsonic';
import { streamUrlTrackId } from '../utils/resolvePlaybackUrl';
import { effectiveLoudnessPreAnalysisAttenuationDb } from '../utils/loudnessPreAnalysisSlider';
import { normalizationAlmostEqual } from '../utils/normalizationCompare';
+1 -1
View File
@@ -38,7 +38,7 @@ const hoisted = vi.hoisted(() => {
});
vi.mock('@tauri-apps/api/core', () => ({ invoke: hoisted.invokeMock }));
vi.mock('../api/subsonic', () => ({ buildStreamUrl: hoisted.buildStreamUrlMock }));
vi.mock('../api/subsonicStreamUrl', () => ({ buildStreamUrl: hoisted.buildStreamUrlMock }));
vi.mock('../utils/redactSubsonicUrl', () => ({ redactSubsonicUrlForLog: hoisted.redactMock }));
vi.mock('./authStore', () => ({ useAuthStore: { getState: () => hoisted.auth } }));
vi.mock('./playerStore', () => ({
+1 -1
View File
@@ -1,5 +1,5 @@
import { buildStreamUrl } from '../api/subsonicStreamUrl';
import { invoke } from '@tauri-apps/api/core';
import { buildStreamUrl } from '../api/subsonic';
import { redactSubsonicUrlForLog } from '../utils/redactSubsonicUrl';
import { useAuthStore } from './authStore';
import { usePlayerStore } from './playerStore';
+1 -1
View File
@@ -31,7 +31,7 @@ const hoisted = vi.hoisted(() => {
});
vi.mock('@tauri-apps/api/core', () => ({ invoke: hoisted.invokeMock }));
vi.mock('../api/subsonic', () => ({ buildStreamUrl: hoisted.buildStreamUrlMock }));
vi.mock('../api/subsonicStreamUrl', () => ({ buildStreamUrl: hoisted.buildStreamUrlMock }));
vi.mock('./authStore', () => ({ useAuthStore: { getState: () => hoisted.authState } }));
vi.mock('./playerStore', () => ({
usePlayerStore: {
+1 -1
View File
@@ -1,5 +1,5 @@
import { buildStreamUrl } from '../api/subsonicStreamUrl';
import { invoke } from '@tauri-apps/api/core';
import { buildStreamUrl } from '../api/subsonic';
import { useAuthStore } from './authStore';
import { usePlayerStore } from './playerStore';
import { bumpWaveformRefreshGen } from './waveformRefreshGen';
+1 -1
View File
@@ -1,10 +1,10 @@
import { buildStreamUrl } from '../api/subsonicStreamUrl';
import { getAlbum } from '../api/subsonicLibrary';
import { getArtist } from '../api/subsonicArtists';
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 } from '../api/subsonic';
import { useAuthStore } from './authStore';
import { showToast } from '../utils/toast';
import { useOfflineJobStore, cancelledDownloads } from './offlineJobStore';
+1 -1
View File
@@ -1,6 +1,6 @@
import { reportNowPlaying } from '../api/subsonicScrobble';
import { invoke } from '@tauri-apps/api/core';
import { lastfmGetTrackLoved, lastfmUpdateNowPlaying } from '../api/lastfm';
import { reportNowPlaying } from '../api/subsonic';
import { setDeferHotCachePrefetch } from '../utils/hotCacheGate';
import { orbitBulkGuard } from '../utils/orbitBulkGuard';
import { sameQueueTrackId } from '../utils/queueIdentity';
+1 -1
View File
@@ -1,7 +1,7 @@
import { buildStreamUrl } from '../api/subsonicStreamUrl';
import type { TrackPreviewLocation } from './authStoreTypes';
import { create } from 'zustand';
import { invoke } from '@tauri-apps/api/core';
import { buildStreamUrl } from '../api/subsonic';
import { usePlayerStore } from './playerStore';
import { useAuthStore } from './authStore';
import { useOrbitStore } from './orbitStore';
+1 -1
View File
@@ -13,7 +13,7 @@ const { invokeMock, setEntryMock, buildStreamUrlMock } = vi.hoisted(() => ({
}));
vi.mock('@tauri-apps/api/core', () => ({ invoke: invokeMock }));
vi.mock('../api/subsonic', () => ({ buildStreamUrl: buildStreamUrlMock }));
vi.mock('../api/subsonicStreamUrl', () => ({ buildStreamUrl: buildStreamUrlMock }));
vi.mock('./hotCacheStore', () => ({
useHotCacheStore: { getState: () => ({ setEntry: setEntryMock }) },
}));
+1 -1
View File
@@ -1,6 +1,6 @@
import { buildStreamUrl } from '../api/subsonicStreamUrl';
import type { Track } from './playerStoreTypes';
import { invoke } from '@tauri-apps/api/core';
import { buildStreamUrl } from '../api/subsonic';
import { useHotCacheStore } from './hotCacheStore';
/**
* Promote a track whose stream cache is full to the on-disk hot cache.
+1 -1
View File
@@ -23,7 +23,7 @@ const { setRatingMock, recordSkipStarMock, playerSetStateMock, playerStateGet }
};
});
vi.mock('../api/subsonic', () => ({ setRating: setRatingMock }));
vi.mock('../api/subsonicStarRating', () => ({ setRating: setRatingMock }));
vi.mock('./authStore', () => ({
useAuthStore: { getState: () => ({ recordSkipStarManualAdvance: recordSkipStarMock }) },
}));
+1 -1
View File
@@ -1,5 +1,5 @@
import { setRating } from '../api/subsonicStarRating';
import type { Track } from './playerStoreTypes';
import { setRating } from '../api/subsonic';
import { useAuthStore } from './authStore';
import { usePlayerStore } from './playerStore';
/**
+2 -2
View File
@@ -7,8 +7,7 @@
*
* // In the test:
* vi.mock('@/api/subsonic');
* import { buildStreamUrl } from '@/api/subsonic';
* import { sampleAlbumWithSongs, mockStreamUrl } from '@/test/mocks/subsonic';
* * import { sampleAlbumWithSongs, mockStreamUrl } from '@/test/mocks/subsonic';
*
* beforeEach(() => {
* vi.mocked(getAlbum).mockResolvedValue(sampleAlbumWithSongs);
@@ -18,6 +17,7 @@
* Realistic shape matters more than perfect coverage these fixtures
* mirror what Navidrome actually returns for common queries.
*/
import { buildStreamUrl } from '@/api/subsonicStreamUrl';
import { getAlbum } from '@/api/subsonicLibrary';
import type { SubsonicSong, SubsonicAlbum, SubsonicPlaylist } from '@/api/subsonicTypes';
import { makeSubsonicSong } from '@/test/helpers/factories';
+1 -1
View File
@@ -1,7 +1,7 @@
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { getCachedBlob } from './imageCache';
import PsysonicLogo from '../components/PsysonicLogo';
+1 -1
View File
@@ -1,8 +1,8 @@
import { buildCoverArtUrl } from '../api/subsonicStreamUrl';
import { getAlbumList } from '../api/subsonicLibrary';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { writeFile } from '@tauri-apps/plugin-fs';
import { downloadDir, join } from '@tauri-apps/api/path';
import { buildCoverArtUrl } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
// Catppuccin Macchiato palette
const M = {
+1 -1
View File
@@ -1,4 +1,4 @@
import { buildStreamUrl } from '../api/subsonic';
import { buildStreamUrl } from '../api/subsonicStreamUrl';
import { useOfflineStore } from '../store/offlineStore';
import { useHotCacheStore } from '../store/hotCacheStore';