refrain 0.2.2__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 (116) hide show
  1. {refrain-0.2.2 → refrain-0.2.4}/.github/ISSUE_TEMPLATE/bug_report.yml +1 -1
  2. {refrain-0.2.2 → refrain-0.2.4}/.gitignore +1 -0
  3. {refrain-0.2.2 → refrain-0.2.4}/CHANGELOG.md +252 -1
  4. {refrain-0.2.2 → refrain-0.2.4}/PKG-INFO +7 -4
  5. {refrain-0.2.2 → refrain-0.2.4}/README.md +6 -3
  6. {refrain-0.2.2 → refrain-0.2.4}/ROADMAP.md +114 -0
  7. {refrain-0.2.2 → refrain-0.2.4}/docs/architecture.md +41 -10
  8. refrain-0.2.4/docs/bluetooth.md +136 -0
  9. {refrain-0.2.2 → refrain-0.2.4}/docs/faq.md +44 -0
  10. refrain-0.2.4/docs/screenshots/live-log.png +0 -0
  11. refrain-0.2.4/docs/screenshots/settings-advanced.png +0 -0
  12. refrain-0.2.4/docs/screenshots/settings-general.png +0 -0
  13. refrain-0.2.4/docs/screenshots/settings-sources.png +0 -0
  14. refrain-0.2.4/docs/screenshots/settings-updates.png +0 -0
  15. refrain-0.2.4/docs/screenshots/update-dialog.png +0 -0
  16. refrain-0.2.4/docs/screenshots/welcome.png +0 -0
  17. refrain-0.2.4/docs/test-matrix.md +169 -0
  18. {refrain-0.2.2 → refrain-0.2.4}/packaging/appimage/AppImageBuilder.yml +1 -1
  19. refrain-0.2.4/packaging/aur/refrain/.SRCINFO +20 -0
  20. {refrain-0.2.2 → refrain-0.2.4}/packaging/aur/refrain/PKGBUILD +5 -2
  21. {refrain-0.2.2 → refrain-0.2.4}/packaging/flatpak/io.github.Rockykln.Refrain.metainfo.xml +43 -0
  22. {refrain-0.2.2 → refrain-0.2.4}/pyproject.toml +1 -1
  23. refrain-0.2.4/src/refrain/__init__.py +1 -0
  24. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/app.py +197 -17
  25. refrain-0.2.4/src/refrain/assets/icons/menu-update.svg +3 -0
  26. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/autostart.py +25 -15
  27. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/config.py +47 -7
  28. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/daemon.py +108 -56
  29. refrain-0.2.4/src/refrain/discord_rpc.py +207 -0
  30. refrain-0.2.4/src/refrain/i18n/refrain_de.qm +0 -0
  31. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/i18n/refrain_de.ts +274 -84
  32. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/logging_setup.py +19 -11
  33. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/sources/bluetooth.py +31 -2
  34. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/sources/mpris.py +15 -7
  35. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/sources/mpris_server.py +48 -11
  36. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/timing.py +36 -0
  37. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/ui/log_window.py +11 -6
  38. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/ui/settings_window.py +72 -1
  39. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/ui/tray.py +20 -4
  40. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/ui/update_dialog.py +69 -18
  41. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/ui/welcome_dialog.py +12 -4
  42. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/updater.py +70 -8
  43. {refrain-0.2.2 → refrain-0.2.4}/tests/test_autostart.py +28 -0
  44. {refrain-0.2.2 → refrain-0.2.4}/tests/test_config.py +24 -0
  45. {refrain-0.2.2 → refrain-0.2.4}/tests/test_daemon_idle.py +60 -0
  46. refrain-0.2.4/tests/test_discord_rpc.py +180 -0
  47. {refrain-0.2.2 → refrain-0.2.4}/tests/test_timing.py +47 -1
  48. {refrain-0.2.2 → refrain-0.2.4}/tests/test_updater.py +38 -0
  49. refrain-0.2.2/docs/screenshots/live-log.png +0 -0
  50. refrain-0.2.2/docs/screenshots/settings-advanced.png +0 -0
  51. refrain-0.2.2/docs/screenshots/settings-general.png +0 -0
  52. refrain-0.2.2/docs/screenshots/settings-sources.png +0 -0
  53. refrain-0.2.2/docs/screenshots/settings-updates.png +0 -0
  54. refrain-0.2.2/docs/screenshots/update-dialog.png +0 -0
  55. refrain-0.2.2/docs/screenshots/welcome.png +0 -0
  56. refrain-0.2.2/src/refrain/__init__.py +0 -1
  57. refrain-0.2.2/src/refrain/discord_rpc.py +0 -107
  58. refrain-0.2.2/src/refrain/i18n/refrain_de.qm +0 -0
  59. {refrain-0.2.2 → refrain-0.2.4}/.editorconfig +0 -0
  60. {refrain-0.2.2 → refrain-0.2.4}/.gitattributes +0 -0
  61. {refrain-0.2.2 → refrain-0.2.4}/.github/FUNDING.yml +0 -0
  62. {refrain-0.2.2 → refrain-0.2.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  63. {refrain-0.2.2 → refrain-0.2.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  64. {refrain-0.2.2 → refrain-0.2.4}/.github/codeql/codeql-config.yml +0 -0
  65. {refrain-0.2.2 → refrain-0.2.4}/.github/dependabot.yml +0 -0
  66. {refrain-0.2.2 → refrain-0.2.4}/.github/pull_request_template.md +0 -0
  67. {refrain-0.2.2 → refrain-0.2.4}/.github/workflows/codeql.yml +0 -0
  68. {refrain-0.2.2 → refrain-0.2.4}/.github/workflows/release.yml +0 -0
  69. {refrain-0.2.2 → refrain-0.2.4}/.github/workflows/security.yml +0 -0
  70. {refrain-0.2.2 → refrain-0.2.4}/.github/workflows/tests.yml +0 -0
  71. {refrain-0.2.2 → refrain-0.2.4}/CODE_OF_CONDUCT.md +0 -0
  72. {refrain-0.2.2 → refrain-0.2.4}/CONTRIBUTING.md +0 -0
  73. {refrain-0.2.2 → refrain-0.2.4}/LICENSE +0 -0
  74. {refrain-0.2.2 → refrain-0.2.4}/Makefile +0 -0
  75. {refrain-0.2.2 → refrain-0.2.4}/SECURITY.md +0 -0
  76. {refrain-0.2.2 → refrain-0.2.4}/docs/screenshots/README.md +0 -0
  77. {refrain-0.2.2 → refrain-0.2.4}/docs/screenshots/discord-rpc.png +0 -0
  78. {refrain-0.2.2 → refrain-0.2.4}/docs/screenshots/notification.png +0 -0
  79. {refrain-0.2.2 → refrain-0.2.4}/docs/screenshots/tray-menu.png +0 -0
  80. {refrain-0.2.2 → refrain-0.2.4}/packaging/README.md +0 -0
  81. {refrain-0.2.2 → refrain-0.2.4}/packaging/aur/refrain-git/PKGBUILD +0 -0
  82. {refrain-0.2.2 → refrain-0.2.4}/packaging/flatpak/io.github.Rockykln.Refrain.desktop +0 -0
  83. {refrain-0.2.2 → refrain-0.2.4}/packaging/flatpak/io.github.Rockykln.Refrain.yaml +0 -0
  84. {refrain-0.2.2 → refrain-0.2.4}/packaging/flatpak/python-deps.json +0 -0
  85. {refrain-0.2.2 → refrain-0.2.4}/requirements-dev.txt +0 -0
  86. {refrain-0.2.2 → refrain-0.2.4}/requirements.txt +0 -0
  87. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/__main__.py +0 -0
  88. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/assets/icons/github-mark.svg +0 -0
  89. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/assets/icons/refrain.png +0 -0
  90. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/assets/icons/refrain.svg +0 -0
  91. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/assets/icons/tray-paused-dark.svg +0 -0
  92. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/assets/icons/tray-paused.svg +0 -0
  93. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/assets/icons/tray-playing-dark.svg +0 -0
  94. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/assets/icons/tray-playing.svg +0 -0
  95. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/assets/icons/tray-stopped-dark.svg +0 -0
  96. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/assets/icons/tray-stopped.svg +0 -0
  97. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/assets/refrain.desktop +0 -0
  98. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/cover_art.py +0 -0
  99. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/cover_fetcher.py +0 -0
  100. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/paths.py +0 -0
  101. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/single_instance.py +0 -0
  102. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/sources/__init__.py +0 -0
  103. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/sources/base.py +0 -0
  104. {refrain-0.2.2 → refrain-0.2.4}/src/refrain/ui/__init__.py +0 -0
  105. {refrain-0.2.2 → refrain-0.2.4}/tests/__init__.py +0 -0
  106. {refrain-0.2.2 → refrain-0.2.4}/tests/conftest.py +0 -0
  107. {refrain-0.2.2 → refrain-0.2.4}/tests/test_cover_art.py +0 -0
  108. {refrain-0.2.2 → refrain-0.2.4}/tests/test_cover_fetcher.py +0 -0
  109. {refrain-0.2.2 → refrain-0.2.4}/tests/test_daemon_album_format.py +0 -0
  110. {refrain-0.2.2 → refrain-0.2.4}/tests/test_discord_listening.py +0 -0
  111. {refrain-0.2.2 → refrain-0.2.4}/tests/test_imports.py +0 -0
  112. {refrain-0.2.2 → refrain-0.2.4}/tests/test_mpris_dispatch.py +0 -0
  113. {refrain-0.2.2 → refrain-0.2.4}/tests/test_paths.py +0 -0
  114. {refrain-0.2.2 → refrain-0.2.4}/tests/test_sources_base.py +0 -0
  115. {refrain-0.2.2 → refrain-0.2.4}/tests/test_tray_init.py +0 -0
  116. {refrain-0.2.2 → refrain-0.2.4}/tests/test_update_dialog_body.py +0 -0
@@ -13,7 +13,7 @@ body:
13
13
  attributes:
14
14
  label: Refrain version
15
15
  description: Run `refrain --version` or check the bottom-right of the settings window.
16
- placeholder: "0.1.4"
16
+ placeholder: "0.2.3"
17
17
  validations:
18
18
  required: true
19
19
 
@@ -39,6 +39,7 @@ env/
39
39
  # Local config / runtime
40
40
  *.log
41
41
  .refrain.local.toml
42
+ *.local.md
42
43
 
43
44
  # Build artifacts (Flatpak / packaging)
44
45
  .flatpak-builder/
@@ -7,6 +7,250 @@ 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
+
138
+ ## [0.2.3] - 2026-05-07
139
+
140
+ A reliability + portability pass driven by hands-on testing across
141
+ Ubuntu 25.04, CentOS Stream 10, and the existing CachyOS daily driver.
142
+ No breaking changes — same config, same UI, same Python floor; the
143
+ release just makes Refrain behave correctly on more distros and tightens
144
+ a handful of long-standing rough edges.
145
+
146
+ ### Added
147
+
148
+ - **Sandboxed Discord IPC bridge** — Snap and Flatpak Discord builds
149
+ publish their `discord-ipc-N` socket inside the sandbox tree instead
150
+ of `$XDG_RUNTIME_DIR`, so pypresence's stock discovery never finds it.
151
+ `_bridge_sandboxed_ipc_socket` probes the three known sandbox paths
152
+ (Flatpak instance dir, Flatpak config-dir layout, Snap path) and
153
+ symlinks the first hit into `$XDG_RUNTIME_DIR` before each connect
154
+ attempt. The welcome-dialog probe walks the same paths so first-run
155
+ diagnostics turn green on those installs.
156
+ - **Cancel button in the update dialog** — `_apply_appimage` now
157
+ reads in 64 KiB chunks and polls a cancellation callback between
158
+ blocks. The dialog repurposes its "Later" button as "Cancel" while a
159
+ download is running, treats window-close as cancel, and removes the
160
+ partial `.AppImage.new` on abort. `UpdateResult.cancelled` lets the
161
+ dialog suppress the failure popup for user-initiated aborts.
162
+ - **Orphan-download self-heal** — `cleanup_orphan_downloads` runs once
163
+ at startup so a `*.AppImage.new` left behind by a SIGKILL or power
164
+ loss mid-download doesn't sit next to the binary forever.
165
+ - **Browser hints expanded** — Floorp, Waterfox, Mullvad Browser, Tor
166
+ Browser, and ungoogled-chromium are now in `DEFAULT_BROWSER_HINTS`
167
+ and surface as Settings checkboxes. Existing configs keep their
168
+ saved list (no auto-migration); the new entries appear unticked
169
+ until the user toggles them.
170
+ - **iTunes-catalog duration as MPRIS override** — when MPRIS reports
171
+ an obviously-wrong `mpris:length` (Apple Music's preview-clip 14 s,
172
+ the playlist-total instead of the track length, or a stale value
173
+ carried over from the previous track), `pick_effective_duration_ms`
174
+ prefers the iTunes value if the two disagree by more than 15 % or
175
+ if MPRIS sits in the preview-clip band on a song iTunes considers
176
+ full-length. 8 new unit tests in `test_timing.py`. Discord no
177
+ longer briefly flips a 2:11 song's total to 0:14 / 7:21 mid-track.
178
+ - **`docs/test-matrix.md`** — eight Tier-1 distros that together
179
+ cover ≈ 95 % of the realistic user base, plus Tier-2 spot-checks,
180
+ Tier-3 desktop / compositor combinations, and a six-step smoke
181
+ check ("tray + theme parity", "settings round-trip", "MPRIS to
182
+ Discord", "Bluetooth", "update-dialog cleanup", "restart cycle")
183
+ per row.
184
+
185
+ ### Changed
186
+
187
+ - **Qt style plugin path augmentation also walks Debian / Ubuntu and
188
+ Fedora / RHEL / openSUSE layouts** — the v0.2.2 fix only checked
189
+ `/usr/lib/qt6/plugins` (Arch). pip / pipx installs on those distros
190
+ now also pick up the system Breeze plugin instead of falling back
191
+ to Fusion. Detection picks the first existing path among
192
+ `/usr/lib/<arch-triple>/qt6/plugins`, `/usr/lib64/qt6/plugins`,
193
+ `/usr/lib/qt6/plugins`.
194
+ - **"No system tray" error** rewritten to cover GNOME, MATE, XFCE,
195
+ Hyprland / Sway / i3 / river, plus a "should work, try logging
196
+ out" hint for Plasma / Cinnamon / LXQt / Budgie. Previously the
197
+ fix advice was GNOME-AppIndicator-only, leaving everyone else to
198
+ guess.
199
+ - **`refrain --install-desktop`** rewrites the installed
200
+ `Exec=` line to point at the actual launcher path of the running
201
+ install. Previously the bundled `Exec=refrain` only worked when
202
+ `refrain` was on `$PATH` (distro packages, pipx); AppImage and
203
+ source-checkout users got a broken menu entry. Resolution order
204
+ matches the autostart logic — extracted into the new
205
+ `autostart.resolve_exec_line(extra_args)` helper.
206
+ - **MPRIS source duration handling decouples drift-resync from the
207
+ payload's preview-clip flag** — the drift-skip still keys off the
208
+ raw MPRIS-reported duration (because the position-field looping is
209
+ a property of MPRIS' preview-clip mode), but the Discord activity
210
+ payload's start/end fields key off the *effective* duration so a
211
+ brief MPRIS preview-clip glitch on a full-length song doesn't kill
212
+ the progress bar.
213
+ - **PyGObject "not installed" warning fires once per startup**
214
+ instead of twice (eager init from `app.py` plus lazy fallback in
215
+ `MPRISServer.start` both used to log). The install hint also lists
216
+ the Fedora / RHEL / openSUSE package name (`python3-gobject`)
217
+ alongside the Arch and Debian ones.
218
+
219
+ ### Fixed
220
+
221
+ - **Settings / Log / Update windows now carry the Refrain icon
222
+ explicitly** — on GNOME Wayland the title-based heuristic was
223
+ matching "Settings" against `org.gnome.Settings` and rendering the
224
+ gnome-control-center gear icon in the dock. The WelcomeDialog
225
+ already set its own icon; the other three top-level windows did
226
+ not.
227
+ - **Bluetooth fast-fail when `org.bluez` is not present** — every
228
+ poll cycle on a VM / minimal install previously triggered a
229
+ `service_start_timeout=25000ms` D-Bus activation timeout, blocking
230
+ the worker thread for the full 25 s. `NameHasOwner` returns
231
+ instantly without triggering activation; the three entry points
232
+ (`read`, `_call_method`, `list_paired_devices`) now short-circuit
233
+ when the owner check returns False.
234
+ - **Logging gaps closed in six places** — `setup_logging` runs
235
+ before `Config.load` so the "created default config" /
236
+ "config unreadable" messages reach the file log; toggling the log
237
+ level in Settings now applies to the running root logger via
238
+ `_apply_log_level` on `settings.applied`; the "no system tray"
239
+ early-exit gets a `log.error()`; `os.execvp`-based restarts call
240
+ `logging.shutdown()` first so the file handler's last buffered
241
+ line isn't lost; `Config.load` exception path adds `exc_info=True`;
242
+ `setup_logging` degrades gracefully when the XDG state dir or log
243
+ file can't be opened (console handler always attaches first).
244
+ - **Catch-all exception branches in `DiscordRPC._ensure_connected`,
245
+ `MPRISSource._call_method_on`, and `BluetoothSource._call_method`**
246
+ now use `log.exception()` so the traceback lands in `refrain.log`
247
+ for unexpected errors. Previously they only logged `str(exc)`,
248
+ losing the stack exactly when it would matter most.
249
+ - **Qt-internal log records routed through `refrain.qt`** instead
250
+ of the bare `qt` logger, so they parent under the project
251
+ namespace and show up alongside everything else when the user
252
+ filters by name.
253
+
10
254
  ## [0.2.2] - 2026-05-06
11
255
 
12
256
  Two roadmap features pulled forward from v0.3 plus a substantial batch
@@ -568,7 +812,14 @@ with a proper, installable Linux app.
568
812
  pip-audit, trufflehog, release), Dependabot, issue + PR templates,
569
813
  `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`.
570
814
 
571
- [Unreleased]: https://github.com/Rockykln/refrain/compare/v0.1.2...HEAD
815
+ [Unreleased]: https://github.com/Rockykln/refrain/compare/v0.2.3...HEAD
816
+ [0.2.3]: https://github.com/Rockykln/refrain/compare/v0.2.2...v0.2.3
817
+ [0.2.2]: https://github.com/Rockykln/refrain/compare/v0.2.1...v0.2.2
818
+ [0.2.1]: https://github.com/Rockykln/refrain/compare/v0.2.0...v0.2.1
819
+ [0.2.0]: https://github.com/Rockykln/refrain/compare/v0.1.5...v0.2.0
820
+ [0.1.5]: https://github.com/Rockykln/refrain/compare/v0.1.4...v0.1.5
821
+ [0.1.4]: https://github.com/Rockykln/refrain/compare/v0.1.3...v0.1.4
822
+ [0.1.3]: https://github.com/Rockykln/refrain/compare/v0.1.2...v0.1.3
572
823
  [0.1.2]: https://github.com/Rockykln/refrain/compare/v0.1.1...v0.1.2
573
824
  [0.1.1]: https://github.com/Rockykln/refrain/compare/v0.1.0...v0.1.1
574
825
  [0.1.0]: https://github.com/Rockykln/refrain/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refrain
3
- Version: 0.2.2
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
@@ -134,9 +134,10 @@ and `~/.local/share/icons/`. To undo: `refrain --uninstall-desktop`.
134
134
 
135
135
  ### Tested on
136
136
 
137
- - KDE Plasma 6 (Wayland) — primary target
138
- - KDE Plasma 6 (X11)
139
- - GNOME (with the [AppIndicator and KStatusNotifierItem](https://extensions.gnome.org/extension/615/appindicator-support/) extension)
137
+ See [`docs/test-matrix.md`](docs/test-matrix.md) for the full Tier-1 / Tier-2 list, the per-row smoke checks, and which distros are explicitly out-of-scope (Python or glibc floor too low).
138
+
139
+ - **Tier 1 (must pass every release):** CachyOS, Arch Linux, Fedora 42, Ubuntu 24.04 LTS, Debian 13, openSUSE Tumbleweed, Linux Mint 22, Manjaro Stable.
140
+ - **Desktops:** KDE Plasma 6 (Wayland is the primary target, X11 also covered), GNOME with the [AppIndicator and KStatusNotifierItem](https://extensions.gnome.org/extension/615/appindicator-support/) extension, XFCE / Cinnamon / LXQt / Budgie via their native or AppIndicator-bridged tray, MATE with `mate-applet-statusnotifier`, tiling WMs (Hyprland / Sway / i3 / river) via a SNI-capable status bar.
140
141
 
141
142
  ## First-time setup
142
143
 
@@ -318,6 +319,8 @@ tray + controls running.
318
319
 
319
320
  - [Architecture overview](docs/architecture.md) — threads, D-Bus surface, file paths
320
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
321
324
  - [Roadmap](ROADMAP.md)
322
325
  - [Changelog](CHANGELOG.md)
323
326
  - [Contributing](CONTRIBUTING.md) — dev setup, testing, code style
@@ -100,9 +100,10 @@ and `~/.local/share/icons/`. To undo: `refrain --uninstall-desktop`.
100
100
 
101
101
  ### Tested on
102
102
 
103
- - KDE Plasma 6 (Wayland) — primary target
104
- - KDE Plasma 6 (X11)
105
- - GNOME (with the [AppIndicator and KStatusNotifierItem](https://extensions.gnome.org/extension/615/appindicator-support/) extension)
103
+ See [`docs/test-matrix.md`](docs/test-matrix.md) for the full Tier-1 / Tier-2 list, the per-row smoke checks, and which distros are explicitly out-of-scope (Python or glibc floor too low).
104
+
105
+ - **Tier 1 (must pass every release):** CachyOS, Arch Linux, Fedora 42, Ubuntu 24.04 LTS, Debian 13, openSUSE Tumbleweed, Linux Mint 22, Manjaro Stable.
106
+ - **Desktops:** KDE Plasma 6 (Wayland is the primary target, X11 also covered), GNOME with the [AppIndicator and KStatusNotifierItem](https://extensions.gnome.org/extension/615/appindicator-support/) extension, XFCE / Cinnamon / LXQt / Budgie via their native or AppIndicator-bridged tray, MATE with `mate-applet-statusnotifier`, tiling WMs (Hyprland / Sway / i3 / river) via a SNI-capable status bar.
106
107
 
107
108
  ## First-time setup
108
109
 
@@ -284,6 +285,8 @@ tray + controls running.
284
285
 
285
286
  - [Architecture overview](docs/architecture.md) — threads, D-Bus surface, file paths
286
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
287
290
  - [Roadmap](ROADMAP.md)
288
291
  - [Changelog](CHANGELOG.md)
289
292
  - [Contributing](CONTRIBUTING.md) — dev setup, testing, code style
@@ -169,6 +169,120 @@ What's done, what's next, what's deliberately not in scope.
169
169
  in the wheel**, **MPRIS dispatch logging at INFO** so
170
170
  Next/Previous routing is visible in the live log.
171
171
 
172
+ ## Done — v0.2.3
173
+
174
+ - **Distro-portability sweep** driven by hands-on testing.
175
+ - Qt style plugin path augmentation extended to Debian / Ubuntu
176
+ (`/usr/lib/<arch-triple>/qt6/plugins`) and Fedora / RHEL / openSUSE
177
+ (`/usr/lib64/qt6/plugins`); v0.2.2 only handled the Arch layout.
178
+ - Snap and Flatpak Discord builds now reachable out of the box —
179
+ `_bridge_sandboxed_ipc_socket` symlinks the sandboxed
180
+ `discord-ipc-N` into `$XDG_RUNTIME_DIR` before each pypresence
181
+ connect attempt.
182
+ - Bluetooth fast-fail via `NameHasOwner('org.bluez')` so VMs and
183
+ minimal installs without bluez no longer block the daemon's
184
+ poll cycle for 25 s waiting on D-Bus service activation.
185
+ - "No system tray" error covers GNOME, MATE, XFCE, and the
186
+ tiling-WM crowd (Hyprland / Sway / i3 / river); was
187
+ GNOME-AppIndicator-only.
188
+ - `--install-desktop` rewrites `Exec=` in the installed `.desktop`
189
+ to match the running install (AppImage path / venv shim / system
190
+ binary), so AppImage and source-checkout users get a working
191
+ menu entry.
192
+ - Browser hints expanded to cover Floorp, Waterfox, Mullvad
193
+ Browser, Tor Browser, ungoogled-chromium.
194
+ - **iTunes-catalog duration as MPRIS override** —
195
+ `pick_effective_duration_ms` chooses iTunes when MPRIS and iTunes
196
+ disagree by more than 15 % or when MPRIS sits in the preview-clip
197
+ band on a song iTunes considers full-length. Apple Music's MPRIS
198
+ surface no longer makes Discord briefly flip a 2:11 song's total
199
+ to 0:14 / 7:21 mid-track.
200
+ - **Update-dialog Cancel + orphan-download self-heal** — `Cancel`
201
+ button + window-close handler abort an in-flight AppImage
202
+ download cleanly and remove the partial `.new`. A separate
203
+ `cleanup_orphan_downloads` runs at startup so a SIGKILL or power
204
+ loss mid-download doesn't leave a stale `*.AppImage.new` next to
205
+ the binary forever.
206
+ - **Per-window icons** on Settings / Log / Update dialogs so GNOME
207
+ Shell's title-based heuristic on Wayland doesn't render them with
208
+ the gnome-control-center gear icon in the dock.
209
+ - **Logging audit** closed six gaps: `setup_logging` runs before
210
+ `Config.load` so config-load messages reach the file log; the
211
+ log-level toggle in Settings applies live; `setup_logging` degrades
212
+ gracefully when the XDG state dir is unwritable; `os.execvp`-based
213
+ restarts flush log handlers first; catch-all exception branches in
214
+ `DiscordRPC._ensure_connected`, `MPRISSource._call_method_on` and
215
+ `BluetoothSource._call_method` switched to `log.exception()` so
216
+ tracebacks land in `refrain.log`.
217
+ - **`docs/test-matrix.md`** — eight Tier-1 distros, Tier-2 spot-check
218
+ list, Tier-3 desktop / compositor sweep, six-step smoke checks,
219
+ out-of-scope reasons for the floor of unsupported distros (RHEL 9
220
+ / Rocky 9 / Alma 9 / Debian 11 / Ubuntu 22.04 / Alpine).
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
+
172
286
  ## Up next — v0.3
173
287
 
174
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.