You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Design the Provider seam both TIDAL and Apple plug into: the interface (search, link resolve, catalog metadata, quality tiers, download, lyrics, artwork), where it lives, how the bridge and QML consume it without changing the TIDAL path's behavior, how provider-specific quality concepts (TIDAL tiers vs Apple's AAC/ALAC/Atmos) map into one model the chooser can present, and how ownership/library detection stays provider-aware. Constraint: additive only; TIDAL behavior unchanged.
Design: one fused Provider interface that both TIDAL and Apple implement now — TIDAL by thin delegation (no rewrites), Apple as the first green-field implementer. Grounded in docs/research/provider-seam-analysis.md (the seam inventory and its two invariants) and the engine decision (ticket #8).
The five decisions
Both providers implement the seam immediately. TIDAL's implementation delegates to the existing helper/tidal.py, config.py, and download.py bodies unchanged; the bridge routes through the interface instead of reaching them directly. The existing test suite pins TIDAL behavior during the mechanical call-routing. Lives in a new waves/providers/ package: base.py (interface + neutral types), tidal.py, apple.py.
The row-dict schema is the contract. Each provider builds the exact plain dicts QML consumes today (search payload, result rows, queue rows) from its own engine objects; shared builder code is extracted where identical; the field-name contract is documented in base.py. Zero QML work for Apple beyond tier words. The _objs id-bucket pattern stays per-provider.
Namespaced string ids — tidal:123, apple:456 — as the one format everywhere new (ownership rows, on-disk tags, _objs buckets, queue rows, _JobSpec). The ownership store backfills existing rows as tidal:; legacy bare ids read as tidal. On disk, a generic namespaced tag is written alongside the legacy WAVES_TIDAL_* tags, so every file already in a library stays recognized. Ownership gates become provider-scoped automatically: owning on TIDAL never satisfies an Apple gate (per-provider Ownership, per CONTEXT.md).
One quality model, Waves-owned. The four rungs LOW < HIGH < LOSSLESS < HI_RES_LOSSLESS become a Waves enum (no longer tidalapi's Quality); each provider maps its engine codecs onto it (Apple: AAC 256 → HIGH, ALAC 16/44.1 → LOSSLESS, ALAC 24/192 → HI_RES_LOSSLESS). Audio type (stereo/Atmos) stays orthogonal per the glossary. The queue's pinned-quality string parses through the Waves enum, retiring the tidalapi Quality(raw) parse point (backend.py:7586). The chooser renders provider detail ("ALAC 24/192") as label text, never as rank. The three rank comparison sites (ownership store, engine gate, bridge gate) keep their scale.
Composition, capability flags, and the fence.Download takes a Provider: stream resolution becomes provider.resolve_stream(...) returning a neutral StreamInfo (replacing TrackStreamInfo's tidalapi payloads); the TIDAL refusal taxonomy becomes a per-provider classify_refusal hook; TIDAL's Atmos session-swap machinery stays fenced inside TidalProvider — never generalized. Capability flags on the interface keep My TIDAL, Browse, mixes, and videos TIDAL-only without if-branches. No separate in-code Engine abstraction layer in v1: the fallback-engine swap (WorldObs-v3, ticket Choose the Apple engine approach #8) happens behind the same AppleProvider methods.
@dataclassclassStreamInfo:
urls: list[str] # segment/BTS/plain URLsfile_extension: strcodecs: strrequires_flac_extraction: booldelivered: dict# {tier, audio_type, bit_depth, sample_rate}replay_gain: dict|None# TIDAL-measured; None on Apple → tags left untagged (existing rule)
How the bridge consumes it
self.providers: dict[str, Provider] keyed by provider id. Search fans out over enabled providers and sections results per provider (v1 shape per the map). _JobSpec carries (provider_id, kind, namespaced_id) and resolves via get_object at dispatch instead of holding a raw tidalapi object — behavior-preserving for TIDAL. Settings rows for per-provider quality ride the existing schema-driven SettingsPage (ticket #11's territory).
Constraint check
Additive-only holds by construction: TIDAL's bodies don't move — they get wrapped; the refactor is call-routing with the suite as the safety net. The two research invariants (id namespacing, unified rank scale) are decisions 3 and 4.
Graduated fog
The fog line "library layout, ownership and IN LIBRARY/DOWNLOADED detection across providers" is now specifiable — the id/tag/ownership mechanics above settled its half; the remaining question (recognizing Apple files, per-provider badge verdicts, path-template sharing) graduates to its own ticket.
Activity
ranokay commentedon Sep 1, 2026
Resolution
Design: one fused
Providerinterface that both TIDAL and Apple implement now — TIDAL by thin delegation (no rewrites), Apple as the first green-field implementer. Grounded indocs/research/provider-seam-analysis.md(the seam inventory and its two invariants) and the engine decision (ticket #8).The five decisions
helper/tidal.py,config.py, anddownload.pybodies unchanged; the bridge routes through the interface instead of reaching them directly. The existing test suite pins TIDAL behavior during the mechanical call-routing. Lives in a newwaves/providers/package:base.py(interface + neutral types),tidal.py,apple.py.base.py. Zero QML work for Apple beyond tier words. The_objsid-bucket pattern stays per-provider.tidal:123,apple:456— as the one format everywhere new (ownership rows, on-disk tags,_objsbuckets, queue rows,_JobSpec). The ownership store backfills existing rows astidal:; legacy bare ids read as tidal. On disk, a generic namespaced tag is written alongside the legacyWAVES_TIDAL_*tags, so every file already in a library stays recognized. Ownership gates become provider-scoped automatically: owning on TIDAL never satisfies an Apple gate (per-provider Ownership, per CONTEXT.md).LOW < HIGH < LOSSLESS < HI_RES_LOSSLESSbecome a Waves enum (no longer tidalapi'sQuality); each provider maps its engine codecs onto it (Apple: AAC 256 → HIGH, ALAC 16/44.1 → LOSSLESS, ALAC 24/192 → HI_RES_LOSSLESS). Audio type (stereo/Atmos) stays orthogonal per the glossary. The queue's pinned-quality string parses through the Waves enum, retiring the tidalapiQuality(raw)parse point (backend.py:7586). The chooser renders provider detail ("ALAC 24/192") as label text, never as rank. The three rank comparison sites (ownership store, engine gate, bridge gate) keep their scale.Downloadtakes aProvider: stream resolution becomesprovider.resolve_stream(...)returning a neutralStreamInfo(replacingTrackStreamInfo's tidalapi payloads); the TIDAL refusal taxonomy becomes a per-providerclassify_refusalhook; TIDAL's Atmos session-swap machinery stays fenced insideTidalProvider— never generalized. Capability flags on the interface keep My TIDAL, Browse, mixes, and videos TIDAL-only withoutif-branches. No separate in-code Engine abstraction layer in v1: the fallback-engine swap (WorldObs-v3, ticket Choose the Apple engine approach #8) happens behind the sameAppleProvidermethods.The interface sketch
How the bridge consumes it
self.providers: dict[str, Provider]keyed by provider id. Search fans out over enabled providers and sections results per provider (v1 shape per the map)._JobSpeccarries(provider_id, kind, namespaced_id)and resolves viaget_objectat dispatch instead of holding a raw tidalapi object — behavior-preserving for TIDAL. Settings rows for per-provider quality ride the existing schema-driven SettingsPage (ticket #11's territory).Constraint check
Additive-only holds by construction: TIDAL's bodies don't move — they get wrapped; the refactor is call-routing with the suite as the safety net. The two research invariants (id namespacing, unified rank scale) are decisions 3 and 4.
Graduated fog
The fog line "library layout, ownership and IN LIBRARY/DOWNLOADED detection across providers" is now specifiable — the id/tag/ownership mechanics above settled its half; the remaining question (recognizing Apple files, per-provider badge verdicts, path-template sharing) graduates to its own ticket.