fix(home): scope Because you listened rail to sidebar library (#964)

* fix(home): scope Because you listened rail to sidebar library

Similar-artist album picks used getArtist without library filtering;
session cache also ignored musicLibraryFilterVersion. Filter picks to
the scoped album set and key cache/reserve by filter version.

* docs(changelog): credit zunoz on Psysonic Discord for PR #964

* test: satisfy SubsonicAlbum required fields in new unit tests

Fix tsc --noEmit CI: songCount and duration are required on SubsonicAlbum.
This commit is contained in:
cucadmuh
2026-06-03 22:57:35 +03:00
committed by GitHub
parent be21f7834f
commit e76dac87ae
7 changed files with 124 additions and 30 deletions
+23
View File
@@ -150,6 +150,29 @@ export async function filterSongsToActiveLibrary(songs: SubsonicSong[]): Promise
return filterSongsToServerLibrary(songs, activeServerId);
}
/** Client-side album scope filter — same album-id set as {@link filterSongsToServerLibrary}. */
export function filterAlbumsByScopedAlbumIds(
albums: SubsonicAlbum[],
allowed: Set<string> | null,
): SubsonicAlbum[] {
if (!allowed || allowed.size === 0) return albums;
return albums.filter(a => allowed.has(a.id));
}
export async function filterAlbumsToServerLibrary(
albums: SubsonicAlbum[],
serverId: string,
): Promise<SubsonicAlbum[]> {
const allowed = await albumIdsInLibraryScope(serverId);
return filterAlbumsByScopedAlbumIds(albums, allowed);
}
export async function filterAlbumsToActiveLibrary(albums: SubsonicAlbum[]): Promise<SubsonicAlbum[]> {
const { activeServerId } = useAuthStore.getState();
if (!activeServerId) return albums;
return filterAlbumsToServerLibrary(albums, activeServerId);
}
/** When scoped to one library, ask the server for more similar tracks — many will be filtered out client-side. */
export function similarSongsRequestCount(desired: number): number {
const { activeServerId, musicLibraryFilterByServer } = useAuthStore.getState();
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import type { SubsonicAlbum } from './subsonicTypes';
import { filterAlbumsByScopedAlbumIds } from './subsonicLibrary';
const album = (id: string): SubsonicAlbum => ({
id,
name: id,
artist: 'a',
artistId: '1',
songCount: 1,
duration: 1,
});
describe('filterAlbumsByScopedAlbumIds', () => {
it('returns all albums when scope is unset', () => {
const albums = [album('a'), album('b')];
expect(filterAlbumsByScopedAlbumIds(albums, null)).toEqual(albums);
});
it('keeps only albums in the scoped id set', () => {
const albums = [album('a'), album('b'), album('c')];
expect(filterAlbumsByScopedAlbumIds(albums, new Set(['a', 'c']))).toEqual([album('a'), album('c')]);
});
});