mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
refactor(dedup): route inline shuffle + union-dedupe through existing utils (Phase L, part 2) (#691)
Findings 3 + 4 of the dedup audit — replace hand-rolled copies with the utils that already exist: - shuffleArray (utils/playback/shuffleArray.ts): BecauseYouLikeRail's local shuffle<T>, plus the inline Fisher-Yates loops in RandomAlbums, AlbumDetail and Home. - dedupeById (utils/dedupeById.ts): the identical seen-Set/filter union-dedupe block in the fetchByGenres of Albums, NewReleases and RandomAlbums. The extracted utils are character-identical to the inline loops, so no behaviour change. RandomMix's biased `.sort(() => Math.random() - 0.5)` is intentionally left alone — swapping it would change behaviour.
This commit is contained in:
committed by
GitHub
parent
5231169a71
commit
0153435787
@@ -3,6 +3,7 @@ import { getArtist, getArtistInfo } from '../api/subsonicArtists';
|
||||
import { getAlbum } from '../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||
import { songToTrack } from '../utils/playback/songToTrack';
|
||||
import { shuffleArray } from '../utils/playback/shuffleArray';
|
||||
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -39,15 +40,6 @@ const SHOW_COUNT = 3;
|
||||
const PICKS_HISTORY_SIZE = 30;
|
||||
const COVER_SIZE = 300;
|
||||
|
||||
function shuffle<T>(arr: T[]): T[] {
|
||||
const out = arr.slice();
|
||||
for (let i = out.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[out[i], out[j]] = [out[j], out[i]];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
interface Anchor {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -162,7 +154,7 @@ export default function BecauseYouLikeRail({
|
||||
const recentAnchors = new Set(anchorHistory.slice(-cooldown));
|
||||
const eligibleRaw = pool.filter(a => !recentAnchors.has(a.id));
|
||||
const eligible = eligibleRaw.length > 0 ? eligibleRaw : pool.slice();
|
||||
const candidates = shuffle(eligible);
|
||||
const candidates = shuffleArray(eligible);
|
||||
const recentPicks = new Set(picksHistory);
|
||||
|
||||
(async () => {
|
||||
@@ -182,7 +174,7 @@ export default function BecauseYouLikeRail({
|
||||
const similar = (info.similarArtist ?? []).filter(s => s.id);
|
||||
if (similar.length === 0) continue;
|
||||
|
||||
const sampled = shuffle(similar).slice(0, SIMILAR_PICK);
|
||||
const sampled = shuffleArray(similar).slice(0, SIMILAR_PICK);
|
||||
const results = await Promise.all(
|
||||
sampled.map(s => getArtist(s.id).catch(() => null))
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { setRating, star, unstar } from '../api/subsonicStarRating';
|
||||
import { getArtistInfo } from '../api/subsonicArtists';
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import { songToTrack } from '../utils/playback/songToTrack';
|
||||
import { shuffleArray } from '../utils/playback/shuffleArray';
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
@@ -103,11 +104,7 @@ const handleShuffleAll = () => {
|
||||
if (!t.genre && albumGenre) t.genre = albumGenre;
|
||||
return t;
|
||||
});
|
||||
const shuffled = [...tracks];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
const shuffled = shuffleArray(tracks);
|
||||
if (shuffled[0]) playTrack(shuffled[0], shuffled);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getAlbumsByGenre } from '../api/subsonicGenres';
|
||||
import { getAlbumList, getAlbum } from '../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||
import { songToTrack } from '../utils/playback/songToTrack';
|
||||
import { dedupeById } from '../utils/dedupeById';
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
|
||||
import AlbumCard from '../components/AlbumCard';
|
||||
import GenreFilterBar from '../components/GenreFilterBar';
|
||||
@@ -41,8 +42,7 @@ function sanitizeFilename(name: string): string {
|
||||
|
||||
async function fetchByGenres(genres: string[]): Promise<SubsonicAlbum[]> {
|
||||
const results = await Promise.all(genres.map(g => getAlbumsByGenre(g, 500, 0)));
|
||||
const seen = new Set<string>();
|
||||
return results.flat().filter(a => { if (seen.has(a.id)) return false; seen.add(a.id); return true; });
|
||||
return dedupeById(results.flat());
|
||||
}
|
||||
|
||||
export default function Albums() {
|
||||
|
||||
+2
-6
@@ -16,6 +16,7 @@ import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '../uti
|
||||
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
||||
import { bumpPerfCounter } from '../utils/perf/perfTelemetry';
|
||||
import { dedupeById } from '../utils/dedupeById';
|
||||
import { shuffleArray } from '../utils/playback/shuffleArray';
|
||||
|
||||
/** Match Random Albums overshoot when mix filter uses album/artist axes so hero + discover row can still fill. */
|
||||
const HOME_RANDOM_FETCH = 100;
|
||||
@@ -87,12 +88,7 @@ export default function Home() {
|
||||
setMostPlayed(dedupeById(f));
|
||||
setRecentlyPlayed(dedupeById(rp));
|
||||
setDiscoverSongs(dedupeById(songs));
|
||||
const shuffled = [...artists];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
setRandomArtists(dedupeById(shuffled).slice(0, 16));
|
||||
setRandomArtists(dedupeById(shuffleArray(artists)).slice(0, 16));
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { buildDownloadUrl } from '../api/subsonicStreamUrl';
|
||||
import { getAlbumsByGenre } from '../api/subsonicGenres';
|
||||
import { getAlbumList, getAlbum } from '../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||
import { dedupeById } from '../utils/dedupeById';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { CheckSquare2, Download, HardDriveDownload, ListMusic } from 'lucide-react';
|
||||
import AlbumCard from '../components/AlbumCard';
|
||||
@@ -25,9 +26,7 @@ function sanitizeFilename(name: string): string {
|
||||
|
||||
async function fetchByGenres(genres: string[]): Promise<SubsonicAlbum[]> {
|
||||
const results = await Promise.all(genres.map(g => getAlbumsByGenre(g, 500, 0)));
|
||||
const seen = new Set<string>();
|
||||
const union = results.flat().filter(a => { if (seen.has(a.id)) return false; seen.add(a.id); return true; });
|
||||
return union.sort((a, b) => (b.year ?? 0) - (a.year ?? 0));
|
||||
return dedupeById(results.flat()).sort((a, b) => (b.year ?? 0) - (a.year ?? 0));
|
||||
}
|
||||
|
||||
export default function NewReleases() {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { buildDownloadUrl } from '../api/subsonicStreamUrl';
|
||||
import { getAlbumsByGenre } from '../api/subsonicGenres';
|
||||
import { getAlbumList, getAlbum } from '../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||
import { dedupeById } from '../utils/dedupeById';
|
||||
import { shuffleArray } from '../utils/playback/shuffleArray';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { RefreshCw, CheckSquare2, Download, HardDriveDownload } from 'lucide-react';
|
||||
import AlbumCard from '../components/AlbumCard';
|
||||
@@ -29,13 +31,7 @@ function sanitizeFilename(name: string): string {
|
||||
|
||||
async function fetchByGenres(genres: string[]): Promise<SubsonicAlbum[]> {
|
||||
const results = await Promise.all(genres.map(g => getAlbumsByGenre(g, 500, 0)));
|
||||
const seen = new Set<string>();
|
||||
const union = results.flat().filter(a => { if (seen.has(a.id)) return false; seen.add(a.id); return true; });
|
||||
for (let i = union.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[union[i], union[j]] = [union[j], union[i]];
|
||||
}
|
||||
const pool = union.slice(0, GENRE_UNION_PREFILTER_CAP);
|
||||
const pool = shuffleArray(dedupeById(results.flat())).slice(0, GENRE_UNION_PREFILTER_CAP);
|
||||
const filtered = await filterAlbumsByMixRatings(pool, getMixMinRatingsConfigFromAuth());
|
||||
return filtered.slice(0, ALBUM_COUNT);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user