Files
psysonic/src/utils/buildInfiniteQueueCandidates.ts
T
Frank Stellmacher 006635de4b refactor(api): F.49 — extract library + artists + ratings (#614)
Three domain-eng modules peel ~316 LOC of fetch/mapping out of
`api/subsonic.ts`:

- `subsonicLibrary.ts` — browse + random + per-song fetch
  (`getMusicDirectory`, `getMusicIndexes`, `getMusicFolders`,
  `getRandomAlbums`, `getAlbumList`, `getRandomSongs`,
  `getRandomSongsFiltered`, `getSong`, `getAlbum`,
  `filterSongsToActiveLibrary`, `similarSongsRequestCount`, plus
  the private `albumIdsInActiveLibraryScope` cache).
- `subsonicArtists.ts` — artist endpoints (`getArtists`, `getArtist`,
  `getArtistInfo`, `getTopSongs`, `getSimilarSongs2`,
  `getSimilarSongs`). Uses Library's `filterSongsToActiveLibrary` and
  `similarSongsRequestCount` for the per-library scoping fallback.
- `subsonicRatings.ts` — `parseSubsonicEntityStarRating` parser plus
  `prefetchArtistUserRatings` and `prefetchAlbumUserRatings` workers
  with the shared 7-min cache. Calls back into Library/Artists for
  the per-id fetch.

51 external call sites migrated to direct imports. No re-export
shims in `subsonic.ts`. Statistics endpoints still in `subsonic.ts`
keep their `RATING_CACHE_TTL` constant locally (same 7-min window).

subsonic.ts: 1078 → 762 LOC (−316).
2026-05-13 00:23:53 +02:00

62 lines
2.4 KiB
TypeScript

import { getSimilarSongs2, getTopSongs } from '../api/subsonicArtists';
import { getRandomSongs } from '../api/subsonicLibrary';
import type { Track } from '../store/playerStoreTypes';
import {
enrichSongsForMixRatingFilter,
getMixMinRatingsConfigFromAuth,
passesMixMinRatings,
} from './mixRatingFilter';
import { shuffleArray } from './shuffleArray';
import { songToTrack } from './songToTrack';
/**
* Infinite queue source strategy (Instant Mix-like):
* 1) Prefer artist-driven candidates (Top + Similar) around the current track.
* 2) Fallback to random songs when artist-driven fetches are empty.
*/
export async function buildInfiniteQueueCandidates(
seedTrack: Track | null,
existingIds: Set<string>,
count = 5,
): Promise<Track[]> {
const RANDOM_TOPUP_BATCH_SIZE = Math.max(10, count * 2);
const RANDOM_TOPUP_MAX_BATCHES = 8;
const artistId = seedTrack?.artistId?.trim() || null;
const artistName = seedTrack?.artist?.trim() || null;
const [similar, top] = await Promise.all([
artistId ? getSimilarSongs2(artistId).catch(() => []) : Promise.resolve([]),
artistName ? getTopSongs(artistName).catch(() => []) : Promise.resolve([]),
]);
const seedId = seedTrack?.id ?? null;
const mixCfg = getMixMinRatingsConfigFromAuth();
const mixedSources = [...top, ...similar];
const filteredMixedSongs = mixCfg.enabled
? (await enrichSongsForMixRatingFilter(mixedSources, mixCfg)).filter(s => passesMixMinRatings(s, mixCfg))
: mixedSources;
const out: Track[] = shuffleArray(
filteredMixedSongs
.map(songToTrack)
.filter(t => t.id !== seedId && !existingIds.has(t.id)),
)
.slice(0, count)
.map(t => ({ ...t, autoAdded: true as const }));
const seenIds = new Set<string>([...existingIds, ...out.map(t => t.id)]);
for (let b = 0; out.length < count && b < RANDOM_TOPUP_MAX_BATCHES; b++) {
const random = await getRandomSongs(RANDOM_TOPUP_BATCH_SIZE, seedTrack?.genre).catch(() => []);
if (!random.length) break;
const filteredRandomSongs = mixCfg.enabled
? (await enrichSongsForMixRatingFilter(random, mixCfg)).filter(s => passesMixMinRatings(s, mixCfg))
: random;
for (const track of shuffleArray(filteredRandomSongs.map(songToTrack))) {
if (track.id === seedId || seenIds.has(track.id)) continue;
out.push({ ...track, autoAdded: true as const });
seenIds.add(track.id);
if (out.length >= count) break;
}
}
return out.slice(0, count);
}