refrain 0.2.0__tar.gz → 0.2.2__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 (128) hide show
  1. {refrain-0.2.0 → refrain-0.2.2}/.github/workflows/release.yml +6 -2
  2. {refrain-0.2.0 → refrain-0.2.2}/.github/workflows/tests.yml +7 -2
  3. {refrain-0.2.0 → refrain-0.2.2}/.gitignore +1 -1
  4. {refrain-0.2.0 → refrain-0.2.2}/CHANGELOG.md +203 -0
  5. {refrain-0.2.0 → refrain-0.2.2}/CONTRIBUTING.md +21 -18
  6. {refrain-0.2.0 → refrain-0.2.2}/PKG-INFO +21 -3
  7. {refrain-0.2.0 → refrain-0.2.2}/README.md +20 -2
  8. {refrain-0.2.0 → refrain-0.2.2}/ROADMAP.md +66 -5
  9. refrain-0.2.2/docs/screenshots/settings-advanced.png +0 -0
  10. refrain-0.2.2/docs/screenshots/settings-general.png +0 -0
  11. refrain-0.2.2/docs/screenshots/settings-sources.png +0 -0
  12. refrain-0.2.2/docs/screenshots/settings-updates.png +0 -0
  13. refrain-0.2.2/docs/screenshots/welcome.png +0 -0
  14. {refrain-0.2.0 → refrain-0.2.2}/packaging/appimage/AppImageBuilder.yml +1 -1
  15. {refrain-0.2.0 → refrain-0.2.2}/packaging/aur/refrain/PKGBUILD +2 -2
  16. {refrain-0.2.0 → refrain-0.2.2}/packaging/flatpak/io.github.Rockykln.Refrain.metainfo.xml +35 -0
  17. {refrain-0.2.0 → refrain-0.2.2}/pyproject.toml +4 -5
  18. refrain-0.2.2/src/refrain/__init__.py +1 -0
  19. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/app.py +115 -24
  20. refrain-0.2.2/src/refrain/assets/icons/refrain.png +0 -0
  21. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/config.py +37 -5
  22. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/cover_art.py +42 -4
  23. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/cover_fetcher.py +21 -4
  24. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/daemon.py +284 -116
  25. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/discord_rpc.py +7 -1
  26. refrain-0.2.2/src/refrain/i18n/refrain_de.qm +0 -0
  27. refrain-0.2.2/src/refrain/i18n/refrain_de.ts +483 -0
  28. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/logging_setup.py +8 -0
  29. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/sources/bluetooth.py +5 -5
  30. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/sources/mpris.py +149 -52
  31. refrain-0.2.2/src/refrain/sources/mpris_server.py +360 -0
  32. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/timing.py +12 -1
  33. refrain-0.2.2/src/refrain/ui/settings_window.py +683 -0
  34. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/ui/tray.py +40 -30
  35. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/ui/welcome_dialog.py +127 -29
  36. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/updater.py +46 -7
  37. {refrain-0.2.0 → refrain-0.2.2}/tests/test_config.py +10 -4
  38. {refrain-0.2.0 → refrain-0.2.2}/tests/test_cover_art.py +6 -3
  39. {refrain-0.2.0 → refrain-0.2.2}/tests/test_daemon_album_format.py +22 -5
  40. {refrain-0.2.0 → refrain-0.2.2}/tests/test_daemon_idle.py +26 -3
  41. {refrain-0.2.0 → refrain-0.2.2}/tests/test_mpris_dispatch.py +5 -5
  42. refrain-0.2.2/tests/test_tray_init.py +86 -0
  43. {refrain-0.2.0 → refrain-0.2.2}/tests/test_update_dialog_body.py +15 -9
  44. refrain-0.2.0/docs/screenshots/settings-advanced.png +0 -0
  45. refrain-0.2.0/docs/screenshots/settings-general.png +0 -0
  46. refrain-0.2.0/docs/screenshots/settings-sources.png +0 -0
  47. refrain-0.2.0/docs/screenshots/settings-updates.png +0 -0
  48. refrain-0.2.0/src/refrain/__init__.py +0 -1
  49. refrain-0.2.0/src/refrain/i18n/refrain_de.qm +0 -0
  50. refrain-0.2.0/src/refrain/i18n/refrain_de.ts +0 -342
  51. refrain-0.2.0/src/refrain/i18n/refrain_en.qm +0 -0
  52. refrain-0.2.0/src/refrain/i18n/refrain_en.ts +0 -340
  53. refrain-0.2.0/src/refrain/i18n/refrain_es.qm +0 -0
  54. refrain-0.2.0/src/refrain/i18n/refrain_es.ts +0 -340
  55. refrain-0.2.0/src/refrain/i18n/refrain_fr.qm +0 -0
  56. refrain-0.2.0/src/refrain/i18n/refrain_fr.ts +0 -340
  57. refrain-0.2.0/src/refrain/i18n/refrain_it.qm +0 -0
  58. refrain-0.2.0/src/refrain/i18n/refrain_it.ts +0 -340
  59. refrain-0.2.0/src/refrain/i18n/refrain_ja.qm +0 -0
  60. refrain-0.2.0/src/refrain/i18n/refrain_ja.ts +0 -340
  61. refrain-0.2.0/src/refrain/i18n/refrain_nl.qm +0 -0
  62. refrain-0.2.0/src/refrain/i18n/refrain_nl.ts +0 -340
  63. refrain-0.2.0/src/refrain/i18n/refrain_pl.qm +0 -0
  64. refrain-0.2.0/src/refrain/i18n/refrain_pl.ts +0 -340
  65. refrain-0.2.0/src/refrain/i18n/refrain_pt.qm +0 -0
  66. refrain-0.2.0/src/refrain/i18n/refrain_pt.ts +0 -340
  67. refrain-0.2.0/src/refrain/i18n/refrain_zh_CN.qm +0 -0
  68. refrain-0.2.0/src/refrain/i18n/refrain_zh_CN.ts +0 -340
  69. refrain-0.2.0/src/refrain/sources/dbus_watcher.py +0 -136
  70. refrain-0.2.0/src/refrain/ui/settings_window.py +0 -342
  71. {refrain-0.2.0 → refrain-0.2.2}/.editorconfig +0 -0
  72. {refrain-0.2.0 → refrain-0.2.2}/.gitattributes +0 -0
  73. {refrain-0.2.0 → refrain-0.2.2}/.github/FUNDING.yml +0 -0
  74. {refrain-0.2.0 → refrain-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  75. {refrain-0.2.0 → refrain-0.2.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  76. {refrain-0.2.0 → refrain-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  77. {refrain-0.2.0 → refrain-0.2.2}/.github/codeql/codeql-config.yml +0 -0
  78. {refrain-0.2.0 → refrain-0.2.2}/.github/dependabot.yml +0 -0
  79. {refrain-0.2.0 → refrain-0.2.2}/.github/pull_request_template.md +0 -0
  80. {refrain-0.2.0 → refrain-0.2.2}/.github/workflows/codeql.yml +0 -0
  81. {refrain-0.2.0 → refrain-0.2.2}/.github/workflows/security.yml +0 -0
  82. {refrain-0.2.0 → refrain-0.2.2}/CODE_OF_CONDUCT.md +0 -0
  83. {refrain-0.2.0 → refrain-0.2.2}/LICENSE +0 -0
  84. {refrain-0.2.0 → refrain-0.2.2}/Makefile +0 -0
  85. {refrain-0.2.0 → refrain-0.2.2}/SECURITY.md +0 -0
  86. {refrain-0.2.0 → refrain-0.2.2}/docs/architecture.md +0 -0
  87. {refrain-0.2.0 → refrain-0.2.2}/docs/faq.md +0 -0
  88. {refrain-0.2.0 → refrain-0.2.2}/docs/screenshots/README.md +0 -0
  89. {refrain-0.2.0 → refrain-0.2.2}/docs/screenshots/discord-rpc.png +0 -0
  90. {refrain-0.2.0 → refrain-0.2.2}/docs/screenshots/live-log.png +0 -0
  91. {refrain-0.2.0 → refrain-0.2.2}/docs/screenshots/notification.png +0 -0
  92. {refrain-0.2.0 → refrain-0.2.2}/docs/screenshots/tray-menu.png +0 -0
  93. {refrain-0.2.0 → refrain-0.2.2}/docs/screenshots/update-dialog.png +0 -0
  94. {refrain-0.2.0 → refrain-0.2.2}/packaging/README.md +0 -0
  95. {refrain-0.2.0 → refrain-0.2.2}/packaging/aur/refrain-git/PKGBUILD +0 -0
  96. {refrain-0.2.0 → refrain-0.2.2}/packaging/flatpak/io.github.Rockykln.Refrain.desktop +0 -0
  97. {refrain-0.2.0 → refrain-0.2.2}/packaging/flatpak/io.github.Rockykln.Refrain.yaml +0 -0
  98. {refrain-0.2.0 → refrain-0.2.2}/packaging/flatpak/python-deps.json +0 -0
  99. {refrain-0.2.0 → refrain-0.2.2}/requirements-dev.txt +0 -0
  100. {refrain-0.2.0 → refrain-0.2.2}/requirements.txt +0 -0
  101. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/__main__.py +0 -0
  102. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/assets/icons/github-mark.svg +0 -0
  103. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/assets/icons/refrain.svg +0 -0
  104. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/assets/icons/tray-paused-dark.svg +0 -0
  105. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/assets/icons/tray-paused.svg +0 -0
  106. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/assets/icons/tray-playing-dark.svg +0 -0
  107. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/assets/icons/tray-playing.svg +0 -0
  108. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/assets/icons/tray-stopped-dark.svg +0 -0
  109. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/assets/icons/tray-stopped.svg +0 -0
  110. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/assets/refrain.desktop +0 -0
  111. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/autostart.py +0 -0
  112. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/paths.py +0 -0
  113. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/single_instance.py +0 -0
  114. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/sources/__init__.py +0 -0
  115. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/sources/base.py +0 -0
  116. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/ui/__init__.py +0 -0
  117. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/ui/log_window.py +0 -0
  118. {refrain-0.2.0 → refrain-0.2.2}/src/refrain/ui/update_dialog.py +0 -0
  119. {refrain-0.2.0 → refrain-0.2.2}/tests/__init__.py +0 -0
  120. {refrain-0.2.0 → refrain-0.2.2}/tests/conftest.py +0 -0
  121. {refrain-0.2.0 → refrain-0.2.2}/tests/test_autostart.py +0 -0
  122. {refrain-0.2.0 → refrain-0.2.2}/tests/test_cover_fetcher.py +0 -0
  123. {refrain-0.2.0 → refrain-0.2.2}/tests/test_discord_listening.py +0 -0
  124. {refrain-0.2.0 → refrain-0.2.2}/tests/test_imports.py +0 -0
  125. {refrain-0.2.0 → refrain-0.2.2}/tests/test_paths.py +0 -0
  126. {refrain-0.2.0 → refrain-0.2.2}/tests/test_sources_base.py +0 -0
  127. {refrain-0.2.0 → refrain-0.2.2}/tests/test_timing.py +0 -0
  128. {refrain-0.2.0 → refrain-0.2.2}/tests/test_updater.py +0 -0
@@ -31,9 +31,13 @@ jobs:
31
31
  - name: Install + run tests (gate the release)
32
32
  run: |
33
33
  python -m pip install --upgrade pip
34
- # System libs that dbus-python links against during build.
34
+ # System libs: dbus-python needs the *-dev headers at build
35
+ # time; PySide6.QtWidgets needs the EGL/xkbcommon/GL/fontconfig
36
+ # runtime libs to import (even under QT_QPA_PLATFORM=offscreen).
35
37
  sudo apt-get update
36
- sudo apt-get install -y libdbus-1-dev libglib2.0-dev pkg-config
38
+ sudo apt-get install -y \
39
+ libdbus-1-dev libglib2.0-dev pkg-config \
40
+ libegl1 libxkbcommon0 libfontconfig1 libgl1
37
41
  # Install the package itself so the test imports get every
38
42
  # runtime dep (PySide6, pypresence, dbus-python) — without
39
43
  # this the new test_daemon_*.py files fail at collection.
@@ -26,11 +26,16 @@ jobs:
26
26
  - name: Install runtime + test dependencies
27
27
  # The newer tests import refrain modules that pull PySide6 /
28
28
  # pypresence / dbus-python at module load, so the runtime deps
29
- # have to be present in CI too.
29
+ # have to be present in CI too. PySide6.QtWidgets requires the
30
+ # EGL / xkbcommon / GL / fontconfig system libs even when run
31
+ # under QT_QPA_PLATFORM=offscreen — without them the import
32
+ # itself fails before the test can opt out.
30
33
  run: |
31
34
  python -m pip install --upgrade pip
32
35
  sudo apt-get update
33
- sudo apt-get install -y libdbus-1-dev libglib2.0-dev pkg-config
36
+ sudo apt-get install -y \
37
+ libdbus-1-dev libglib2.0-dev pkg-config \
38
+ libegl1 libxkbcommon0 libfontconfig1 libgl1
34
39
  pip install -e ".[dev]"
35
40
 
36
41
  - name: Compile-check every module
@@ -32,7 +32,7 @@ env/
32
32
  .DS_Store
33
33
 
34
34
  # AI assistants
35
- .claude/
35
+ .claude*/
36
36
  .cursor/
37
37
  .aider*
38
38
 
@@ -7,6 +7,209 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.2] - 2026-05-06
11
+
12
+ Two roadmap features pulled forward from v0.3 plus a substantial batch
13
+ of reliability + UX work driven by live-testing the v0.2.1 build.
14
+
15
+ ### Added
16
+
17
+ - **Multiple Discord profiles** — per-source `client_id_mpris` and
18
+ `client_id_bluetooth` fields in `Config.discord` plus matching UI in
19
+ Settings → General → Discord. Empty falls back to the default
20
+ Client ID. The daemon reconnects RPC under the source-specific ID
21
+ the moment the active source flips, so each source can render with
22
+ its own application name + uploaded artwork in the user's profile.
23
+ - **MPRIS-server mode** — Refrain registers itself as
24
+ `org.mpris.MediaPlayer2.refrain` on the session bus so KDE Plasma's
25
+ panel media-controls applet (and KDE Connect, GNOME Shell, Mako
26
+ notifications, …) drive the same Play/Pause/Next/Previous as the
27
+ tray, and render the same track Discord renders. Implemented with
28
+ `dbus-python` + a daemon GLib main loop running in its own thread
29
+ so it doesn't compete with Qt's event loop. Falls back gracefully
30
+ when PyGObject isn't installed.
31
+ - **iTunes track duration in the cover-fetcher cache** —
32
+ `trackTimeMillis` from the iTunes Search API rides along with
33
+ `cover_url` / `song_url` in the on-disk cache and exposed via
34
+ `CoverFetcher.get_duration_ms()`. Used as a defensive fallback when
35
+ MPRIS reports an obviously-wrong preview-clip length.
36
+ - **Per-player MPRIS timeout blacklist** — if a property `Get` times
37
+ out we banish that bus name for 5 s so a hung player (typically
38
+ plasma-browser-integration on a frozen Apple Music tab) can't keep
39
+ eating the daemon's poll cycle one tick at a time.
40
+
41
+ ### Changed
42
+
43
+ - **Welcome wizard** redesigned: 56×56 icon-badge + title + subtitle
44
+ header, intro line about tray-driven controls, dedicated diagnostics
45
+ card with two clearly-labelled rows (Discord / iTunes), Discord
46
+ Application ID block with helper text + readable italic styling
47
+ (the previous `palette(mid)` tone went invisible on Plasma Breeze
48
+ dark themes), Skip ghost-style on the left + Apply primary on the
49
+ right.
50
+ - **Welcome wizard now opens Settings on confirmation** instead of
51
+ silently dropping into the tray, and reloads the Settings form so
52
+ the just-saved Discord Client ID appears in the input field.
53
+ - **`compute_rpc_start_ts` accepts `is_preview_clip=True`** to skip
54
+ the drift-resync path on tracks whose MPRIS Position field loops
55
+ 0→8s instead of advancing monotonically. Discord's elapsed counter
56
+ no longer resets every ~8 s on preview-mode playback.
57
+ - **Reset-all-settings preserves every Discord Client ID** (default +
58
+ per-source mpris + per-source bluetooth) and the dialog text now
59
+ states this explicitly. Reset-dialog button labels are localised
60
+ (`Reset` / `Zurücksetzen` / `Cancel` / `Abbrechen`) instead of the
61
+ generic Yes / No.
62
+ - **Default `notify_delay_ms` lowered from 600 ms to 0 ms** — cover
63
+ cache hits fire instantly, cache misses still get the existing
64
+ retry loop up to 2 s for the iTunes download.
65
+ - **Default `poll_interval_ms` lowered from 1 s to 500 ms** — track
66
+ changes, position updates, and tray controls feel noticeably more
67
+ responsive without measurable CPU impact.
68
+ - **Idle detection skips preview-clip-length tracks** (duration <
69
+ 30 s) — Apple Music keeps reporting the same metadata while a
70
+ preview replays, and clearing Discord under those conditions hid
71
+ the activity mid-listen.
72
+ - **Idle-detection log fires once per dangling-track instance**
73
+ instead of every poll tick.
74
+
75
+ ### Fixed
76
+
77
+ - **Discord RPC connect now happens within ~1 s of the first track
78
+ detection** (was: up to 51 s). Root cause: dbus-python's default
79
+ 25 s reply timeout combined with plasma-browser-integration
80
+ introspection hangs blocked the daemon's poll. Fix: pass
81
+ `introspect=False` on every `bus.get_object` and `timeout=0.5` on
82
+ every property `Get` and method dispatch.
83
+ - **Discord RPC reconnects eagerly when the user changes their
84
+ Application ID** (welcome wizard or Settings → General). Previously
85
+ the new pipe wasn't established until the next *playing-state*
86
+ poll, so paused users sat there waiting forever.
87
+ - **Skip / Previous reach Apple Music reliably**: dispatch tries
88
+ the browser-native MPRIS player (chromium / firefox) first, then
89
+ falls back to plasma-browser-integration. Plasma's wrapper claims
90
+ `CanGoNext=True` on Apple Music but its Next routes through a
91
+ flaky mediaSession path; the browser-native player reaches the
92
+ same handler via a more reliable path.
93
+ - **Per-property `_safe_get` inside MPRIS read** — chromium MPRIS
94
+ rejecting one optional property (notably `DesktopEntry`, with a
95
+ generic `Error.Failed`) no longer drops the whole player from our
96
+ candidate list, which previously hid the chromium player entirely
97
+ from the skip-fallback chain.
98
+ - **All apple-music candidates** beyond the metadata winner join the
99
+ control-fallback list, so the chromium player is reachable for
100
+ skip dispatches even when plasma wins the metadata pick.
101
+ - **Notification fires immediately on cover-cache hits** (50 ms vs.
102
+ the previous unconditional 600-1500 ms `notify_delay_ms` wait).
103
+ - **Discord progress bar dropped entirely on preview-clip mode**
104
+ (MPRIS duration < 30 s) — no `start`, no `end` in the activity
105
+ payload — so the elapsed timer doesn't loop nonsensically with
106
+ the 8-second preview.
107
+ - **`update_config` invocation from the welcome wizard** now goes
108
+ through `settings.applied.emit(config)` (already QueuedConnection-
109
+ wired to the worker thread) instead of `QMetaObject.invokeMethod
110
+ (… Q_ARG(object, …))`, which PySide6 rejects with "Unable to find
111
+ a QMetaType for 'object'" — that exception was silently breaking
112
+ the post-wizard handshake on first-run.
113
+ - **Welcome wizard X / Esc** marks `first_run_complete=True` (via
114
+ `reject()` override) so the wizard doesn't re-appear on the next
115
+ launch when the user dismisses without confirming.
116
+ - **Welcome wizard Apply with empty ID** asks for confirmation
117
+ before silently saving "no Discord".
118
+ - **Refrain's own published MPRIS bus name skipped** during the
119
+ source read loop — reading our own publish path back was
120
+ circular and added noticeable latency.
121
+ - **`_control` exception handling** — a TypeError in
122
+ `getattr(src, action)()` no longer kills the entire dispatch
123
+ before the follow-up polls fire.
124
+ - **Reset preserves per-source Discord IDs** (`client_id_mpris` /
125
+ `client_id_bluetooth`), not just the default `client_id`.
126
+ - **dbus.proxies / dbus.connection log spam silenced** — their
127
+ Introspect-error tracebacks against unrelated MPRIS players
128
+ drowned the live log in noise that has nothing to do with
129
+ refrain.
130
+
131
+ ### Removed
132
+
133
+ - **Dead `_diag_label` shim** in `WelcomeDialog` that was only there
134
+ for a backwards-compat path no longer in use.
135
+
136
+ ## [0.2.1] - 2026-05-06
137
+
138
+ Polish release built around the v0.2.0 surface. The Settings window got
139
+ a UI overhaul, two notification cover-art races are gone, and the
140
+ external code-review punch list landed in full (atomic config writes,
141
+ AppImage size validation, `.ts` source files no longer ship in the
142
+ wheel, dead D-Bus watcher module deleted).
143
+
144
+ ### Added
145
+
146
+ - **Auto-restart on Discord client_id change** — Apply now triggers an
147
+ in-process re-exec when the client_id field has changed, same
148
+ pattern as the language switch. pypresence binds to the client_id
149
+ at connect time, so a fresh process is the simplest way to re-init
150
+ cleanly.
151
+ - **AppImage update size validation** — `_apply_appimage` now compares
152
+ the downloaded file against the GitHub Releases manifest's
153
+ `appimage_size` before replacing the live binary. A truncated
154
+ download is rejected and the tmp file cleaned up instead of
155
+ bricking the next launch.
156
+ - **MPRIS dispatch logging** — `Next` / `Previous` / `PlayPause` log
157
+ which player actually received the call (INFO level). Diagnoses
158
+ "Next pauses instead of skipping" type bugs where a fallback
159
+ player handles the action wrong.
160
+ - **RPC track-change diagnostics** — log raw `pos`, `dur`, `start_ts`
161
+ on every recompute so duration mismatches (browser MPRIS reporting
162
+ preview-clip lengths, etc.) are visible in the live log.
163
+
164
+ ### Changed
165
+
166
+ - **Settings window UI overhaul** — every tab uses a consistent
167
+ `QGroupBox` + form-layout with left-aligned titles via stylesheet
168
+ (overrides Plasma Breeze's centered default). Inputs are
169
+ fixed-width: 220 px default, 360 px for the Discord group whose
170
+ Client ID placeholder + privacy-mode labels need more room.
171
+ `FieldsStayAtSizeHint` + per-widget `setFixedWidth` is the only
172
+ combo that holds across both Fusion (offscreen tests) and Breeze
173
+ (Plasma) — `AllNonFixedFieldsGrow` was ignoring fixed-width caps.
174
+ - **Discord RPC reconnect cap lowered to 15 s** — autostart launches
175
+ refrain before Discord is ready; the previous 60 s ceiling meant
176
+ the user could sit there for almost a minute after Discord
177
+ finished loading before refrain noticed and connected.
178
+ - **Atomic config writes** — `Config.save` now writes to a `.tmp`
179
+ sibling and `os.replace`s into place. A crash mid-write would
180
+ otherwise leave an empty / half-written TOML and refrain
181
+ silently falls back to defaults on parse error, losing every
182
+ setting the user picked.
183
+ - **Discord client_id Apply re-init** — wired `restartRequested`
184
+ emission so picking up a new client_id no longer requires a
185
+ manual restart.
186
+
187
+ ### Fixed
188
+
189
+ - **Notification cover flicker** — `notify-send -i` now carries the
190
+ same image as the `--hint string:image-path:...` payload. KDE
191
+ Plasma briefly rendered the `-i refrain` brand badge (~50–100 ms)
192
+ while it loaded the image-path file from disk; using the same
193
+ file for both makes the transition invisible.
194
+ - **Discord activity-card cover flicker** — defer the first RPC
195
+ update for a new track until the iTunes-search cover URL is in
196
+ cache, capped at 3 polls (~3 s). Without this, Discord briefly
197
+ rendered the `refrain` brand fallback before the real cover
198
+ paint, exactly the issue cover-fetcher was supposed to fix.
199
+ - **Group titles centered on Plasma Breeze** — explicit stylesheet
200
+ overrides Breeze's default centered `QGroupBox::title`.
201
+
202
+ ### Removed
203
+
204
+ - **Dead `dbus_watcher` module** — 155 lines of `MPRISWatcher` /
205
+ `BluetoothWatcher` machinery that was never wired up after the
206
+ signal-driven path was abandoned for v0.2.x. The companion
207
+ `_refresh_watchers` and `_on_external_change` slots in
208
+ `Daemon` went with it.
209
+ - **`.ts` translation sources from the wheel** — only the compiled
210
+ `.qm` files ship now. Linguist sources stay in the source tree
211
+ for translator PRs.
212
+
10
213
  ## [0.2.0] - 2026-05-06
11
214
 
12
215
  First minor-version bump. Five planned v0.2 features land together
@@ -122,31 +122,34 @@ descriptions instead.
122
122
 
123
123
  ## Translations
124
124
 
125
- User-visible strings live wrapped in `tr()` calls; the source list is
126
- maintained in `src/refrain/i18n/refrain_<lang>.ts` (Qt Linguist format).
127
- German is shipped complete. French, Spanish, Italian, Portuguese, Dutch,
128
- Polish, Japanese, and Simplified Chinese have stub `.ts` files with all
129
- source strings already extracted, marked
130
- `<translation type="unfinished"></translation>` — those are the ones we'd
131
- love community PRs for.
125
+ User-visible strings live wrapped in `tr()` calls; translations are
126
+ kept in `src/refrain/i18n/refrain_<lang>.ts` (Qt Linguist format).
127
+ English is the source language. German is the only translation we
128
+ ship complete right now.
132
129
 
133
- To translate one of the stub languages end-to-end:
130
+ To add a new language end-to-end:
134
131
 
135
132
  ```sh
136
- # 1. Pick a language file. Linguist (the GUI) is friendlier than
137
- # editing XML by hand.
138
- pyside6-linguist src/refrain/i18n/refrain_fr.ts
133
+ # 1. Copy the German translation as a starting point (gives you all
134
+ # the strings already extracted).
135
+ cp src/refrain/i18n/refrain_de.ts src/refrain/i18n/refrain_fr.ts
139
136
 
140
- # 2. Replace each "unfinished" translation with the real string.
137
+ # 2. Open it in Qt Linguist (friendlier than editing XML by hand) and
138
+ # replace each German line with your translation.
139
+ pyside6-linguist src/refrain/i18n/refrain_fr.ts
141
140
 
142
- # 3. Compile to .qm and run the result locally.
141
+ # 3. Compile to .qm.
143
142
  make i18n
143
+
144
+ # 4. Add your code to settings_window.py's language combo so users can
145
+ # pick it from the Localization group, then run Refrain locally to
146
+ # smoke-test.
144
147
  LANG=fr_FR.UTF-8 refrain
145
148
  ```
146
149
 
147
- If you've wrapped any new strings on the source side, run
148
- `make i18n-update` first — it refreshes every `.ts` file's
149
- source-string list while preserving existing translations.
150
+ If you've wrapped any new English strings on the source side, run
151
+ `make i18n-update` first — it refreshes the `.ts` file's source-string
152
+ list while preserving existing translations.
150
153
 
151
- PRs that just translate a `.ts` (no source changes) are very welcome.
152
- So is native-speaker review of an existing translation.
154
+ PRs that add a new language (or fix typos in an existing one) are
155
+ very welcome.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refrain
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -162,7 +162,9 @@ same file; you almost never need to touch it by hand.
162
162
 
163
163
  ```toml
164
164
  [discord]
165
- client_id = "" # paste your own Application ID here
165
+ client_id = "" # default Application ID — paste yours here
166
+ client_id_mpris = "" # optional per-source override (browser / Apple Music)
167
+ client_id_bluetooth = "" # optional per-source override (Bluetooth headphones)
166
168
 
167
169
  [sources]
168
170
  mpris_enabled = true
@@ -177,12 +179,28 @@ autostart = false
177
179
  notifications = true
178
180
  cover_art = true
179
181
  show_buttons = true
182
+ notify_delay_ms = 0 # 0 = fire ASAP; the cover-art retry loop still waits up to ~2 s
180
183
 
181
184
  [advanced]
182
- poll_interval_ms = 1000
185
+ poll_interval_ms = 500
183
186
  log_level = "INFO"
187
+ cover_cache_size = 200 # disk cap for cached covers
188
+ idle_grace_s = 30 # clear status when same track plays past duration + grace; 0 disables
189
+ language = "system" # "system" follows QLocale; "en" / "de" / … force a translation
184
190
  ```
185
191
 
192
+ Per-source `client_id_*` fields let Apple Music render under one Discord
193
+ application (with the album-grid as artwork) and Bluetooth headphones under
194
+ another (with a generic Bluetooth glyph). Empty falls back to the default
195
+ `client_id`.
196
+
197
+ ## MPRIS server
198
+
199
+ Refrain publishes itself as `org.mpris.MediaPlayer2.refrain` on the session
200
+ bus, so KDE Plasma's panel media-controls applet (and KDE Connect, GNOME
201
+ Shell, Mako, …) drive the same Play/Pause/Next/Previous as the tray and
202
+ render the same track Discord renders.
203
+
186
204
  ## Tray
187
205
 
188
206
  <p align="center">
@@ -128,7 +128,9 @@ same file; you almost never need to touch it by hand.
128
128
 
129
129
  ```toml
130
130
  [discord]
131
- client_id = "" # paste your own Application ID here
131
+ client_id = "" # default Application ID — paste yours here
132
+ client_id_mpris = "" # optional per-source override (browser / Apple Music)
133
+ client_id_bluetooth = "" # optional per-source override (Bluetooth headphones)
132
134
 
133
135
  [sources]
134
136
  mpris_enabled = true
@@ -143,12 +145,28 @@ autostart = false
143
145
  notifications = true
144
146
  cover_art = true
145
147
  show_buttons = true
148
+ notify_delay_ms = 0 # 0 = fire ASAP; the cover-art retry loop still waits up to ~2 s
146
149
 
147
150
  [advanced]
148
- poll_interval_ms = 1000
151
+ poll_interval_ms = 500
149
152
  log_level = "INFO"
153
+ cover_cache_size = 200 # disk cap for cached covers
154
+ idle_grace_s = 30 # clear status when same track plays past duration + grace; 0 disables
155
+ language = "system" # "system" follows QLocale; "en" / "de" / … force a translation
150
156
  ```
151
157
 
158
+ Per-source `client_id_*` fields let Apple Music render under one Discord
159
+ application (with the album-grid as artwork) and Bluetooth headphones under
160
+ another (with a generic Bluetooth glyph). Empty falls back to the default
161
+ `client_id`.
162
+
163
+ ## MPRIS server
164
+
165
+ Refrain publishes itself as `org.mpris.MediaPlayer2.refrain` on the session
166
+ bus, so KDE Plasma's panel media-controls applet (and KDE Connect, GNOME
167
+ Shell, Mako, …) drive the same Play/Pause/Next/Previous as the tray and
168
+ render the same track Discord renders.
169
+
152
170
  ## Tray
153
171
 
154
172
  <p align="center">
@@ -145,6 +145,30 @@ What's done, what's next, what's deliberately not in scope.
145
145
  - **Window-title cleanup** — no more "Refrain — Settings — Refrain"
146
146
  triple; Qt's auto-suffix is the only place "Refrain" appears.
147
147
 
148
+ ## Done — v0.2.1
149
+
150
+ - **Settings UI overhaul** — consistent `QGroupBox` + form-layout
151
+ across every tab, left-aligned group titles via stylesheet
152
+ (Plasma Breeze centers them by default), fixed-width inputs
153
+ (220 px default, 360 px in the Discord group). Switched from
154
+ `AllNonFixedFieldsGrow` to `FieldsStayAtSizeHint` because the
155
+ former silently ignored fixed-width caps on Plasma Breeze.
156
+ - **Notification cover-flicker fix** — `notify-send -i` now
157
+ carries the same image as the `--hint string:image-path:`
158
+ payload so KDE Plasma can't briefly render the brand badge.
159
+ - **Discord RPC cover flicker fix** — defer the first activity
160
+ update for a new track until the iTunes cover URL is in cache,
161
+ capped at ~3 polls so cover-less songs still update.
162
+ - **Auto-restart on Discord client_id Apply** — same pattern as
163
+ the language switch, since pypresence binds to the client_id
164
+ at connect time.
165
+ - **AppImage update size validation** + tmp cleanup, **atomic
166
+ config writes** (tmp + os.replace), **dead `dbus_watcher`
167
+ module removed** (155 lines), **RPC reconnect backoff capped
168
+ at 15 s** instead of 60 s, **`.ts` source files no longer ship
169
+ in the wheel**, **MPRIS dispatch logging at INFO** so
170
+ Next/Previous routing is visible in the live log.
171
+
148
172
  ## Up next — v0.3
149
173
 
150
174
  - **Polishing the wrapped i18n surface** — wrap the remaining
@@ -156,6 +180,48 @@ What's done, what's next, what's deliberately not in scope.
156
180
  signed tag, so AUR users get the exact same commit the release
157
181
  workflow ships.
158
182
 
183
+ ## Done — v0.2.2
184
+
185
+ - **Multiple Discord profiles** — per-source `client_id_mpris` and
186
+ `client_id_bluetooth` overrides on top of the default `client_id`,
187
+ so Apple Music can render under one Discord application (with the
188
+ album-grid as artwork) and Bluetooth headphones under another (with
189
+ a generic Bluetooth glyph). Daemon reconnects RPC the moment the
190
+ active source flips.
191
+ - **MPRIS-server mode** — Refrain publishes itself as
192
+ `org.mpris.MediaPlayer2.refrain` so KDE Plasma's panel media-controls
193
+ applet, KDE Connect, GNOME Shell, etc. drive the same Play/Pause/
194
+ Next/Previous as the tray and render the same track. Built on
195
+ `dbus-python` with a GLib main loop in its own thread so it
196
+ doesn't conflict with Qt's event loop.
197
+ - **Welcome wizard redesign** — icon-badge header, subtitle, dedicated
198
+ diagnostics card with two clearly-labelled probe rows, readable
199
+ helper text on Plasma Breeze Dark (was invisible with `palette(mid)`).
200
+ Apply now opens Settings automatically with the just-saved Discord
201
+ ID pre-filled.
202
+ - **Live-tier responsiveness** — default `poll_interval_ms` 1 s → 500 ms,
203
+ `notify_delay_ms` 600 ms → 0 ms (cache hits fire instantly), Discord
204
+ RPC connect happens within ~1 s of first track detection (was up to
205
+ 51 s due to dbus-python's 25 s default reply timeout combined with
206
+ plasma-browser-integration introspection hangs — fixed via
207
+ `introspect=False` + per-property `timeout=0.5` + per-player
208
+ blacklist when a player times out).
209
+ - **iTunes `trackTimeMillis` fallback** for Apple Music preview-clip
210
+ durations (< 30 s). Discord drops `start`/`end` entirely on preview
211
+ mode so the elapsed counter doesn't reset every 8 s.
212
+ - **Skip / Previous reach Apple Music reliably** — chromium-native
213
+ MPRIS dispatched first, plasma-browser-integration as fallback,
214
+ cascade follow-up polls (0/50/150/350/750 ms) so the track-change
215
+ reflects in Discord + tray within ~250 ms of detection.
216
+ - **Reset preserves all three Discord IDs**, dialog button labels
217
+ localised (`Reset` / `Zurücksetzen`), text states what's preserved
218
+ explicitly.
219
+ - **Welcome wizard X / Esc** marks `first_run_complete=True` so the
220
+ wizard doesn't re-appear; Apply with empty ID asks for confirmation.
221
+ - **Theme-aware text colors** — every helper-text label switched from
222
+ `palette(mid)` to `palette(text)` after Plasma Breeze Dark
223
+ rendered them invisible.
224
+
159
225
  ## Maybe — v0.3+
160
226
 
161
227
  - **Flathub submission**, second attempt. The manifest under
@@ -167,11 +233,6 @@ What's done, what's next, what's deliberately not in scope.
167
233
  - **Cover-art replacement notifications**: re-send the desktop notification
168
234
  via `--replace-id` once the cover finishes downloading, so the embed
169
235
  swaps in even when the initial retry window times out.
170
- - **Multiple Discord profiles**: per-source client IDs so Bluetooth and
171
- Apple Music Web can render with different artwork in the user's profile.
172
- - **MPRIS-server mode**: expose Refrain as an MPRIS player itself, so KDE
173
- Plasma's media controls in the panel show the same track Refrain shows
174
- in Discord.
175
236
 
176
237
  ## Deliberately not in scope
177
238
 
@@ -30,7 +30,7 @@ AppDir:
30
30
  id: io.github.Rockykln.Refrain
31
31
  name: Refrain
32
32
  icon: refrain
33
- version: 0.2.0
33
+ version: 0.2.2
34
34
  exec: usr/bin/python3
35
35
  exec_args: -m refrain $@
36
36
 
@@ -1,7 +1,7 @@
1
1
  # Maintainer: Rockykln <contact@rockykln.com>
2
2
 
3
3
  pkgname=refrain
4
- pkgver=0.2.0
4
+ pkgver=0.2.2
5
5
  pkgrel=1
6
6
  pkgdesc="Discord Rich Presence for Apple Music on Linux"
7
7
  arch=('any')
@@ -20,7 +20,7 @@ makedepends=(
20
20
  'python-wheel'
21
21
  )
22
22
  source=("$pkgname-$pkgver.tar.gz::https://github.com/Rockykln/refrain/archive/v$pkgver.tar.gz")
23
- sha256sums=('d88df042b1ce2d403c65a3c0c6883f0de4eda038c5f44e1fb8f4d5b54bddd5cf')
23
+ sha256sums=('SKIP') # set after release tag is pushed: updpkgsums
24
24
 
25
25
  build() {
26
26
  cd "$pkgname-$pkgver"
@@ -51,6 +51,41 @@
51
51
  <content_rating type="oars-1.1" />
52
52
 
53
53
  <releases>
54
+ <release version="0.2.2" date="2026-05-06">
55
+ <description>
56
+ <p>
57
+ Two roadmap features pulled forward: per-source Discord
58
+ client IDs (so Apple Music and Bluetooth can render under
59
+ different Discord applications, with their own artwork in
60
+ the user's profile), and MPRIS-server mode (Refrain
61
+ publishes itself as org.mpris.MediaPlayer2.refrain so KDE
62
+ Plasma's panel media-controls applet, KDE Connect, GNOME
63
+ Shell, etc. drive the same Play / Pause / Next / Previous
64
+ as the tray and render the same track Discord renders).
65
+ </p>
66
+ </description>
67
+ </release>
68
+ <release version="0.2.1" date="2026-05-06">
69
+ <description>
70
+ <p>
71
+ Settings UI overhaul: every tab now uses a consistent
72
+ GroupBox + form-layout with left-aligned titles, fixed-width
73
+ inputs that hold across Plasma Breeze and other Qt styles,
74
+ and a compact 220 px field default (360 px for the Discord
75
+ group whose values need more room). Notification cover-art
76
+ flicker is gone — `notify-send -i` and `image-path` now
77
+ carry the same image so KDE Plasma can't briefly render
78
+ the brand badge. Discord RPC defers the first update for
79
+ a new track until the cover URL is in cache so the brand
80
+ fallback never flashes in the activity card. Apply now
81
+ auto-restarts when the Discord client_id changes (same
82
+ pattern as the language switch). Dead D-Bus watcher code
83
+ removed (155 lines), atomic config writes, AppImage
84
+ updater validates downloaded size, RPC reconnect backoff
85
+ capped at 15 s instead of 60 s.
86
+ </p>
87
+ </description>
88
+ </release>
54
89
  <release version="0.2.0" date="2026-05-06">
55
90
  <description>
56
91
  <p>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "refrain"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Discord Rich Presence for Apple Music on Linux"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -50,12 +50,11 @@ Changelog = "https://github.com/Rockykln/refrain/blob/main/CHANGELOG.md"
50
50
 
51
51
  [tool.hatch.build.targets.wheel]
52
52
  packages = ["src/refrain"]
53
- # Include compiled translations + bundled assets in the wheel.
54
- # .qm files are pre-compiled (via `make i18n`) and committed; .ts files
55
- # are the editable Linguist sources for translators.
53
+ # Include compiled translations + bundled assets in the wheel. Only the
54
+ # pre-compiled .qm files ship — .ts (Linguist source) files belong in
55
+ # the source tree for translators to edit, not in the runtime wheel.
56
56
  artifacts = [
57
57
  "src/refrain/i18n/*.qm",
58
- "src/refrain/i18n/*.ts",
59
58
  "src/refrain/assets/**/*",
60
59
  ]
61
60
 
@@ -0,0 +1 @@
1
+ __version__ = "0.2.2"