metadata: per-track listeners, and an artist profile the search hero can ask for

CatalogTrack gains artist_id — Deezer sends it on every search row and it was
simply unparsed — and rank, its 0-1,000,000 popularity figure. Together they
let a listener count be resolved for the PERFORMING artist of each row rather
than for the query's top artist, so a "panda bear" search shows Daft Punk's
"Doin' it Right (feat. Panda Bear)" at its real 464,046.

Resolution rides the existing deezer_linked_mbid -> lastfm_listener_ranks chain
behind a new catalog_track_listeners command, deliberately kept separate from
catalog_search: that one is typed against per keystroke and must never wait on
MusicBrainz's 1 req/s gate.

catalog_artist_profile backs the hero's middle third — MusicBrainz tags off the
cached artist document plus one Last.fm artist.getinfo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
KeelrLeekandClaude Opus 5 committed 2026-08-03 07:03:03 -05:00
1 parent f8f56f4398
commit dc918c98ef
3 files changed
+285

No files matched your search

+55
View File
@@ -2533,6 +2533,46 @@ pub async fn catalog_artist_popular(
.ok_or_else(|| "artist popular lookup failed".to_string())
}
/// Genres and Last.fm totals for the artist a search is about — the search
/// hero's stat line and tag chips. Deferred like every other supplement: it
/// rides MusicBrainz's gate and must never hold up the results.
#[tauri::command]
pub async fn catalog_artist_profile(
state: State<'_, LibraryState>,
artist_id: u64,
) -> Result<metadata::CatalogArtistProfile, String> {
state.touch_catalog_activity();
Ok(state.metadata.artist_profile(artist_id).await)
}
/// One search-result row, reduced to what a listener lookup needs.
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrackListenerQuery {
/// The PERFORMING artist's Deezer id. `None` for a row the provider gave no
/// artist id for, which simply gets no count — never a guess from the name.
pub artist_id: Option<u64>,
pub title: String,
}
/// Last.fm listener counts for a set of search-result rows, answered in order.
///
/// Deferred on purpose: `catalog_search` is typed against per keystroke and must
/// stay three Deezer calls, so the counts are asked for separately once the
/// results are on screen. See `MetadataClient::track_listeners`.
#[tauri::command]
pub async fn catalog_track_listeners(
state: State<'_, LibraryState>,
tracks: Vec<TrackListenerQuery>,
) -> Result<Vec<Option<u64>>, String> {
state.touch_catalog_activity();
let rows: Vec<(Option<u64>, String)> = tracks
.into_iter()
.map(|t| (t.artist_id, t.title))
.collect();
Ok(state.metadata.track_listeners(&rows).await)
}
#[tauri::command]
pub async fn catalog_related(
state: State<'_, LibraryState>,
@@ -4217,6 +4257,21 @@ pub async fn waveform(
result
}
/// What the audio thread is doing right now.
///
/// The webview learns about playback only from events, so a RELOAD leaves it
/// believing nothing is playing while audio carries on — the scrubber moved
/// under the words "Nothing playing". This is the boot-time answer, the same
/// shape of recovery `app_status` already provides for the Soulseek connection.
#[tauri::command]
pub fn player_state(players: State<'_, PlayerState>) -> crate::playback::PlaybackSnapshot {
players
.last_transport
.lock()
.map(|snapshot| snapshot.clone())
.unwrap_or_default()
}
/// Player transport: action ∈ pause | resume | stop, or seek/volume with value.
#[tauri::command]
pub fn player_control(