audio streaming app plyr.fm
38
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: decouple for-you probe from cache, fix tag persistence across feeds (#1285)

three bugs from staging:

1. the probe $effect called forYouCache.fetch(), which synchronously
reads this.loading ($state) — creating a reactive dependency. any
cache state change (e.g. from setTags) re-triggered the probe, and
if tag-filtered results were empty, forYouAvailable flipped to false,
hiding the switcher. fix: use a raw fetch(limit=1) with no reactive
cache reads.

2. tag state wasn't shared between feeds. ForYouCache initialized
activeTags as empty, not from localStorage. switching feeds didn't
sync tags. tags set while in for-you mode weren't persisted. fix:
both caches initialize from localStorage.active_tags; toggleFeed
syncs tags from outgoing to incoming cache; onTagsChange persists
regardless of mode.

3. empty state said "no tracks yet" when tags filtered to zero. fix:
show "no tracks match these tags" when active tags are set.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

authored by

nate nowack
Claude Opus 4.6 (1M context)
and committed by
GitHub
24a59db9 25de94c7

+50 -23
+11 -1
frontend/src/lib/for-you.svelte.ts
··· 8 8 cold_start: boolean; 9 9 } 10 10 11 + function loadSavedTags(): string[] { 12 + if (typeof window === 'undefined') return []; 13 + try { 14 + const saved = localStorage.getItem('active_tags'); 15 + return saved ? JSON.parse(saved) : []; 16 + } catch { 17 + return []; 18 + } 19 + } 20 + 11 21 class ForYouCache { 12 22 tracks = $state<Track[]>([]); 13 23 loading = $state(false); ··· 15 25 nextCursor = $state<string | null>(null); 16 26 hasMore = $state(false); 17 27 coldStart = $state(false); 18 - activeTags = $state<string[]>([]); 28 + activeTags = $state<string[]>(loadSavedTags()); 19 29 20 30 private buildUrl(): URL { 21 31 const url = new URL(`${API_URL}/for-you/`);
+38 -21
frontend/src/routes/+page.svelte
··· 12 12 import { queue } from '$lib/queue.svelte'; 13 13 import { tracksCache, fetchTopTracks } from '$lib/tracks.svelte'; 14 14 import { forYouCache } from '$lib/for-you.svelte'; 15 + import { API_URL } from '$lib/config'; 15 16 import { networkArtistsCache } from '$lib/network-artists.svelte'; 16 17 import type { Track } from '$lib/types'; 17 18 import { auth } from '$lib/auth.svelte'; ··· 38 39 let loadingMore = $derived(feedMode === 'for-you' ? forYouCache.loadingMore : tracksCache.loadingMore); 39 40 let hasMore = $derived(feedMode === 'for-you' ? forYouCache.hasMore : tracksCache.hasMore); 40 41 let hasTracks = $derived(tracks.length > 0); 42 + let hasActiveTags = $derived( 43 + feedMode === 'for-you' ? forYouCache.activeTags.length > 0 : tracksCache.activeTags.length > 0 44 + ); 41 45 let initialLoad = $state(true); 42 46 43 47 // top tracks (most liked) ··· 140 144 } 141 145 }); 142 146 143 - // probe for-you feed availability when authenticated 147 + // probe for-you availability — lightweight fetch decoupled from cache 148 + // so tag filtering and cache state changes don't re-trigger this 144 149 $effect(() => { 145 150 if (auth.isAuthenticated) { 146 - forYouCache.fetch().then(() => { 147 - forYouAvailable = forYouCache.tracks.length > 0; 148 - // if user had for-you selected but it's now empty, fall back to latest 149 - if (feedMode === 'for-you' && !forYouAvailable) { 150 - feedMode = 'latest'; 151 - if (typeof window !== 'undefined') { 151 + fetch(`${API_URL}/for-you/?limit=1`, { credentials: 'include' }) 152 + .then((r) => (r.ok ? r.json() : null)) 153 + .then((data) => { 154 + forYouAvailable = (data?.tracks?.length ?? 0) > 0; 155 + if (!forYouAvailable && feedMode === 'for-you') { 156 + feedMode = 'latest'; 152 157 localStorage.setItem('feedMode', 'latest'); 153 158 } 154 - } 155 - }); 159 + // if starting in for-you mode, populate the cache 160 + if (forYouAvailable && feedMode === 'for-you') { 161 + forYouCache.fetch(); 162 + } 163 + }) 164 + .catch(() => { 165 + forYouAvailable = false; 166 + }); 156 167 } else { 157 168 forYouAvailable = false; 158 169 if (feedMode === 'for-you') { 159 170 feedMode = 'latest'; 160 - if (typeof window !== 'undefined') { 161 - localStorage.setItem('feedMode', 'latest'); 162 - } 171 + localStorage.setItem('feedMode', 'latest'); 163 172 } 164 173 } 165 174 }); ··· 210 219 } 211 220 212 221 function toggleFeed() { 222 + // sync tags from the cache we're leaving to the one we're entering 223 + const currentTags = 224 + feedMode === 'latest' ? [...tracksCache.activeTags] : [...forYouCache.activeTags]; 213 225 const next: FeedMode = feedMode === 'latest' ? 'for-you' : 'latest'; 214 226 feedMode = next; 215 - if (typeof window !== 'undefined') { 216 - localStorage.setItem('feedMode', next); 217 - } 218 - // fetch if the target cache is empty 219 - if (next === 'for-you' && forYouCache.tracks.length === 0) { 220 - forYouCache.fetch(); 221 - } else if (next === 'latest' && tracksCache.tracks.length === 0) { 222 - tracksCache.fetch(); 227 + localStorage.setItem('feedMode', next); 228 + 229 + // setTags resets pagination and fetches with the synced tags 230 + if (next === 'for-you') { 231 + forYouCache.setTags(currentTags); 232 + } else { 233 + tracksCache.setTags(currentTags); 223 234 } 224 235 } 225 236 ··· 354 365 } else { 355 366 tracksCache.setTags(tags); 356 367 } 368 + // persist tag selection regardless of feed mode 369 + if (tags.length > 0) { 370 + localStorage.setItem('active_tags', JSON.stringify(tags)); 371 + } else { 372 + localStorage.removeItem('active_tags'); 373 + } 357 374 }} 358 375 hiddenTags={preferences.hiddenTags} 359 376 /> ··· 364 381 <WaveLoading size="lg" message="loading tracks..." /> 365 382 </div> 366 383 {:else if !hasTracks} 367 - <p class="empty">no tracks yet</p> 384 + <p class="empty">{hasActiveTags ? 'no tracks match these tags' : 'no tracks yet'}</p> 368 385 {:else} 369 386 <div class="track-list"> 370 387 {#each tracks as track, i (track.id)}
+1 -1
loq.toml
··· 232 232 233 233 [[rules]] 234 234 path = "frontend/src/routes/+page.svelte" 235 - max_lines = 635 235 + max_lines = 652 236 236 237 237 [[rules]] 238 238 path = "frontend/src/lib/components/AlbumUploadForm.svelte"