mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
109 lines
4.6 KiB
TypeScript
109 lines
4.6 KiB
TypeScript
/**
|
|
* Index-first metadata resolvers for the Now Playing page (issue #1046).
|
|
*
|
|
* The local library index is first-class: when SQLite has the row, Now Playing
|
|
* reads it there; Subsonic/network is fallback only on index miss / index off /
|
|
* not ready. This mirrors the in-tree index-first family (`queueTrackResolver`,
|
|
* `offlineLibraryIndexLoad`, `useQueueTrackEnrichment`) rather than adding a
|
|
* fourth always-network path.
|
|
*
|
|
* Gate split (PR #1049 review): the index arm runs whenever there is a playback
|
|
* server id — including when the server is unreachable, the whole point of
|
|
* index-first for offline-pinned playback. The reachability guard
|
|
* (`shouldAttemptSubsonicForServer`, no trackId) lives only in each resolver's
|
|
* **network fallback arm**, so offline reads still succeed from SQLite.
|
|
*
|
|
* `artistInfo` (bio / similar) has no index source and stays network-only — it
|
|
* is intentionally absent here.
|
|
*/
|
|
import { libraryGetTrack, libraryGetTracksByAlbum } from '@/api/library';
|
|
import { getArtistForServer, getTopSongsForServer } from '@/api/subsonicArtists';
|
|
import { getAlbumForServer, getSongForServer } from '@/api/subsonicLibrary';
|
|
import type { SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes';
|
|
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
|
import { loadAlbumFromLibraryIndex, loadArtistFromLibraryIndex } from '@/features/offline';
|
|
import { trackToSong } from '@/utils/library/advancedSearchLocal';
|
|
import { libraryIsReady } from '@/utils/library/libraryReady';
|
|
|
|
const TOP_SONGS_LIMIT = 5;
|
|
|
|
/** Album card — index `loadAlbumFromLibraryIndex`, else `getAlbumForServer`. */
|
|
export async function resolveNpAlbum(
|
|
serverId: string,
|
|
albumId: string,
|
|
): Promise<{ album: SubsonicAlbum; songs: SubsonicSong[] } | null> {
|
|
if (await libraryIsReady(serverId)) {
|
|
try {
|
|
const hit = await loadAlbumFromLibraryIndex(serverId, albumId);
|
|
if (hit) return hit;
|
|
} catch { /* index error → network fallback */ }
|
|
}
|
|
if (!shouldAttemptSubsonicForServer(serverId)) return null;
|
|
return getAlbumForServer(serverId, albumId);
|
|
}
|
|
|
|
/** Discography — index `loadArtistFromLibraryIndex().albums`, else `getArtistForServer().albums`. */
|
|
export async function resolveNpDiscography(
|
|
serverId: string,
|
|
artistId: string,
|
|
): Promise<SubsonicAlbum[]> {
|
|
if (await libraryIsReady(serverId)) {
|
|
try {
|
|
const hit = await loadArtistFromLibraryIndex(serverId, artistId);
|
|
// Empty albums == miss: the index may not carry this artist's albums yet;
|
|
// let the network arm try before settling on an empty discography.
|
|
if (hit && hit.albums.length > 0) return hit.albums;
|
|
} catch { /* index error → network fallback */ }
|
|
}
|
|
if (!shouldAttemptSubsonicForServer(serverId)) return [];
|
|
const artist = await getArtistForServer(serverId, artistId);
|
|
return artist.albums;
|
|
}
|
|
|
|
/**
|
|
* Most played — derive from the artist's own discography albums (same bucket the
|
|
* discography card uses), sorted by play_count. This is deterministic: it can't
|
|
* pull the wrong artist's tracks the way an FTS-on-name query could. Network
|
|
* `getTopSongsForServer` is the fallback on index miss / off / unreachable.
|
|
*/
|
|
export async function resolveNpTopSongs(
|
|
serverId: string,
|
|
artistId: string | undefined,
|
|
artistName: string,
|
|
): Promise<SubsonicSong[]> {
|
|
if (artistId && await libraryIsReady(serverId)) {
|
|
try {
|
|
const hit = await loadArtistFromLibraryIndex(serverId, artistId);
|
|
if (hit && hit.albums.length > 0) {
|
|
const perAlbum = await Promise.all(
|
|
hit.albums.map(a => libraryGetTracksByAlbum(serverId, a.id).catch(() => [])),
|
|
);
|
|
const songs = perAlbum
|
|
.flat()
|
|
.map(trackToSong)
|
|
.sort((a, b) => (b.playCount ?? 0) - (a.playCount ?? 0))
|
|
.slice(0, TOP_SONGS_LIMIT);
|
|
if (songs.length > 0) return songs;
|
|
}
|
|
} catch { /* index error → network fallback */ }
|
|
}
|
|
if (!shouldAttemptSubsonicForServer(serverId)) return [];
|
|
return getTopSongsForServer(serverId, artistName);
|
|
}
|
|
|
|
/** Song-level meta — index `libraryGetTrack` → `trackToSong`, else `getSongForServer`. */
|
|
export async function resolveNpSongMeta(
|
|
serverId: string,
|
|
songId: string,
|
|
): Promise<SubsonicSong | null> {
|
|
if (await libraryIsReady(serverId)) {
|
|
try {
|
|
const dto = await libraryGetTrack(serverId, songId);
|
|
if (dto) return trackToSong(dto);
|
|
} catch { /* index error → network fallback */ }
|
|
}
|
|
// Network arm keeps its own byte-style guard (`shouldAttemptSubsonicForServer`
|
|
// with the trackId + psysonic-local:// skip) — unchanged from #1042.
|
|
return getSongForServer(serverId, songId);
|
|
}
|