Commit Graph
33 Commits
Author SHA1 Message Date
vicrodh 07e86d329d fix(integrations): Discord presence backs off for five minutes after a failed connect
With Discord closed, every now-playing edge (two per track: the immediate
publish and the poll's) tried the IPC socket and the crate logged
"find_pipe: could not find pipe" each time, so a listening session read
as an error stream. A failed connect now sets a five-minute deadline,
logs one line at info, and later edges push nothing until it passes;
opting the integration in again clears the deadline. The crate's own
warning is demoted to error in the default log filter. Pure backoff rule
covered by a unit test.

The Discover preferences store also logged "unavailable" when its lazy
init ran before the session restored; that startup order is normal and
the handle is rebound on activation, so it is a debug line unless a real
user directory fails to open.

cc
2026-09-13 18:35:09 -06:00
vicrodh 7e30ad769d fix(musicbrainz): identity-based artist matching, never an unrelated first result (#768)
Swim Deep's Artist page showed Deep Purple's founding year and members. Two
defects combined: `search_artist` emitted `artist:Swim Deep` unquoted (the
second word leaks into the global query; Deep Purple ranks first at 100,
Swim Deep second at 88), and `resolve_artist` fell back unconditionally to
the first result when no exact name scored >= 90. Name-keyed caches then
made such guesses durable and shareable across homonyms.

New `qbz-integrations::musicbrainz::identity` — pure decisions plus
orchestration over injected fetcher/store/catalog boundaries:
- query `artist:"<lucene-escaped name>"`, URL-encoded once;
- containment rule: exactly one exact-name candidate with score >= 90, else
  None — the first-result fallback no longer exists;
- source direction (Qobuz artist id -> MBID): the page's own Popular-Track
  ISRCs (max 3, guests excluded) vote among the exact-name candidates using
  every credit of every recording that carries the ISRC; ties, conflicts and
  failures fail closed;
- scene direction (MBID + catalog scope -> Qobuz id): a unique exact-name
  Qobuz artist is provisional; several homonyms are split by their own
  top-track ISRCs (max 2 each); no evidence omits the row instead of taking
  max(albums_count);
- four `*_v2` cache tables keyed by the stable identifier + matcher version,
  carrying evidence kind, expiring at read time (verified 30 d, provisional
  7 d, search windows 7 d, ISRC credits 30 d); legacy name-keyed rows are
  neither read nor written; scene cache key bumped to v2 with the real
  account territory (no scope -> no cache), rows re-derived on replay.

Core: `catalog_scope()`, `musicbrainz_resolve_source_artist`, store/catalog
adapters; `musicbrainz_resolve_artist` and the musician resolver go through
the containment rule. Artist page passes {qobuz id, own ISRCs} to core and
keeps its generation guard; QML, playback, auth and reco crates untouched.

Tests: qbz-integrations 59 (query encoding, #768 ordering fixture, ISRC
voting incl. collaborations/dupes/conflicts, cache keys/TTL/version/legacy,
bounded fan-out with request counting), qbz-core 40, qbz-qt 782.

cc
2026-09-11 09:46:02 -06:00
vicrodh 857cb16e87 fix(mb): retry MusicBrainz 503s and transport failures behind a shared limiter
- Route every endpoint through a new send_with_retry: it waits on the rate
  limiter, GETs, logs the x-ratelimit budget, and on a 503/429 backs off the
  server's Retry-After (capped 8s) and retries, up to 3 attempts. get_artist_tags
  and get_recording_isrcs, which skipped status handling entirely, now go
  through it too.
- Also retry transient TRANSPORT failures (a hung connection to the request
  timeout, a drop or refusal). MusicBrainz under load hangs instead of returning
  503, which surfaced as "error sending request for url". Capped at 2 attempts
  with a 6s request timeout so a total failure is ~13s, not ~33s, while a hang
  that clears still recovers on the second try.
- Share ONE process-wide direct rate limiter across every client (core, the tag
  editor, remote metadata) so several clients on one public IP pace as one 1.1s
  stream instead of each pacing itself.
- Log MusicBrainz's x-ratelimit-limit/remaining/reset headers (debug on 2xx,
  warn on 429/503) as the only signal of whether the bucket was ours or MB's.

Tests: 503-then-200, a dropped connection, and shared-limiter serialization,
each red-to-green. Dev-only deps (tokio test features + rustls/aws-lc-rs to
install the crypto provider); no runtime dependency added.

cc
2026-09-10 13:23:53 -06:00
vicrodh 8964544891 chore(deps): reqwest 0.13 (rustls-no-provider; query/form are opt-in features now)
cc
2026-08-29 09:00:56 -06:00
vicrodh 6d8c951a38 chore(deps): rusqlite 0.40 (u64 casts; a latent two-statement execute fixed)
rusqlite 0.40 removed ToSql/FromSql for u64 (SQLite integers are i64):
every u64 column now reads/writes through an explicit i64 cast. It also
rejects a multi-statement execute() instead of silently running only the
first statement — which is what catalog.rs's editions/logical_albums
prune had been doing all along; it is an execute_batch now, so the
logical_albums prune finally runs.

cc
2026-08-29 08:36:21 -06:00
vicrodh c7a3012955 feat(local-library): complete catalog and metadata workflows
Add bounded native library models, complete grouping rails and scan progress. Bring source-aware metadata editing, media information and collection offline preflight to Qt, and repair inherited Plex album artwork without overriding healthy tokens.

cx
2026-08-28 18:50:05 -06:00
vicrodh 75a668ed12 feat(metadata): expand the local tag editor
Promote metadata editing to a routed full-page workflow with tabular per-track fields, ordered artist credits, provider identifiers, and compilation handling.

Add bounded artwork selection and retrieval through MusicBrainz, Discogs, and Last.fm. Direct and sidecar saves preflight immutable rows, verify rich tag writes, cache covers by content, and update exact database ids transactionally.

cx
2026-08-24 19:46:59 -06:00
vicrodh b6deec0aaf feat(integrations): search and fetch remote album metadata
The pure converters have been here for a while; what was missing was the thin
async layer that puts a client in front of them. It lives here rather than in
the frontend for the reason ADR-006 gives: which provider answers, how a query
is built and what an empty answer means are domain decisions, and a second
frontend must not re-derive them.

The Tauri reference is v2_remote_metadata_search / _get_album with its State
arguments removed. A provider that is disabled, rate-limited or simply silent
answers with an empty result set rather than an error, because "nobody has this
record" and "the server said no" look identical to someone staring at a list —
and the distinction that does matter rides its own flag.

Discogs needs no credentials wired: its client already goes through the proxy.

cc
2026-08-21 18:57:22 -06:00
vicrodh 016eb3cd23 feat(qt): artist scene, musician page and the shared scene-discovery engine
Both views were absent from the Qt frontend, not partial. This lands them
together with the core engine they need and the two entry points that reach
them.

- qbz-core gains scene discovery ("artists from the same place"): a
  cancellable run with a phase for the progress label and a typed
  SceneDiscoveryError, so a view can tell offline, all-upstreams-failed and
  an honest empty result apart. CancelToken, ScenePhase and SceneDiscoveryError
  are re-exported for frontends.
- qbz-integrations: MusicBrainz location resolution plus its cache layer.
- qbz-qt: ArtistSceneView with its three scene components, MusicianPageView
  and MusicianModal, backed by artist_scene_qt.rs, musician_qt.rs and the two
  bridges. The five-branch musician dispatcher is wired to all six of its
  consumers. Both scene doors are live: the sidebar location link and the
  overflow row that shipped grey with action: "stub".
- Translations: the new msgids in all eight locales.

Three defects fixed on the way, all with the same signature — a comment
asserting a mechanism that does not exist:

- Volume opened at 100 %. The comment claimed the poll loop mirrored it from
  the engine; now_playing::set_volume has two callers and neither runs in a
  local session.
- mbAvailable was documented as "MusicBrainz enabled AND resolved to an MBID"
  while being assigned from the opt-in alone. Consumers now test the MBID.
- Session restore could not resume from a cold cursor ("cannot resume - no
  audio data available") for want of a has_loaded_audio() guard. Investigation
  in qbz-nix-docs/qt-frontend/2026-08-14-cold-cursor-resume.md.

Contract and evidence:
qbz-nix-docs/qt-frontend/2026-08-14-artist-scene-musician/00-CONTRACT.md,
which wins over specs 01..09 and records 20 deliberate divergences from the
reference in section 6.

cc
2026-08-14 19:25:11 -06:00
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