mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
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:
@@ -450,6 +450,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* Navidrome's composer role list can include artists with zero composer album credits (e.g. Apollo 440 with performer albums only). Composers browse/search now drops rows where `stats.composer.albumCount` is zero so ghost composer cards no longer appear.
|
* Navidrome's composer role list can include artists with zero composer album credits (e.g. Apollo 440 with performer albums only). Composers browse/search now drops rows where `stats.composer.albumCount` is zero so ghost composer cards no longer appear.
|
||||||
|
|
||||||
|
|
||||||
|
### Mainstage — Because you listened respects sidebar library
|
||||||
|
|
||||||
|
**By [@cucadmuh](https://github.com/cucadmuh), reported by zunoz on the Psysonic Discord, PR [#964](https://github.com/Psychotoxical/psysonic/pull/964)**
|
||||||
|
|
||||||
|
* The recommendation rail picks albums from Last.fm similar artists via `getArtist`, which can ignore `musicFolderId` — picks are now filtered to the scoped library album set, and the rail cache invalidates when the sidebar library filter changes.
|
||||||
|
|
||||||
|
|
||||||
### In-page browse — virtual scroll and cover-art priority
|
### In-page browse — virtual scroll and cover-art priority
|
||||||
|
|
||||||
**By [@cucadmuh](https://github.com/cucadmuh), PR [#783](https://github.com/Psychotoxical/psysonic/pull/783)**
|
**By [@cucadmuh](https://github.com/cucadmuh), PR [#783](https://github.com/Psychotoxical/psysonic/pull/783)**
|
||||||
|
|||||||
@@ -150,6 +150,29 @@ export async function filterSongsToActiveLibrary(songs: SubsonicSong[]): Promise
|
|||||||
return filterSongsToServerLibrary(songs, activeServerId);
|
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. */
|
/** 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 {
|
export function similarSongsRequestCount(desired: number): number {
|
||||||
const { activeServerId, musicLibraryFilterByServer } = useAuthStore.getState();
|
const { activeServerId, musicLibraryFilterByServer } = useAuthStore.getState();
|
||||||
|
|||||||
@@ -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')]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getArtist, getArtistInfo } from '../api/subsonicArtists';
|
import { getArtist, getArtistInfo } from '../api/subsonicArtists';
|
||||||
import { getAlbum } from '../api/subsonicLibrary';
|
import { filterAlbumsToActiveLibrary, getAlbum } from '../api/subsonicLibrary';
|
||||||
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||||
import { songToTrack } from '../utils/playback/songToTrack';
|
import { songToTrack } from '../utils/playback/songToTrack';
|
||||||
import { shuffleArray } from '../utils/playback/shuffleArray';
|
import { shuffleArray } from '../utils/playback/shuffleArray';
|
||||||
@@ -56,6 +56,7 @@ const ROW_STAGGER_MS = 150;
|
|||||||
// ── Module-level reserve: next batch pre-fetched in background after each display ──
|
// ── Module-level reserve: next batch pre-fetched in background after each display ──
|
||||||
type BecauseReserve = {
|
type BecauseReserve = {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
filterVersion: number;
|
||||||
// poolKey intentionally omitted — reserve is valid for any pool state on the
|
// poolKey intentionally omitted — reserve is valid for any pool state on the
|
||||||
// same server. Pool (top-played artists) changes slowly; showing a slightly-off
|
// same server. Pool (top-played artists) changes slowly; showing a slightly-off
|
||||||
// anchor once before the next fill corrects it is far better than showing a
|
// anchor once before the next fill corrects it is far better than showing a
|
||||||
@@ -96,9 +97,11 @@ async function resolvePicks(
|
|||||||
|
|
||||||
const picks: SubsonicAlbum[] = [];
|
const picks: SubsonicAlbum[] = [];
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
if (!r || r.albums.length === 0) continue;
|
if (!r) continue;
|
||||||
const fresh = r.albums.filter(a => !recentPicks.has(a.id));
|
const albums = await filterAlbumsToActiveLibrary(r.albums);
|
||||||
const choice = fresh.length > 0 ? fresh : r.albums;
|
if (albums.length === 0) continue;
|
||||||
|
const fresh = albums.filter(a => !recentPicks.has(a.id));
|
||||||
|
const choice = fresh.length > 0 ? fresh : albums;
|
||||||
const album = choice[Math.floor(Math.random() * choice.length)];
|
const album = choice[Math.floor(Math.random() * choice.length)];
|
||||||
picks.push(album);
|
picks.push(album);
|
||||||
if (picks.length >= SHOW_COUNT) break;
|
if (picks.length >= SHOW_COUNT) break;
|
||||||
@@ -182,6 +185,7 @@ async function fetchBecauseYouLike(
|
|||||||
async function fillBecauseReserve(
|
async function fillBecauseReserve(
|
||||||
pool: BecauseYouLikeAnchor[],
|
pool: BecauseYouLikeAnchor[],
|
||||||
serverId: string,
|
serverId: string,
|
||||||
|
filterVersion: number,
|
||||||
anchorHistKey: string | null,
|
anchorHistKey: string | null,
|
||||||
picksHistKey: string | null,
|
picksHistKey: string | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -190,10 +194,10 @@ async function fillBecauseReserve(
|
|||||||
try {
|
try {
|
||||||
const result = await fetchBecauseYouLike(pool, anchorHistKey, picksHistKey);
|
const result = await fetchBecauseYouLike(pool, anchorHistKey, picksHistKey);
|
||||||
if (result) {
|
if (result) {
|
||||||
_becauseReserve = { serverId, ...result };
|
_becauseReserve = { serverId, filterVersion, ...result };
|
||||||
// Also refresh the session snapshot so a quick leave→return can pick up
|
// Also refresh the session snapshot so a quick leave→return can pick up
|
||||||
// newer cards even before the reserve is explicitly consumed.
|
// newer cards even before the reserve is explicitly consumed.
|
||||||
writeBecauseYouLikeCache({ serverId, anchor: result.anchor, recs: result.recs });
|
writeBecauseYouLikeCache({ serverId, filterVersion, anchor: result.anchor, recs: result.recs });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* Network failure — next visit falls back to a fresh fetch. */
|
/* Network failure — next visit falls back to a fresh fetch. */
|
||||||
@@ -312,8 +316,12 @@ function picksHistoryKey(serverId: string | null): string | null {
|
|||||||
return serverId ? `${PICKS_HISTORY_KEY_PREFIX}${serverId}` : null;
|
return serverId ? `${PICKS_HISTORY_KEY_PREFIX}${serverId}` : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasValidReserve(serverId: string | null): boolean {
|
function hasValidReserve(serverId: string | null, filterVersion: number): boolean {
|
||||||
return _becauseReserve != null && _becauseReserve.serverId === (serverId ?? '');
|
return (
|
||||||
|
_becauseReserve != null &&
|
||||||
|
_becauseReserve.serverId === (serverId ?? '') &&
|
||||||
|
_becauseReserve.filterVersion === filterVersion
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BecauseYouLikeRail({
|
export default function BecauseYouLikeRail({
|
||||||
@@ -324,6 +332,7 @@ export default function BecauseYouLikeRail({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const activeServerId = useAuthStore(s => s.activeServerId);
|
const activeServerId = useAuthStore(s => s.activeServerId);
|
||||||
|
const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion);
|
||||||
const pool = useMemo(
|
const pool = useMemo(
|
||||||
() => buildAnchorPool([mostPlayed, recentlyPlayed ?? [], starred ?? []], TOP_ARTIST_POOL),
|
() => buildAnchorPool([mostPlayed, recentlyPlayed ?? [], starred ?? []], TOP_ARTIST_POOL),
|
||||||
[mostPlayed, recentlyPlayed, starred],
|
[mostPlayed, recentlyPlayed, starred],
|
||||||
@@ -336,18 +345,18 @@ export default function BecauseYouLikeRail({
|
|||||||
// revalidate) > skeleton. Both checks work without poolKey so they fire correctly on the
|
// revalidate) > skeleton. Both checks work without poolKey so they fire correctly on the
|
||||||
// first render when pool is still [] (Home.tsx loads mostPlayed asynchronously).
|
// first render when pool is still [] (Home.tsx loads mostPlayed asynchronously).
|
||||||
const [anchor, setAnchor] = useState<BecauseYouLikeAnchor | null>(() => {
|
const [anchor, setAnchor] = useState<BecauseYouLikeAnchor | null>(() => {
|
||||||
if (hasValidReserve(activeServerId)) return _becauseReserve!.anchor;
|
if (hasValidReserve(activeServerId, musicLibraryFilterVersion)) return _becauseReserve!.anchor;
|
||||||
return readBecauseYouLikeCache(activeServerId)?.anchor ?? null;
|
return readBecauseYouLikeCache(activeServerId, musicLibraryFilterVersion)?.anchor ?? null;
|
||||||
});
|
});
|
||||||
const [recs, setRecs] = useState<SubsonicAlbum[]>(() => {
|
const [recs, setRecs] = useState<SubsonicAlbum[]>(() => {
|
||||||
if (hasValidReserve(activeServerId)) return _becauseReserve!.recs;
|
if (hasValidReserve(activeServerId, musicLibraryFilterVersion)) return _becauseReserve!.recs;
|
||||||
return readBecauseYouLikeCache(activeServerId)?.recs ?? [];
|
return readBecauseYouLikeCache(activeServerId, musicLibraryFilterVersion)?.recs ?? [];
|
||||||
});
|
});
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [narrow, setNarrow] = useState(false);
|
const [narrow, setNarrow] = useState(false);
|
||||||
const [refreshing, setRefreshing] = useState(() => {
|
const [refreshing, setRefreshing] = useState(() => {
|
||||||
if (hasValidReserve(activeServerId)) return false;
|
if (hasValidReserve(activeServerId, musicLibraryFilterVersion)) return false;
|
||||||
const snap = readBecauseYouLikeCache(activeServerId);
|
const snap = readBecauseYouLikeCache(activeServerId, musicLibraryFilterVersion);
|
||||||
return !snap || snap.recs.length === 0;
|
return !snap || snap.recs.length === 0;
|
||||||
});
|
});
|
||||||
const skeletonSlots = useBecauseRowSlotCount(refreshing, SHOW_COUNT);
|
const skeletonSlots = useBecauseRowSlotCount(refreshing, SHOW_COUNT);
|
||||||
@@ -358,12 +367,12 @@ export default function BecauseYouLikeRail({
|
|||||||
* (synchronous, before browser paint) or fall back to session cache (stale-
|
* (synchronous, before browser paint) or fall back to session cache (stale-
|
||||||
* while-revalidate), only clearing to skeleton when nothing is available. */
|
* while-revalidate), only clearing to skeleton when nothing is available. */
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (hasValidReserve(activeServerId)) {
|
if (hasValidReserve(activeServerId, musicLibraryFilterVersion)) {
|
||||||
setAnchor(_becauseReserve!.anchor);
|
setAnchor(_becauseReserve!.anchor);
|
||||||
setRecs(_becauseReserve!.recs);
|
setRecs(_becauseReserve!.recs);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
} else {
|
} else {
|
||||||
const snap = readBecauseYouLikeCache(activeServerId);
|
const snap = readBecauseYouLikeCache(activeServerId, musicLibraryFilterVersion);
|
||||||
if (snap && snap.recs.length > 0) {
|
if (snap && snap.recs.length > 0) {
|
||||||
setAnchor(snap.anchor);
|
setAnchor(snap.anchor);
|
||||||
setRecs(snap.recs);
|
setRecs(snap.recs);
|
||||||
@@ -374,7 +383,7 @@ export default function BecauseYouLikeRail({
|
|||||||
setRecs([]);
|
setRecs([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [activeServerId, poolKey]);
|
}, [activeServerId, musicLibraryFilterVersion, poolKey]);
|
||||||
|
|
||||||
// 696px ≙ exactly 2 BecauseCards side-by-side (2*340 + 16 gap). Below that
|
// 696px ≙ exactly 2 BecauseCards side-by-side (2*340 + 16 gap). Below that
|
||||||
// the hero-style cards stretch full-width and dwarf the rest of the page,
|
// the hero-style cards stretch full-width and dwarf the rest of the page,
|
||||||
@@ -410,11 +419,10 @@ export default function BecauseYouLikeRail({
|
|||||||
|
|
||||||
const anchorHistKey = anchorHistoryKey(activeServerId);
|
const anchorHistKey = anchorHistoryKey(activeServerId);
|
||||||
const picksHistKey = picksHistoryKey(activeServerId);
|
const picksHistKey = picksHistoryKey(activeServerId);
|
||||||
const snap = readBecauseYouLikeCache(activeServerId);
|
const snap = readBecauseYouLikeCache(activeServerId, musicLibraryFilterVersion);
|
||||||
|
|
||||||
// Consume module-level reserve (keyed by serverId only — poolKey omitted so
|
// Consume module-level reserve (keyed by server + library scope).
|
||||||
// the reserve is usable even before pool has loaded on first render).
|
const reserved = hasValidReserve(activeServerId, musicLibraryFilterVersion) ? _becauseReserve : null;
|
||||||
const reserved = hasValidReserve(activeServerId) ? _becauseReserve : null;
|
|
||||||
_becauseReserve = null;
|
_becauseReserve = null;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -433,11 +441,16 @@ export default function BecauseYouLikeRail({
|
|||||||
setAnchor(reserved.anchor);
|
setAnchor(reserved.anchor);
|
||||||
setRecs(reserved.recs);
|
setRecs(reserved.recs);
|
||||||
if (activeServerId) {
|
if (activeServerId) {
|
||||||
writeBecauseYouLikeCache({ serverId: activeServerId, anchor: reserved.anchor, recs: reserved.recs });
|
writeBecauseYouLikeCache({
|
||||||
|
serverId: activeServerId,
|
||||||
|
filterVersion: musicLibraryFilterVersion,
|
||||||
|
anchor: reserved.anchor,
|
||||||
|
recs: reserved.recs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
// Pre-fetch the next batch so the next visit is also instant.
|
// Pre-fetch the next batch so the next visit is also instant.
|
||||||
void fillBecauseReserve(pool, activeServerId, anchorHistKey, picksHistKey);
|
void fillBecauseReserve(pool, activeServerId, musicLibraryFilterVersion, anchorHistKey, picksHistKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +459,7 @@ export default function BecauseYouLikeRail({
|
|||||||
// for the next mount instead of swapping cards mid-visit.
|
// for the next mount instead of swapping cards mid-visit.
|
||||||
if (snap && snap.recs.length > 0) {
|
if (snap && snap.recs.length > 0) {
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
void fillBecauseReserve(pool, activeServerId, anchorHistKey, picksHistKey);
|
void fillBecauseReserve(pool, activeServerId, musicLibraryFilterVersion, anchorHistKey, picksHistKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,11 +489,16 @@ export default function BecauseYouLikeRail({
|
|||||||
setAnchor(result.anchor);
|
setAnchor(result.anchor);
|
||||||
setRecs(result.recs);
|
setRecs(result.recs);
|
||||||
if (activeServerId) {
|
if (activeServerId) {
|
||||||
writeBecauseYouLikeCache({ serverId: activeServerId, anchor: result.anchor, recs: result.recs });
|
writeBecauseYouLikeCache({
|
||||||
|
serverId: activeServerId,
|
||||||
|
filterVersion: musicLibraryFilterVersion,
|
||||||
|
anchor: result.anchor,
|
||||||
|
recs: result.recs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
// Pre-fetch next batch so the next visit is instant.
|
// Pre-fetch next batch so the next visit is instant.
|
||||||
void fillBecauseReserve(pool, activeServerId, anchorHistKey, picksHistKey);
|
void fillBecauseReserve(pool, activeServerId, musicLibraryFilterVersion, anchorHistKey, picksHistKey);
|
||||||
} else {
|
} else {
|
||||||
// Network failed — restore session cache if available.
|
// Network failed — restore session cache if available.
|
||||||
if (snap) {
|
if (snap) {
|
||||||
@@ -500,7 +518,7 @@ export default function BecauseYouLikeRail({
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [pool, activeServerId, disableArtwork, poolKey]);
|
}, [pool, activeServerId, musicLibraryFilterVersion, disableArtwork, poolKey]);
|
||||||
|
|
||||||
useLibraryCoverPrefetch(
|
useLibraryCoverPrefetch(
|
||||||
disableArtwork || recs.length === 0 ? [] : [{ albums: recs, priority: 'high' }],
|
disableArtwork || recs.length === 0 ? [] : [{ albums: recs, priority: 'high' }],
|
||||||
|
|||||||
+2
-2
@@ -162,7 +162,7 @@ export default function Home() {
|
|||||||
if (!wasPrePopulated) applyFeedSnapshot(cached);
|
if (!wasPrePopulated) applyFeedSnapshot(cached);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
void warmHomeMainstageCovers(cached);
|
void warmHomeMainstageCovers(cached);
|
||||||
const becauseSnap = readBecauseYouLikeCache(activeServerId);
|
const becauseSnap = readBecauseYouLikeCache(activeServerId, musicLibraryFilterVersion);
|
||||||
void primeAlbumCoversForDisplay(becauseSnap?.recs ?? [], HOME_BECAUSE_CARD_COVER_CSS_PX, {
|
void primeAlbumCoversForDisplay(becauseSnap?.recs ?? [], HOME_BECAUSE_CARD_COVER_CSS_PX, {
|
||||||
limit: 6,
|
limit: 6,
|
||||||
});
|
});
|
||||||
@@ -193,7 +193,7 @@ export default function Home() {
|
|||||||
applyFeedSnapshot(snap);
|
applyFeedSnapshot(snap);
|
||||||
if (!cancelled) setLoading(false);
|
if (!cancelled) setLoading(false);
|
||||||
void warmHomeMainstageCovers(snap);
|
void warmHomeMainstageCovers(snap);
|
||||||
const becauseSnap = readBecauseYouLikeCache(activeServerId);
|
const becauseSnap = readBecauseYouLikeCache(activeServerId, musicLibraryFilterVersion);
|
||||||
void primeAlbumCoversForDisplay(becauseSnap?.recs ?? [], HOME_BECAUSE_CARD_COVER_CSS_PX, {
|
void primeAlbumCoversForDisplay(becauseSnap?.recs ?? [], HOME_BECAUSE_CARD_COVER_CSS_PX, {
|
||||||
limit: 6,
|
limit: 6,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
clearBecauseYouLikeCache,
|
||||||
|
readBecauseYouLikeCache,
|
||||||
|
writeBecauseYouLikeCache,
|
||||||
|
} from './becauseYouLikeCache';
|
||||||
|
|
||||||
|
describe('becauseYouLikeCache', () => {
|
||||||
|
it('invalidates when music library filter version changes', () => {
|
||||||
|
clearBecauseYouLikeCache();
|
||||||
|
writeBecauseYouLikeCache({
|
||||||
|
serverId: 'srv-1',
|
||||||
|
filterVersion: 1,
|
||||||
|
anchor: { id: 'a1', name: 'Artist' },
|
||||||
|
recs: [{ id: 'alb-1', name: 'Album', artist: 'Artist', artistId: 'a1', songCount: 1, duration: 1 }],
|
||||||
|
});
|
||||||
|
expect(readBecauseYouLikeCache('srv-1', 1)?.recs).toHaveLength(1);
|
||||||
|
expect(readBecauseYouLikeCache('srv-1', 2)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ export type BecauseYouLikeAnchor = { id: string; name: string };
|
|||||||
|
|
||||||
export type BecauseYouLikeSnapshot = {
|
export type BecauseYouLikeSnapshot = {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
filterVersion: number;
|
||||||
savedAt: number;
|
savedAt: number;
|
||||||
anchor: BecauseYouLikeAnchor;
|
anchor: BecauseYouLikeAnchor;
|
||||||
recs: SubsonicAlbum[];
|
recs: SubsonicAlbum[];
|
||||||
@@ -14,9 +15,10 @@ let snapshot: BecauseYouLikeSnapshot | null = null;
|
|||||||
|
|
||||||
export function readBecauseYouLikeCache(
|
export function readBecauseYouLikeCache(
|
||||||
serverId: string | null | undefined,
|
serverId: string | null | undefined,
|
||||||
|
filterVersion: number,
|
||||||
): BecauseYouLikeSnapshot | null {
|
): BecauseYouLikeSnapshot | null {
|
||||||
if (!serverId || !snapshot) return null;
|
if (!serverId || !snapshot) return null;
|
||||||
if (snapshot.serverId !== serverId) return null;
|
if (snapshot.serverId !== serverId || snapshot.filterVersion !== filterVersion) return null;
|
||||||
if (Date.now() - snapshot.savedAt > TTL_MS) return null;
|
if (Date.now() - snapshot.savedAt > TTL_MS) return null;
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user