Reworks the 4th Discover tab per owner feedback (the track-heavy v1 surfaced already-heard artists). Verified against three sources that Last.fm has NO recommendations API (getRecommendedArtists removed; web is PJAX HTML behind the website cookie, which the API session key cannot reach) — so recommendations are replicated from the documented similarity recipe, and ListenBrainz provides the genuinely clean personalized feeds. New lineup (each row paints progressively as its builder resolves): - Recommended Artists (Last.fm artist.getSimilar seeded by last-month top, minus the heard set; "Similar to X, Y" subtitle on the artist card). - Recommended Albums (Last.fm artist.getTopAlbums minus your scrobbled albums). - Fresh Releases (ListenBrainz, from artists you follow). - Weekly Exploration + Weekly Jams (ListenBrainz curated playlists, by source_patch). - Deep-cut albums from artists you know (Qobuz catalog, not heard in-app). - Cold-start fallback: Qobuz editorial top albums + artists. The "not heard" filter is now ARTIST-level (the v1 bug derived artists from unheard TRACKS) and compares against the LOCAL reco history + a light Last.fm/LB top-set, not a full history sweep. Album validation (UPC -> fuzzy text) added. qbz-integrations gains Last.fm get_artist_top_albums/get_user_top_albums and ListenBrainz get_created_for_playlists/get_playlist_tracks/get_fresh_releases. NOTE: the Cloudflare proxy was switched to allow ALL Last.fm read methods (anything with ".get"), so future reads need NO proxy redeploy — but the live proxy must be redeployed ONCE with this change or all Last.fm reads keep 400ing. cargo check -p qbz green. cc
140 lines
4.9 KiB
Plaintext
140 lines
4.9 KiB
Plaintext
// Discover > Recommendations — the 4th tab.
|
|
//
|
|
// External-recommendations engine (Last.fm + ListenBrainz -> Qobuz). Artist /
|
|
// album / track carousels, each with a `length > 0` self-hide gate; the
|
|
// controller paints each row the moment its builder resolves (progressive).
|
|
// Data lives in ExternalRecoState (crate::external_reco). Reuses the shared
|
|
// Carousel / ArtistCarousel / SlimCarousel components.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { ExternalRecoState } from "../state.slint";
|
|
import { Carousel } from "Carousel.slint";
|
|
import { SlimCarousel } from "SlimCarousel.slint";
|
|
import { ArtistCarousel } from "ArtistCarousel.slint";
|
|
import { HomeSkeleton } from "HomeSkeleton.slint";
|
|
|
|
export component ExternalRecoView inherits VerticalLayout {
|
|
callback open-album(string);
|
|
callback open-artist(string);
|
|
callback media-action(string, string, string);
|
|
callback play-track(string);
|
|
|
|
property <bool> any-content:
|
|
ExternalRecoState.rec-artists.length > 0
|
|
|| ExternalRecoState.rec-albums.albums.length > 0
|
|
|| ExternalRecoState.fresh-releases.albums.length > 0
|
|
|| ExternalRecoState.weekly-exploration.length > 0
|
|
|| ExternalRecoState.weekly-jams.length > 0
|
|
|| ExternalRecoState.deep-cut-albums.albums.length > 0
|
|
|| ExternalRecoState.top-albums.albums.length > 0
|
|
|| ExternalRecoState.top-artists.length > 0;
|
|
|
|
spacing: 40px;
|
|
alignment: start;
|
|
|
|
if ExternalRecoState.loading && !root.any-content: HomeSkeleton { }
|
|
|
|
// Recommended Artists (Last.fm similar, not heard) — with "Similar to…".
|
|
if ExternalRecoState.rec-artists.length > 0: ArtistCarousel {
|
|
title: @tr("Recommended Artists");
|
|
artists: ExternalRecoState.rec-artists;
|
|
artist-clicked(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
|
|
// Recommended Albums (Last.fm artist top-albums, not scrobbled).
|
|
if ExternalRecoState.rec-albums.albums.length > 0: Carousel {
|
|
section: ExternalRecoState.rec-albums;
|
|
album-clicked(id) => {
|
|
root.open-album(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
|
|
// Fresh Releases (ListenBrainz, from artists you follow).
|
|
if ExternalRecoState.fresh-releases.albums.length > 0: Carousel {
|
|
section: ExternalRecoState.fresh-releases;
|
|
album-clicked(id) => {
|
|
root.open-album(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
|
|
// Weekly Exploration (ListenBrainz discovery playlist).
|
|
if ExternalRecoState.weekly-exploration.length > 0: SlimCarousel {
|
|
title: @tr("Weekly Exploration");
|
|
items: ExternalRecoState.weekly-exploration;
|
|
item-clicked(id) => {
|
|
root.play-track(id);
|
|
}
|
|
}
|
|
|
|
// Weekly Jams (ListenBrainz familiar playlist).
|
|
if ExternalRecoState.weekly-jams.length > 0: SlimCarousel {
|
|
title: @tr("Weekly Jams");
|
|
items: ExternalRecoState.weekly-jams;
|
|
item-clicked(id) => {
|
|
root.play-track(id);
|
|
}
|
|
}
|
|
|
|
// Deep cuts from artists you know (Qobuz catalog albums).
|
|
if ExternalRecoState.deep-cut-albums.albums.length > 0: Carousel {
|
|
section: ExternalRecoState.deep-cut-albums;
|
|
album-clicked(id) => {
|
|
root.open-album(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
|
|
// Cold-start fallback — Qobuz editorial top albums.
|
|
if ExternalRecoState.top-albums.albums.length > 0: Carousel {
|
|
section: ExternalRecoState.top-albums;
|
|
album-clicked(id) => {
|
|
root.open-album(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
|
|
// Cold-start fallback — Qobuz editorial top artists.
|
|
if ExternalRecoState.top-artists.length > 0: ArtistCarousel {
|
|
title: @tr("Popular artists on Qobuz");
|
|
artists: ExternalRecoState.top-artists;
|
|
artist-clicked(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
|
|
// Empty state.
|
|
if !root.any-content && !ExternalRecoState.loading: VerticalLayout {
|
|
spacing: 8px;
|
|
Text {
|
|
text: @tr("No recommendations yet");
|
|
color: Theme.text-primary;
|
|
font-size: 18px;
|
|
font-weight: Typography.semibold;
|
|
}
|
|
Text {
|
|
text: @tr("Connect Last.fm or ListenBrainz in Settings, or play more music, to get personalized recommendations.");
|
|
color: Theme.text-muted;
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
}
|