refrain 0.2.6__tar.gz → 0.2.7__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.6 → refrain-0.2.7}/CHANGELOG.md +92 -1
  2. {refrain-0.2.6 → refrain-0.2.7}/PKG-INFO +11 -2
  3. {refrain-0.2.6 → refrain-0.2.7}/README.md +10 -1
  4. {refrain-0.2.6 → refrain-0.2.7}/ROADMAP.md +87 -4
  5. {refrain-0.2.6 → refrain-0.2.7}/docs/screenshots/README.md +4 -2
  6. refrain-0.2.7/docs/screenshots/live-log.png +0 -0
  7. {refrain-0.2.6 → refrain-0.2.7}/docs/screenshots/settings-advanced.png +0 -0
  8. {refrain-0.2.6 → refrain-0.2.7}/docs/screenshots/settings-general.png +0 -0
  9. {refrain-0.2.6 → refrain-0.2.7}/docs/screenshots/settings-sources.png +0 -0
  10. refrain-0.2.7/docs/screenshots/settings-updates.png +0 -0
  11. refrain-0.2.7/docs/screenshots/tray-menu.png +0 -0
  12. refrain-0.2.7/docs/screenshots/update-dialog.png +0 -0
  13. {refrain-0.2.6 → refrain-0.2.7}/packaging/appimage/AppImageBuilder.yml +1 -1
  14. {refrain-0.2.6 → refrain-0.2.7}/packaging/aur/refrain/.SRCINFO +3 -3
  15. {refrain-0.2.6 → refrain-0.2.7}/packaging/aur/refrain/PKGBUILD +2 -2
  16. {refrain-0.2.6 → refrain-0.2.7}/packaging/flatpak/io.github.Rockykln.Refrain.metainfo.xml +18 -0
  17. {refrain-0.2.6 → refrain-0.2.7}/pyproject.toml +1 -1
  18. refrain-0.2.7/src/refrain/__init__.py +1 -0
  19. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/app.py +39 -26
  20. refrain-0.2.7/src/refrain/assets/icons/menu-quit.svg +3 -0
  21. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/discord_rpc.py +17 -0
  22. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_de.qm +0 -0
  23. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_de.ts +14 -14
  24. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_es.qm +0 -0
  25. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_es.ts +14 -14
  26. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_fr.qm +0 -0
  27. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_fr.ts +14 -14
  28. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_it.qm +0 -0
  29. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_it.ts +14 -14
  30. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_ja.qm +0 -0
  31. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_ja.ts +14 -14
  32. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_pl.qm +0 -0
  33. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_pl.ts +14 -14
  34. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_pt.qm +0 -0
  35. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_pt.ts +14 -14
  36. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_ru.qm +0 -0
  37. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_ru.ts +14 -14
  38. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_zh_CN.qm +0 -0
  39. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/i18n/refrain_zh_CN.ts +14 -14
  40. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/ui/settings_window.py +10 -25
  41. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/ui/tray.py +70 -15
  42. refrain-0.2.6/docs/screenshots/live-log.png +0 -0
  43. refrain-0.2.6/docs/screenshots/settings-updates.png +0 -0
  44. refrain-0.2.6/docs/screenshots/tray-menu.png +0 -0
  45. refrain-0.2.6/docs/screenshots/update-dialog.png +0 -0
  46. refrain-0.2.6/src/refrain/__init__.py +0 -1
  47. {refrain-0.2.6 → refrain-0.2.7}/.editorconfig +0 -0
  48. {refrain-0.2.6 → refrain-0.2.7}/.gitattributes +0 -0
  49. {refrain-0.2.6 → refrain-0.2.7}/.github/FUNDING.yml +0 -0
  50. {refrain-0.2.6 → refrain-0.2.7}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  51. {refrain-0.2.6 → refrain-0.2.7}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  52. {refrain-0.2.6 → refrain-0.2.7}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  53. {refrain-0.2.6 → refrain-0.2.7}/.github/codeql/codeql-config.yml +0 -0
  54. {refrain-0.2.6 → refrain-0.2.7}/.github/dependabot.yml +0 -0
  55. {refrain-0.2.6 → refrain-0.2.7}/.github/pull_request_template.md +0 -0
  56. {refrain-0.2.6 → refrain-0.2.7}/.github/workflows/codeql.yml +0 -0
  57. {refrain-0.2.6 → refrain-0.2.7}/.github/workflows/release.yml +0 -0
  58. {refrain-0.2.6 → refrain-0.2.7}/.github/workflows/security.yml +0 -0
  59. {refrain-0.2.6 → refrain-0.2.7}/.github/workflows/tests.yml +0 -0
  60. {refrain-0.2.6 → refrain-0.2.7}/.gitignore +0 -0
  61. {refrain-0.2.6 → refrain-0.2.7}/CODE_OF_CONDUCT.md +0 -0
  62. {refrain-0.2.6 → refrain-0.2.7}/CONTRIBUTING.md +0 -0
  63. {refrain-0.2.6 → refrain-0.2.7}/LICENSE +0 -0
  64. {refrain-0.2.6 → refrain-0.2.7}/Makefile +0 -0
  65. {refrain-0.2.6 → refrain-0.2.7}/SECURITY.md +0 -0
  66. {refrain-0.2.6 → refrain-0.2.7}/docs/architecture.md +0 -0
  67. {refrain-0.2.6 → refrain-0.2.7}/docs/bluetooth.md +0 -0
  68. {refrain-0.2.6 → refrain-0.2.7}/docs/faq.md +0 -0
  69. {refrain-0.2.6 → refrain-0.2.7}/docs/screenshots/discord-rpc.png +0 -0
  70. {refrain-0.2.6 → refrain-0.2.7}/docs/screenshots/notification.png +0 -0
  71. {refrain-0.2.6 → refrain-0.2.7}/docs/screenshots/welcome.png +0 -0
  72. {refrain-0.2.6 → refrain-0.2.7}/docs/test-matrix.md +0 -0
  73. {refrain-0.2.6 → refrain-0.2.7}/packaging/README.md +0 -0
  74. {refrain-0.2.6 → refrain-0.2.7}/packaging/aur/refrain-git/PKGBUILD +0 -0
  75. {refrain-0.2.6 → refrain-0.2.7}/packaging/flatpak/io.github.Rockykln.Refrain.desktop +0 -0
  76. {refrain-0.2.6 → refrain-0.2.7}/packaging/flatpak/io.github.Rockykln.Refrain.yaml +0 -0
  77. {refrain-0.2.6 → refrain-0.2.7}/packaging/flatpak/python-deps.json +0 -0
  78. {refrain-0.2.6 → refrain-0.2.7}/requirements-dev.txt +0 -0
  79. {refrain-0.2.6 → refrain-0.2.7}/requirements.txt +0 -0
  80. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/__main__.py +0 -0
  81. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/icons/github-mark.svg +0 -0
  82. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/icons/menu-update.svg +0 -0
  83. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/icons/refrain.png +0 -0
  84. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/icons/refrain.svg +0 -0
  85. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/icons/tray-paused-dark.svg +0 -0
  86. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/icons/tray-paused.svg +0 -0
  87. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/icons/tray-playing-dark.svg +0 -0
  88. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/icons/tray-playing.svg +0 -0
  89. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/icons/tray-stopped-dark.svg +0 -0
  90. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/icons/tray-stopped.svg +0 -0
  91. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/assets/refrain.desktop +0 -0
  92. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/autostart.py +0 -0
  93. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/config.py +0 -0
  94. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/cover_art.py +0 -0
  95. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/cover_fetcher.py +0 -0
  96. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/daemon.py +0 -0
  97. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/logging_setup.py +0 -0
  98. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/paths.py +0 -0
  99. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/single_instance.py +0 -0
  100. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/sources/__init__.py +0 -0
  101. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/sources/base.py +0 -0
  102. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/sources/bluetooth.py +0 -0
  103. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/sources/mpris.py +0 -0
  104. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/sources/mpris_server.py +0 -0
  105. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/timing.py +0 -0
  106. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/ui/__init__.py +0 -0
  107. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/ui/log_window.py +0 -0
  108. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/ui/update_dialog.py +0 -0
  109. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/ui/welcome_dialog.py +0 -0
  110. {refrain-0.2.6 → refrain-0.2.7}/src/refrain/updater.py +0 -0
  111. {refrain-0.2.6 → refrain-0.2.7}/tests/__init__.py +0 -0
  112. {refrain-0.2.6 → refrain-0.2.7}/tests/conftest.py +0 -0
  113. {refrain-0.2.6 → refrain-0.2.7}/tests/test_autostart.py +0 -0
  114. {refrain-0.2.6 → refrain-0.2.7}/tests/test_config.py +0 -0
  115. {refrain-0.2.6 → refrain-0.2.7}/tests/test_cover_art.py +0 -0
  116. {refrain-0.2.6 → refrain-0.2.7}/tests/test_cover_fetcher.py +0 -0
  117. {refrain-0.2.6 → refrain-0.2.7}/tests/test_daemon_album_format.py +0 -0
  118. {refrain-0.2.6 → refrain-0.2.7}/tests/test_daemon_idle.py +0 -0
  119. {refrain-0.2.6 → refrain-0.2.7}/tests/test_discord_listening.py +0 -0
  120. {refrain-0.2.6 → refrain-0.2.7}/tests/test_discord_rpc.py +0 -0
  121. {refrain-0.2.6 → refrain-0.2.7}/tests/test_imports.py +0 -0
  122. {refrain-0.2.6 → refrain-0.2.7}/tests/test_mpris_dispatch.py +0 -0
  123. {refrain-0.2.6 → refrain-0.2.7}/tests/test_paths.py +0 -0
  124. {refrain-0.2.6 → refrain-0.2.7}/tests/test_sources_base.py +0 -0
  125. {refrain-0.2.6 → refrain-0.2.7}/tests/test_timing.py +0 -0
  126. {refrain-0.2.6 → refrain-0.2.7}/tests/test_tray_init.py +0 -0
  127. {refrain-0.2.6 → refrain-0.2.7}/tests/test_update_dialog_body.py +0 -0
  128. {refrain-0.2.6 → refrain-0.2.7}/tests/test_updater.py +0 -0
@@ -7,6 +7,96 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.7] - 2026-05-07
11
+
12
+ Tray-menu polish + Discord-RPC reliability fixes. Every tray action
13
+ now carries an icon, song-info rows render in normal menu text
14
+ colour instead of greyed-out indented rows, middle-click on the
15
+ tray toggles play/pause, and Discord handshake errors no longer
16
+ spam the log with unexpected-exception tracebacks.
17
+
18
+ ### Added
19
+
20
+ - **Middle-click on the tray icon toggles play/pause.** Clicks the
21
+ PlayPause command on whichever MPRIS source Refrain is currently
22
+ attached to — same path as the tray-menu Play/Pause item, but
23
+ reachable without opening the menu. Plain left-click still opens
24
+ Settings; right-click still shows the menu.
25
+ - **Tray menu actions now all carry icons.** Once any QMenu item
26
+ has an icon, the menu reserves the icon column for *all* items —
27
+ so adding the red ✕ to Quit Refrain made every previously
28
+ text-only action read as misaligned with blank icon space. Action
29
+ items now use freedesktop theme icons (`configure`,
30
+ `view-list-text`, `view-refresh`, `media-skip-backward`,
31
+ `media-playback-start`, `media-playback-pause`,
32
+ `media-skip-forward`) which match the user's Plasma / GNOME /
33
+ Breeze icon set. Update + Quit keep their bundled coloured-accent
34
+ SVGs. Source strings for the playback / restart actions dropped
35
+ their unicode-glyph prefixes (`⏮ ⏵ ⏸ ⏭ ⟳`) so the new icons
36
+ don't read as duplicates; all 9 .ts files updated to match. The
37
+ Quit ✕ glyph was redrawn to fill its 16×16 viewBox edge-to-edge
38
+ so it visually matches the theme icons' pixel weight.
39
+ - **Song-info rows in the tray menu now render with proper icons
40
+ and full-strength text colour.** Title / Artist / Progress /
41
+ Discord-status used to be `setEnabled(False)` so KDE's DBusMenu
42
+ rendered them muted-grey + indented + iconless next to the white
43
+ action labels below. They're now enabled (a stray click opens
44
+ Settings — their natural "tell me more" target) and carry
45
+ `view-media-track`, `view-media-artist`, `chronometer`, and
46
+ `network-connect` / `network-disconnect` icons. The `♪` prefix
47
+ on the title and the `●/○` prefix on the Discord-status row are
48
+ gone — the icons replace them.
49
+
50
+ ### Fixed
51
+
52
+ - **Settings → Updates "Latest release notes" QTextBrowser was
53
+ parent-less.** `_build_updates_tab` called
54
+ `notes_group.setLayout(QVBoxLayout())` on a QGroupBox that
55
+ already had a QFormLayout installed by `_new_group`. Qt refuses
56
+ to install a second layout (logs "QLayout: Attempting to add
57
+ QLayout to QGroupBox which already has a layout") so the new
58
+ VBoxLayout was discarded and the QTextBrowser ended up as a
59
+ free-floating top-level widget kept alive only by the Python
60
+ reference. In production it still painted because the parent
61
+ page laid it out somewhere reasonable, but it was outside the
62
+ widget tree (`findChildren(QTextBrowser)` returned 0). The view
63
+ is now added via the existing form layout's `addRow`.
64
+ - **Discord handshake errors logged as ERROR-level unexpected
65
+ exceptions with a full traceback every retry.** When Discord
66
+ responded to a `pypresence.Presence.connect()` with an error
67
+ message (e.g. "User logged out" — DiscordError code 1000, or
68
+ "Invalid Client ID"), the bare `except Exception:` clause in
69
+ `_ensure_connected` swallowed it as a programmer bug and emitted
70
+ a noisy traceback every backoff cycle. `pypresence.exceptions.
71
+ DiscordError` is now caught explicitly and logged at INFO; the
72
+ back-off jumps straight to the 15 s ceiling so Refrain doesn't
73
+ retry tightly while the user is signing back into Discord or
74
+ fixing a typo'd client_id.
75
+ - **First Discord push after a forced reconnect could be deduped
76
+ out of existence.** The presence-update dedupe cache lived on
77
+ the orchestrator across `Presence` instance replacements; after
78
+ a reconnect with byte-identical payload data, the new pipe
79
+ carried no state on Discord's side but our cache still claimed
80
+ "we already pushed this", so the user saw "no activity" until
81
+ metadata changed. `_last_payload` is now reset to None in the
82
+ successful-connect path of `_ensure_connected`.
83
+ - **Empty artist row in tray menu when nothing was playing.** The
84
+ artist QAction stayed visible with empty text whenever no track
85
+ was active, rendering as a tall blank line right under
86
+ "(nothing playing)". Now hidden until a real track populates it.
87
+ - **Settings → Updates "Latest release notes" pane stayed empty
88
+ on every Refrain restart within the 24-hour update-check
89
+ cooldown.** `maybe_check_on_startup` short-circuited *before*
90
+ fetching anything when the cooldown was active, so
91
+ `releaseInfoFetched` never fired and `set_latest_release` never
92
+ populated the inline pane. The v0.2.6 workaround — re-fetching
93
+ on Updates-tab activation with `manual=True` — additionally
94
+ fired the "up to date" / "update available" popups on every tab
95
+ visit, which was its own bug. Startup now always fetches the
96
+ release info (silent, no popups) so the pane is populated
97
+ immediately; the cooldown only gates whether that fetch *also*
98
+ triggers the auto-popup. The on-tab-activation fetch is gone.
99
+
10
100
  ## [0.2.6] - 2026-05-07
11
101
 
12
102
  Localization expansion plus a bundle of reliability fixes accumulated
@@ -979,7 +1069,8 @@ with a proper, installable Linux app.
979
1069
  pip-audit, trufflehog, release), Dependabot, issue + PR templates,
980
1070
  `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`.
981
1071
 
982
- [Unreleased]: https://github.com/Rockykln/refrain/compare/v0.2.6...HEAD
1072
+ [Unreleased]: https://github.com/Rockykln/refrain/compare/v0.2.7...HEAD
1073
+ [0.2.7]: https://github.com/Rockykln/refrain/compare/v0.2.6...v0.2.7
983
1074
  [0.2.6]: https://github.com/Rockykln/refrain/compare/v0.2.5...v0.2.6
984
1075
  [0.2.5]: https://github.com/Rockykln/refrain/compare/v0.2.4...v0.2.5
985
1076
  [0.2.4]: https://github.com/Rockykln/refrain/compare/v0.2.3...v0.2.4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refrain
3
- Version: 0.2.6
3
+ Version: 0.2.7
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
@@ -153,6 +153,15 @@ registers their own (free, takes 30 seconds):
153
153
  4. Launch Refrain → *Settings → General → Discord Client ID* → paste,
154
154
  *Apply*.
155
155
 
156
+ The first time you launch Refrain without a configured ID, the
157
+ welcome wizard pops up with the setup steps + a live diagnostics
158
+ panel that probes your D-Bus session and Discord IPC socket so you
159
+ know up front whether your environment can host the RPC at all.
160
+
161
+ <p align="center">
162
+ <img src="docs/screenshots/welcome.png" alt="Welcome wizard" width="560"/>
163
+ </p>
164
+
156
165
  That's it. The status will appear in Discord on the next track change.
157
166
 
158
167
  ## Configuration
@@ -187,7 +196,7 @@ poll_interval_ms = 500
187
196
  log_level = "INFO"
188
197
  cover_cache_size = 200 # disk cap for cached covers
189
198
  idle_grace_s = 30 # clear status when same track plays past duration + grace; 0 disables
190
- language = "system" # "system" follows QLocale; "en" / "de" / … force a translation
199
+ language = "system" # "system" follows QLocale; "en", "de", "es", "fr", "pt", "it", "ru", "pl", "ja", "zh_CN" force a translation
191
200
  ```
192
201
 
193
202
  Per-source `client_id_*` fields let Apple Music render under one Discord
@@ -119,6 +119,15 @@ registers their own (free, takes 30 seconds):
119
119
  4. Launch Refrain → *Settings → General → Discord Client ID* → paste,
120
120
  *Apply*.
121
121
 
122
+ The first time you launch Refrain without a configured ID, the
123
+ welcome wizard pops up with the setup steps + a live diagnostics
124
+ panel that probes your D-Bus session and Discord IPC socket so you
125
+ know up front whether your environment can host the RPC at all.
126
+
127
+ <p align="center">
128
+ <img src="docs/screenshots/welcome.png" alt="Welcome wizard" width="560"/>
129
+ </p>
130
+
122
131
  That's it. The status will appear in Discord on the next track change.
123
132
 
124
133
  ## Configuration
@@ -153,7 +162,7 @@ poll_interval_ms = 500
153
162
  log_level = "INFO"
154
163
  cover_cache_size = 200 # disk cap for cached covers
155
164
  idle_grace_s = 30 # clear status when same track plays past duration + grace; 0 disables
156
- language = "system" # "system" follows QLocale; "en" / "de" / … force a translation
165
+ language = "system" # "system" follows QLocale; "en", "de", "es", "fr", "pt", "it", "ru", "pl", "ja", "zh_CN" force a translation
157
166
  ```
158
167
 
159
168
  Per-source `client_id_*` fields let Apple Music render under one Discord
@@ -285,14 +285,97 @@ What's done, what's next, what's deliberately not in scope.
285
285
 
286
286
  ## Up next — v0.3
287
287
 
288
- - **Polishing the wrapped i18n surface** — wrap the remaining
289
- fixed-position strings (date formatters, advanced-tab subtitles,
290
- bluetooth picker labels) so the German build covers everything a
291
- user sees, not just the high-traffic widgets.
292
288
  - **Stable-release AUR build** that doesn't rely on the GitHub
293
289
  release tarball — switch to a `git`-source PKGBUILD pinned to the
294
290
  signed tag, so AUR users get the exact same commit the release
295
291
  workflow ships.
292
+ - **Wrap the remaining fixed-position strings** (date formatters,
293
+ bluetooth picker labels) so every language reaches 100% coverage,
294
+ not just the high-traffic widgets.
295
+
296
+ ## Done — v0.2.7
297
+
298
+ - **Tray-menu polish.** Every action carries an icon now —
299
+ freedesktop theme icons (`configure`, `view-list-text`,
300
+ `view-refresh`, `media-skip-{backward,forward}`,
301
+ `media-playback-{start,pause}`) for built-in actions plus the
302
+ bundled coloured-accent SVGs for Update / Quit. Source strings
303
+ for the playback / restart / Discord-status entries dropped
304
+ their unicode-glyph prefixes (`⏮ ⏵ ⏸ ⏭ ⟳ ●/○`); all 9 .ts
305
+ files updated. Quit ✕ glyph redrawn to fill its 16×16 viewBox.
306
+ - **Song-info rows render with proper colour + icons.** Title /
307
+ Artist / Progress / Discord-status used to be `setEnabled(False)`
308
+ so KDE / GNOME's DBusMenu painted them muted-grey + indented +
309
+ iconless. They're now enabled (a click opens Settings) and
310
+ carry `view-media-{track,artist}`, `chronometer`, and
311
+ `network-{connect,disconnect}` icons.
312
+ - **Middle-click on the tray icon toggles play/pause.** Same
313
+ PlayPause command as the tray-menu Play/Pause item, but
314
+ reachable without opening the menu.
315
+ - **Discord handshake errors no longer crash-log.** `pypresence.
316
+ exceptions.DiscordError` (raised when Discord rejects the
317
+ handshake — "User logged out", "Invalid Client ID") fell
318
+ through the bare `except Exception:` and emitted a full
319
+ traceback every retry cycle. Now caught explicitly, logged at
320
+ INFO, with backoff jumping straight to the 15 s ceiling so
321
+ Refrain doesn't retry tightly while you sign back into Discord.
322
+ - **First Discord push after a forced reconnect can no longer be
323
+ deduped out.** `_last_payload` is reset in the connect-success
324
+ path so the dedup cache from a previous Presence instance
325
+ doesn't suppress the first push on the new pipe.
326
+ - **Settings → Updates "Latest release notes" pane populates on
327
+ every restart.** v0.2.6's startup auto-check short-circuited
328
+ before fetching when the 24h cooldown was active; the v0.2.6
329
+ workaround re-fetched on every Updates-tab visit. Both gone —
330
+ startup runs a silent fetch (no popups, no cooldown bump) so
331
+ the pane is populated immediately, while the cooldown still
332
+ gates the auto-nag popup.
333
+ - **Empty artist row in tray menu when nothing was playing**
334
+ (`setVisible(False)` until a real track populates it).
335
+
336
+ ## Done — v0.2.6
337
+
338
+ - **8 new UI languages** at 127/127 strings each: Spanish (`es`),
339
+ French (`fr`), Portuguese (`pt`), Italian (`it`), Russian
340
+ (`ru`), Polish (`pl`), Japanese (`ja`), Simplified Chinese
341
+ (`zh_CN`). Settings → Advanced → Language dropdown lists each
342
+ under its native endonym. Together with English + German this
343
+ covers ~3 billion native speakers.
344
+ - **GitHub release bodies were one-line "Full Changelog: …"**
345
+ because the workflow used `generate_release_notes: true`. Now
346
+ extracts the matching `## [X.Y.Z]` section from CHANGELOG.md
347
+ via awk and feeds it as `body_path`; existing v0.1.0 – v0.2.5
348
+ bodies were backfilled out-of-band.
349
+ - **Reliability sweep**: `os.execvp` Restart fallback to
350
+ `python -m refrain` when `argv[0]` isn't runnable;
351
+ Config.save .tmp cleanup on disk-full / read-only;
352
+ `Config._format_value` escapes `\n / \r / \t`;
353
+ `Config._construct` coerces wrongly-typed primitives (e.g.
354
+ `cover_cache_size = "200"`) instead of crashing in
355
+ `_prune_cover_cache`; cover-art `_write_cache` +
356
+ `download_cover_image` clean up `.tmp` siblings on disk
357
+ failure; `_bridge_sandboxed_ipc_socket` permission-error wrap;
358
+ iTunes Search shape-checks; `SessionBusUnavailable` exception
359
+ surfacing instead of bare DBusException; Apply error toast for
360
+ non-writable config dirs; `setup_logging`/`_apply_log_level`
361
+ coerce non-string `log_level`.
362
+
363
+ ## Done — v0.2.5
364
+
365
+ - **AUR + Flatpak self-update spawns a terminal automatically**
366
+ running the package-manager command (`yay -Syu refrain` /
367
+ `flatpak update io.github.Rockykln.Refrain`) instead of just
368
+ showing the command in a popup. Falls back to the previous
369
+ message-box hint when no terminal is on PATH (probe list:
370
+ konsole, gnome-terminal, xfce4-terminal, kitty, alacritty,
371
+ foot, xterm, wezterm).
372
+ - **pip auto-update detects "no-op" exits.** PyPI's CDN can lag
373
+ the GitHub Releases API by minutes, so a "0.2.X available"
374
+ check could succeed while pip exited 0 with "Requirement
375
+ already satisfied" (no upgrade). Refrain now parses pip's
376
+ stdout to distinguish "Successfully installed" from
377
+ "Requirement already satisfied" and tells the user to retry
378
+ shortly instead of restarting into the same version.
296
379
 
297
380
  ## Done — v0.2.2
298
381
 
@@ -4,14 +4,16 @@ The README references screenshots from this directory. To regenerate them:
4
4
 
5
5
  | File | What to capture |
6
6
  |----------------------------|------------------------------------------------------|
7
- | `tray-menu.png` | Right-click the Refrain tray icon while a track plays. The full menu, with track name, artist, time and player controls visible. |
7
+ | `tray-menu.png` | Right-click the Refrain tray icon while a track plays. Captures title / artist / progress / Discord-status info rows + Previous / Play-Pause / Next / Settings / Live log / Restart / Quit, all with theme icons. |
8
8
  | `settings-general.png` | Settings window → *General* tab, showing Discord client ID, Privacy combo, autostart, notifications, cover art, buttons. |
9
9
  | `settings-sources.png` | Settings window → *Sources* tab, showing MPRIS toggle, Bluetooth toggle, and the paired-device dropdown populated. |
10
- | `settings-updates.png` | Settings window → *Updates* tab. Auto-check on, last-checked timestamp visible, hint text wrapped. |
10
+ | `settings-updates.png` | Settings window → *Updates* tab. Auto-check on, last-checked timestamp visible, the "Latest release notes" pane populated from CHANGELOG. |
11
+ | `settings-advanced.png` | Settings window → *Advanced* tab, showing the Language dropdown (11 entries: System default + 10 native endonyms), poll interval, log level, cover cache size, idle grace, and the Reset-all-settings button. |
11
12
  | `update-dialog.png` | The update-available dialog (only appears when a new version is found). Trigger it locally by editing `__version__` to something older and clicking *Check for updates now*. |
12
13
  | `discord-rpc.png` | Cropped section of your Discord profile showing the "Listening to" card with cover art + elapsed/total timer. |
13
14
  | `notification.png` | A desktop notification fired by Refrain on track change. KDE Plasma's notification with the album cover at left. |
14
15
  | `live-log.png` | The live-log window, opened via tray → *Live log…*, with at least 10 lines of streamed log. |
16
+ | `welcome.png` | First-run welcome wizard — shown when no `client_id` is configured yet. Diagnostics panel + Discord-app guidance. |
15
17
 
16
18
  ## How to capture on KDE Plasma
17
19
 
@@ -30,7 +30,7 @@ AppDir:
30
30
  id: io.github.Rockykln.Refrain
31
31
  name: Refrain
32
32
  icon: refrain
33
- version: 0.2.6
33
+ version: 0.2.7
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.5
3
+ pkgver = 0.2.6
4
4
  pkgrel = 1
5
5
  url = https://github.com/Rockykln/refrain
6
6
  arch = any
@@ -14,7 +14,7 @@ pkgbase = refrain
14
14
  depends = python-dbus
15
15
  depends = pyside6
16
16
  optdepends = python-gobject: enables MPRIS-server publication so Plasma media controls reach Refrain
17
- source = refrain-0.2.5.tar.gz::https://github.com/Rockykln/refrain/archive/v0.2.5.tar.gz
18
- sha256sums = 7ee7b991fab3ba2149fe72745b1432ef11abd7714ffba71cafd3bf1b6af5a6c4
17
+ source = refrain-0.2.6.tar.gz::https://github.com/Rockykln/refrain/archive/v0.2.6.tar.gz
18
+ sha256sums = d5d99a19be2b7414e7d7d1324c7893d2a227a60d15c14969d69102f9d2850294
19
19
 
20
20
  pkgname = refrain
@@ -1,7 +1,7 @@
1
1
  # Maintainer: Rockykln <contact@rockykln.com>
2
2
 
3
3
  pkgname=refrain
4
- pkgver=0.2.6
4
+ pkgver=0.2.7
5
5
  pkgrel=1
6
6
  pkgdesc="Discord Rich Presence for Apple Music on Linux"
7
7
  arch=('any')
@@ -23,7 +23,7 @@ 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
- sha256sums=('7ee7b991fab3ba2149fe72745b1432ef11abd7714ffba71cafd3bf1b6af5a6c4')
26
+ sha256sums=('d5d99a19be2b7414e7d7d1324c7893d2a227a60d15c14969d69102f9d2850294')
27
27
 
28
28
  build() {
29
29
  cd "$pkgname-$pkgver"
@@ -51,6 +51,24 @@
51
51
  <content_rating type="oars-1.1" />
52
52
 
53
53
  <releases>
54
+ <release version="0.2.7" date="2026-05-07">
55
+ <description>
56
+ <p>
57
+ Tray menu polish + Discord-RPC reliability. Every tray
58
+ action now carries an icon (theme-matched on Plasma /
59
+ GNOME / Breeze), the song-info rows render in normal text
60
+ colour with proper icons instead of greyed-out indented
61
+ rows, and middle-clicking the tray icon toggles
62
+ play/pause. Discord handshake errors (e.g. "User logged
63
+ out", "Invalid Client ID") are now caught explicitly
64
+ instead of crash-logged as unexpected exceptions, with a
65
+ longer back-off so Refrain doesn't retry tightly while
66
+ you're signing back in. The Settings → Updates
67
+ "Latest release notes" pane populates on every restart
68
+ regardless of the 24-hour update-check cooldown.
69
+ </p>
70
+ </description>
71
+ </release>
54
72
  <release version="0.2.6" date="2026-05-07">
55
73
  <description>
56
74
  <p>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "refrain"
7
- version = "0.2.6"
7
+ version = "0.2.7"
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.7"
@@ -390,25 +390,35 @@ class UpdateOrchestrator(QObject):
390
390
  self._thread: QThread | None = None
391
391
  self._worker: _UpdateCheckWorker | None = None
392
392
  self._manual = False
393
+ # When True, the in-flight check is purely for populating the
394
+ # in-app release-notes pane — suppress all popups and don't
395
+ # bump last_check_ts (which gates the auto-nag cooldown).
396
+ self._silent = False
393
397
 
394
398
  @property
395
399
  def latest(self) -> ReleaseInfo | None:
396
400
  return self._latest
397
401
 
398
402
  def maybe_check_on_startup(self) -> None:
403
+ """Always fire one fetch on startup so the Settings → Updates
404
+ pane has data immediately. The 24-hour cooldown only gates
405
+ whether this fetch can ALSO trigger the auto-popup for new
406
+ releases — within the cooldown the fetch runs silently and
407
+ only refreshes the inline release-notes pane."""
399
408
  if not self._config.update.auto_check:
400
409
  return
401
410
  elapsed = time.time() - self._config.update.last_check_ts
402
- if elapsed < self.UPDATE_CHECK_COOLDOWN_S:
403
- log.debug("Auto-update check skipped (cooldown, %.0fs since last)", elapsed)
404
- return
405
- self.check_now(manual=False)
411
+ silent = elapsed < self.UPDATE_CHECK_COOLDOWN_S
412
+ if silent:
413
+ log.debug("Auto-update check running silently (cooldown active)")
414
+ self.check_now(manual=False, silent=silent)
406
415
 
407
- def check_now(self, manual: bool = True) -> None:
416
+ def check_now(self, manual: bool = True, silent: bool = False) -> None:
408
417
  if self._thread is not None:
409
418
  log.debug("Update check already in progress")
410
419
  return
411
420
  self._manual = manual
421
+ self._silent = silent
412
422
  self._thread = QThread()
413
423
  self._thread.setObjectName("refrain-update-check")
414
424
  self._worker = _UpdateCheckWorker()
@@ -418,11 +428,21 @@ class UpdateOrchestrator(QObject):
418
428
  self._thread.start()
419
429
 
420
430
  def _on_check_finished(self, release: ReleaseInfo | None) -> None:
421
- self._config.update.last_check_ts = int(time.time())
422
- try:
423
- self._config.save()
424
- except Exception as e:
425
- log.debug("Could not persist last_check_ts: %s", e)
431
+ manual = self._manual
432
+ silent = self._silent
433
+ self._manual = False
434
+ self._silent = False
435
+
436
+ # Only bump the cooldown timestamp when this was a real auto-
437
+ # nag check. A silent pane-refresh shouldn't reset the cooldown
438
+ # — otherwise every startup would block the next day's auto-
439
+ # nag. Manual checks always count as "I just checked".
440
+ if not silent:
441
+ self._config.update.last_check_ts = int(time.time())
442
+ try:
443
+ self._config.save()
444
+ except Exception as e:
445
+ log.debug("Could not persist last_check_ts: %s", e)
426
446
 
427
447
  if self._thread is not None:
428
448
  self._thread.quit()
@@ -430,9 +450,6 @@ class UpdateOrchestrator(QObject):
430
450
  self._thread = None
431
451
  self._worker = None
432
452
 
433
- manual = self._manual
434
- self._manual = False
435
-
436
453
  # Always fan out the raw fetch result so subscribers (Settings
437
454
  # tab, etc.) can render whatever state they want regardless of
438
455
  # whether this constitutes "an update". Cache it too so a
@@ -441,6 +458,15 @@ class UpdateOrchestrator(QObject):
441
458
  self._latest = release
442
459
  self.releaseInfoFetched.emit(release)
443
460
 
461
+ # Silent fetches stop here — the pane is populated, no nags.
462
+ if silent:
463
+ log.info(
464
+ "Silent update check finished: latest=%s, current=%s",
465
+ release.version if release else "?",
466
+ __version__,
467
+ )
468
+ return
469
+
444
470
  if release is None:
445
471
  log.info("Update check: no release info returned")
446
472
  if manual:
@@ -608,19 +634,6 @@ def main() -> int:
608
634
  )
609
635
  updater.releaseInfoFetched.connect(settings.set_latest_release)
610
636
 
611
- # Auto-fetch on first visit to Settings → Updates so the inline
612
- # release-notes pane populates without the user needing to click
613
- # "Check for updates now". Skipped when we already have a cached
614
- # release this session — re-fetching every tab open is wasteful
615
- # and can hit GitHub's unauthenticated rate limit on multi-restart
616
- # debug sessions. The 24-hour startup cooldown stays in place for
617
- # actual auto-checks; this is opt-in via tab activation.
618
- def _maybe_fetch_for_tab() -> None:
619
- if updater.latest is None and updater._thread is None:
620
- updater.check_now(manual=True)
621
-
622
- settings.updatesTabRequested.connect(_maybe_fetch_for_tab)
623
-
624
637
  # Log-window wireup
625
638
  def _show_log() -> None:
626
639
  log_window.show()
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
2
+ <path d="M 2.5 4 L 4 2.5 L 8 6.5 L 12 2.5 L 13.5 4 L 9.5 8 L 13.5 12 L 12 13.5 L 8 9.5 L 4 13.5 L 2.5 12 L 6.5 8 Z" fill="#da4453"/>
3
+ </svg>
@@ -143,6 +143,12 @@ class DiscordRPC:
143
143
  p.connect()
144
144
  self._presence = p
145
145
  self._backoff_s = 2.0
146
+ # A fresh IPC pipe carries no state on Discord's side —
147
+ # so the dedup cache from a *previous* presence object
148
+ # would wrongly skip the first update on the new
149
+ # connection (Discord would then keep showing nothing
150
+ # until the daemon picks up a metadata change).
151
+ self._last_payload = None
146
152
  log.info("Discord RPC connected")
147
153
  return True
148
154
  except (
@@ -153,6 +159,17 @@ class DiscordRPC:
153
159
  OSError,
154
160
  ) as e:
155
161
  log.debug("Discord RPC connect failed: %s", e)
162
+ except ppx.DiscordError as e:
163
+ # Discord accepted the IPC pipe but sent back an error
164
+ # response to the handshake. Common reasons:
165
+ # - "User logged out": user signed out of Discord
166
+ # - "Invalid Client ID": client_id is malformed / not a
167
+ # Discord application (Settings → Discord input typo)
168
+ # These aren't bugs in Refrain — log at INFO and back off
169
+ # longer than the standard transient-error retry, since
170
+ # the user has to take action (sign back in, fix ID).
171
+ log.info("Discord RPC handshake rejected: %s", e)
172
+ self._backoff_s = self._max_backoff_s
156
173
  except Exception:
157
174
  log.exception("Discord RPC connect unexpected error")
158
175
  self._presence = None
@@ -420,24 +420,24 @@ Nach dem Bestätigen unten im Einstellungen-Fenster auf „Übernehmen“ klicke
420
420
  <message>
421
421
  <location filename="../ui/tray.py" line="81"/>
422
422
  <location filename="../ui/tray.py" line="174"/>
423
- <source>○ Discord: not connected</source>
424
- <translation>○ Discord: nicht verbunden</translation>
423
+ <source>Discord: not connected</source>
424
+ <translation>Discord: nicht verbunden</translation>
425
425
  </message>
426
426
  <message>
427
427
  <location filename="../ui/tray.py" line="84"/>
428
- <source>⏮ Previous</source>
429
- <translation>⏮ Zurück</translation>
428
+ <source>Previous</source>
429
+ <translation>Zurück</translation>
430
430
  </message>
431
431
  <message>
432
432
  <location filename="../ui/tray.py" line="86"/>
433
433
  <location filename="../ui/tray.py" line="159"/>
434
- <source>⏵ Play</source>
435
- <translation>⏵ Wiedergabe</translation>
434
+ <source>Play</source>
435
+ <translation>Wiedergabe</translation>
436
436
  </message>
437
437
  <message>
438
438
  <location filename="../ui/tray.py" line="88"/>
439
- <source>⏭ Next</source>
440
- <translation>⏭ Weiter</translation>
439
+ <source>Next</source>
440
+ <translation>Weiter</translation>
441
441
  </message>
442
442
  <message>
443
443
  <location filename="../ui/tray.py" line="102"/>
@@ -457,8 +457,8 @@ Nach dem Bestätigen unten im Einstellungen-Fenster auf „Übernehmen“ klicke
457
457
  </message>
458
458
  <message>
459
459
  <location filename="../ui/tray.py" line="111"/>
460
- <source>⟳ Restart Refrain</source>
461
- <translation>⟳ Refrain neu starten</translation>
460
+ <source>Restart Refrain</source>
461
+ <translation>Refrain neu starten</translation>
462
462
  </message>
463
463
  <message>
464
464
  <location filename="../ui/tray.py" line="113"/>
@@ -467,8 +467,8 @@ Nach dem Bestätigen unten im Einstellungen-Fenster auf „Übernehmen“ klicke
467
467
  </message>
468
468
  <message>
469
469
  <location filename="../ui/tray.py" line="157"/>
470
- <source>⏸ Pause</source>
471
- <translation>⏸ Pause</translation>
470
+ <source>Pause</source>
471
+ <translation>Pause</translation>
472
472
  </message>
473
473
  <message>
474
474
  <location filename="../ui/tray.py" line="164"/>
@@ -477,8 +477,8 @@ Nach dem Bestätigen unten im Einstellungen-Fenster auf „Übernehmen“ klicke
477
477
  </message>
478
478
  <message>
479
479
  <location filename="../ui/tray.py" line="172"/>
480
- <source>● Discord: connected</source>
481
- <translation>● Discord: verbunden</translation>
480
+ <source>Discord: connected</source>
481
+ <translation>Discord: verbunden</translation>
482
482
  </message>
483
483
  </context>
484
484
  <context>