sidebar and artist pipeline: one list in the rail, recents anchored to ids, and popular takes its own lane
This commit is contained in:
1 parent
606250c361
commit
eba29824c9
12 files changed
+1595
-281
No files matched your search
+24
-1
@@ -67,7 +67,25 @@ export const playerControl = (action, value, rate) =>
|
||||
export const waveform = (path, buckets) => invoke("waveform", { path, buckets });
|
||||
export const libraryAlbums = () => invoke("library_albums");
|
||||
export const albumTracks = (albumId) => invoke("album_tracks", { albumId });
|
||||
export const recordPlay = (artist, title) => invoke("record_play", { artist, title });
|
||||
// `path` anchors the play to a library album via tracks.path (UNIQUE). Without
|
||||
// it the row still records, but with no album — so it cannot appear in recents.
|
||||
// `cat` carries catalog identity for a STREAMED play, which never touches the
|
||||
// local library: { albumId, albumTitle, albumArtist, coverUrl }.
|
||||
export const recordPlay = (artist, title, path, cat = {}) =>
|
||||
invoke("record_play", {
|
||||
artist,
|
||||
title,
|
||||
path: path ?? null,
|
||||
catalogAlbumId: cat.albumId != null ? String(cat.albumId) : null,
|
||||
albumTitle: cat.albumTitle ?? null,
|
||||
albumArtist: cat.albumArtist ?? null,
|
||||
coverUrl: cat.coverUrl ?? null,
|
||||
});
|
||||
// Destructive, not undoable: wipes `plays` only. Returns rows removed.
|
||||
export const clearPlayHistory = () => invoke("clear_play_history");
|
||||
export const playRowCount = () => invoke("play_row_count");
|
||||
export const recentAlbums = (limit, minSecs) =>
|
||||
invoke("recent_albums", { limit: limit ?? null, minSecs: minSecs ?? null });
|
||||
export const addPlaySeconds = (playId, secs) => invoke("add_play_seconds", { playId, secs });
|
||||
export const artistPlayStats = (artist) => invoke("artist_play_stats", { artist });
|
||||
// The WebView blocks target="_blank"; external links open via the backend.
|
||||
@@ -110,6 +128,11 @@ export const catalogArtistHeroSeed = (artistId) =>
|
||||
invoke("catalog_artist_hero_seed", { artistId });
|
||||
export const catalogArtistOverview = (artistId) =>
|
||||
checkedArtistPayload("catalog_artist_overview", artistId);
|
||||
// Popular's fast lane: the Last.fm-ordered rows with listener counts, waiting
|
||||
// on nothing but the linked MBID and one Last.fm call. Patched over the
|
||||
// overview's budgeted (cold: unranked) list; the full page still lands last.
|
||||
export const catalogArtistPopular = (artistId) =>
|
||||
invoke("catalog_artist_popular", { artistId });
|
||||
export const pickArtistHero = (artistId) => invoke("pick_artist_hero", { artistId });
|
||||
export const clearArtistHero = (artistId) => invoke("clear_artist_hero", { artistId });
|
||||
export const catalogRelated = (artistId) => invoke("catalog_related", { artistId });
|
||||
|
||||
+63
-1
@@ -105,6 +105,27 @@ export async function refreshPlaylists() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- recently played (the sidebar's list) ----
|
||||
// A play row is opened after ~1s of audio, so without a floor this list is a
|
||||
// log of skips. 45s is the floor the live table was measured against.
|
||||
export const RECENTS_FLOOR_SECS = 45;
|
||||
const RECENTS_LIMIT = 20;
|
||||
|
||||
export const recentsStore = $state({
|
||||
albums: [], // [{ album_id, artist, title, art_path, last_played_at }]
|
||||
loaded: false,
|
||||
});
|
||||
|
||||
export async function refreshRecents() {
|
||||
try {
|
||||
recentsStore.albums = await api.recentAlbums(RECENTS_LIMIT, RECENTS_FLOOR_SECS);
|
||||
} catch {
|
||||
// backend not available
|
||||
} finally {
|
||||
recentsStore.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
const MIB = 1024 * 1024;
|
||||
|
||||
function isPending(value) {
|
||||
@@ -2071,6 +2092,26 @@ export async function openCatalogArtist(artistId, seedArtist = null) {
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Popular's fast lane. The overview paints the section immediately (its
|
||||
// supplements are budgeted), so a cold page's rows start in Deezer's
|
||||
// order with no listener counts. The real list — Last.fm order and
|
||||
// counts — needs none of the dates/recordings work the full page also
|
||||
// waits for, so it is asked for on its own and patched in the moment it
|
||||
// lands (~2s cold, instant warm). Last writer wins against the overview:
|
||||
// warm, the two lists are computed from identical inputs and cannot
|
||||
// differ. The full page stays authoritative once it arrives, and a
|
||||
// replayed page already shows the last visit's FINAL list — patching a
|
||||
// pool-shaped list over either would be a downgrade.
|
||||
void api.catalogArtistPopular(artistId)
|
||||
.then((tracks) => {
|
||||
if (!active() || !tracks?.length) return;
|
||||
const current = catalog.artistPage;
|
||||
if (!current || !sameArtistId(current.artist?.id, artistId)) return;
|
||||
if (current.art_preview === false || current.replayed) return;
|
||||
catalog.artistPage = { ...current, popular_tracks: tracks };
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
try {
|
||||
// Overview, identity art, and the first hero all remain immediate.
|
||||
// Give rapid back-to-back navigation a brief chance to settle before
|
||||
@@ -4116,7 +4157,12 @@ function listenTick(positionSecs) {
|
||||
if (!title || !artist || artist.includes("…")) return;
|
||||
const key = `${artist}\u0000${title}`;
|
||||
if (listenSession?.key !== key) {
|
||||
// The outgoing track's final seconds go in here — which is the moment
|
||||
// its album can cross the floor. Re-read once that write has landed;
|
||||
// waiting only on the fixed timer below meant a track that crossed 45s
|
||||
// late never showed up until something else happened to refresh.
|
||||
listenFlush();
|
||||
setTimeout(refreshRecents, 1500);
|
||||
listenSession = { key, artist, title, pending: 0, playIdP: null };
|
||||
listenLastPos = null;
|
||||
}
|
||||
@@ -4125,8 +4171,24 @@ function listenTick(positionSecs) {
|
||||
if (delta <= 0 || delta > 3) return;
|
||||
listenSession.pending += delta;
|
||||
if (!listenSession.playIdP && listenSession.pending >= 1) {
|
||||
listenSession.playIdP = api.recordPlay(artist, title);
|
||||
// Two possible anchors, both exact ids, never a name. A local track is
|
||||
// tied to its library album through tracks.path; a STREAMED one — which
|
||||
// is most listening here — through the catalog album id, carrying its
|
||||
// own title/artist/cover because there is no local row to read them
|
||||
// back from.
|
||||
const q = player.queue?.[player.queueIndex] ?? null;
|
||||
listenSession.playIdP = api.recordPlay(artist, title, t?.path ?? null, {
|
||||
albumId: player.catalogRef?.albumId ?? null,
|
||||
albumTitle: q?.album ?? null,
|
||||
albumArtist: q?.artist ?? null,
|
||||
coverUrl: q?.artUrl ?? null,
|
||||
});
|
||||
listenSession.playIdP.catch(() => {});
|
||||
// The row is opened at ~1s but only earns its place in recents once it
|
||||
// passes the floor, so re-read after the track has had time to count.
|
||||
listenSession.playIdP
|
||||
.then(() => setTimeout(refreshRecents, RECENTS_FLOOR_SECS * 1000 + 2000))
|
||||
.catch(() => {});
|
||||
}
|
||||
if (listenSession.pending >= LISTEN_FLUSH_SECS) listenFlush();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user