fix(mix): rating filter across mixes and Lucky Mix queue fill (#714)

* fix(mix): apply rating filter across mixes and fix Lucky Mix queue fill

Invalidate entity rating cache on setRating and stop negative-caching
unrated artists/albums so filters see fresh stars. Honor UI rating
overrides, wire Instant Mix and CLI paths, fix Random Mix filter order,
and align Lucky Mix progress with the real player queue length.

* docs(changelog): document mix rating filter and Lucky Mix queue fix
This commit is contained in:
cucadmuh
2026-05-15 12:45:02 +03:00
committed by GitHub
parent cfcbbd79e4
commit 02fd828049
11 changed files with 323 additions and 58 deletions
@@ -1,6 +1,7 @@
import { join } from '@tauri-apps/api/path';
import { invoke } from '@tauri-apps/api/core';
import { getSimilarSongs2, getSimilarSongs, getTopSongs } from '../../api/subsonicArtists';
import { filterSongsForLuckyMixRatings, getMixMinRatingsConfigFromAuth } from '../mix/mixRatingFilter';
import { buildDownloadUrl } from '../../api/subsonicStreamUrl';
import { useAuthStore } from '../../store/authStore';
import { usePlayerStore } from '../../store/playerStore';
@@ -107,10 +108,13 @@ export async function startInstantMix(
try {
const similar = await getSimilarSongs(song.id, 50);
if (serverId) useAuthStore.getState().setAudiomuseNavidromeIssue(serverId, false);
const mixCfg = getMixMinRatingsConfigFromAuth();
const ratedFiltered = await filterSongsForLuckyMixRatings(
similar.filter(s => s.id !== song.id),
mixCfg,
);
const shuffled = shuffleArray(
similar
.filter(s => s.id !== song.id)
.map(s => ({ ...songToTrack(s), radioAdded: true as const })),
ratedFiltered.map(s => ({ ...songToTrack(s), radioAdded: true as const })),
);
if (shuffled.length > 0) {
const aid = song.artistId?.trim() || undefined;
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest';
import type { SubsonicSong } from '../../api/subsonicTypes';
import { filterRandomMixSongs } from './randomMixHelpers';
function song(id: string): SubsonicSong {
return {
id,
title: 't',
artist: 'A',
album: 'Al',
albumId: 'alb',
duration: 1,
artistUserRating: 1,
};
}
describe('filterRandomMixSongs', () => {
it('applies mix rating filter even when audiobook exclusion is off', () => {
const cfg = { enabled: true, minSong: 0, minAlbum: 0, minArtist: 2 };
const out = filterRandomMixSongs([song('1'), song('2')], {
excludeAudiobooks: false,
customGenreBlacklist: [],
mixRatingCfg: { ...cfg, minArtist: 2 },
});
expect(out).toHaveLength(0);
const kept = filterRandomMixSongs([song('1'), { ...song('2'), artistUserRating: 4 }], {
excludeAudiobooks: false,
customGenreBlacklist: [],
mixRatingCfg: cfg,
});
expect(kept).toHaveLength(1);
expect(kept[0].id).toBe('2');
});
});
@@ -24,6 +24,7 @@ interface FilterArgs {
export function filterRandomMixSongs(songs: SubsonicSong[], args: FilterArgs): SubsonicSong[] {
const { excludeAudiobooks, customGenreBlacklist, mixRatingCfg } = args;
return songs.filter(song => {
if (!passesMixMinRatings(song, mixRatingCfg)) return false;
if (!excludeAudiobooks) return true;
const checkText = (text: string) => {
const t = text.toLowerCase();
@@ -35,7 +36,6 @@ export function filterRandomMixSongs(songs: SubsonicSong[], args: FilterArgs): S
if (song.title && checkText(song.title)) return false;
if (song.album && checkText(song.album)) return false;
if (song.artist && checkText(song.artist)) return false;
if (!passesMixMinRatings(song, mixRatingCfg)) return false;
return true;
});
}