refrain 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. {refrain-0.2.3 → refrain-0.2.4}/CHANGELOG.md +128 -0
  2. {refrain-0.2.3 → refrain-0.2.4}/PKG-INFO +3 -1
  3. {refrain-0.2.3 → refrain-0.2.4}/README.md +2 -0
  4. {refrain-0.2.3 → refrain-0.2.4}/ROADMAP.md +64 -0
  5. {refrain-0.2.3 → refrain-0.2.4}/docs/architecture.md +41 -10
  6. refrain-0.2.4/docs/bluetooth.md +136 -0
  7. {refrain-0.2.3 → refrain-0.2.4}/docs/faq.md +44 -0
  8. refrain-0.2.4/docs/screenshots/live-log.png +0 -0
  9. refrain-0.2.4/docs/screenshots/settings-advanced.png +0 -0
  10. refrain-0.2.4/docs/screenshots/settings-general.png +0 -0
  11. refrain-0.2.4/docs/screenshots/settings-sources.png +0 -0
  12. refrain-0.2.4/docs/screenshots/settings-updates.png +0 -0
  13. refrain-0.2.4/docs/screenshots/update-dialog.png +0 -0
  14. refrain-0.2.4/docs/screenshots/welcome.png +0 -0
  15. {refrain-0.2.3 → refrain-0.2.4}/packaging/appimage/AppImageBuilder.yml +1 -1
  16. {refrain-0.2.3 → refrain-0.2.4}/packaging/aur/refrain/.SRCINFO +4 -3
  17. {refrain-0.2.3 → refrain-0.2.4}/packaging/aur/refrain/PKGBUILD +1 -4
  18. {refrain-0.2.3 → refrain-0.2.4}/packaging/flatpak/io.github.Rockykln.Refrain.metainfo.xml +22 -0
  19. {refrain-0.2.3 → refrain-0.2.4}/pyproject.toml +1 -1
  20. refrain-0.2.4/src/refrain/__init__.py +1 -0
  21. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/app.py +46 -20
  22. refrain-0.2.4/src/refrain/assets/icons/menu-update.svg +3 -0
  23. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/config.py +33 -6
  24. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/daemon.py +87 -42
  25. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/discord_rpc.py +40 -3
  26. refrain-0.2.4/src/refrain/i18n/refrain_de.qm +0 -0
  27. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/i18n/refrain_de.ts +274 -84
  28. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/sources/mpris.py +5 -5
  29. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/sources/mpris_server.py +37 -8
  30. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/ui/log_window.py +5 -5
  31. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/ui/settings_window.py +55 -1
  32. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/ui/tray.py +20 -4
  33. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/ui/update_dialog.py +19 -18
  34. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/updater.py +13 -4
  35. {refrain-0.2.3 → refrain-0.2.4}/tests/test_config.py +24 -0
  36. {refrain-0.2.3 → refrain-0.2.4}/tests/test_daemon_idle.py +60 -0
  37. refrain-0.2.4/tests/test_discord_rpc.py +180 -0
  38. {refrain-0.2.3 → refrain-0.2.4}/tests/test_updater.py +38 -0
  39. refrain-0.2.3/docs/screenshots/live-log.png +0 -0
  40. refrain-0.2.3/docs/screenshots/settings-advanced.png +0 -0
  41. refrain-0.2.3/docs/screenshots/settings-general.png +0 -0
  42. refrain-0.2.3/docs/screenshots/settings-sources.png +0 -0
  43. refrain-0.2.3/docs/screenshots/settings-updates.png +0 -0
  44. refrain-0.2.3/docs/screenshots/update-dialog.png +0 -0
  45. refrain-0.2.3/docs/screenshots/welcome.png +0 -0
  46. refrain-0.2.3/src/refrain/__init__.py +0 -1
  47. refrain-0.2.3/src/refrain/i18n/refrain_de.qm +0 -0
  48. {refrain-0.2.3 → refrain-0.2.4}/.editorconfig +0 -0
  49. {refrain-0.2.3 → refrain-0.2.4}/.gitattributes +0 -0
  50. {refrain-0.2.3 → refrain-0.2.4}/.github/FUNDING.yml +0 -0
  51. {refrain-0.2.3 → refrain-0.2.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  52. {refrain-0.2.3 → refrain-0.2.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  53. {refrain-0.2.3 → refrain-0.2.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  54. {refrain-0.2.3 → refrain-0.2.4}/.github/codeql/codeql-config.yml +0 -0
  55. {refrain-0.2.3 → refrain-0.2.4}/.github/dependabot.yml +0 -0
  56. {refrain-0.2.3 → refrain-0.2.4}/.github/pull_request_template.md +0 -0
  57. {refrain-0.2.3 → refrain-0.2.4}/.github/workflows/codeql.yml +0 -0
  58. {refrain-0.2.3 → refrain-0.2.4}/.github/workflows/release.yml +0 -0
  59. {refrain-0.2.3 → refrain-0.2.4}/.github/workflows/security.yml +0 -0
  60. {refrain-0.2.3 → refrain-0.2.4}/.github/workflows/tests.yml +0 -0
  61. {refrain-0.2.3 → refrain-0.2.4}/.gitignore +0 -0
  62. {refrain-0.2.3 → refrain-0.2.4}/CODE_OF_CONDUCT.md +0 -0
  63. {refrain-0.2.3 → refrain-0.2.4}/CONTRIBUTING.md +0 -0
  64. {refrain-0.2.3 → refrain-0.2.4}/LICENSE +0 -0
  65. {refrain-0.2.3 → refrain-0.2.4}/Makefile +0 -0
  66. {refrain-0.2.3 → refrain-0.2.4}/SECURITY.md +0 -0
  67. {refrain-0.2.3 → refrain-0.2.4}/docs/screenshots/README.md +0 -0
  68. {refrain-0.2.3 → refrain-0.2.4}/docs/screenshots/discord-rpc.png +0 -0
  69. {refrain-0.2.3 → refrain-0.2.4}/docs/screenshots/notification.png +0 -0
  70. {refrain-0.2.3 → refrain-0.2.4}/docs/screenshots/tray-menu.png +0 -0
  71. {refrain-0.2.3 → refrain-0.2.4}/docs/test-matrix.md +0 -0
  72. {refrain-0.2.3 → refrain-0.2.4}/packaging/README.md +0 -0
  73. {refrain-0.2.3 → refrain-0.2.4}/packaging/aur/refrain-git/PKGBUILD +0 -0
  74. {refrain-0.2.3 → refrain-0.2.4}/packaging/flatpak/io.github.Rockykln.Refrain.desktop +0 -0
  75. {refrain-0.2.3 → refrain-0.2.4}/packaging/flatpak/io.github.Rockykln.Refrain.yaml +0 -0
  76. {refrain-0.2.3 → refrain-0.2.4}/packaging/flatpak/python-deps.json +0 -0
  77. {refrain-0.2.3 → refrain-0.2.4}/requirements-dev.txt +0 -0
  78. {refrain-0.2.3 → refrain-0.2.4}/requirements.txt +0 -0
  79. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/__main__.py +0 -0
  80. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/assets/icons/github-mark.svg +0 -0
  81. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/assets/icons/refrain.png +0 -0
  82. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/assets/icons/refrain.svg +0 -0
  83. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/assets/icons/tray-paused-dark.svg +0 -0
  84. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/assets/icons/tray-paused.svg +0 -0
  85. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/assets/icons/tray-playing-dark.svg +0 -0
  86. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/assets/icons/tray-playing.svg +0 -0
  87. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/assets/icons/tray-stopped-dark.svg +0 -0
  88. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/assets/icons/tray-stopped.svg +0 -0
  89. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/assets/refrain.desktop +0 -0
  90. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/autostart.py +0 -0
  91. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/cover_art.py +0 -0
  92. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/cover_fetcher.py +0 -0
  93. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/logging_setup.py +0 -0
  94. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/paths.py +0 -0
  95. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/single_instance.py +0 -0
  96. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/sources/__init__.py +0 -0
  97. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/sources/base.py +0 -0
  98. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/sources/bluetooth.py +0 -0
  99. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/timing.py +0 -0
  100. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/ui/__init__.py +0 -0
  101. {refrain-0.2.3 → refrain-0.2.4}/src/refrain/ui/welcome_dialog.py +0 -0
  102. {refrain-0.2.3 → refrain-0.2.4}/tests/__init__.py +0 -0
  103. {refrain-0.2.3 → refrain-0.2.4}/tests/conftest.py +0 -0
  104. {refrain-0.2.3 → refrain-0.2.4}/tests/test_autostart.py +0 -0
  105. {refrain-0.2.3 → refrain-0.2.4}/tests/test_cover_art.py +0 -0
  106. {refrain-0.2.3 → refrain-0.2.4}/tests/test_cover_fetcher.py +0 -0
  107. {refrain-0.2.3 → refrain-0.2.4}/tests/test_daemon_album_format.py +0 -0
  108. {refrain-0.2.3 → refrain-0.2.4}/tests/test_discord_listening.py +0 -0
  109. {refrain-0.2.3 → refrain-0.2.4}/tests/test_imports.py +0 -0
  110. {refrain-0.2.3 → refrain-0.2.4}/tests/test_mpris_dispatch.py +0 -0
  111. {refrain-0.2.3 → refrain-0.2.4}/tests/test_paths.py +0 -0
  112. {refrain-0.2.3 → refrain-0.2.4}/tests/test_sources_base.py +0 -0
  113. {refrain-0.2.3 → refrain-0.2.4}/tests/test_timing.py +0 -0
  114. {refrain-0.2.3 → refrain-0.2.4}/tests/test_tray_init.py +0 -0
  115. {refrain-0.2.3 → refrain-0.2.4}/tests/test_update_dialog_body.py +0 -0
@@ -7,6 +7,134 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.4] - 2026-05-07
11
+
12
+ A polish + reliability release built on the v0.2.3 distro-portability
13
+ work. The headline items are time-display consistency across all
14
+ three surfaces (tray / Discord / Plasma panel), Snap/Flatpak Discord
15
+ support out of the box, a full inline changelog in Settings, and
16
+ ~20 % more test coverage to lock the new behaviour in. No breaking
17
+ changes — same config schema, same Python floor, same UI layout.
18
+
19
+ ### Added
20
+
21
+ - **Inline release notes in Settings → Updates tab.** The tab
22
+ previously only had an auto-check toggle, last-checked label and a
23
+ "Check now" button — the actual release notes only showed up in
24
+ the popup when an update was available. The tab now carries a
25
+ QTextBrowser that renders the same Markdown the popup uses, plus
26
+ "Current version" and "Latest known" labels. `UpdateOrchestrator`
27
+ gained a `releaseInfoFetched(release | None)` signal that fires
28
+ after every check (auto or manual, success or failure) so the tab
29
+ refreshes its contents regardless of the result.
30
+ - **`docs/bluetooth.md`** — first-time-setup walkthrough covering
31
+ bluez install per distro, pairing recipe, AVRCP verification,
32
+ per-source Discord profile setup, and a Troubleshooting section
33
+ for the common breakage modes (no AVRCP exposed, missing
34
+ control, multiple paired devices, empty dropdown).
35
+ - **FAQ entries** for "Refrain isn't picking up my browser"
36
+ (covers the `playerctl` diagnostic, the Snap-confined-browser
37
+ workaround, and the Firefox `about:config` toggle), and
38
+ "How do I add a browser that isn't in the list".
39
+
40
+ ### Changed
41
+
42
+ - **Tray progress + published MPRIS metadata + Discord activity
43
+ payload all use the iTunes-corrected duration.** v0.2.3 already
44
+ fixed Discord; the tray's "0:42 / 7:21 (-6:39)" ticker and the
45
+ `mpris:length` we publish to Plasma's panel were still using the
46
+ raw `mpris:length` and showed inconsistent numbers when MPRIS
47
+ briefly lied about a song's duration. Hoisted
48
+ `pick_effective_duration_ms` into `_dispatch` so all three
49
+ surfaces see the same value every tick. Tray position is also
50
+ now clamped to duration so a "2:30 / 0:14 (-0:00)" display can't
51
+ happen during a brief MPRIS preview-clip glitch on a longer song.
52
+ - **`os.chmod(tmp, 0o755)` on AppImage update replaced by mode
53
+ preservation** — read the running AppImage's mode and mirror it,
54
+ with `0o700` as fallback. CodeQL's `py/overly-permissive-file`
55
+ warning is gone, and an AppImage that the user installed at
56
+ `0o700` stays `0o700` across upgrades.
57
+ - **DiscordRPC dedupes identical consecutive payloads** instead of
58
+ hammering the IPC channel on every poll. The daemon ticks every
59
+ 500 ms but Discord rate-limits presence updates to 5 per 20 s, so
60
+ ~75 % of those ticks were silently being dropped on the Discord
61
+ side anyway. Now the second-and-on identical payload is a no-op
62
+ on our side too.
63
+ - **Cover-wait defer back to 3 polls (~1.5 s)**. v0.2.2 lowered it
64
+ to 1 poll to feel more "live", but that meant Discord briefly
65
+ rendered the `refrain` brand fallback as the *large* image while
66
+ iTunes search returned, then flipped to the cover — visible
67
+ flicker on every track change. With 3 polls Discord typically
68
+ transitions straight from "no activity" to the cover with no
69
+ flash. Bounded: songs that have no iTunes match still update
70
+ after 1.5 s with the brand fallback.
71
+ - **Tray menu's "Update available" line now carries a coloured
72
+ icon** (Breeze accent blue, `assets/icons/menu-update.svg`)
73
+ instead of just a unicode `⬆` arrow in the same white as every
74
+ other line. Visually distinguishes the update notification from
75
+ Settings / Live log / Restart in the menu's icon column.
76
+ - **Browser hint defaults expanded** with Floorp, Waterfox, Mullvad
77
+ Browser, Tor Browser and ungoogled-chromium. Existing configs
78
+ keep their saved list (no auto-migration); the new entries appear
79
+ unticked in Settings → Sources until the user toggles them.
80
+ - **~20 English-only UI strings wrapped in `tr()`** —
81
+ `update_dialog.py` (status labels, button labels, header HTML),
82
+ `log_window.py` (toolbar Level/Auto-scroll/Copy/Clear/Close), and
83
+ `app.py` module-level QMessageBox calls (now via
84
+ `QCoreApplication.translate`). Re-ran `lupdate6` + `lrelease6`
85
+ against `refrain_de.ts`: 127/127 finished German translations.
86
+ - **`docs/architecture.md` refreshed** to v0.2.x reality — the
87
+ diagram showed a "1 Hz tick" (default has been 500 ms since
88
+ v0.2.2), the worker-thread block was missing `MPRISServer`, the
89
+ GLib thread for dbus-python signal dispatch was undocumented, and
90
+ the "does not export any custom interfaces" line was wrong since
91
+ the v0.2.2 MPRIS-server publication. Plus a new section on
92
+ Discord IPC sandbox bridging (Snap/Flatpak).
93
+
94
+ ### Fixed
95
+
96
+ - **Tray tooltip cleared on track change.** After a paused-to-paused
97
+ track switch, the tooltip briefly showed "Song B • 1:30 / 2:11"
98
+ using Song A's elapsed counter while the new track waited for its
99
+ first `progressTick`. `set_track` now drops the stale progress
100
+ line when the title text actually changed.
101
+ - **`compute_idle_state` honours the iTunes-corrected duration.**
102
+ Previously the deadline keyed off `track.duration_ms` so a
103
+ 7:21-playlist-total-on-a-2:11-song lie made dangling-tab cleanup
104
+ fire 5 minutes too late. New optional `effective_duration_ms`
105
+ parameter; the daemon passes the same value the RPC payload
106
+ uses.
107
+ - **Snap and Flatpak Discord builds reachable out of the box.**
108
+ Their IPC socket lives inside the sandbox tree
109
+ (`$XDG_RUNTIME_DIR/app/com.discordapp.Discord/`,
110
+ `~/.var/app/com.discordapp.Discord/config/discord/`,
111
+ `~/snap/discord/current/.config/discord/`), so pypresence's stock
112
+ discovery never finds it. `_bridge_sandboxed_ipc_socket` symlinks
113
+ the first sandbox socket it finds into `$XDG_RUNTIME_DIR` before
114
+ each connect attempt, and sweeps stale symlinks left behind when
115
+ the sandbox path's target is removed (Snap uninstall, Flatpak
116
+ remove, host reboot).
117
+ - **Config drops unknown TOML keys instead of nuking the whole
118
+ file.** A single typo or a key written by a *newer* Refrain that
119
+ the user has since downgraded from used to make
120
+ `Config.from_dict` raise `TypeError`, caught by the surrounding
121
+ `except` and falling back to defaults for *every* setting.
122
+ `_construct()` now filters the payload through dataclass fields
123
+ before `**`-splatting; the offending keys get a single WARNING in
124
+ the log naming the section, the rest of the section survives.
125
+ - **Stale comments + doc references cleaned up.** Four "1 Hz" call-
126
+ outs in `mpris.py` / `daemon.py` (default has been 500 ms since
127
+ v0.2.2), plus one reference to the AppImage runtime that no
128
+ longer matches the AppRun layout.
129
+
130
+ ### Tests
131
+
132
+ - **+13 unit tests** covering the new helpers — `DiscordRPC` payload
133
+ dedup (4), `_bridge_sandboxed_ipc_socket` (4), `compute_idle_state`
134
+ with `effective_duration_ms` (2), `cleanup_orphan_downloads` (3).
135
+ Total: 113 → 125, all green, ruff + bandit + pip-audit clean,
136
+ Dependabot 0 outstanding alerts.
137
+
10
138
  ## [0.2.3] - 2026-05-07
11
139
 
12
140
  A reliability + portability pass driven by hands-on testing across
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refrain
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Discord Rich Presence for Apple Music on Linux
5
5
  Project-URL: Homepage, https://github.com/Rockykln/refrain
6
6
  Project-URL: Repository, https://github.com/Rockykln/refrain
@@ -319,6 +319,8 @@ tray + controls running.
319
319
 
320
320
  - [Architecture overview](docs/architecture.md) — threads, D-Bus surface, file paths
321
321
  - [FAQ](docs/faq.md)
322
+ - [Bluetooth quick-start](docs/bluetooth.md) — pair + AVRCP setup walkthrough
323
+ - [Test matrix](docs/test-matrix.md) — supported distros, smoke-check checklist
322
324
  - [Roadmap](ROADMAP.md)
323
325
  - [Changelog](CHANGELOG.md)
324
326
  - [Contributing](CONTRIBUTING.md) — dev setup, testing, code style
@@ -285,6 +285,8 @@ tray + controls running.
285
285
 
286
286
  - [Architecture overview](docs/architecture.md) — threads, D-Bus surface, file paths
287
287
  - [FAQ](docs/faq.md)
288
+ - [Bluetooth quick-start](docs/bluetooth.md) — pair + AVRCP setup walkthrough
289
+ - [Test matrix](docs/test-matrix.md) — supported distros, smoke-check checklist
288
290
  - [Roadmap](ROADMAP.md)
289
291
  - [Changelog](CHANGELOG.md)
290
292
  - [Contributing](CONTRIBUTING.md) — dev setup, testing, code style
@@ -219,6 +219,70 @@ What's done, what's next, what's deliberately not in scope.
219
219
  out-of-scope reasons for the floor of unsupported distros (RHEL 9
220
220
  / Rocky 9 / Alma 9 / Debian 11 / Ubuntu 22.04 / Alpine).
221
221
 
222
+ ## Done — v0.2.4
223
+
224
+ - **Time-display consistency across tray + Plasma panel + Discord**.
225
+ v0.2.3 already fixed Discord; the tray's progress label and the
226
+ published MPRIS Metadata still keyed off the raw `mpris:length`
227
+ and showed inconsistent numbers when MPRIS briefly lied about a
228
+ song's duration. `pick_effective_duration_ms` is now hoisted into
229
+ `_dispatch` so all three surfaces see the same iTunes-corrected
230
+ value every tick. Plus position is clamped to duration so a
231
+ "2:30 / 0:14 (-0:00)" line can't render during a brief MPRIS
232
+ preview-clip glitch.
233
+ - **Snap and Flatpak Discord builds reachable out of the box**.
234
+ Their IPC socket lives inside the sandbox tree
235
+ (`xdg-run/app/com.discordapp.Discord/`,
236
+ `~/.var/app/com.discordapp.Discord/.../`,
237
+ `~/snap/discord/current/.config/discord/`) which pypresence
238
+ doesn't probe. `_bridge_sandboxed_ipc_socket` symlinks the first
239
+ sandbox socket it finds into `$XDG_RUNTIME_DIR` before each
240
+ connect attempt + sweeps stale symlinks left behind when the
241
+ Discord install is removed.
242
+ - **Inline release notes in Settings → Updates tab**. The tab
243
+ carries a QTextBrowser that renders the same Markdown as the
244
+ popup, plus "Current version" + "Latest known" labels.
245
+ `releaseInfoFetched(release | None)` signal fires after every
246
+ check (auto / manual, success / failure) so the tab refreshes
247
+ regardless of the result.
248
+ - **Discord activity card always shows the Refrain brand badge as
249
+ small_image**. Previously the badge was only in `large_image` as
250
+ a fallback when cover-art lookup failed; now it's the small-icon
251
+ corner of every payload, so the cover gets the visual focus and
252
+ hovering reveals "Refrain" as the source app. Plus the
253
+ cover-wait defer is back to 3 polls (~1.5 s at 500 ms tick) so
254
+ Discord usually goes straight from "no activity" to "cover" with
255
+ no brand flash on the way.
256
+ - **Config drops unknown TOML keys** instead of nuking the whole
257
+ file. A typo or a key written by a newer Refrain that the user
258
+ has since downgraded from used to make `Config.from_dict` raise,
259
+ caught by the surrounding except, and fall back to defaults for
260
+ every setting. Now the offending key gets a single warning, the
261
+ rest of the section survives.
262
+ - **DiscordRPC dedupes identical consecutive payloads**. The
263
+ daemon ticks every 500 ms but Discord rate-limits presence
264
+ updates to 5/20 s — most ticks were silently dropped on the
265
+ Discord side anyway. Now the second-and-on identical payload
266
+ is a no-op on our side too.
267
+ - **Browser hints expanded**: Floorp, Waterfox, Mullvad Browser,
268
+ Tor Browser, ungoogled-chromium.
269
+ - **`docs/bluetooth.md`** — first-time-setup walkthrough covering
270
+ bluez install per distro, pairing recipe, AVRCP verification,
271
+ per-source Discord profile setup, and Troubleshooting.
272
+ - **FAQ entries** for "Refrain isn't picking up my browser"
273
+ (covers `playerctl` diagnostic + Snap-confined-browser workaround
274
+ + Firefox `about:config` toggle) and "How do I add a browser
275
+ that isn't in the list".
276
+ - **`docs/architecture.md` refreshed** to v0.2.x reality — diagram
277
+ no longer claims "1 Hz tick" (default has been 500 ms since
278
+ v0.2.2), `MPRISServer` block added, `org.mpris.MediaPlayer2.refrain`
279
+ publish documented, GLib thread for dbus-python signal dispatch
280
+ documented, new section on Discord IPC sandbox bridging.
281
+ - **+13 unit tests**: `DiscordRPC` payload dedup (4),
282
+ `_bridge_sandboxed_ipc_socket` (4), `compute_idle_state` with
283
+ `effective_duration_ms` (2), `cleanup_orphan_downloads` (3).
284
+ Total: 113 → 125, all green.
285
+
222
286
  ## Up next — v0.3
223
287
 
224
288
  - **Polishing the wrapped i18n surface** — wrap the remaining
@@ -11,26 +11,33 @@ Refrain is one process with two threads and one external IPC socket.
11
11
  │ ├─ TrayIcon (QSystemTrayIcon) ←─── status / track │
12
12
  │ ├─ SettingsWindow (QDialog, hidden after Apply) │
13
13
  │ ├─ LogWindow (QDialog, on-demand) │
14
+ │ ├─ WelcomeDialog (first-run only) │
14
15
  │ ├─ UpdateOrchestrator │
15
16
  │ └─ UpdateDialog │
16
17
  │ │
17
18
  │ Worker thread (Qt event loop, QThread) │
18
19
  │ ├─ DaemonWorker │
19
- │ │ ├─ MPRISSource ──► Session DBus │
20
+ │ │ ├─ MPRISSource ──► Session DBus (read) │
20
21
  │ │ ├─ BluetoothSource ──► System DBus (org.bluez) │
21
22
  │ │ ├─ CoverFetcher ──► iTunes Search (HTTPS) │
22
- │ │ └─ DiscordRPC ──► Discord IPC socket │
23
- │ └─ Timer (QTimer, 1 Hz tick) │
23
+ │ │ ├─ DiscordRPC ──► Discord IPC socket │
24
+ │ │ └─ MPRISServer ──► Session DBus (publish own) │
25
+ │ └─ Timer (QTimer, 500 ms default poll, configurable) │
26
+ │ │
27
+ │ GLib thread (only when PyGObject is available) │
28
+ │ └─ GLib.MainLoop — pumps dbus-python signals so Plasma's panel │
29
+ │ PlayPause / Next / Previous reach our MPRISServer methods. │
24
30
  └─────────────────────────────────────────────────────────────────┘
25
31
  ```
26
32
 
27
33
  ## Why a worker thread?
28
34
 
29
- The polling loop (1 Hz) reads MPRIS / BlueZ properties via D-Bus. Some of
30
- those calls block briefly. iTunes Search lookups can take up to 5 s.
31
- Discord IPC writes can stall when Discord is restarting. Doing any of
32
- that on the GUI thread would make the tray icon and settings window
33
- freeze every poll.
35
+ The polling loop (default 500 ms, configurable via
36
+ `advanced.poll_interval_ms`) reads MPRIS / BlueZ properties via D-Bus.
37
+ Some of those calls block briefly. iTunes Search lookups can take up
38
+ to 5 s. Discord IPC writes can stall when Discord is restarting.
39
+ Doing any of that on the GUI thread would make the tray icon and
40
+ settings window freeze every poll.
34
41
 
35
42
  The worker thread runs a Qt event loop (driven by `QTimer`, *not* a
36
43
  `while/sleep` loop — that detail matters: a sleep loop blocks the event
@@ -84,6 +91,30 @@ to `/usr/share/applications/refrain.desktop` instead.
84
91
  | `org.mpris.MediaPlayer2.Player` | session| Track metadata + Play/Next/Prev |
85
92
  | `org.bluez.MediaPlayer1` | system | AVRCP track + Play/Pause/Next/Prev|
86
93
  | `org.bluez.Device1` (via ObjectManager) | system | Paired-device enumeration |
94
+ | `org.freedesktop.DBus.NameHasOwner` | system | Fast-fail check before activating `org.bluez` (avoids a 25 s service-activation timeout on hosts without bluez)|
95
+
96
+ ## D-Bus interfaces published
97
+
98
+ Refrain publishes two well-known names on the **session** bus:
99
+
100
+ | Bus name | Purpose |
101
+ |---------------------------------------|------------------------------------------------|
102
+ | `io.github.Rockykln.Refrain` | Single-instance lock. No object path; just the name reservation. |
103
+ | `org.mpris.MediaPlayer2.refrain` | Refrain itself as an MPRIS player. KDE Plasma's panel media controls applet, KDE Connect, GNOME Shell etc. drive the same Play/Pause/Next/Previous as the tray, and render the same track Discord renders. Implements the standard MPRIS root + Player interfaces. |
104
+
105
+ The MPRIS-server publication needs PyGObject (`gi`) for a GLib main loop
106
+ to pump dbus-python signal dispatch. When PyGObject isn't installed,
107
+ Refrain logs a warning at startup and falls back to read-only mode —
108
+ the rest of the app works, but Plasma's panel can't drive playback.
109
+
110
+ ## Discord IPC discovery
87
111
 
88
- Refrain also registers exactly one well-known name on the session bus
89
- for the single-instance lock; it does not export any custom interfaces.
112
+ The standard path is `$XDG_RUNTIME_DIR/discord-ipc-N` (N=0..9). Snap
113
+ and Flatpak Discord builds put their socket inside the sandbox tree
114
+ instead (`$XDG_RUNTIME_DIR/app/com.discordapp.Discord/discord-ipc-N`,
115
+ `~/.var/app/com.discordapp.Discord/config/discord/discord-ipc-N`,
116
+ `~/snap/discord/current/.config/discord/discord-ipc-N`).
117
+ `DiscordRPC._bridge_sandboxed_ipc_socket` symlinks the first sandbox
118
+ socket it finds into `$XDG_RUNTIME_DIR` before each connect attempt,
119
+ and sweeps stale symlinks left behind by previously-uninstalled
120
+ Discord builds.
@@ -0,0 +1,136 @@
1
+ # Bluetooth quick-start
2
+
3
+ Refrain's Bluetooth source reads playback metadata via BlueZ AVRCP —
4
+ the standard Bluetooth audio metadata profile. Anything that pairs as
5
+ an A2DP audio source and exposes `org.bluez.MediaPlayer1` over D-Bus
6
+ will work: iPhones, Android phones, dedicated music players, even
7
+ some car head-units.
8
+
9
+ This guide walks through getting it set up the first time on KDE
10
+ Plasma, GNOME, or any other desktop with `bluez` running. If
11
+ something is missing on your distro, the
12
+ [Troubleshooting](#troubleshooting) section at the end covers the
13
+ usual suspects.
14
+
15
+ ## Prerequisites
16
+
17
+ - BlueZ ≥ 5 with the AVRCP profile enabled (default on every modern
18
+ desktop distro).
19
+ - A Bluetooth adapter that's powered on. `bluetoothctl show` should
20
+ list at least one controller.
21
+ - Your desktop's Bluetooth applet running (KDE's
22
+ bluedevil-applet, GNOME's bluetooth-panel, blueman, …). Refrain
23
+ doesn't manage pairing itself — it only reads metadata from
24
+ already-connected devices.
25
+
26
+ If `bluetoothctl` isn't installed, install it via your package
27
+ manager:
28
+
29
+ | Distro | Command |
30
+ |---|---|
31
+ | Arch / CachyOS / Manjaro | `sudo pacman -S bluez bluez-utils` |
32
+ | Debian / Ubuntu / Mint | `sudo apt install bluez bluez-tools` |
33
+ | Fedora / RHEL / Rocky | `sudo dnf install bluez bluez-tools` |
34
+ | openSUSE | `sudo zypper install bluez` |
35
+
36
+ ## Pairing
37
+
38
+ 1. Put the phone (or other source) into pairing mode. On iOS this is
39
+ *Settings → Bluetooth*; on Android it's *Settings → Connected
40
+ devices*.
41
+ 2. Open your desktop's Bluetooth applet, scan for new devices, click
42
+ *Pair* on the phone entry, confirm the matching PIN on both ends.
43
+ 3. Enable *Audio*. Some applets surface this as a toggle after
44
+ pairing; others auto-enable it. Verify with:
45
+ ```sh
46
+ bluetoothctl info <MAC>
47
+ ```
48
+ Look for `UUIDs: ... A/V Remote Control Target ... AudioSource ...`
49
+ in the output. If those are missing, the phone is paired but not
50
+ advertising AVRCP — disconnect, repair, and tick the *Audio
51
+ profile* box this time.
52
+
53
+ ## First playback
54
+
55
+ 1. Make sure the phone is **connected** (not just paired). The
56
+ applet shows a connected indicator; CLI:
57
+ ```sh
58
+ bluetoothctl info <MAC> | grep "Connected:"
59
+ ```
60
+ should show `Connected: yes`.
61
+ 2. Start music on the phone. Spotify, Apple Music, the iOS Music
62
+ app, anything that publishes track metadata via the AVRCP
63
+ profile.
64
+ 3. In Refrain, open *Settings → Sources → Bluetooth*:
65
+ - Toggle **Enable Bluetooth source** on.
66
+ - Pick the device from the dropdown. It should show the phone's
67
+ Bluetooth name (e.g. *Rocky's iPhone*) and its MAC address.
68
+ - Hit **Apply**.
69
+ 4. Within ~1 s, the tray menu shows the track title + artist.
70
+ Within ~2 s, Discord renders the listening status.
71
+
72
+ The dropdown's `(auto-detect)` entry picks whichever device is
73
+ currently exposing AVRCP — useful if you switch between phone and
74
+ headphones with the same Refrain config. The MAC-pinned variant is
75
+ stricter but stable when multiple sources are connected at once.
76
+
77
+ ## Per-source Discord profile (optional)
78
+
79
+ You can give Bluetooth its own Discord application so the status
80
+ renders with a different icon than the browser's Apple Music
81
+ playback:
82
+
83
+ 1. Register a second Discord application at
84
+ <https://discord.com/developers/applications> — call it e.g.
85
+ "Refrain (Bluetooth)" and upload a Bluetooth glyph as the icon.
86
+ 2. Copy the new Client ID.
87
+ 3. *Settings → General → Bluetooth Client ID* — paste, *Apply*.
88
+
89
+ Refrain reconnects RPC under the per-source ID the moment a track
90
+ arrives from Bluetooth.
91
+
92
+ ## Troubleshooting
93
+
94
+ ### Refrain says "no track" while music is clearly playing on the phone
95
+
96
+ - Confirm AVRCP is actually working:
97
+ ```sh
98
+ busctl --system call org.bluez /org/bluez/hci0/dev_<MAC_with_underscores> \
99
+ org.freedesktop.DBus.Properties Get ss org.bluez.MediaPlayer1 Track
100
+ ```
101
+ This should return the current track. If it errors with
102
+ `org.bluez.Error.DoesNotExist`, the phone isn't exposing AVRCP — try
103
+ disconnect / reconnect, and verify the *Audio profile* box on the
104
+ pairing.
105
+ - Check the tray-menu source label or the live log
106
+ (tray → *Live log…*). Look for `Track change [bluetooth]: …` lines.
107
+ If you see `[mpris]` instead, the browser is winning the source
108
+ race — close the music tab so Bluetooth becomes the only candidate.
109
+
110
+ ### `bluetoothd` D-Bus activation timeout warning in the log
111
+
112
+ You'll see something like
113
+ `Bluetooth: GetManagedObjects failed: ... service_start_timeout=25000ms`
114
+ on a system without `bluez` installed (typical of VMs and minimal
115
+ installs). v0.2.3+ fast-fails before the activation timeout fires;
116
+ on older builds you'd want to disable the Bluetooth source toggle.
117
+
118
+ ### The dropdown is empty
119
+
120
+ That means BlueZ is running but no `org.bluez.Device1` entries exist.
121
+ Pair at least one device first (Section "Pairing" above), then click
122
+ **Apply** in Refrain to refresh the dropdown.
123
+
124
+ ### Track shows but Play/Pause/Next/Previous don't work
125
+
126
+ AVRCP control depends on the phone's app supporting
127
+ `AVRCP-CT 1.4` or higher. iOS Music, Apple Music, and Spotify all
128
+ do. Some Android battery-saver settings disable AVRCP control —
129
+ check the per-app battery / background settings on the phone.
130
+
131
+ ### Multiple paired devices, wrong one gets picked
132
+
133
+ Set *Settings → Sources → Bluetooth → Device* to the specific MAC.
134
+ The `(auto-detect)` mode picks the first eligible AVRCP player from
135
+ BlueZ's enumeration order, which isn't stable when several phones
136
+ are paired.
@@ -37,6 +37,50 @@ Install the **AppIndicator and KStatusNotifierItem** GNOME Shell
37
37
  extension. Refrain's tray uses `QSystemTrayIcon`, which on GNOME requires
38
38
  that extension to be visible.
39
39
 
40
+ ## Refrain isn't picking up my browser.
41
+
42
+ Refrain reads track metadata from the browser's MPRIS publication. If
43
+ nothing shows up while Apple Music is playing, run `playerctl -l` in a
44
+ terminal — it lists every MPRIS player on your session bus.
45
+
46
+ - **Empty list (or no Firefox / Chromium entry):** the browser isn't
47
+ publishing MPRIS at all. Common causes:
48
+ - **Firefox**: MPRIS is off by default on some installs. Open
49
+ `about:config` → set `media.hardwaremediakeys.enabled = true` →
50
+ fully quit Firefox (close all windows + wait for `pgrep firefox`
51
+ to be empty) → relaunch.
52
+ - **Snap-confined browsers** (Snap Firefox, Snap Chromium on
53
+ Ubuntu): the snap sandbox blocks D-Bus session-bus access for
54
+ MPRIS. Switch to a deb-channel browser:
55
+ - Mozilla's official Firefox deb:
56
+ <https://support.mozilla.org/en-US/kb/install-firefox-linux>
57
+ - Brave deb: <https://brave.com/linux/>
58
+ - Chromium from Debian/Ubuntu deb (not the Snap).
59
+ - **Flatpak browsers** without `--talk-name=org.mpris.MediaPlayer2.*`:
60
+ Flathub builds usually have it; self-built ones may not.
61
+ - **Browser shows but track isn't picked up:** check that `xesam:url`
62
+ contains `music.apple.com`:
63
+ ```sh
64
+ playerctl --player=firefox metadata | grep xesam:url
65
+ ```
66
+ If the URL field is empty or points elsewhere, Apple Music's tab
67
+ isn't the active media tab — switch to it and start playback.
68
+
69
+ ## How do I add a browser that isn't in the Settings list?
70
+
71
+ *Settings → Sources → Detected browsers → Other (comma-separated)*.
72
+ Enter a substring of the browser's MPRIS bus name. Find it via
73
+ `playerctl -l` while a media tab is playing — e.g. for Floorp the
74
+ substring is `floorp`. Save with *Apply*.
75
+
76
+ ## Bluetooth: how do I get my iPhone / phone showing up?
77
+
78
+ See the dedicated walkthrough at [`docs/bluetooth.md`](bluetooth.md).
79
+ Quick version: pair the phone in your desktop's Bluetooth manager,
80
+ connect it, start music on the phone, then in Refrain
81
+ *Settings → Sources → Bluetooth* turn the toggle on and pick the
82
+ device from the dropdown.
83
+
40
84
  ## Refrain is already running but the settings window won't reopen.
41
85
 
42
86
  Click the tray icon. The settings window is normally hidden, not closed —
@@ -30,7 +30,7 @@ AppDir:
30
30
  id: io.github.Rockykln.Refrain
31
31
  name: Refrain
32
32
  icon: refrain
33
- version: 0.2.3
33
+ version: 0.2.4
34
34
  exec: usr/bin/python3
35
35
  exec_args: -m refrain $@
36
36
 
@@ -1,6 +1,6 @@
1
1
  pkgbase = refrain
2
2
  pkgdesc = Discord Rich Presence for Apple Music on Linux
3
- pkgver = 0.2.2
3
+ pkgver = 0.2.3
4
4
  pkgrel = 1
5
5
  url = https://github.com/Rockykln/refrain
6
6
  arch = any
@@ -13,7 +13,8 @@ pkgbase = refrain
13
13
  depends = python-pypresence
14
14
  depends = python-dbus
15
15
  depends = pyside6
16
- source = refrain-0.2.2.tar.gz::https://github.com/Rockykln/refrain/archive/v0.2.2.tar.gz
17
- sha256sums = db07e0783b3f44a7c54d4b8ee2ebe33f3dc577707a03bd42f5958ddae3d5ac12
16
+ optdepends = python-gobject: enables MPRIS-server publication so Plasma media controls reach Refrain
17
+ source = refrain-0.2.3.tar.gz::https://github.com/Rockykln/refrain/archive/v0.2.3.tar.gz
18
+ sha256sums = 29f2371393d855f508730238a096cbdc85219616a9fab240745c1072a005b23d
18
19
 
19
20
  pkgname = refrain
@@ -1,7 +1,7 @@
1
1
  # Maintainer: Rockykln <contact@rockykln.com>
2
2
 
3
3
  pkgname=refrain
4
- pkgver=0.2.3
4
+ pkgver=0.2.4
5
5
  pkgrel=1
6
6
  pkgdesc="Discord Rich Presence for Apple Music on Linux"
7
7
  arch=('any')
@@ -23,9 +23,6 @@ makedepends=(
23
23
  'python-wheel'
24
24
  )
25
25
  source=("$pkgname-$pkgver.tar.gz::https://github.com/Rockykln/refrain/archive/v$pkgver.tar.gz")
26
- # Placeholder — recompute after the GitHub release exists:
27
- # curl -sLO "https://github.com/Rockykln/refrain/archive/v${pkgver}.tar.gz"
28
- # sha256sum "v${pkgver}.tar.gz"
29
26
  sha256sums=('SKIP')
30
27
 
31
28
  build() {
@@ -51,6 +51,28 @@
51
51
  <content_rating type="oars-1.1" />
52
52
 
53
53
  <releases>
54
+ <release version="0.2.4" date="2026-05-07">
55
+ <description>
56
+ <p>
57
+ Polish + reliability release on top of v0.2.3. Time-display
58
+ consistency across tray, Plasma panel media-controls applet
59
+ and Discord (all three now read the same iTunes-corrected
60
+ duration). Snap and Flatpak Discord builds reachable out of
61
+ the box via sandbox-aware IPC-socket bridging. Inline
62
+ release notes in Settings → Updates. Discord activity card
63
+ always carries the Refrain brand as small_image so the
64
+ cover gets the visual focus. Config drops unknown TOML
65
+ keys instead of nuking the whole file when a newer-version
66
+ key is encountered. DiscordRPC dedupes identical
67
+ consecutive payloads. Browser hints expanded with Floorp,
68
+ Waterfox, Mullvad Browser, Tor Browser, ungoogled-chromium.
69
+ New docs/bluetooth.md walkthrough and FAQ entries on
70
+ browser MPRIS troubleshooting. +13 unit tests, total now
71
+ 125, all green; ruff + bandit + pip-audit + Dependabot all
72
+ clean.
73
+ </p>
74
+ </description>
75
+ </release>
54
76
  <release version="0.2.3" date="2026-05-07">
55
77
  <description>
56
78
  <p>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "refrain"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "Discord Rich Presence for Apple Music on Linux"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1 @@
1
+ __version__ = "0.2.4"