Post-v1 features: offline, radio, plugin API, registry, media controls, social, Windows/macOS, track matching #13

Closed
opened 2026-10-06 21:15:25 +02:00 by SBDeveloper · 1 comment
Owner

Problem Statement

v1 plays Deezer and local files, but it stops at the basics. The user cannot listen without a network, music stops when the Queue runs out, Plugins only ship with the app, desktop media controls and social features are missing, and the same song from different Sources shows up as unrelated Tracks. Windows/macOS users risk losing the official Deezer app's deezer:// handling. These were decided during the initial design session and deferred from v1.

Solution

Deliver the deferred features as one umbrella, in dependency order:

  1. Offline Tracks: the user marks Tracks for offline use. The audio is held in the app's private encrypted cache and plays with no network, for any Source.
  2. Radio: when the Queue runs out, the Source continues with generated Tracks (e.g. Deezer Flow), controlled by an autoplay toggle.
  3. Public plugin API: the user installs third-party Plugins from a file or URL, sees the capabilities requested, and consents.
  4. Plugin registry: a curated index to browse and install Plugins, after manual install works.
  5. MPRIS / media keys / notifications: the OS shows now-playing info and routes media keys to the player.
  6. Social integrations: Discord RPC, scrobbling and lyrics, as bundled or optional event Plugins.
  7. Windows/macOS: the app runs there, and claims deezer:// only when the user opts in.
  8. Track matching: the same song from several Sources can be merged into one Track.

Status and delivery order

Re-ordered together with the UI rework (#32), because the rework gives the remaining features their home in the UI. Order: headless core (#34, #17, #39, #29) → UI shell (#36, #40, #41, #42, #43, #44) → plugins (#18–#21, #45, #27, #25, #26, #22) → Windows/macOS (#28) and fuzzy matching (#30).

  • 1. Offline Tracks: #15, #16, #31 done.
  • 2. Radio: #17 done.
  • 3. Public plugin API: #18, #19, #20, #21 (DSP, events), #45 (plugin UI views and slots), #47 (install and update with consent), #48 (manage installed Plugins) done.
  • 4. Plugin registry: #22 done
  • 5. MPRIS / media keys / notifications: #23, #24 done
  • 6. Social integrations: Discord #25, scrobbling #26 (ListenBrainz), lyrics #27 (LRCLIB) done.
  • 7. Windows/macOS: #28 done
  • 8. Track matching: Recordings with ISRC #29, fuzzy suggestions and un-group #30 done

Remaining: none.

UI rework and its slices: #32.

User Stories

Offline Tracks

  1. As a listener, I want to mark a Track, album or Playlist as offline, so that I can play it without a network.
  2. As a listener, I want offline to work for any Source, so that local-style behaviour is consistent across Deezer and third-party Sources.
  3. As a listener, I want to see which Tracks are offline, so that I know what plays without a network.
  4. As a listener, I want offline Tracks to play with no network and no Source login, so that I can listen on a plane.
  5. As a listener, I want to remove an Offline Track, so that I can free disk space.
  6. As a listener, I want to see cache size used, so that I can manage disk.
  7. As a listener, I want a separate Quality cap for streaming and for caching, so that I can stream on mobile data cheaply but cache lossless.
  8. As a privacy-minded user, I want the cache encrypted at rest with a per-install key, so that the files cannot be copied out and played elsewhere.
  9. As a listener, I want an interrupted caching job to resume or retry, so that flaky networks do not leave broken Offline Tracks.
  10. As a listener, I want an Offline Track whose Source stops allowing it (e.g. expired Premium) to be flagged, so that I know why it will not play.
  11. As a Plugin author, I want the cache to need no support from my Source beyond the encoded byte stream, so that my Source gets offline for free.

Radio
12. As a listener, I want playback to continue when the Queue runs out, so that music does not stop.
13. As a listener, I want an autoplay toggle, so that I can turn Radio off.
14. As a listener, I want Radio seeded by the last played Track or chosen seed, so that it matches my taste.
15. As a listener, I want Radio Tracks to come from the Source of the seed (e.g. Deezer Flow), so that continuation is relevant.
16. As a listener, I want Radio Tracks to be visible in the Queue, so that I can skip or remove them.
17. As a listener, I want Radio to stop gracefully if the Source has no continuation or is offline, so that playback ends cleanly.
18. As a Plugin author, I want an optional continuation capability on a Source, so that Sources without Radio simply do not append anything.

Public plugin API
19. As a user, I want to install a Plugin from a .zip file, so that I can try third-party extensions.
20. As a user, I want to install a Plugin from a URL, so that I do not need to download it by hand.
21. As a user, I want to see the capabilities a Plugin requests (Sources, playback events, UI views, DSP) before install and consent, so that I know what it can do.
22. As a user, I want install to fail clearly on an invalid manifest, missing wasm or bad archive, so that I can fix the problem.
23. As a user, I want to enable, disable and uninstall Plugins, so that I stay in control.
24. As a user, I want a Plugin to be denied any capability I did not consent to, so that consent is meaningful.
25. As a user, I want secrets like the ARL never to leave the Plugin that owns them, so that other Plugins cannot read my login.
26. As a Plugin author, I want a documented manifest (wasm plus optional UI bundle), so that I can package my Plugin.
27. As a Plugin author, I want to add a Source, react to playback events, add UI views and process audio, so that I can extend every part of the player.
28. As a Plugin author, I want bundled Deezer and local-files Plugins to use the same API, so that the contract is proven.
29. As a user, I want a misbehaving Plugin to be isolated, so that it cannot crash the player.

Plugin registry
30. As a user, I want to browse a curated index of Plugins, so that I can discover them safely.
31. As a user, I want to install from the registry with one action using the same consent flow, so that registry and manual install behave the same.
32. As a user, I want to see available updates, so that I keep Plugins current.
33. As a maintainer, I want the index to be a curated list, so that I control what is offered.

MPRIS / media keys / notifications
34. As a Linux user, I want the desktop to show now-playing metadata and artwork over MPRIS, so that my panel and lock screen show it.
35. As a user, I want media keys (play/pause, next, previous, stop) to control the player, so that I do not need the window.
36. As a user, I want a notification on track change, optionally, so that I know what started playing.
37. As a user, I want the desktop's seek and volume controls to work, so that integrations behave like other players.

Social integrations
38. As a user, I want Discord rich presence of the current Track, so that friends see what I play.
39. As a user, I want scrobbling to a service, so that my history is recorded.
40. As a user, I want lyrics for the current Track, so that I can sing along.
41. As a user, I want each social integration to be optional and off by default, so that nothing is shared without my choice.
42. As a Plugin author, I want these built purely from playback events, so that the same API supports third-party ones.

Windows/macOS
43. As a Windows or macOS user, I want the app to run on my platform, so that I can use it.
44. As a Windows or macOS user, I want claiming deezer:// to be opt-in, so that the official Deezer app keeps working.
45. As a Windows or macOS user, I want to be told when another app owns deezer://, so that I understand why login callbacks go elsewhere.
46. As a user, I want the pasted-ARL fallback to work everywhere, so that login works without the protocol handler.

Track matching
47. As a listener, I want the same song from several Sources grouped into one Recording (ISRC first, then user-accepted fuzzy suggestions), so that my Library has no duplicates.
48. As a listener, I want to choose which Source plays a Recording, so that I can prefer local or lossless.
49. As a listener, I want playback to fall back to another Source when the preferred one is unavailable, so that Recordings keep playing.
50. As a listener, I want to un-group wrongly matched Tracks, so that fuzzy matching can be corrected.
51. As a listener, I want matching to be optional, so that the v1 behaviour of separate Tracks remains available.

Implementation Decisions

  • Offline Tracks live in the host, not in Sources. A Source only provides an encoded byte stream (ADR 0002); the host caches that stream. The host plays Offline Tracks through the normal decode/DSP/output path.
  • Cache is encrypted at rest with a per-install key. Cache entries are keyed by Track identity (Source + track id) and record the quality cached.
  • Settings gain a caching Quality cap separate from the streaming Quality cap. The cap definition follows the glossary (standard, high, lossless).
  • Offline status is part of the Track data the Core exposes to the UI, and changes are emitted as Core events.
  • Radio: the host appends a Source-provided continuation when the Queue runs out and autoplay is on. A Source declares an optional continuation capability seeded from a Track. Appended Tracks are ordinary Queue entries.
  • Public plugin API: a Plugin package is a .zip with a manifest, a wasm module, and an optional UI bundle. The manifest declares capabilities (Sources, playback events, UI views, DSP). The host enforces capabilities at runtime; undeclared or unconsented ones are denied.
  • Install flow: from file or URL, validate, show requested capabilities, record consent, then enable. Consent is stored per Plugin and re-asked if requested capabilities grow on update.
  • Secrets (ARL) are stored and used only inside the owning Plugin; no host API returns them to other Plugins.
  • Plugin registry is a curated index consumed by the same install flow; built after manual install.
  • MPRIS, media keys and notifications are host adapters driven by the Core event stream and Core playback commands. They contain no player logic.
  • Social integrations (Discord RPC, scrobbling, lyrics) are event Plugins that use only the public plugin API, bundled or optional.
  • Windows/macOS: protocol claim for deezer:// is opt-in on those platforms; Linux keeps default claim. Existing deezer:// link handling and pasted-ARL fallback are unchanged.
  • Track matching: implemented as Recordings (ADR 0003): ISRC matches group Library Tracks automatically, Tracks keep their single Source, and a Source ranking with an optional pin decides which plays, with fallback. Fuzzy matches are user-accepted suggestions, planned after the rework. Grouping is opt-in at the fuzzy level and un-grouping is a deletion. Playlists are unified as part of #32 (one Playlist type, optionally linked to a Source).
  • Delivery order: see Status and delivery order above. Everything is done except the plugin registry (#22), Windows/macOS (#28) and fuzzy matching (#30).

Testing Decisions

  • Good tests exercise external behaviour only, through the Core public API and its event stream, never internals such as cache file layout or wasm calls.
  • One seam: Core integration tests in the core crate, using the fake-source plugin, the fake gw-light server and the test sink from the shared test helpers. No new seams.
  • Offline Tracks: mark offline with the fake Source, drop the network/Source, assert playback still emits the same events; assert caching quality cap and removal; assert cache files are not plain audio.
  • Radio: let the Queue run out with autoplay on/off and assert the Queue gets (or does not get) Source-continuation Tracks; Source without continuation appends nothing.
  • Plugin API: install a fixture package, assert consent gating, capability denial, invalid package errors, and that one Plugin cannot read another's secrets.
  • MPRIS/media keys/social: assert the Core events and commands they depend on; the thin OS adapters themselves are not tested. Event Plugins are tested as Plugins through the plugin host.
  • Track matching: assert merging by ISRC, fuzzy fallback, preferred Source, fallback playback, and un-merge via Core.
  • Prior art: existing Core integration tests for playback, queue, library, playlists, deezer favourites/login, links and plugin_host.

Out of Scope

  • The unexplored design branches in the backlog: Library storage and Favourite sync strategy, search across Sources (merged vs per-Source), offline-network behaviour beyond Offline Tracks, conflict handling for Source Playlists edited in the app and on Deezer. These need design (grilling) before they can be specced.
  • Finding the exact URL that starts Deezer's browser login; the ADR 0001 already records the current login URL, so this is a lookup, not a feature.
  • UI visual design and Tauri shell tests.
  • Anything not listed in the backlog.

Further Notes

  • Source: docs/post-v1.md, decided during the initial design session. Uses GLOSSARY.md terms (Track, Source, Library, Plugin, Queue, Radio, Quality cap, Offline Tracks).
  • Consistent with ADR 0002 (WASM plugins, Sources as plugins; host owns offline cache) and ADR 0001 (Deezer private API with ARL).
  • Track matching tension resolved by ADR 0003 (group, not merge) and the linked-Playlist decision in #32 (the Source Playlist term is removed).
  • docs/post-v1.md has been removed.
## Problem Statement v1 plays Deezer and local files, but it stops at the basics. The user cannot listen without a network, music stops when the Queue runs out, Plugins only ship with the app, desktop media controls and social features are missing, and the same song from different Sources shows up as unrelated Tracks. Windows/macOS users risk losing the official Deezer app's `deezer://` handling. These were decided during the initial design session and deferred from v1. ## Solution Deliver the deferred features as one umbrella, in dependency order: 1. **Offline Tracks**: the user marks Tracks for offline use. The audio is held in the app's private encrypted cache and plays with no network, for any Source. 2. **Radio**: when the Queue runs out, the Source continues with generated Tracks (e.g. Deezer Flow), controlled by an autoplay toggle. 3. **Public plugin API**: the user installs third-party Plugins from a file or URL, sees the capabilities requested, and consents. 4. **Plugin registry**: a curated index to browse and install Plugins, after manual install works. 5. **MPRIS / media keys / notifications**: the OS shows now-playing info and routes media keys to the player. 6. **Social integrations**: Discord RPC, scrobbling and lyrics, as bundled or optional event Plugins. 7. **Windows/macOS**: the app runs there, and claims `deezer://` only when the user opts in. 8. **Track matching**: the same song from several Sources can be merged into one Track. ## Status and delivery order Re-ordered together with the UI rework (#32), because the rework gives the remaining features their home in the UI. Order: headless core (#34, #17, #39, #29) → UI shell (#36, #40, #41, #42, #43, #44) → plugins (#18–#21, #45, #27, #25, #26, #22) → Windows/macOS (#28) and fuzzy matching (#30). - [x] 1. Offline Tracks: #15, #16, #31 done. - [x] 2. Radio: #17 done. - [x] 3. Public plugin API: #18, #19, #20, #21 (DSP, events), #45 (plugin UI views and slots), #47 (install and update with consent), #48 (manage installed Plugins) done. - [x] 4. Plugin registry: #22 done - [x] 5. MPRIS / media keys / notifications: #23, #24 done - [x] 6. Social integrations: Discord #25, scrobbling #26 (ListenBrainz), lyrics #27 (LRCLIB) done. - [x] 7. Windows/macOS: #28 done - [x] 8. Track matching: Recordings with ISRC #29, fuzzy suggestions and un-group #30 done Remaining: none. UI rework and its slices: #32. ## User Stories Offline Tracks 1. As a listener, I want to mark a Track, album or Playlist as offline, so that I can play it without a network. 2. As a listener, I want offline to work for any Source, so that local-style behaviour is consistent across Deezer and third-party Sources. 3. As a listener, I want to see which Tracks are offline, so that I know what plays without a network. 4. As a listener, I want offline Tracks to play with no network and no Source login, so that I can listen on a plane. 5. As a listener, I want to remove an Offline Track, so that I can free disk space. 6. As a listener, I want to see cache size used, so that I can manage disk. 7. As a listener, I want a separate Quality cap for streaming and for caching, so that I can stream on mobile data cheaply but cache lossless. 8. As a privacy-minded user, I want the cache encrypted at rest with a per-install key, so that the files cannot be copied out and played elsewhere. 9. As a listener, I want an interrupted caching job to resume or retry, so that flaky networks do not leave broken Offline Tracks. 10. As a listener, I want an Offline Track whose Source stops allowing it (e.g. expired Premium) to be flagged, so that I know why it will not play. 11. As a Plugin author, I want the cache to need no support from my Source beyond the encoded byte stream, so that my Source gets offline for free. Radio 12. As a listener, I want playback to continue when the Queue runs out, so that music does not stop. 13. As a listener, I want an autoplay toggle, so that I can turn Radio off. 14. As a listener, I want Radio seeded by the last played Track or chosen seed, so that it matches my taste. 15. As a listener, I want Radio Tracks to come from the Source of the seed (e.g. Deezer Flow), so that continuation is relevant. 16. As a listener, I want Radio Tracks to be visible in the Queue, so that I can skip or remove them. 17. As a listener, I want Radio to stop gracefully if the Source has no continuation or is offline, so that playback ends cleanly. 18. As a Plugin author, I want an optional continuation capability on a Source, so that Sources without Radio simply do not append anything. Public plugin API 19. As a user, I want to install a Plugin from a .zip file, so that I can try third-party extensions. 20. As a user, I want to install a Plugin from a URL, so that I do not need to download it by hand. 21. As a user, I want to see the capabilities a Plugin requests (Sources, playback events, UI views, DSP) before install and consent, so that I know what it can do. 22. As a user, I want install to fail clearly on an invalid manifest, missing wasm or bad archive, so that I can fix the problem. 23. As a user, I want to enable, disable and uninstall Plugins, so that I stay in control. 24. As a user, I want a Plugin to be denied any capability I did not consent to, so that consent is meaningful. 25. As a user, I want secrets like the ARL never to leave the Plugin that owns them, so that other Plugins cannot read my login. 26. As a Plugin author, I want a documented manifest (wasm plus optional UI bundle), so that I can package my Plugin. 27. As a Plugin author, I want to add a Source, react to playback events, add UI views and process audio, so that I can extend every part of the player. 28. As a Plugin author, I want bundled Deezer and local-files Plugins to use the same API, so that the contract is proven. 29. As a user, I want a misbehaving Plugin to be isolated, so that it cannot crash the player. Plugin registry 30. As a user, I want to browse a curated index of Plugins, so that I can discover them safely. 31. As a user, I want to install from the registry with one action using the same consent flow, so that registry and manual install behave the same. 32. As a user, I want to see available updates, so that I keep Plugins current. 33. As a maintainer, I want the index to be a curated list, so that I control what is offered. MPRIS / media keys / notifications 34. As a Linux user, I want the desktop to show now-playing metadata and artwork over MPRIS, so that my panel and lock screen show it. 35. As a user, I want media keys (play/pause, next, previous, stop) to control the player, so that I do not need the window. 36. As a user, I want a notification on track change, optionally, so that I know what started playing. 37. As a user, I want the desktop's seek and volume controls to work, so that integrations behave like other players. Social integrations 38. As a user, I want Discord rich presence of the current Track, so that friends see what I play. 39. As a user, I want scrobbling to a service, so that my history is recorded. 40. As a user, I want lyrics for the current Track, so that I can sing along. 41. As a user, I want each social integration to be optional and off by default, so that nothing is shared without my choice. 42. As a Plugin author, I want these built purely from playback events, so that the same API supports third-party ones. Windows/macOS 43. As a Windows or macOS user, I want the app to run on my platform, so that I can use it. 44. As a Windows or macOS user, I want claiming `deezer://` to be opt-in, so that the official Deezer app keeps working. 45. As a Windows or macOS user, I want to be told when another app owns `deezer://`, so that I understand why login callbacks go elsewhere. 46. As a user, I want the pasted-ARL fallback to work everywhere, so that login works without the protocol handler. Track matching 47. As a listener, I want the same song from several Sources grouped into one Recording (ISRC first, then user-accepted fuzzy suggestions), so that my Library has no duplicates. 48. As a listener, I want to choose which Source plays a Recording, so that I can prefer local or lossless. 49. As a listener, I want playback to fall back to another Source when the preferred one is unavailable, so that Recordings keep playing. 50. As a listener, I want to un-group wrongly matched Tracks, so that fuzzy matching can be corrected. 51. As a listener, I want matching to be optional, so that the v1 behaviour of separate Tracks remains available. ## Implementation Decisions - Offline Tracks live in the host, not in Sources. A Source only provides an encoded byte stream (ADR 0002); the host caches that stream. The host plays Offline Tracks through the normal decode/DSP/output path. - Cache is encrypted at rest with a per-install key. Cache entries are keyed by Track identity (Source + track id) and record the quality cached. - Settings gain a caching Quality cap separate from the streaming Quality cap. The cap definition follows the glossary (standard, high, lossless). - Offline status is part of the Track data the Core exposes to the UI, and changes are emitted as Core events. - Radio: the host appends a Source-provided continuation when the Queue runs out and autoplay is on. A Source declares an optional continuation capability seeded from a Track. Appended Tracks are ordinary Queue entries. - Public plugin API: a Plugin package is a .zip with a manifest, a wasm module, and an optional UI bundle. The manifest declares capabilities (Sources, playback events, UI views, DSP). The host enforces capabilities at runtime; undeclared or unconsented ones are denied. - Install flow: from file or URL, validate, show requested capabilities, record consent, then enable. Consent is stored per Plugin and re-asked if requested capabilities grow on update. - Secrets (ARL) are stored and used only inside the owning Plugin; no host API returns them to other Plugins. - Plugin registry is a curated index consumed by the same install flow; built after manual install. - MPRIS, media keys and notifications are host adapters driven by the Core event stream and Core playback commands. They contain no player logic. - Social integrations (Discord RPC, scrobbling, lyrics) are event Plugins that use only the public plugin API, bundled or optional. - Windows/macOS: protocol claim for `deezer://` is opt-in on those platforms; Linux keeps default claim. Existing `deezer://` link handling and pasted-ARL fallback are unchanged. - Track matching: implemented as Recordings (ADR 0003): ISRC matches group Library Tracks automatically, Tracks keep their single Source, and a Source ranking with an optional pin decides which plays, with fallback. Fuzzy matches are user-accepted suggestions, planned after the rework. Grouping is opt-in at the fuzzy level and un-grouping is a deletion. Playlists are unified as part of #32 (one Playlist type, optionally linked to a Source). - Delivery order: see Status and delivery order above. Everything is done except the plugin registry (#22), Windows/macOS (#28) and fuzzy matching (#30). ## Testing Decisions - Good tests exercise external behaviour only, through the `Core` public API and its event stream, never internals such as cache file layout or wasm calls. - One seam: `Core` integration tests in the core crate, using the fake-source plugin, the fake gw-light server and the test sink from the shared test helpers. No new seams. - Offline Tracks: mark offline with the fake Source, drop the network/Source, assert playback still emits the same events; assert caching quality cap and removal; assert cache files are not plain audio. - Radio: let the Queue run out with autoplay on/off and assert the Queue gets (or does not get) Source-continuation Tracks; Source without continuation appends nothing. - Plugin API: install a fixture package, assert consent gating, capability denial, invalid package errors, and that one Plugin cannot read another's secrets. - MPRIS/media keys/social: assert the Core events and commands they depend on; the thin OS adapters themselves are not tested. Event Plugins are tested as Plugins through the plugin host. - Track matching: assert merging by ISRC, fuzzy fallback, preferred Source, fallback playback, and un-merge via Core. - Prior art: existing Core integration tests for playback, queue, library, playlists, deezer favourites/login, links and plugin_host. ## Out of Scope - The unexplored design branches in the backlog: Library storage and Favourite sync strategy, search across Sources (merged vs per-Source), offline-network behaviour beyond Offline Tracks, conflict handling for Source Playlists edited in the app and on Deezer. These need design (grilling) before they can be specced. - Finding the exact URL that starts Deezer's browser login; the ADR 0001 already records the current login URL, so this is a lookup, not a feature. - UI visual design and Tauri shell tests. - Anything not listed in the backlog. ## Further Notes - Source: `docs/post-v1.md`, decided during the initial design session. Uses `GLOSSARY.md` terms (Track, Source, Library, Plugin, Queue, Radio, Quality cap, Offline Tracks). - Consistent with ADR 0002 (WASM plugins, Sources as plugins; host owns offline cache) and ADR 0001 (Deezer private API with ARL). - Track matching tension resolved by ADR 0003 (group, not merge) and the linked-Playlist decision in #32 (the Source Playlist term is removed). - docs/post-v1.md has been removed.
Author
Owner

Verified in code: every item is delivered (#22, #28, #30 closed, code and tests present). Closing.

Verified in code: every item is delivered (#22, #28, #30 closed, code and tests present). Closing.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
SBDeveloper/medley#13
No description provided.