diff --git a/src/api/subsonic.async.test.ts b/src/api/subsonic.async.test.ts index dd87df04..3a7bb35b 100644 --- a/src/api/subsonic.async.test.ts +++ b/src/api/subsonic.async.test.ts @@ -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'; diff --git a/src/api/subsonic.contract.test.ts b/src/api/subsonic.contract.test.ts index f8b9b4c1..4d10df54 100644 --- a/src/api/subsonic.contract.test.ts +++ b/src/api/subsonic.contract.test.ts @@ -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'; diff --git a/src/api/subsonic.ts b/src/api/subsonic.ts index be8bb97e..860a2079 100644 --- a/src/api/subsonic.ts +++ b/src/api/subsonic.ts @@ -330,195 +330,23 @@ export async function fetchStatisticsFormatSample(): Promise { - 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 { - 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 { - 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 { - const params: Record = {}; - 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 { - const params: Record = {}; - 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 { - 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 { - 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 { - 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 { - 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 { - try { - await api('scrobble.view', { id, time, submission: true }); - } catch { - // best effort - } -} -export async function reportNowPlaying(id: string): Promise { - 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 { - 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 { @@ -627,15 +455,6 @@ export async function savePlayQueue(songIds: string[], current?: string, positio } // ─── Now Playing ────────────────────────────────────────────── -export async function getNowPlaying(): Promise { - 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 { - 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; - } -} diff --git a/src/api/subsonicAlbumInfo.ts b/src/api/subsonicAlbumInfo.ts new file mode 100644 index 00000000..7686e2c6 --- /dev/null +++ b/src/api/subsonicAlbumInfo.ts @@ -0,0 +1,11 @@ +import { api } from './subsonicClient'; +import type { AlbumInfo } from './subsonicTypes'; + +export async function getAlbumInfo2(albumId: string): Promise { + try { + const data = await api<{ albumInfo: AlbumInfo }>('getAlbumInfo2.view', { id: albumId }); + return data.albumInfo ?? null; + } catch { + return null; + } +} diff --git a/src/api/subsonicGenres.ts b/src/api/subsonicGenres.ts new file mode 100644 index 00000000..52c49dbf --- /dev/null +++ b/src/api/subsonicGenres.ts @@ -0,0 +1,23 @@ +import { api, libraryFilterParams } from './subsonicClient'; +import type { SubsonicAlbum, SubsonicGenre } from './subsonicTypes'; + +export async function getGenres(): Promise { + 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 { + 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]; +} diff --git a/src/api/subsonicLyrics.ts b/src/api/subsonicLyrics.ts new file mode 100644 index 00000000..7a3f2b51 --- /dev/null +++ b/src/api/subsonicLyrics.ts @@ -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 { + 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; + } +} diff --git a/src/api/subsonicScrobble.ts b/src/api/subsonicScrobble.ts new file mode 100644 index 00000000..a05ed49b --- /dev/null +++ b/src/api/subsonicScrobble.ts @@ -0,0 +1,29 @@ +import { api } from './subsonicClient'; +import type { SubsonicNowPlaying } from './subsonicTypes'; + +export async function scrobbleSong(id: string, time: number): Promise { + try { + await api('scrobble.view', { id, time, submission: true }); + } catch { + // best effort + } +} + +export async function reportNowPlaying(id: string): Promise { + try { + await api('scrobble.view', { id, submission: false }); + } catch { + // best effort + } +} + +export async function getNowPlaying(): Promise { + 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 []; + } +} diff --git a/src/api/subsonicSearch.ts b/src/api/subsonicSearch.ts new file mode 100644 index 00000000..e369e3fe --- /dev/null +++ b/src/api/subsonicSearch.ts @@ -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 { + 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 { + const data = await api<{ searchResult3: { song?: SubsonicSong[] } }>('search3.view', { + query, + artistCount: 0, + albumCount: 0, + songCount, + songOffset, + ...libraryFilterParams(), + }); + return data.searchResult3?.song ?? []; +} diff --git a/src/api/subsonicStarRating.ts b/src/api/subsonicStarRating.ts new file mode 100644 index 00000000..0645ee73 --- /dev/null +++ b/src/api/subsonicStarRating.ts @@ -0,0 +1,64 @@ +import { api, libraryFilterParams } from './subsonicClient'; +import type { + EntityRatingSupportLevel, + StarredResults, + SubsonicAlbum, + SubsonicArtist, + SubsonicSong, +} from './subsonicTypes'; + +export async function getStarred(): Promise { + 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 { + const params: Record = {}; + 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 { + const params: Record = {}; + 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 { + 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 { + 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'; + } +} diff --git a/src/api/subsonicStreamUrl.ts b/src/api/subsonicStreamUrl.ts new file mode 100644 index 00000000..67410a47 --- /dev/null +++ b/src/api/subsonicStreamUrl.ts @@ -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()}`; +} diff --git a/src/app/AppShell.tsx b/src/app/AppShell.tsx index 5e6dad03..cc758e52 100644 --- a/src/app/AppShell.tsx +++ b/src/app/AppShell.tsx @@ -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'; diff --git a/src/app/TauriEventBridge.tsx b/src/app/TauriEventBridge.tsx index d89105f0..401f43de 100644 --- a/src/app/TauriEventBridge.tsx +++ b/src/app/TauriEventBridge.tsx @@ -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'; diff --git a/src/components/AlbumCard.tsx b/src/components/AlbumCard.tsx index df5ce1f3..e75481f0 100644 --- a/src/components/AlbumCard.tsx +++ b/src/components/AlbumCard.tsx @@ -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'; diff --git a/src/components/AlbumHeader.tsx b/src/components/AlbumHeader.tsx index 8bf54038..9e76b407 100644 --- a/src/components/AlbumHeader.tsx +++ b/src/components/AlbumHeader.tsx @@ -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'; diff --git a/src/components/ArtistCardLocal.tsx b/src/components/ArtistCardLocal.tsx index ce0afb6d..61d62e52 100644 --- a/src/components/ArtistCardLocal.tsx +++ b/src/components/ArtistCardLocal.tsx @@ -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'; diff --git a/src/components/BecauseYouLikeRail.tsx b/src/components/BecauseYouLikeRail.tsx index e86e3e62..93b2cd2f 100644 --- a/src/components/BecauseYouLikeRail.tsx +++ b/src/components/BecauseYouLikeRail.tsx @@ -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'; diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 0b03f2c1..92123bda 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -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'; diff --git a/src/components/FullscreenPlayer.tsx b/src/components/FullscreenPlayer.tsx index f22f67bc..88a35129 100644 --- a/src/components/FullscreenPlayer.tsx +++ b/src/components/FullscreenPlayer.tsx @@ -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'; diff --git a/src/components/GenreFilterBar.tsx b/src/components/GenreFilterBar.tsx index 2a942726..41a9b5d6 100644 --- a/src/components/GenreFilterBar.tsx +++ b/src/components/GenreFilterBar.tsx @@ -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[]; diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 82fb6092..f69cef4f 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -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'; diff --git a/src/components/HostApprovalQueue.tsx b/src/components/HostApprovalQueue.tsx index f83e5dfb..bd9562bf 100644 --- a/src/components/HostApprovalQueue.tsx +++ b/src/components/HostApprovalQueue.tsx @@ -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'; diff --git a/src/components/LiveSearch.tsx b/src/components/LiveSearch.tsx index 3ea5f157..117df282 100644 --- a/src/components/LiveSearch.tsx +++ b/src/components/LiveSearch.tsx @@ -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'; diff --git a/src/components/MiniContextMenu.tsx b/src/components/MiniContextMenu.tsx index a1b3db7f..07c8efa6 100644 --- a/src/components/MiniContextMenu.tsx +++ b/src/components/MiniContextMenu.tsx @@ -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 { diff --git a/src/components/MiniPlayer.tsx b/src/components/MiniPlayer.tsx index 3882ff16..fba12b96 100644 --- a/src/components/MiniPlayer.tsx +++ b/src/components/MiniPlayer.tsx @@ -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'; diff --git a/src/components/MobilePlayerView.tsx b/src/components/MobilePlayerView.tsx index 15582b7a..12fc836f 100644 --- a/src/components/MobilePlayerView.tsx +++ b/src/components/MobilePlayerView.tsx @@ -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'; diff --git a/src/components/MobileSearchOverlay.tsx b/src/components/MobileSearchOverlay.tsx index 29eba877..9f0ce15f 100644 --- a/src/components/MobileSearchOverlay.tsx +++ b/src/components/MobileSearchOverlay.tsx @@ -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'; diff --git a/src/components/NowPlayingDropdown.tsx b/src/components/NowPlayingDropdown.tsx index 25b1607e..26ac54ec 100644 --- a/src/components/NowPlayingDropdown.tsx +++ b/src/components/NowPlayingDropdown.tsx @@ -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'; diff --git a/src/components/OrbitGuestQueue.tsx b/src/components/OrbitGuestQueue.tsx index 7d2c0fcc..1300ab0e 100644 --- a/src/components/OrbitGuestQueue.tsx +++ b/src/components/OrbitGuestQueue.tsx @@ -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'; diff --git a/src/components/PlayerBar.tsx b/src/components/PlayerBar.tsx index 1531463a..8b4ae2cb 100644 --- a/src/components/PlayerBar.tsx +++ b/src/components/PlayerBar.tsx @@ -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'; diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index c5498ab5..e6cb5645 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -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'; diff --git a/src/components/SongCard.tsx b/src/components/SongCard.tsx index 2890f759..91e10a0b 100644 --- a/src/components/SongCard.tsx +++ b/src/components/SongCard.tsx @@ -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'; diff --git a/src/components/VirtualSongList.tsx b/src/components/VirtualSongList.tsx index eabd3926..f4c7b354 100644 --- a/src/components/VirtualSongList.tsx +++ b/src/components/VirtualSongList.tsx @@ -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'; diff --git a/src/config/shortcutActions.ts b/src/config/shortcutActions.ts index 06ef92e1..1e297b31 100644 --- a/src/config/shortcutActions.ts +++ b/src/config/shortcutActions.ts @@ -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'; diff --git a/src/hooks/useLyrics.ts b/src/hooks/useLyrics.ts index b92c0ab0..7923c0ea 100644 --- a/src/hooks/useLyrics.ts +++ b/src/hooks/useLyrics.ts @@ -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'; diff --git a/src/hotCachePrefetch.ts b/src/hotCachePrefetch.ts index ddc3f2ae..24952e61 100644 --- a/src/hotCachePrefetch.ts +++ b/src/hotCachePrefetch.ts @@ -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'; diff --git a/src/pages/AdvancedSearch.tsx b/src/pages/AdvancedSearch.tsx index a8ff2370..3cd9314c 100644 --- a/src/pages/AdvancedSearch.tsx +++ b/src/pages/AdvancedSearch.tsx @@ -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'; diff --git a/src/pages/AlbumDetail.tsx b/src/pages/AlbumDetail.tsx index 14150fa3..74d744b1 100644 --- a/src/pages/AlbumDetail.tsx +++ b/src/pages/AlbumDetail.tsx @@ -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'; diff --git a/src/pages/Albums.tsx b/src/pages/Albums.tsx index b7505216..224283a6 100644 --- a/src/pages/Albums.tsx +++ b/src/pages/Albums.tsx @@ -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'; diff --git a/src/pages/ArtistDetail.tsx b/src/pages/ArtistDetail.tsx index 60226d3f..928a0450 100644 --- a/src/pages/ArtistDetail.tsx +++ b/src/pages/ArtistDetail.tsx @@ -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'; diff --git a/src/pages/Artists.tsx b/src/pages/Artists.tsx index 3a6dbeea..4d899645 100644 --- a/src/pages/Artists.tsx +++ b/src/pages/Artists.tsx @@ -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'; diff --git a/src/pages/ComposerDetail.tsx b/src/pages/ComposerDetail.tsx index f37207cd..8d2c1d8e 100644 --- a/src/pages/ComposerDetail.tsx +++ b/src/pages/ComposerDetail.tsx @@ -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'; diff --git a/src/pages/DeviceSync.tsx b/src/pages/DeviceSync.tsx index ecb9159f..595dbba6 100644 --- a/src/pages/DeviceSync.tsx +++ b/src/pages/DeviceSync.tsx @@ -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'; diff --git a/src/pages/Favorites.tsx b/src/pages/Favorites.tsx index 70ea9775..018bc95e 100644 --- a/src/pages/Favorites.tsx +++ b/src/pages/Favorites.tsx @@ -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'; diff --git a/src/pages/GenreDetail.tsx b/src/pages/GenreDetail.tsx index 05b87d1a..3e2994c2 100644 --- a/src/pages/GenreDetail.tsx +++ b/src/pages/GenreDetail.tsx @@ -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'; diff --git a/src/pages/Genres.tsx b/src/pages/Genres.tsx index 25fdd1f2..f9c58657 100644 --- a/src/pages/Genres.tsx +++ b/src/pages/Genres.tsx @@ -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 = [ diff --git a/src/pages/InternetRadio.tsx b/src/pages/InternetRadio.tsx index cff4bb62..05431100 100644 --- a/src/pages/InternetRadio.tsx +++ b/src/pages/InternetRadio.tsx @@ -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'; diff --git a/src/pages/LabelAlbums.tsx b/src/pages/LabelAlbums.tsx index fd256b99..6916b6b5 100644 --- a/src/pages/LabelAlbums.tsx +++ b/src/pages/LabelAlbums.tsx @@ -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'; diff --git a/src/pages/LosslessAlbums.tsx b/src/pages/LosslessAlbums.tsx index f17b26d6..cbdf4fa6 100644 --- a/src/pages/LosslessAlbums.tsx +++ b/src/pages/LosslessAlbums.tsx @@ -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'; diff --git a/src/pages/MostPlayed.tsx b/src/pages/MostPlayed.tsx index 675f14e4..ad6cbb92 100644 --- a/src/pages/MostPlayed.tsx +++ b/src/pages/MostPlayed.tsx @@ -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'; diff --git a/src/pages/NewReleases.tsx b/src/pages/NewReleases.tsx index 6aa84fd2..17a37889 100644 --- a/src/pages/NewReleases.tsx +++ b/src/pages/NewReleases.tsx @@ -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'; diff --git a/src/pages/NowPlaying.tsx b/src/pages/NowPlaying.tsx index 68134629..fb0654a4 100644 --- a/src/pages/NowPlaying.tsx +++ b/src/pages/NowPlaying.tsx @@ -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, diff --git a/src/pages/OfflineLibrary.tsx b/src/pages/OfflineLibrary.tsx index c54fc59d..d2e47ecc 100644 --- a/src/pages/OfflineLibrary.tsx +++ b/src/pages/OfflineLibrary.tsx @@ -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'; diff --git a/src/pages/PlaylistDetail.tsx b/src/pages/PlaylistDetail.tsx index bc16fad2..88c5b78b 100644 --- a/src/pages/PlaylistDetail.tsx +++ b/src/pages/PlaylistDetail.tsx @@ -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'; diff --git a/src/pages/Playlists.tsx b/src/pages/Playlists.tsx index 87a916af..d4d130d2 100644 --- a/src/pages/Playlists.tsx +++ b/src/pages/Playlists.tsx @@ -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'; diff --git a/src/pages/RandomAlbums.tsx b/src/pages/RandomAlbums.tsx index 1c77a948..a2ca67fe 100644 --- a/src/pages/RandomAlbums.tsx +++ b/src/pages/RandomAlbums.tsx @@ -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'; diff --git a/src/pages/RandomMix.tsx b/src/pages/RandomMix.tsx index aa9ee696..389cd1a8 100644 --- a/src/pages/RandomMix.tsx +++ b/src/pages/RandomMix.tsx @@ -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'; diff --git a/src/pages/SearchResults.tsx b/src/pages/SearchResults.tsx index bdf6873a..d501f0c5 100644 --- a/src/pages/SearchResults.tsx +++ b/src/pages/SearchResults.tsx @@ -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'; diff --git a/src/pages/Tracks.tsx b/src/pages/Tracks.tsx index 667c0c43..e0687f09 100644 --- a/src/pages/Tracks.tsx +++ b/src/pages/Tracks.tsx @@ -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'; diff --git a/src/store/applyQueueHistorySnapshot.ts b/src/store/applyQueueHistorySnapshot.ts index 08df62b8..f4fdaea9 100644 --- a/src/store/applyQueueHistorySnapshot.ts +++ b/src/store/applyQueueHistorySnapshot.ts @@ -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 { diff --git a/src/store/audioEventHandlers.ts b/src/store/audioEventHandlers.ts index a5f70e2b..0e1ce1e4 100644 --- a/src/store/audioEventHandlers.ts +++ b/src/store/audioEventHandlers.ts @@ -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'; diff --git a/src/store/initAudioListeners.ts b/src/store/initAudioListeners.ts index 05d66713..30ab243e 100644 --- a/src/store/initAudioListeners.ts +++ b/src/store/initAudioListeners.ts @@ -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'; diff --git a/src/store/loudnessRefresh.test.ts b/src/store/loudnessRefresh.test.ts index 0487a2e7..f9815ce9 100644 --- a/src/store/loudnessRefresh.test.ts +++ b/src/store/loudnessRefresh.test.ts @@ -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', () => ({ diff --git a/src/store/loudnessRefresh.ts b/src/store/loudnessRefresh.ts index f87d4a0f..983b08fb 100644 --- a/src/store/loudnessRefresh.ts +++ b/src/store/loudnessRefresh.ts @@ -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'; diff --git a/src/store/loudnessReseed.test.ts b/src/store/loudnessReseed.test.ts index c9370f4a..bdd28843 100644 --- a/src/store/loudnessReseed.test.ts +++ b/src/store/loudnessReseed.test.ts @@ -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: { diff --git a/src/store/loudnessReseed.ts b/src/store/loudnessReseed.ts index d1f85891..a2cfe682 100644 --- a/src/store/loudnessReseed.ts +++ b/src/store/loudnessReseed.ts @@ -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'; diff --git a/src/store/offlineStore.ts b/src/store/offlineStore.ts index 8528812e..9a3d6389 100644 --- a/src/store/offlineStore.ts +++ b/src/store/offlineStore.ts @@ -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'; diff --git a/src/store/playTrackAction.ts b/src/store/playTrackAction.ts index c8a51c9c..e694382c 100644 --- a/src/store/playTrackAction.ts +++ b/src/store/playTrackAction.ts @@ -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'; diff --git a/src/store/previewStore.ts b/src/store/previewStore.ts index 06eb73cb..6c6e7669 100644 --- a/src/store/previewStore.ts +++ b/src/store/previewStore.ts @@ -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'; diff --git a/src/store/promoteStreamCache.test.ts b/src/store/promoteStreamCache.test.ts index 47d9804f..ab043459 100644 --- a/src/store/promoteStreamCache.test.ts +++ b/src/store/promoteStreamCache.test.ts @@ -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 }) }, })); diff --git a/src/store/promoteStreamCache.ts b/src/store/promoteStreamCache.ts index 4714c695..f6d389be 100644 --- a/src/store/promoteStreamCache.ts +++ b/src/store/promoteStreamCache.ts @@ -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. diff --git a/src/store/skipStarRating.test.ts b/src/store/skipStarRating.test.ts index d3e58e8a..be3e38f6 100644 --- a/src/store/skipStarRating.test.ts +++ b/src/store/skipStarRating.test.ts @@ -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 }) }, })); diff --git a/src/store/skipStarRating.ts b/src/store/skipStarRating.ts index 941f01c1..fd18e225 100644 --- a/src/store/skipStarRating.ts +++ b/src/store/skipStarRating.ts @@ -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'; /** diff --git a/src/test/mocks/subsonic.ts b/src/test/mocks/subsonic.ts index 19250418..8108deea 100644 --- a/src/test/mocks/subsonic.ts +++ b/src/test/mocks/subsonic.ts @@ -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'; diff --git a/src/utils/exportAlbumCard.ts b/src/utils/exportAlbumCard.ts index 8083cb7f..f03e6928 100644 --- a/src/utils/exportAlbumCard.ts +++ b/src/utils/exportAlbumCard.ts @@ -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'; diff --git a/src/utils/exportNewAlbums.ts b/src/utils/exportNewAlbums.ts index 8d4a234a..df0bf8ac 100644 --- a/src/utils/exportNewAlbums.ts +++ b/src/utils/exportNewAlbums.ts @@ -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 = { diff --git a/src/utils/resolvePlaybackUrl.ts b/src/utils/resolvePlaybackUrl.ts index bb36dbb5..226ffe9c 100644 --- a/src/utils/resolvePlaybackUrl.ts +++ b/src/utils/resolvePlaybackUrl.ts @@ -1,4 +1,4 @@ -import { buildStreamUrl } from '../api/subsonic'; +import { buildStreamUrl } from '../api/subsonicStreamUrl'; import { useOfflineStore } from '../store/offlineStore'; import { useHotCacheStore } from '../store/hotCacheStore';