refrain 0.2.0__tar.gz → 0.2.3__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 (132) hide show
  1. {refrain-0.2.0 → refrain-0.2.3}/.github/ISSUE_TEMPLATE/bug_report.yml +1 -1
  2. {refrain-0.2.0 → refrain-0.2.3}/.github/workflows/release.yml +6 -2
  3. {refrain-0.2.0 → refrain-0.2.3}/.github/workflows/tests.yml +7 -2
  4. {refrain-0.2.0 → refrain-0.2.3}/.gitignore +2 -1
  5. {refrain-0.2.0 → refrain-0.2.3}/CHANGELOG.md +327 -1
  6. {refrain-0.2.0 → refrain-0.2.3}/CONTRIBUTING.md +21 -18
  7. {refrain-0.2.0 → refrain-0.2.3}/PKG-INFO +25 -6
  8. {refrain-0.2.0 → refrain-0.2.3}/README.md +24 -5
  9. {refrain-0.2.0 → refrain-0.2.3}/ROADMAP.md +116 -5
  10. refrain-0.2.3/docs/screenshots/settings-advanced.png +0 -0
  11. refrain-0.2.3/docs/screenshots/settings-general.png +0 -0
  12. refrain-0.2.3/docs/screenshots/settings-sources.png +0 -0
  13. refrain-0.2.3/docs/screenshots/settings-updates.png +0 -0
  14. refrain-0.2.3/docs/screenshots/welcome.png +0 -0
  15. refrain-0.2.3/docs/test-matrix.md +169 -0
  16. {refrain-0.2.0 → refrain-0.2.3}/packaging/appimage/AppImageBuilder.yml +1 -1
  17. refrain-0.2.3/packaging/aur/refrain/.SRCINFO +19 -0
  18. {refrain-0.2.0 → refrain-0.2.3}/packaging/aur/refrain/PKGBUILD +8 -2
  19. {refrain-0.2.0 → refrain-0.2.3}/packaging/flatpak/io.github.Rockykln.Refrain.metainfo.xml +56 -0
  20. {refrain-0.2.0 → refrain-0.2.3}/pyproject.toml +4 -5
  21. refrain-0.2.3/src/refrain/__init__.py +1 -0
  22. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/app.py +278 -33
  23. refrain-0.2.3/src/refrain/assets/icons/refrain.png +0 -0
  24. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/autostart.py +25 -15
  25. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/config.py +51 -6
  26. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/cover_art.py +42 -4
  27. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/cover_fetcher.py +21 -4
  28. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/daemon.py +293 -118
  29. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/discord_rpc.py +72 -3
  30. refrain-0.2.3/src/refrain/i18n/refrain_de.qm +0 -0
  31. refrain-0.2.3/src/refrain/i18n/refrain_de.ts +483 -0
  32. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/logging_setup.py +27 -11
  33. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/sources/bluetooth.py +36 -7
  34. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/sources/mpris.py +159 -54
  35. refrain-0.2.3/src/refrain/sources/mpris_server.py +368 -0
  36. refrain-0.2.3/src/refrain/timing.py +89 -0
  37. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/ui/log_window.py +6 -1
  38. refrain-0.2.3/src/refrain/ui/settings_window.py +700 -0
  39. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/ui/tray.py +40 -30
  40. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/ui/update_dialog.py +54 -4
  41. refrain-0.2.3/src/refrain/ui/welcome_dialog.py +327 -0
  42. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/updater.py +103 -11
  43. {refrain-0.2.0 → refrain-0.2.3}/tests/test_autostart.py +28 -0
  44. {refrain-0.2.0 → refrain-0.2.3}/tests/test_config.py +10 -4
  45. {refrain-0.2.0 → refrain-0.2.3}/tests/test_cover_art.py +6 -3
  46. {refrain-0.2.0 → refrain-0.2.3}/tests/test_daemon_album_format.py +22 -5
  47. {refrain-0.2.0 → refrain-0.2.3}/tests/test_daemon_idle.py +26 -3
  48. {refrain-0.2.0 → refrain-0.2.3}/tests/test_mpris_dispatch.py +5 -5
  49. {refrain-0.2.0 → refrain-0.2.3}/tests/test_timing.py +47 -1
  50. refrain-0.2.3/tests/test_tray_init.py +86 -0
  51. {refrain-0.2.0 → refrain-0.2.3}/tests/test_update_dialog_body.py +15 -9
  52. refrain-0.2.0/docs/screenshots/settings-advanced.png +0 -0
  53. refrain-0.2.0/docs/screenshots/settings-general.png +0 -0
  54. refrain-0.2.0/docs/screenshots/settings-sources.png +0 -0
  55. refrain-0.2.0/docs/screenshots/settings-updates.png +0 -0
  56. refrain-0.2.0/src/refrain/__init__.py +0 -1
  57. refrain-0.2.0/src/refrain/i18n/refrain_de.qm +0 -0
  58. refrain-0.2.0/src/refrain/i18n/refrain_de.ts +0 -342
  59. refrain-0.2.0/src/refrain/i18n/refrain_en.qm +0 -0
  60. refrain-0.2.0/src/refrain/i18n/refrain_en.ts +0 -340
  61. refrain-0.2.0/src/refrain/i18n/refrain_es.qm +0 -0
  62. refrain-0.2.0/src/refrain/i18n/refrain_es.ts +0 -340
  63. refrain-0.2.0/src/refrain/i18n/refrain_fr.qm +0 -0
  64. refrain-0.2.0/src/refrain/i18n/refrain_fr.ts +0 -340
  65. refrain-0.2.0/src/refrain/i18n/refrain_it.qm +0 -0
  66. refrain-0.2.0/src/refrain/i18n/refrain_it.ts +0 -340
  67. refrain-0.2.0/src/refrain/i18n/refrain_ja.qm +0 -0
  68. refrain-0.2.0/src/refrain/i18n/refrain_ja.ts +0 -340
  69. refrain-0.2.0/src/refrain/i18n/refrain_nl.qm +0 -0
  70. refrain-0.2.0/src/refrain/i18n/refrain_nl.ts +0 -340
  71. refrain-0.2.0/src/refrain/i18n/refrain_pl.qm +0 -0
  72. refrain-0.2.0/src/refrain/i18n/refrain_pl.ts +0 -340
  73. refrain-0.2.0/src/refrain/i18n/refrain_pt.qm +0 -0
  74. refrain-0.2.0/src/refrain/i18n/refrain_pt.ts +0 -340
  75. refrain-0.2.0/src/refrain/i18n/refrain_zh_CN.qm +0 -0
  76. refrain-0.2.0/src/refrain/i18n/refrain_zh_CN.ts +0 -340
  77. refrain-0.2.0/src/refrain/sources/dbus_watcher.py +0 -136
  78. refrain-0.2.0/src/refrain/timing.py +0 -42
  79. refrain-0.2.0/src/refrain/ui/settings_window.py +0 -342
  80. refrain-0.2.0/src/refrain/ui/welcome_dialog.py +0 -221
  81. {refrain-0.2.0 → refrain-0.2.3}/.editorconfig +0 -0
  82. {refrain-0.2.0 → refrain-0.2.3}/.gitattributes +0 -0
  83. {refrain-0.2.0 → refrain-0.2.3}/.github/FUNDING.yml +0 -0
  84. {refrain-0.2.0 → refrain-0.2.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  85. {refrain-0.2.0 → refrain-0.2.3}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  86. {refrain-0.2.0 → refrain-0.2.3}/.github/codeql/codeql-config.yml +0 -0
  87. {refrain-0.2.0 → refrain-0.2.3}/.github/dependabot.yml +0 -0
  88. {refrain-0.2.0 → refrain-0.2.3}/.github/pull_request_template.md +0 -0
  89. {refrain-0.2.0 → refrain-0.2.3}/.github/workflows/codeql.yml +0 -0
  90. {refrain-0.2.0 → refrain-0.2.3}/.github/workflows/security.yml +0 -0
  91. {refrain-0.2.0 → refrain-0.2.3}/CODE_OF_CONDUCT.md +0 -0
  92. {refrain-0.2.0 → refrain-0.2.3}/LICENSE +0 -0
  93. {refrain-0.2.0 → refrain-0.2.3}/Makefile +0 -0
  94. {refrain-0.2.0 → refrain-0.2.3}/SECURITY.md +0 -0
  95. {refrain-0.2.0 → refrain-0.2.3}/docs/architecture.md +0 -0
  96. {refrain-0.2.0 → refrain-0.2.3}/docs/faq.md +0 -0
  97. {refrain-0.2.0 → refrain-0.2.3}/docs/screenshots/README.md +0 -0
  98. {refrain-0.2.0 → refrain-0.2.3}/docs/screenshots/discord-rpc.png +0 -0
  99. {refrain-0.2.0 → refrain-0.2.3}/docs/screenshots/live-log.png +0 -0
  100. {refrain-0.2.0 → refrain-0.2.3}/docs/screenshots/notification.png +0 -0
  101. {refrain-0.2.0 → refrain-0.2.3}/docs/screenshots/tray-menu.png +0 -0
  102. {refrain-0.2.0 → refrain-0.2.3}/docs/screenshots/update-dialog.png +0 -0
  103. {refrain-0.2.0 → refrain-0.2.3}/packaging/README.md +0 -0
  104. {refrain-0.2.0 → refrain-0.2.3}/packaging/aur/refrain-git/PKGBUILD +0 -0
  105. {refrain-0.2.0 → refrain-0.2.3}/packaging/flatpak/io.github.Rockykln.Refrain.desktop +0 -0
  106. {refrain-0.2.0 → refrain-0.2.3}/packaging/flatpak/io.github.Rockykln.Refrain.yaml +0 -0
  107. {refrain-0.2.0 → refrain-0.2.3}/packaging/flatpak/python-deps.json +0 -0
  108. {refrain-0.2.0 → refrain-0.2.3}/requirements-dev.txt +0 -0
  109. {refrain-0.2.0 → refrain-0.2.3}/requirements.txt +0 -0
  110. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/__main__.py +0 -0
  111. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/assets/icons/github-mark.svg +0 -0
  112. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/assets/icons/refrain.svg +0 -0
  113. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/assets/icons/tray-paused-dark.svg +0 -0
  114. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/assets/icons/tray-paused.svg +0 -0
  115. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/assets/icons/tray-playing-dark.svg +0 -0
  116. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/assets/icons/tray-playing.svg +0 -0
  117. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/assets/icons/tray-stopped-dark.svg +0 -0
  118. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/assets/icons/tray-stopped.svg +0 -0
  119. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/assets/refrain.desktop +0 -0
  120. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/paths.py +0 -0
  121. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/single_instance.py +0 -0
  122. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/sources/__init__.py +0 -0
  123. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/sources/base.py +0 -0
  124. {refrain-0.2.0 → refrain-0.2.3}/src/refrain/ui/__init__.py +0 -0
  125. {refrain-0.2.0 → refrain-0.2.3}/tests/__init__.py +0 -0
  126. {refrain-0.2.0 → refrain-0.2.3}/tests/conftest.py +0 -0
  127. {refrain-0.2.0 → refrain-0.2.3}/tests/test_cover_fetcher.py +0 -0
  128. {refrain-0.2.0 → refrain-0.2.3}/tests/test_discord_listening.py +0 -0
  129. {refrain-0.2.0 → refrain-0.2.3}/tests/test_imports.py +0 -0
  130. {refrain-0.2.0 → refrain-0.2.3}/tests/test_paths.py +0 -0
  131. {refrain-0.2.0 → refrain-0.2.3}/tests/test_sources_base.py +0 -0
  132. {refrain-0.2.0 → refrain-0.2.3}/tests/test_updater.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
 
@@ -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,13 +32,14 @@ env/
32
32
  .DS_Store
33
33
 
34
34
  # AI assistants
35
- .claude/
35
+ .claude*/
36
36
  .cursor/
37
37
  .aider*
38
38
 
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,325 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.3] - 2026-05-07
11
+
12
+ A reliability + portability pass driven by hands-on testing across
13
+ Ubuntu 25.04, CentOS Stream 10, and the existing CachyOS daily driver.
14
+ No breaking changes — same config, same UI, same Python floor; the
15
+ release just makes Refrain behave correctly on more distros and tightens
16
+ a handful of long-standing rough edges.
17
+
18
+ ### Added
19
+
20
+ - **Sandboxed Discord IPC bridge** — Snap and Flatpak Discord builds
21
+ publish their `discord-ipc-N` socket inside the sandbox tree instead
22
+ of `$XDG_RUNTIME_DIR`, so pypresence's stock discovery never finds it.
23
+ `_bridge_sandboxed_ipc_socket` probes the three known sandbox paths
24
+ (Flatpak instance dir, Flatpak config-dir layout, Snap path) and
25
+ symlinks the first hit into `$XDG_RUNTIME_DIR` before each connect
26
+ attempt. The welcome-dialog probe walks the same paths so first-run
27
+ diagnostics turn green on those installs.
28
+ - **Cancel button in the update dialog** — `_apply_appimage` now
29
+ reads in 64 KiB chunks and polls a cancellation callback between
30
+ blocks. The dialog repurposes its "Later" button as "Cancel" while a
31
+ download is running, treats window-close as cancel, and removes the
32
+ partial `.AppImage.new` on abort. `UpdateResult.cancelled` lets the
33
+ dialog suppress the failure popup for user-initiated aborts.
34
+ - **Orphan-download self-heal** — `cleanup_orphan_downloads` runs once
35
+ at startup so a `*.AppImage.new` left behind by a SIGKILL or power
36
+ loss mid-download doesn't sit next to the binary forever.
37
+ - **Browser hints expanded** — Floorp, Waterfox, Mullvad Browser, Tor
38
+ Browser, and ungoogled-chromium are now in `DEFAULT_BROWSER_HINTS`
39
+ and surface as Settings checkboxes. Existing configs keep their
40
+ saved list (no auto-migration); the new entries appear unticked
41
+ until the user toggles them.
42
+ - **iTunes-catalog duration as MPRIS override** — when MPRIS reports
43
+ an obviously-wrong `mpris:length` (Apple Music's preview-clip 14 s,
44
+ the playlist-total instead of the track length, or a stale value
45
+ carried over from the previous track), `pick_effective_duration_ms`
46
+ prefers the iTunes value if the two disagree by more than 15 % or
47
+ if MPRIS sits in the preview-clip band on a song iTunes considers
48
+ full-length. 8 new unit tests in `test_timing.py`. Discord no
49
+ longer briefly flips a 2:11 song's total to 0:14 / 7:21 mid-track.
50
+ - **`docs/test-matrix.md`** — eight Tier-1 distros that together
51
+ cover ≈ 95 % of the realistic user base, plus Tier-2 spot-checks,
52
+ Tier-3 desktop / compositor combinations, and a six-step smoke
53
+ check ("tray + theme parity", "settings round-trip", "MPRIS to
54
+ Discord", "Bluetooth", "update-dialog cleanup", "restart cycle")
55
+ per row.
56
+
57
+ ### Changed
58
+
59
+ - **Qt style plugin path augmentation also walks Debian / Ubuntu and
60
+ Fedora / RHEL / openSUSE layouts** — the v0.2.2 fix only checked
61
+ `/usr/lib/qt6/plugins` (Arch). pip / pipx installs on those distros
62
+ now also pick up the system Breeze plugin instead of falling back
63
+ to Fusion. Detection picks the first existing path among
64
+ `/usr/lib/<arch-triple>/qt6/plugins`, `/usr/lib64/qt6/plugins`,
65
+ `/usr/lib/qt6/plugins`.
66
+ - **"No system tray" error** rewritten to cover GNOME, MATE, XFCE,
67
+ Hyprland / Sway / i3 / river, plus a "should work, try logging
68
+ out" hint for Plasma / Cinnamon / LXQt / Budgie. Previously the
69
+ fix advice was GNOME-AppIndicator-only, leaving everyone else to
70
+ guess.
71
+ - **`refrain --install-desktop`** rewrites the installed
72
+ `Exec=` line to point at the actual launcher path of the running
73
+ install. Previously the bundled `Exec=refrain` only worked when
74
+ `refrain` was on `$PATH` (distro packages, pipx); AppImage and
75
+ source-checkout users got a broken menu entry. Resolution order
76
+ matches the autostart logic — extracted into the new
77
+ `autostart.resolve_exec_line(extra_args)` helper.
78
+ - **MPRIS source duration handling decouples drift-resync from the
79
+ payload's preview-clip flag** — the drift-skip still keys off the
80
+ raw MPRIS-reported duration (because the position-field looping is
81
+ a property of MPRIS' preview-clip mode), but the Discord activity
82
+ payload's start/end fields key off the *effective* duration so a
83
+ brief MPRIS preview-clip glitch on a full-length song doesn't kill
84
+ the progress bar.
85
+ - **PyGObject "not installed" warning fires once per startup**
86
+ instead of twice (eager init from `app.py` plus lazy fallback in
87
+ `MPRISServer.start` both used to log). The install hint also lists
88
+ the Fedora / RHEL / openSUSE package name (`python3-gobject`)
89
+ alongside the Arch and Debian ones.
90
+
91
+ ### Fixed
92
+
93
+ - **Settings / Log / Update windows now carry the Refrain icon
94
+ explicitly** — on GNOME Wayland the title-based heuristic was
95
+ matching "Settings" against `org.gnome.Settings` and rendering the
96
+ gnome-control-center gear icon in the dock. The WelcomeDialog
97
+ already set its own icon; the other three top-level windows did
98
+ not.
99
+ - **Bluetooth fast-fail when `org.bluez` is not present** — every
100
+ poll cycle on a VM / minimal install previously triggered a
101
+ `service_start_timeout=25000ms` D-Bus activation timeout, blocking
102
+ the worker thread for the full 25 s. `NameHasOwner` returns
103
+ instantly without triggering activation; the three entry points
104
+ (`read`, `_call_method`, `list_paired_devices`) now short-circuit
105
+ when the owner check returns False.
106
+ - **Logging gaps closed in six places** — `setup_logging` runs
107
+ before `Config.load` so the "created default config" /
108
+ "config unreadable" messages reach the file log; toggling the log
109
+ level in Settings now applies to the running root logger via
110
+ `_apply_log_level` on `settings.applied`; the "no system tray"
111
+ early-exit gets a `log.error()`; `os.execvp`-based restarts call
112
+ `logging.shutdown()` first so the file handler's last buffered
113
+ line isn't lost; `Config.load` exception path adds `exc_info=True`;
114
+ `setup_logging` degrades gracefully when the XDG state dir or log
115
+ file can't be opened (console handler always attaches first).
116
+ - **Catch-all exception branches in `DiscordRPC._ensure_connected`,
117
+ `MPRISSource._call_method_on`, and `BluetoothSource._call_method`**
118
+ now use `log.exception()` so the traceback lands in `refrain.log`
119
+ for unexpected errors. Previously they only logged `str(exc)`,
120
+ losing the stack exactly when it would matter most.
121
+ - **Qt-internal log records routed through `refrain.qt`** instead
122
+ of the bare `qt` logger, so they parent under the project
123
+ namespace and show up alongside everything else when the user
124
+ filters by name.
125
+
126
+ ## [0.2.2] - 2026-05-06
127
+
128
+ Two roadmap features pulled forward from v0.3 plus a substantial batch
129
+ of reliability + UX work driven by live-testing the v0.2.1 build.
130
+
131
+ ### Added
132
+
133
+ - **Multiple Discord profiles** — per-source `client_id_mpris` and
134
+ `client_id_bluetooth` fields in `Config.discord` plus matching UI in
135
+ Settings → General → Discord. Empty falls back to the default
136
+ Client ID. The daemon reconnects RPC under the source-specific ID
137
+ the moment the active source flips, so each source can render with
138
+ its own application name + uploaded artwork in the user's profile.
139
+ - **MPRIS-server mode** — Refrain registers itself as
140
+ `org.mpris.MediaPlayer2.refrain` on the session bus so KDE Plasma's
141
+ panel media-controls applet (and KDE Connect, GNOME Shell, Mako
142
+ notifications, …) drive the same Play/Pause/Next/Previous as the
143
+ tray, and render the same track Discord renders. Implemented with
144
+ `dbus-python` + a daemon GLib main loop running in its own thread
145
+ so it doesn't compete with Qt's event loop. Falls back gracefully
146
+ when PyGObject isn't installed.
147
+ - **iTunes track duration in the cover-fetcher cache** —
148
+ `trackTimeMillis` from the iTunes Search API rides along with
149
+ `cover_url` / `song_url` in the on-disk cache and exposed via
150
+ `CoverFetcher.get_duration_ms()`. Used as a defensive fallback when
151
+ MPRIS reports an obviously-wrong preview-clip length.
152
+ - **Per-player MPRIS timeout blacklist** — if a property `Get` times
153
+ out we banish that bus name for 5 s so a hung player (typically
154
+ plasma-browser-integration on a frozen Apple Music tab) can't keep
155
+ eating the daemon's poll cycle one tick at a time.
156
+
157
+ ### Changed
158
+
159
+ - **Welcome wizard** redesigned: 56×56 icon-badge + title + subtitle
160
+ header, intro line about tray-driven controls, dedicated diagnostics
161
+ card with two clearly-labelled rows (Discord / iTunes), Discord
162
+ Application ID block with helper text + readable italic styling
163
+ (the previous `palette(mid)` tone went invisible on Plasma Breeze
164
+ dark themes), Skip ghost-style on the left + Apply primary on the
165
+ right.
166
+ - **Welcome wizard now opens Settings on confirmation** instead of
167
+ silently dropping into the tray, and reloads the Settings form so
168
+ the just-saved Discord Client ID appears in the input field.
169
+ - **`compute_rpc_start_ts` accepts `is_preview_clip=True`** to skip
170
+ the drift-resync path on tracks whose MPRIS Position field loops
171
+ 0→8s instead of advancing monotonically. Discord's elapsed counter
172
+ no longer resets every ~8 s on preview-mode playback.
173
+ - **Reset-all-settings preserves every Discord Client ID** (default +
174
+ per-source mpris + per-source bluetooth) and the dialog text now
175
+ states this explicitly. Reset-dialog button labels are localised
176
+ (`Reset` / `Zurücksetzen` / `Cancel` / `Abbrechen`) instead of the
177
+ generic Yes / No.
178
+ - **Default `notify_delay_ms` lowered from 600 ms to 0 ms** — cover
179
+ cache hits fire instantly, cache misses still get the existing
180
+ retry loop up to 2 s for the iTunes download.
181
+ - **Default `poll_interval_ms` lowered from 1 s to 500 ms** — track
182
+ changes, position updates, and tray controls feel noticeably more
183
+ responsive without measurable CPU impact.
184
+ - **Idle detection skips preview-clip-length tracks** (duration <
185
+ 30 s) — Apple Music keeps reporting the same metadata while a
186
+ preview replays, and clearing Discord under those conditions hid
187
+ the activity mid-listen.
188
+ - **Idle-detection log fires once per dangling-track instance**
189
+ instead of every poll tick.
190
+
191
+ ### Fixed
192
+
193
+ - **Discord RPC connect now happens within ~1 s of the first track
194
+ detection** (was: up to 51 s). Root cause: dbus-python's default
195
+ 25 s reply timeout combined with plasma-browser-integration
196
+ introspection hangs blocked the daemon's poll. Fix: pass
197
+ `introspect=False` on every `bus.get_object` and `timeout=0.5` on
198
+ every property `Get` and method dispatch.
199
+ - **Discord RPC reconnects eagerly when the user changes their
200
+ Application ID** (welcome wizard or Settings → General). Previously
201
+ the new pipe wasn't established until the next *playing-state*
202
+ poll, so paused users sat there waiting forever.
203
+ - **Skip / Previous reach Apple Music reliably**: dispatch tries
204
+ the browser-native MPRIS player (chromium / firefox) first, then
205
+ falls back to plasma-browser-integration. Plasma's wrapper claims
206
+ `CanGoNext=True` on Apple Music but its Next routes through a
207
+ flaky mediaSession path; the browser-native player reaches the
208
+ same handler via a more reliable path.
209
+ - **Per-property `_safe_get` inside MPRIS read** — chromium MPRIS
210
+ rejecting one optional property (notably `DesktopEntry`, with a
211
+ generic `Error.Failed`) no longer drops the whole player from our
212
+ candidate list, which previously hid the chromium player entirely
213
+ from the skip-fallback chain.
214
+ - **All apple-music candidates** beyond the metadata winner join the
215
+ control-fallback list, so the chromium player is reachable for
216
+ skip dispatches even when plasma wins the metadata pick.
217
+ - **Notification fires immediately on cover-cache hits** (50 ms vs.
218
+ the previous unconditional 600-1500 ms `notify_delay_ms` wait).
219
+ - **Discord progress bar dropped entirely on preview-clip mode**
220
+ (MPRIS duration < 30 s) — no `start`, no `end` in the activity
221
+ payload — so the elapsed timer doesn't loop nonsensically with
222
+ the 8-second preview.
223
+ - **`update_config` invocation from the welcome wizard** now goes
224
+ through `settings.applied.emit(config)` (already QueuedConnection-
225
+ wired to the worker thread) instead of `QMetaObject.invokeMethod
226
+ (… Q_ARG(object, …))`, which PySide6 rejects with "Unable to find
227
+ a QMetaType for 'object'" — that exception was silently breaking
228
+ the post-wizard handshake on first-run.
229
+ - **Welcome wizard X / Esc** marks `first_run_complete=True` (via
230
+ `reject()` override) so the wizard doesn't re-appear on the next
231
+ launch when the user dismisses without confirming.
232
+ - **Welcome wizard Apply with empty ID** asks for confirmation
233
+ before silently saving "no Discord".
234
+ - **Refrain's own published MPRIS bus name skipped** during the
235
+ source read loop — reading our own publish path back was
236
+ circular and added noticeable latency.
237
+ - **`_control` exception handling** — a TypeError in
238
+ `getattr(src, action)()` no longer kills the entire dispatch
239
+ before the follow-up polls fire.
240
+ - **Reset preserves per-source Discord IDs** (`client_id_mpris` /
241
+ `client_id_bluetooth`), not just the default `client_id`.
242
+ - **dbus.proxies / dbus.connection log spam silenced** — their
243
+ Introspect-error tracebacks against unrelated MPRIS players
244
+ drowned the live log in noise that has nothing to do with
245
+ refrain.
246
+
247
+ ### Removed
248
+
249
+ - **Dead `_diag_label` shim** in `WelcomeDialog` that was only there
250
+ for a backwards-compat path no longer in use.
251
+
252
+ ## [0.2.1] - 2026-05-06
253
+
254
+ Polish release built around the v0.2.0 surface. The Settings window got
255
+ a UI overhaul, two notification cover-art races are gone, and the
256
+ external code-review punch list landed in full (atomic config writes,
257
+ AppImage size validation, `.ts` source files no longer ship in the
258
+ wheel, dead D-Bus watcher module deleted).
259
+
260
+ ### Added
261
+
262
+ - **Auto-restart on Discord client_id change** — Apply now triggers an
263
+ in-process re-exec when the client_id field has changed, same
264
+ pattern as the language switch. pypresence binds to the client_id
265
+ at connect time, so a fresh process is the simplest way to re-init
266
+ cleanly.
267
+ - **AppImage update size validation** — `_apply_appimage` now compares
268
+ the downloaded file against the GitHub Releases manifest's
269
+ `appimage_size` before replacing the live binary. A truncated
270
+ download is rejected and the tmp file cleaned up instead of
271
+ bricking the next launch.
272
+ - **MPRIS dispatch logging** — `Next` / `Previous` / `PlayPause` log
273
+ which player actually received the call (INFO level). Diagnoses
274
+ "Next pauses instead of skipping" type bugs where a fallback
275
+ player handles the action wrong.
276
+ - **RPC track-change diagnostics** — log raw `pos`, `dur`, `start_ts`
277
+ on every recompute so duration mismatches (browser MPRIS reporting
278
+ preview-clip lengths, etc.) are visible in the live log.
279
+
280
+ ### Changed
281
+
282
+ - **Settings window UI overhaul** — every tab uses a consistent
283
+ `QGroupBox` + form-layout with left-aligned titles via stylesheet
284
+ (overrides Plasma Breeze's centered default). Inputs are
285
+ fixed-width: 220 px default, 360 px for the Discord group whose
286
+ Client ID placeholder + privacy-mode labels need more room.
287
+ `FieldsStayAtSizeHint` + per-widget `setFixedWidth` is the only
288
+ combo that holds across both Fusion (offscreen tests) and Breeze
289
+ (Plasma) — `AllNonFixedFieldsGrow` was ignoring fixed-width caps.
290
+ - **Discord RPC reconnect cap lowered to 15 s** — autostart launches
291
+ refrain before Discord is ready; the previous 60 s ceiling meant
292
+ the user could sit there for almost a minute after Discord
293
+ finished loading before refrain noticed and connected.
294
+ - **Atomic config writes** — `Config.save` now writes to a `.tmp`
295
+ sibling and `os.replace`s into place. A crash mid-write would
296
+ otherwise leave an empty / half-written TOML and refrain
297
+ silently falls back to defaults on parse error, losing every
298
+ setting the user picked.
299
+ - **Discord client_id Apply re-init** — wired `restartRequested`
300
+ emission so picking up a new client_id no longer requires a
301
+ manual restart.
302
+
303
+ ### Fixed
304
+
305
+ - **Notification cover flicker** — `notify-send -i` now carries the
306
+ same image as the `--hint string:image-path:...` payload. KDE
307
+ Plasma briefly rendered the `-i refrain` brand badge (~50–100 ms)
308
+ while it loaded the image-path file from disk; using the same
309
+ file for both makes the transition invisible.
310
+ - **Discord activity-card cover flicker** — defer the first RPC
311
+ update for a new track until the iTunes-search cover URL is in
312
+ cache, capped at 3 polls (~3 s). Without this, Discord briefly
313
+ rendered the `refrain` brand fallback before the real cover
314
+ paint, exactly the issue cover-fetcher was supposed to fix.
315
+ - **Group titles centered on Plasma Breeze** — explicit stylesheet
316
+ overrides Breeze's default centered `QGroupBox::title`.
317
+
318
+ ### Removed
319
+
320
+ - **Dead `dbus_watcher` module** — 155 lines of `MPRISWatcher` /
321
+ `BluetoothWatcher` machinery that was never wired up after the
322
+ signal-driven path was abandoned for v0.2.x. The companion
323
+ `_refresh_watchers` and `_on_external_change` slots in
324
+ `Daemon` went with it.
325
+ - **`.ts` translation sources from the wheel** — only the compiled
326
+ `.qm` files ship now. Linguist sources stay in the source tree
327
+ for translator PRs.
328
+
10
329
  ## [0.2.0] - 2026-05-06
11
330
 
12
331
  First minor-version bump. Five planned v0.2 features land together
@@ -365,7 +684,14 @@ with a proper, installable Linux app.
365
684
  pip-audit, trufflehog, release), Dependabot, issue + PR templates,
366
685
  `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`.
367
686
 
368
- [Unreleased]: https://github.com/Rockykln/refrain/compare/v0.1.2...HEAD
687
+ [Unreleased]: https://github.com/Rockykln/refrain/compare/v0.2.3...HEAD
688
+ [0.2.3]: https://github.com/Rockykln/refrain/compare/v0.2.2...v0.2.3
689
+ [0.2.2]: https://github.com/Rockykln/refrain/compare/v0.2.1...v0.2.2
690
+ [0.2.1]: https://github.com/Rockykln/refrain/compare/v0.2.0...v0.2.1
691
+ [0.2.0]: https://github.com/Rockykln/refrain/compare/v0.1.5...v0.2.0
692
+ [0.1.5]: https://github.com/Rockykln/refrain/compare/v0.1.4...v0.1.5
693
+ [0.1.4]: https://github.com/Rockykln/refrain/compare/v0.1.3...v0.1.4
694
+ [0.1.3]: https://github.com/Rockykln/refrain/compare/v0.1.2...v0.1.3
369
695
  [0.1.2]: https://github.com/Rockykln/refrain/compare/v0.1.1...v0.1.2
370
696
  [0.1.1]: https://github.com/Rockykln/refrain/compare/v0.1.0...v0.1.1
371
697
  [0.1.0]: https://github.com/Rockykln/refrain/releases/tag/v0.1.0
@@ -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.3
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
 
@@ -162,7 +163,9 @@ same file; you almost never need to touch it by hand.
162
163
 
163
164
  ```toml
164
165
  [discord]
165
- client_id = "" # paste your own Application ID here
166
+ client_id = "" # default Application ID — paste yours here
167
+ client_id_mpris = "" # optional per-source override (browser / Apple Music)
168
+ client_id_bluetooth = "" # optional per-source override (Bluetooth headphones)
166
169
 
167
170
  [sources]
168
171
  mpris_enabled = true
@@ -177,12 +180,28 @@ autostart = false
177
180
  notifications = true
178
181
  cover_art = true
179
182
  show_buttons = true
183
+ notify_delay_ms = 0 # 0 = fire ASAP; the cover-art retry loop still waits up to ~2 s
180
184
 
181
185
  [advanced]
182
- poll_interval_ms = 1000
186
+ poll_interval_ms = 500
183
187
  log_level = "INFO"
188
+ cover_cache_size = 200 # disk cap for cached covers
189
+ 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
184
191
  ```
185
192
 
193
+ Per-source `client_id_*` fields let Apple Music render under one Discord
194
+ application (with the album-grid as artwork) and Bluetooth headphones under
195
+ another (with a generic Bluetooth glyph). Empty falls back to the default
196
+ `client_id`.
197
+
198
+ ## MPRIS server
199
+
200
+ Refrain publishes itself as `org.mpris.MediaPlayer2.refrain` on the session
201
+ bus, so KDE Plasma's panel media-controls applet (and KDE Connect, GNOME
202
+ Shell, Mako, …) drive the same Play/Pause/Next/Previous as the tray and
203
+ render the same track Discord renders.
204
+
186
205
  ## Tray
187
206
 
188
207
  <p align="center">
@@ -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
 
@@ -128,7 +129,9 @@ same file; you almost never need to touch it by hand.
128
129
 
129
130
  ```toml
130
131
  [discord]
131
- client_id = "" # paste your own Application ID here
132
+ client_id = "" # default Application ID — paste yours here
133
+ client_id_mpris = "" # optional per-source override (browser / Apple Music)
134
+ client_id_bluetooth = "" # optional per-source override (Bluetooth headphones)
132
135
 
133
136
  [sources]
134
137
  mpris_enabled = true
@@ -143,12 +146,28 @@ autostart = false
143
146
  notifications = true
144
147
  cover_art = true
145
148
  show_buttons = true
149
+ notify_delay_ms = 0 # 0 = fire ASAP; the cover-art retry loop still waits up to ~2 s
146
150
 
147
151
  [advanced]
148
- poll_interval_ms = 1000
152
+ poll_interval_ms = 500
149
153
  log_level = "INFO"
154
+ cover_cache_size = 200 # disk cap for cached covers
155
+ 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
150
157
  ```
151
158
 
159
+ Per-source `client_id_*` fields let Apple Music render under one Discord
160
+ application (with the album-grid as artwork) and Bluetooth headphones under
161
+ another (with a generic Bluetooth glyph). Empty falls back to the default
162
+ `client_id`.
163
+
164
+ ## MPRIS server
165
+
166
+ Refrain publishes itself as `org.mpris.MediaPlayer2.refrain` on the session
167
+ bus, so KDE Plasma's panel media-controls applet (and KDE Connect, GNOME
168
+ Shell, Mako, …) drive the same Play/Pause/Next/Previous as the tray and
169
+ render the same track Discord renders.
170
+
152
171
  ## Tray
153
172
 
154
173
  <p align="center">