Files
Psychotoxical-psysonic/src/api/subsonicGenres.ts
T
cucadmuh ddf10ee01d feat(genres): local index genre browse with Subsonic fallback (#937)
* feat(genres): genre detail browse via local index with aligned counts

Move genre detail albums/play/shuffle onto the local library index with
Albums-style in-page scroll, session restore, and genre-scoped stash. Unify
genre album totals between the cloud and detail pages via
library_get_genre_album_counts, and fix grouped browse totals to count
distinct albums rather than matching tracks.

* perf(genres): local genre browse with scoped counts cache

Add dedicated Rust genre album pagination and indexes, slice-mode grid
loading, library-filter-aware counts, and a long-lived in-memory catalog
cache invalidated on sync so genre pages avoid repeated full-library SQL.

* fix(genres): restore scroll after album back; play hold-to-shuffle

Pin restore display count in refs so clearing the return stash no longer
reloads the genre grid mid-restore. Load the first SQL page only (60 rows),
use long-press on Play for shuffle, and add genre play tooltips.

* fix(genres): fall back to Subsonic byGenre when local index unavailable

Genre detail album grid now matches All Albums: try library_list_albums_by_genre
first, then getAlbumsByGenre when the index is off, not ready, or errors.

* docs: add CHANGELOG and credits for PR #937
2026-06-01 04:20:18 +03:00

55 lines
1.9 KiB
TypeScript

import { api, libraryFilterParams } from './subsonicClient';
import type { SubsonicAlbum, SubsonicGenre, SubsonicSong } from './subsonicTypes';
export async function getGenres(): Promise<SubsonicGenre[]> {
const data = await api<{ genres: { genre: SubsonicGenre | SubsonicGenre[] } }>('getGenres.view', {
...libraryFilterParams(),
});
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];
}
/** Single page of songs for a genre (Subsonic `getSongsByGenre`, supported by Navidrome). */
export async function getSongsByGenre(genre: string, count = 500, offset = 0): Promise<SubsonicSong[]> {
const data = await api<{ songsByGenre: { song: SubsonicSong | SubsonicSong[] } }>('getSongsByGenre.view', {
genre,
count,
offset,
_t: Date.now(),
...libraryFilterParams(),
});
const raw = data.songsByGenre?.song;
if (!raw) return [];
return Array.isArray(raw) ? raw : [raw];
}
/**
* Every song in a genre, paginated until exhausted. Capped to keep the queue and the
* burst of server requests bounded for very large genres (a handful of sequential pages).
*/
export async function fetchAllSongsByGenre(genre: string, cap = 5000): Promise<SubsonicSong[]> {
const PAGE = 500;
const songs: SubsonicSong[] = [];
for (let offset = 0; songs.length < cap; offset += PAGE) {
const page = await getSongsByGenre(genre, PAGE, offset);
songs.push(...page);
if (page.length < PAGE) break;
}
return songs.length > cap ? songs.slice(0, cap) : songs;
}