mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
e734a8fc43
* feat(genre): add paginated songs-by-genre API Wraps the Subsonic getSongsByGenre endpoint plus a fetchAllSongsByGenre helper that paginates until exhausted, capped to keep the queue and the burst of requests bounded for very large genres. * refactor(playback): extract shared bulk play/shuffle/enqueue helper A single fetchTracks-driven core (loading flag, empty guard, canonical shuffleArray) so async detail-page play buttons stop growing divergent copies. Artist detail now reuses it, dropping its weaker sort-random shuffle. * feat(genre): play, shuffle and queue buttons on the genre view Header buttons load the genre's songs and start ordered or shuffled playback, or append them to the queue. The slice is bounded to stay within the queue resolver's cache budget so every row resolves instead of rendering as a placeholder. Strings added across all nine locales. * docs(changelog): genre play/shuffle buttons (#926)
75 lines
2.9 KiB
TypeScript
75 lines
2.9 KiB
TypeScript
import type { TFunction } from 'i18next';
|
|
import { getAlbum } from '../../api/subsonicLibrary';
|
|
import { getSimilarSongs2, getTopSongs } from '../../api/subsonicArtists';
|
|
import type { SubsonicAlbum, SubsonicArtist } from '../../api/subsonicTypes';
|
|
import type { Track } from '../../store/playerStoreTypes';
|
|
import { songToTrack } from '../playback/songToTrack';
|
|
import { runBulkPlayAll, runBulkShuffle } from '../playback/runBulkPlay';
|
|
|
|
async function fetchAllTracks(albums: SubsonicAlbum[]): Promise<Track[]> {
|
|
const results = await Promise.all(albums.map(a => getAlbum(a.id)));
|
|
const sorted = [...results].sort((a, b) => (a.album.year ?? 0) - (b.album.year ?? 0));
|
|
return sorted.flatMap(r => [...r.songs].sort((a, b) => (a.track ?? 0) - (b.track ?? 0))).map(songToTrack);
|
|
}
|
|
|
|
export interface RunArtistDetailPlayDeps {
|
|
albums: SubsonicAlbum[];
|
|
setPlayAllLoading: (v: boolean) => void;
|
|
playTrack: (track: Track, queue: Track[]) => void;
|
|
}
|
|
|
|
export async function runArtistDetailPlayAll(deps: RunArtistDetailPlayDeps): Promise<void> {
|
|
const { albums, setPlayAllLoading, playTrack } = deps;
|
|
if (albums.length === 0) return;
|
|
await runBulkPlayAll({ fetchTracks: () => fetchAllTracks(albums), setLoading: setPlayAllLoading, playTrack });
|
|
}
|
|
|
|
export async function runArtistDetailShuffle(deps: RunArtistDetailPlayDeps): Promise<void> {
|
|
const { albums, setPlayAllLoading, playTrack } = deps;
|
|
if (albums.length === 0) return;
|
|
await runBulkShuffle({ fetchTracks: () => fetchAllTracks(albums), setLoading: setPlayAllLoading, playTrack });
|
|
}
|
|
|
|
export interface RunArtistDetailStartRadioDeps {
|
|
artist: SubsonicArtist;
|
|
t: TFunction;
|
|
setRadioLoading: (v: boolean) => void;
|
|
playTrack: (track: Track, queue: Track[]) => void;
|
|
enqueue: (tracks: Track[]) => void;
|
|
}
|
|
|
|
export async function runArtistDetailStartRadio(deps: RunArtistDetailStartRadioDeps): Promise<void> {
|
|
const { artist, t, setRadioLoading, playTrack, enqueue } = deps;
|
|
setRadioLoading(true);
|
|
try {
|
|
// Fire both fetches in parallel
|
|
const topPromise = getTopSongs(artist.name);
|
|
const similarPromise = getSimilarSongs2(artist.id, 50);
|
|
|
|
// Start playing as soon as top songs arrive
|
|
const top = await topPromise;
|
|
if (top.length > 0) {
|
|
const firstTrack = songToTrack(top[0]);
|
|
playTrack(firstTrack, [firstTrack]);
|
|
setRadioLoading(false);
|
|
// Enqueue remaining tracks when similar songs arrive
|
|
const similar = await similarPromise;
|
|
const remaining = [...top.slice(1), ...similar].map(songToTrack);
|
|
if (remaining.length > 0) enqueue(remaining);
|
|
} else {
|
|
// No top songs — fall back to similar
|
|
const similar = await similarPromise;
|
|
if (similar.length > 0) {
|
|
const tracks = similar.map(songToTrack);
|
|
playTrack(tracks[0], tracks);
|
|
} else {
|
|
alert(t('artistDetail.noRadio'));
|
|
}
|
|
setRadioLoading(false);
|
|
}
|
|
} catch (e) {
|
|
console.error('Radio start failed', e);
|
|
setRadioLoading(false);
|
|
}
|
|
}
|