fix: random albums performance, image cache memory leak, i18n fix (v1.4.1)

- Remove auto-refresh timer from Random Albums (caused 100ms progress
  interval + 30 concurrent fetches every 30s, eventually freezing the app)
- Limit concurrent image fetches to 5 (was unbounded)
- Cap in-memory object URL cache at 150 entries with revokeObjectURL eviction
- Add cancellation flag to useCachedUrl to prevent setState on unmounted components
- Fix hardcoded "Neueste" page title in New Releases (now uses i18n)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-03-16 19:09:31 +01:00
parent d3ffa30bf5
commit af18aef42a
9 changed files with 62 additions and 57 deletions
+2 -36
View File
@@ -4,23 +4,14 @@ import { getAlbumList, SubsonicAlbum } from '../api/subsonic';
import AlbumCard from '../components/AlbumCard';
import { useTranslation } from 'react-i18next';
const INTERVAL_MS = 30000;
const ALBUM_COUNT = 30;
export default function RandomAlbums() {
const { t } = useTranslation();
const [albums, setAlbums] = useState<SubsonicAlbum[]>([]);
const [loading, setLoading] = useState(true);
const [progress, setProgress] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const progressRef = useRef<ReturnType<typeof setInterval> | null>(null);
const loadingRef = useRef(false);
const clearTimers = () => {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
if (progressRef.current) { clearInterval(progressRef.current); progressRef.current = null; }
};
const load = useCallback(async () => {
if (loadingRef.current) return;
loadingRef.current = true;
@@ -36,27 +27,7 @@ export default function RandomAlbums() {
}
}, []);
const startCycle = useCallback(() => {
clearTimers();
setProgress(0);
const startTime = Date.now();
progressRef.current = setInterval(() => {
setProgress(Math.min((Date.now() - startTime) / INTERVAL_MS * 100, 100));
}, 100);
timerRef.current = setInterval(() => {
load().then(() => startCycle());
}, INTERVAL_MS);
}, [load]);
useEffect(() => {
load().then(() => startCycle());
return clearTimers;
}, [load, startCycle]);
const handleManualRefresh = () => {
clearTimers();
load().then(() => startCycle());
};
useEffect(() => { load(); }, [load]);
return (
<div className="content-body animate-fade-in">
@@ -64,7 +35,7 @@ export default function RandomAlbums() {
<h1 className="page-title" style={{ marginBottom: 0 }}>{t('randomAlbums.title')}</h1>
<button
className="btn btn-ghost"
onClick={handleManualRefresh}
onClick={load}
disabled={loading}
data-tooltip={t('randomAlbums.refresh')}
>
@@ -73,11 +44,6 @@ export default function RandomAlbums() {
</button>
</div>
{/* Countdown progress bar */}
<div className="random-albums-progress">
<div className="random-albums-progress-fill" style={{ width: `${progress}%` }} />
</div>
{loading && albums.length === 0 ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '4rem' }}>
<div className="spinner" />