diff --git a/src/locales/de.ts b/src/locales/de.ts index 8bd291cd..9a74d3b3 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -791,6 +791,9 @@ export const deTranslation = { sortLeast: 'Wenigste Plays zuerst', loadMore: 'Mehr Alben laden', noData: 'Noch keine Wiedergabedaten. Fang an zu hören!', + noArtists: 'Alle Künstler herausgefiltert.', + filterCompilations: 'Sampler-Künstler ausblenden (Various Artists, Soundtracks usw.)', + filterCompilationsShort: 'Sampler ausblenden', }, radio: { title: 'Internetradio', diff --git a/src/locales/en.ts b/src/locales/en.ts index 0a8fdc4d..5b1fa647 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -792,6 +792,9 @@ export const enTranslation = { sortLeast: 'Fewest plays first', loadMore: 'Load more albums', noData: 'No play data yet. Start listening!', + noArtists: 'All artists filtered out.', + filterCompilations: 'Hide compilation artists (Various Artists, Soundtracks, etc.)', + filterCompilationsShort: 'Hide compilations', }, radio: { title: 'Internet Radio', diff --git a/src/pages/MostPlayed.tsx b/src/pages/MostPlayed.tsx index a3331637..2a93d877 100644 --- a/src/pages/MostPlayed.tsx +++ b/src/pages/MostPlayed.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ArrowUpDown, ArrowDown, ArrowUp, TrendingUp } from 'lucide-react'; +import { ArrowUpDown, ArrowDown, ArrowUp, TrendingUp, UsersRound } from 'lucide-react'; import { getAlbumList, SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic'; import { useAuthStore } from '../store/authStore'; import CachedImage from '../components/CachedImage'; @@ -16,11 +16,24 @@ interface ArtistEntry { totalPlays: number; } -function deriveTopArtists(albums: SubsonicAlbum[]): ArtistEntry[] { +const COMPILATION_NAMES = new Set([ + 'various artists', 'various', 'va', 'v.a.', 'v.a', + 'diverse artister', 'diversos artistas', 'artistes variés', + 'vários artistas', 'verschiedene künstler', 'verscheidene artiesten', + 'compilations', 'soundtrack', 'original soundtrack', 'ost', + 'original motion picture soundtrack', 'original score', +]); + +function isCompilation(name: string): boolean { + return COMPILATION_NAMES.has(name.toLowerCase().trim()); +} + +function deriveTopArtists(albums: SubsonicAlbum[], filterCompilations: boolean): ArtistEntry[] { const map = new Map(); for (const a of albums) { const plays = a.playCount ?? 0; if (plays === 0) continue; + if (filterCompilations && isCompilation(a.artist ?? '')) continue; const entry = map.get(a.artistId); if (entry) { entry.totalPlays += plays; @@ -46,8 +59,9 @@ export default function MostPlayed() { const [loadingMore, setLoadingMore] = useState(false); const [hasMore, setHasMore] = useState(true); const [sortAsc, setSortAsc] = useState(false); // false = most plays first + const [filterCompilations, setFilterCompilations] = useState(false); - const topArtists = deriveTopArtists(albums).slice(0, 10); + const topArtists = deriveTopArtists(albums, filterCompilations).slice(0, 10); const load = useCallback(async () => { setLoading(true); @@ -96,9 +110,23 @@ export default function MostPlayed() { {/* ── Top Artists ── */} - {!loading && topArtists.length > 0 && ( + {!loading && (
-

{t('mostPlayed.topArtists')}

+
+

{t('mostPlayed.topArtists')}

+ +
+ {topArtists.length === 0 && ( +
{t('mostPlayed.noArtists')}
+ )}
{topArtists.map((artist, i) => (