Commit Graph
24 Commits
Author SHA1 Message Date
François Laignel 62cc4f9200 remove ring from the dependency tree
The crate `ring` was transitively pulled due to the feature `rustls-tls` being
selected for `reqwest`. In order to ensure `ring` is not pulled,
`rustls-tls-webpki-roots-no-provider` must be used instead.
2026-07-26 21:12:44 +02:00
Afonso Jorge Ramos 96672a459d fix(log): stop logging Last.fm auth token prefixes
Request-token material must never appear in support log bundles.
2026-07-14 13:33:04 +02:00
Victor RH b99598e122 Merge pull request #582 from afonsojramos/bugfix/internal/log-redaction-all-sinks
fix(log): redact secrets on stderr and stop Last.fm body dumps
2026-07-12 19:39:51 -06:00
Afonso Jorge Ramos 66e6efeffd fix(log): redact secrets on stderr and stop Last.fm body dumps
TeeLogger no longer forwards the original Record to env_logger (which
bypassed redaction). Register restored OAuth tokens for literal scrubbing,
and never debug-log the auth.getSession response body.
2026-07-11 23:01:04 +02:00
Afonso Jorge Ramos 7a600a60f4 ci: add workspace cargo test job (exclude Slint UI graph)
Run unit/doc tests for the shipping crates workspace on PRs without
compiling qbz-ui. Fix three green-blockers: local playlist schema for
tests, Track.work fixture, and a stale ListenBrainz doctest.
2026-07-10 00:52:05 +02:00
vicrodh 0347e333a8 feat(reco): redesign Recommendations tab — artist/album-centric + LB playlists
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
2026-06-27 08:34:24 -06:00
vicrodh 408b3c8308 feat(reco): external-recommendations engine (Last.fm + ListenBrainz -> Qobuz)
New frontend-agnostic crate qbz-external-reco: blends Last.fm + ListenBrainz
recommendation signals into the Discover "External Recommendations" carousels,
validating every candidate against the Qobuz catalog (ISRC direct -> MusicBrainz
inc=isrcs bridge -> fuzzy text scorer, gated) before display. Four carousels
(C1 similar artists / C2 similar tracks you haven't heard, C3 listened-but-not-
recently, C4 deep cuts from known artists) capped at 20 with daily rotation over
a larger cache-backed pool, plus a Qobuz-editorial cold-start fallback (top
albums + artists) when no external source is connected. Round-robin blend across
sources. Resolution cache (per-user SQLite, 30d hits / 7d misses, WAL).

Supporting public read methods added to qbz-integrations:
- Last.fm: get_top_artists / get_top_tracks / get_loved_tracks /
  get_recent_tracks / get_similar_tracks (public reads via the Cloudflare proxy).
- ListenBrainz: get_cf_recommendations / get_recent_listens /
  get_metadata_recordings (204/404 -> empty, never error).
- MusicBrainz: get_recording_isrcs (the MBID -> ISRC bridge).

Engine is headless and unit-tested (matching + cache); no qbz-ui dependency.

cc
2026-06-27 02:37:32 -06:00
vicrodh ef38e7a04e feat(slint): port Discord Rich Presence (opt-in) from Tauri
Frontend-agnostic core in qbz-integrations::discord (DiscordRpc +
NowListening) — the Tauri backend minus the tauri::command/State
wrappers: lazy IPC connect, Listening activity (details/state/assets +
"Get QBZ" button + timestamps), and the opt-in Flatpak socket bridge.
qbz-slint discord_rpc controller: process-global client, init AFTER the
session is active (PR #477 persistence fix — both online and offline
entry, never at early boot), push on the track-change and play/pause
edges, clear on logout. Opt-in toggle in Settings > Integrations
(DiscordState, persisted in ui_prefs.discord_rpc_enabled) + a Flatpak
socket-access note. The opt-in is per-machine (ui_prefs) rather than
Tauri's per-user.

cc
2026-06-22 22:56:05 -06:00
vicrodh 3c2f46b756 feat(locallibrary): tag editor remote lookup (MusicBrainz + Discogs)
Complete the tag editor with remote metadata auto-fill.

- qbz-integrations: add frontend-agnostic `discogs` (client + DTOs, Tauri-free)
  and `remote_metadata` (unified DTOs + the pure MusicBrainz/Discogs ->
  RemoteAlbum* converters). Copied from src-tauri so the Slint frontend can do
  remote lookup without depending on the Tauri binary; src-tauri is left fully
  untouched (the production build keeps its own copy — no Tauri-build risk).
- tag_editor.rs: search_remote (provider QbzSelect; MusicBrainzClient::new
  search_releases_extended / DiscogsClient search_releases, generation-guarded),
  select_result, apply_remote (get_release_with_tracks / get_release_metadata ->
  full metadata; overwrites album fields + positional per-track title merge;
  track-count-mismatch + rate-limit toasts), open_in_browser.
- TagEditorModal: a remote-lookup box (provider + Search, a horizontal result
  card strip with select highlight, Apply + open-in-browser). ADR-008: square
  badges, no pills.
- state.slint: RemoteResultItem struct + remote fields/actions on TagEditor*.

LocalLibrary Slint port is now feature-complete (scan/folders, artists images,
tag editor manual+remote, tracks bulk). BUILT BLIND — runtime smoke-test pending.

cc
2026-05-29 15:32:16 -06:00
vicrodh a2d8e43b1c fix(musicbrainz): query MusicBrainz directly per-IP instead of via the proxy
The Cloudflare proxy funnelled every user's MusicBrainz traffic onto a few
egress IPs, and MusicBrainz rate-limits 1 req/s per IP, so the budget was
shared across the whole userbase and produced 503s that worsen with growth.
MB read access needs no key and authentication does not raise the limit, so
the proxy added only the funnel; going per-IP makes the budget per-user.

- qbz-integrations MusicBrainzConfig default is now use_proxy=false (direct).
- client: handle HTTP 503 + Retry-After (not just 429, which the proxy used
  to translate); add parse_retry_after; route search_recording_by_isrc and
  search_artist through handle_response_status.
- src-tauri session_lifecycle: MusicBrainz V2 init/teardown pass use_proxy=false.

Slint inherits this via CoreBridge's default-constructed client.

cc
2026-05-28 14:12:12 -06:00
vicrodh 165674d8a8 feat(slint): you-may-also-like discovery section with sticky dismiss
Closes the network sidebar with the fifth section — tag-based "You may
also like" discovery, with a per-tag dismiss store so a thumbs-down
sticks across artist pages that share the same primary genre tag.

Backend (`qbz-integrations` + `qbz-core`):
  - DiscoveryArtist and DiscoveryResponse DTOs added to
    qbz-integrations::musicbrainz::models.
  - QbzCore::musicbrainz_discover_artists ports
    v2_get_discovery_artists end to end (sans the RecoState local
    "play count > 2" filter, which has no equivalent in the Slint
    shell yet): primary-tag search, exclude seed + similar +
    dismissed, deterministic shuffle keyed on seed_mbid, validate
    each candidate on Qobuz, fall back to the secondary tag if we
    have fewer than 5 matches.
  - normalize_artist_name re-exported at the qbz-core crate root so
    the dismiss handler in qbz-slint can use the same key shape that
    the discovery pipeline filters on.
  - Deterministic shuffle implemented with a simple xorshift PRNG
    seeded from a stable hash of seed_mbid — keeps qbz-core free of
    the rand dep.

Slint controller (`crates/qbz-slint/src/`):
  - discovery_dismiss.rs — small JSON store at
    `<data-dir>/qbz/discovery_dismiss.json` keyed by lowercased tag
    -> list of normalized artist names. dismissed_for_tag is passed
    into the discovery call as the filter source; dismiss() is
    invoked by the thumbs-down handler.
  - artist::load_mb_discovery / apply_mb_discovery /
    remove_discovery_artist plumb the pipeline result through the
    standard load-off-thread / apply-on-UI split. Discovery state
    includes primary_tag so the dismiss handler can persist under
    the right key.
  - main.rs chains discovery after relationships in the same MB
    enrichment task. NetworkSidebarActions.discovery-dismissed (now
    carrying mbid + name) normalizes the name, persists the
    dismiss, and removes the row from the visible list.

Slint UI (`ui/state.slint`, `ui/artist/ArtistPageView.slint`):
  - discovery-tag property added to NetworkSidebarState.
  - YOU MAY ALSO LIKE section after Relationships — one row per
    candidate (SidebarLink + a 24x24 thumbs-down button on the
    right). Clicking the link navigates via the artist-clicked
    callback when a Qobuz id is present.
  - thumbs-down.svg icon added to ui/assets/icons (Lucide
    ThumbsDown).

cc
2026-05-20 10:39:53 -06:00
vicrodh 508609a5d8 refactor(integrations): move MB location and genre helpers to qbz-integrations
Pure-logic move of two helper modules out of src-tauri/src/musicbrainz/
into the qbz-integrations crate, where the MusicBrainzClient and DTOs
already live:

  src-tauri/src/musicbrainz/location_discovery.rs
    -> crates/qbz-integrations/src/musicbrainz/location.rs
  src-tauri/src/musicbrainz/genre_normalization.rs
    -> crates/qbz-integrations/src/musicbrainz/genre.rs

The functions exposed are unchanged: extract_metadata,
compute_affinity_score, build_scene_cache_key, format_life_span_date
(from location); normalize_genre, is_broad_genre, extract_affinity_seeds,
genre_summary (from genre). Imports in src-tauri/src/commands_v2/
integrations.rs are repointed to the new home; the legacy file
declarations are removed from src-tauri/src/musicbrainz/mod.rs. No
behavior change.

Unblocks the Slint side from reaching the same helpers via qbz-core
without depending on src-tauri.

cc
2026-05-20 08:50:50 -06:00
vicrodh c3ce3c6667 fix(ci): drop openssl dependency, use rustls-only for all crates
reqwest default features included native-tls which pulled in
openssl-sys. This broke zigbuild cross-compilation because zig's
C compiler doesn't support the glibc version suffix for openssl
header expansion.

Fix: default-features = false on reqwest in workspace and
qbz-integrations (which had its own reqwest dep with defaults).
Now the entire daemon tree uses rustls-tls exclusively.

cc
2026-04-09 06:48:37 -06:00
vicrodh 1e019becfd fix: resolve correct artist and country for MusicBrainz metadata
Three fixes:
- resolve_artist now prefers exact name matches over highest score,
  fixing "The Warning" resolving to "Fates Warning" (score=100 but
  wrong name vs score=97 exact match)
- resolve_area_country now returns ISO country code alongside name,
  fixing flag display (was showing US flag for German artists)
- country_code updated in location metadata for correct flag rendering

cc
2026-03-31 17:23:00 -06:00
vicrodh 1953774d03 fix: resolve birth country from area hierarchy instead of MB country field
Add resolve_area_country() that walks the MB area hierarchy up to
the country level (e.g., Frankfurt -> Hessen -> Germany).

Use it in v2_musicbrainz_get_artist_metadata to get the real birth
country instead of MB's "country" field (which is where the artist
is active, not where they were born).

Hans Zimmer now shows "Frankfurt am Main, Germany" and clicking
the location shows German composers, not American ones.

cc
2026-03-31 17:17:01 -06:00
vicrodh de29f1e2e0 chore(qconnect): checkpoint current poc state
cx
2026-03-13 17:28:05 -06:00
vicrodh f473065c21 feat: port full MusicBrainz API to V2 crate + add cache persistence
V2 MusicBrainz client now has all methods from legacy:
- search_recording, get_artist_with_relations, get_artist_tags
- search_artists_by_tag, search_artists_by_tag_and_area
- search_release_by_barcode, search_releases_extended
- get_release_with_tracks, browse_artists_by_area
- search_area, get_area_with_relations, resolve_parent_subdivision

V2 MusicBrainz cache now has:
- Settings persistence (is_enabled/set_enabled)
- All legacy cache tables (relations, metadata, scene, qobuz_validation)
- clear_all, cleanup_expired, get_stats

V2 ListenBrainz cache adds:
- mark_listens_sent (batch), get_queue_count, clear_queue

V2 state wrappers (integrations_v2.rs) now include:
- Cache field on both MusicBrainzV2State and ListenBrainzV2State
- init_cache_at, init_from_cache, teardown methods
- ListenBrainzV2State credentials auto-persist to V2 cache

V2 models include all types from legacy (ArtistRelationships,
ArtistMetadata, LocationDiscoveryResponse, ResolvedMusician, etc.)
2026-03-10 14:14:45 -06:00
vicrodh 572d23b935 feat: replace ListenBrainz co-listening with MusicBrainz tag-based discovery
ListenBrainz similar-artists returned mainstream crossover artists (SOAD,
Led Zeppelin for Metallica) instead of genre-accurate results. Now uses
MusicBrainz tag search to find artists sharing the seed's primary genre tag
(e.g. "thrash metal" returns Megadeth, Slayer, Kreator, etc).

- Add search_artists_by_tag to MusicBrainz client
- Deterministic shuffle using seed MBID hash for variety across artists
- Secondary tag fallback when primary yields fewer than 5 results
- Rename label to "You May Also Like", limit results to 6
- Add Last.fm get_similar_artists method (unused, for future use)
- Add tags field to ArtistFullResponse model
- Tighten local history exclusion to play_count > 2
2026-03-08 21:30:01 -06:00
vicrodh 133defbafa fix(security): redact sensitive identifiers and secrets from backend logs
cx
2026-03-01 09:24:42 -06:00
vicrodh ddedca1b5e chore: fix remaining Rust warnings
- Allow dead_code on TokenValidationResponse.code (deserialized but not read)
- Remove unused MusicProvider import in commands_v2.rs
- Restore #[allow(non_snake_case)] on v2_plex_cache_get_tracks
- Replace drop(tokens) with let _ = tokens (drop on reference is no-op)
2026-02-25 20:57:30 -06:00
vicrodh 56b4d467e3 fix(listenbrainz): fix error display, persistence, and build warning
- Remove unused ListenBrainzResponse struct (build warning)
- Fix error display showing [object Object] instead of actual message
- Persist credentials to legacy SQLite cache on connect/disconnect
  so they survive app restarts
2026-02-25 19:13:15 -06:00
vicrodh 59e3141aa6 fix: address 7 code review findings for V2 architecture
HIGH priority fixes:
- Enable WAL mode in qbz-integrations caches (ADR-002)
- v2_auto_login/v2_manual_login now call activate_session()
- RequiresUserSession allows offline sessions (only checks session_activated)

MEDIUM priority fixes:
- Consolidate lifecycle: legacy commands delegate to session_lifecycle
- ListenBrainz teardown now calls disconnect() to clear tokens
- Add SessionActivationFailed degraded reason and emit event on failure
2026-02-15 12:21:17 -06:00
vicrodh 0c50c67a61 feat(v2): add integrations V2 commands using qbz-integrations crate
Wire qbz-integrations to Tauri via new V2 commands:

ListenBrainz V2:
- v2_listenbrainz_get_status, is_enabled, set_enabled
- v2_listenbrainz_connect, disconnect
- v2_listenbrainz_now_playing, scrobble

MusicBrainz V2:
- v2_musicbrainz_is_enabled, set_enabled
- v2_musicbrainz_resolve_track, resolve_artist

Last.fm V2:
- v2_lastfm_get_auth_url, complete_auth
- v2_lastfm_is_authenticated, disconnect
- v2_lastfm_now_playing, scrobble

New module integrations_v2.rs provides Tauri state wrappers for the
Tauri-independent clients from qbz-integrations crate.
2026-02-15 11:11:35 -06:00
vicrodh bd30e7cf24 feat(qbz-integrations): create third-party integrations crate
New crate for Last.fm, ListenBrainz, and MusicBrainz integrations.
Designed to work WITHOUT Tauri for TUI/headless clients.

Modules:
- lastfm: Scrobbling and now-playing via Cloudflare Workers proxy
- listenbrainz: Direct API client with user token auth
- musicbrainz: Entity resolution with rate limiting

Features:
- `cache` (default): SQLite persistence for offline support
  - ListenBrainz: Credential storage, offline listen queue
  - MusicBrainz: Resolved entity cache with TTL

Architecture:
- Pure async Rust, no Tauri dependencies
- Shared error types via IntegrationError enum
- Rate limiting for MusicBrainz (1 req/sec direct, faster via proxy)
2026-02-15 10:36:53 -06:00