mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
feat(tracks): Highly Rated rail + per-card star display (#443)
* feat(tracks): Highly Rated rail + per-card star display Adds a new SongRail above the Random Pick on the Tracks page that surfaces the user's highly-rated tracks (sorted by rating DESC). Auto-hides on non-Navidrome servers and when the library has no rated tracks yet. Reuses the existing SongRail layout, with the standard reroll button forcing a cache bypass. Per-card stars: any SongCard whose `userRating > 0` now shows a small five-star row (filled to the rating value) below the artist line — visible everywhere SongCard is used, not only in the new rail. Read-only display; rating is still done via the row's context menu or the Now Playing star widget. Cache layer in `ndListSongs`: opt-in `cacheMs` parameter (skipped by VirtualSongList; used only by the Highly Rated rail with a 60 s TTL). Cleared on `setRating` mutation so a freshly-rated track shows up on the next page revisit, and on server switch alongside the existing token cache. The reroll button explicitly invalidates before refetching, so a manual refresh always hits the network. * docs(changelog): add #443 Tracks Highly Rated rail entry * chore(credits): add #443 to Psychotoxical contributions
This commit is contained in:
committed by
GitHub
parent
98ff73d17a
commit
1799e90e04
@@ -54,21 +54,45 @@ function mapNdSong(o: Record<string, unknown>): SubsonicSong {
|
||||
};
|
||||
}
|
||||
|
||||
export type NdSongSort = 'title' | 'artist' | 'album' | 'recently_added' | 'play_count';
|
||||
export type NdSongSort = 'title' | 'artist' | 'album' | 'recently_added' | 'play_count' | 'rating';
|
||||
|
||||
/** Optional opt-in cache for `ndListSongs` — keyed by call signature + active server. */
|
||||
type SongsCacheEntry = { data: SubsonicSong[]; expiresAt: number };
|
||||
const songsCache = new Map<string, SongsCacheEntry>();
|
||||
|
||||
function songsCacheKey(
|
||||
baseUrl: string, start: number, end: number, sort: string, order: string,
|
||||
): string {
|
||||
return `${baseUrl}|${start}-${end}|${sort}|${order}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a sorted, paginated slice of all songs via Navidrome's native REST API.
|
||||
* Returns mapped SubsonicSong objects. Throws on auth failure or non-Navidrome.
|
||||
*
|
||||
* `cacheMs` (> 0) opts in to a per-call-signature in-memory cache. Skip for
|
||||
* paginated browsing — only useful for stable-list rails (e.g. Highly Rated)
|
||||
* where a brief staleness window is acceptable in exchange for skipping the
|
||||
* roundtrip on every page revisit.
|
||||
*/
|
||||
export async function ndListSongs(
|
||||
start: number,
|
||||
end: number,
|
||||
sort: NdSongSort = 'title',
|
||||
order: 'ASC' | 'DESC' = 'ASC',
|
||||
cacheMs?: number,
|
||||
): Promise<SubsonicSong[]> {
|
||||
const baseUrl = useAuthStore.getState().getBaseUrl();
|
||||
if (!baseUrl) throw new Error('No server configured');
|
||||
|
||||
const cacheKey = (cacheMs && cacheMs > 0)
|
||||
? songsCacheKey(baseUrl, start, end, sort, order)
|
||||
: null;
|
||||
if (cacheKey) {
|
||||
const hit = songsCache.get(cacheKey);
|
||||
if (hit && hit.expiresAt > Date.now()) return hit.data;
|
||||
}
|
||||
|
||||
const callOnce = async (token: string): Promise<unknown> =>
|
||||
invoke<unknown>('nd_list_songs', { serverUrl: baseUrl, token, sort, order, start, end });
|
||||
|
||||
@@ -88,10 +112,21 @@ export async function ndListSongs(
|
||||
}
|
||||
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.map(s => mapNdSong(s as Record<string, unknown>));
|
||||
const data = raw.map(s => mapNdSong(s as Record<string, unknown>));
|
||||
|
||||
if (cacheKey && cacheMs && cacheMs > 0) {
|
||||
songsCache.set(cacheKey, { data, expiresAt: Date.now() + cacheMs });
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Drop the cached token — call when the active server changes. */
|
||||
/** Drop the cached token AND the songs cache — call when the active server changes. */
|
||||
export function ndClearTokenCache(): void {
|
||||
cachedToken = null;
|
||||
songsCache.clear();
|
||||
}
|
||||
|
||||
/** Drop the songs cache only (e.g. after a rating mutation). */
|
||||
export function ndInvalidateSongsCache(): void {
|
||||
songsCache.clear();
|
||||
}
|
||||
|
||||
@@ -959,6 +959,11 @@ export async function searchSongsPaged(query: string, songCount: number, songOff
|
||||
|
||||
export async function setRating(id: string, rating: number): Promise<void> {
|
||||
await api('setRating.view', { id, rating });
|
||||
// Cached song lists keyed by rating (e.g. Tracks → Highly Rated rail) become
|
||||
// stale immediately. Lazy-import to keep the module dep direction
|
||||
// subsonic ← navidromeBrowse and avoid pulling Tauri internals into shared
|
||||
// type-only consumers.
|
||||
void import('./navidromeBrowse').then(m => m.ndInvalidateSongsCache()).catch(() => {});
|
||||
}
|
||||
|
||||
/** How aggressively we assume `setRating` accepts album/artist ids (OpenSubsonic-style). */
|
||||
|
||||
Reference in New Issue
Block a user