refrain 0.4.6__tar.gz → 0.5.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 (200) hide show
  1. {refrain-0.4.6 → refrain-0.5.2}/.github/ISSUE_TEMPLATE/bug_report.yml +1 -1
  2. {refrain-0.4.6 → refrain-0.5.2}/.github/workflows/security.yml +1 -1
  3. {refrain-0.4.6 → refrain-0.5.2}/.gitignore +0 -5
  4. {refrain-0.4.6 → refrain-0.5.2}/CHANGELOG.md +196 -1
  5. {refrain-0.4.6 → refrain-0.5.2}/CONTRIBUTING.md +13 -2
  6. {refrain-0.4.6 → refrain-0.5.2}/Makefile +8 -1
  7. {refrain-0.4.6 → refrain-0.5.2}/PKG-INFO +65 -6
  8. {refrain-0.4.6 → refrain-0.5.2}/PRIVACY.md +5 -1
  9. {refrain-0.4.6 → refrain-0.5.2}/README.md +64 -5
  10. {refrain-0.4.6 → refrain-0.5.2}/ROADMAP.md +33 -4
  11. {refrain-0.4.6 → refrain-0.5.2}/docs/architecture.md +149 -25
  12. {refrain-0.4.6 → refrain-0.5.2}/docs/bluetooth.md +6 -1
  13. {refrain-0.4.6 → refrain-0.5.2}/docs/faq.md +70 -13
  14. {refrain-0.4.6 → refrain-0.5.2}/docs/lastfm.md +16 -1
  15. {refrain-0.4.6 → refrain-0.5.2}/docs/screenshots/README.md +19 -6
  16. refrain-0.5.2/docs/screenshots/demo-cover.png +0 -0
  17. refrain-0.5.2/docs/screenshots/discord-rpc.png +0 -0
  18. refrain-0.5.2/docs/screenshots/history.png +0 -0
  19. refrain-0.5.2/docs/screenshots/legal.png +0 -0
  20. refrain-0.5.2/docs/screenshots/live-log.png +0 -0
  21. refrain-0.5.2/docs/screenshots/notification.png +0 -0
  22. refrain-0.5.2/docs/screenshots/settings-advanced.png +0 -0
  23. refrain-0.5.2/docs/screenshots/settings-general.png +0 -0
  24. refrain-0.5.2/docs/screenshots/settings-history.png +0 -0
  25. refrain-0.5.2/docs/screenshots/settings-lastfm.png +0 -0
  26. refrain-0.5.2/docs/screenshots/settings-sources.png +0 -0
  27. refrain-0.5.2/docs/screenshots/settings-updates.png +0 -0
  28. refrain-0.5.2/docs/screenshots/tray-menu.png +0 -0
  29. refrain-0.5.2/docs/screenshots/update-dialog.png +0 -0
  30. {refrain-0.4.6 → refrain-0.5.2}/docs/test-matrix.md +40 -4
  31. {refrain-0.4.6 → refrain-0.5.2}/packaging/README.md +37 -18
  32. {refrain-0.4.6 → refrain-0.5.2}/packaging/appimage/AppImageBuilder.yml +1 -1
  33. {refrain-0.4.6 → refrain-0.5.2}/packaging/aur/refrain/.SRCINFO +2 -2
  34. {refrain-0.4.6 → refrain-0.5.2}/packaging/aur/refrain/PKGBUILD +2 -2
  35. {refrain-0.4.6 → refrain-0.5.2}/packaging/aur/refrain-git/.SRCINFO +1 -1
  36. {refrain-0.4.6 → refrain-0.5.2}/packaging/aur/refrain-git/PKGBUILD +1 -1
  37. {refrain-0.4.6 → refrain-0.5.2}/packaging/flatpak/io.github.Rockykln.Refrain.desktop +1 -1
  38. {refrain-0.4.6 → refrain-0.5.2}/packaging/flatpak/io.github.Rockykln.Refrain.metainfo.xml +23 -0
  39. {refrain-0.4.6 → refrain-0.5.2}/packaging/flatpak/io.github.Rockykln.Refrain.yaml +2 -2
  40. {refrain-0.4.6 → refrain-0.5.2}/pyproject.toml +3 -1
  41. refrain-0.5.2/src/refrain/__init__.py +1 -0
  42. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/app.py +124 -3
  43. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/refrain.desktop +1 -1
  44. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/autostart.py +31 -1
  45. refrain-0.5.2/src/refrain/browser.py +125 -0
  46. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/config.py +26 -0
  47. refrain-0.5.2/src/refrain/cover_art.py +353 -0
  48. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/daemon.py +193 -24
  49. refrain-0.5.2/src/refrain/history.py +733 -0
  50. refrain-0.5.2/src/refrain/i18n/refrain_de.qm +0 -0
  51. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_de.ts +403 -152
  52. refrain-0.5.2/src/refrain/i18n/refrain_en.qm +0 -0
  53. refrain-0.5.2/src/refrain/i18n/refrain_en.ts +47 -0
  54. refrain-0.5.2/src/refrain/i18n/refrain_es.qm +0 -0
  55. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_es.ts +403 -152
  56. refrain-0.5.2/src/refrain/i18n/refrain_fr.qm +0 -0
  57. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_fr.ts +403 -152
  58. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_it.qm +0 -0
  59. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_it.ts +403 -152
  60. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_ja.qm +0 -0
  61. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_ja.ts +398 -152
  62. refrain-0.5.2/src/refrain/i18n/refrain_pl.qm +0 -0
  63. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_pl.ts +408 -152
  64. refrain-0.5.2/src/refrain/i18n/refrain_pt.qm +0 -0
  65. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_pt.ts +403 -152
  66. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_ru.qm +0 -0
  67. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_ru.ts +408 -152
  68. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_zh_CN.qm +0 -0
  69. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/i18n/refrain_zh_CN.ts +398 -152
  70. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/scrobble.py +328 -22
  71. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/scrobble_queue.py +25 -1
  72. refrain-0.5.2/src/refrain/song_lengths.py +144 -0
  73. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/sources/base.py +8 -0
  74. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/sources/bluetooth.py +111 -4
  75. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/sources/mpris.py +111 -14
  76. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/sources/mpris_server.py +84 -11
  77. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/timing.py +152 -11
  78. refrain-0.5.2/src/refrain/ui/history_window.py +1003 -0
  79. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/ui/settings_window.py +125 -4
  80. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/ui/tray.py +38 -1
  81. refrain-0.5.2/tests/conftest.py +69 -0
  82. {refrain-0.4.6 → refrain-0.5.2}/tests/test_autostart.py +66 -0
  83. refrain-0.5.2/tests/test_bluetooth_apps.py +70 -0
  84. refrain-0.5.2/tests/test_browser.py +57 -0
  85. refrain-0.5.2/tests/test_button_url.py +40 -0
  86. refrain-0.5.2/tests/test_cover_art.py +251 -0
  87. refrain-0.5.2/tests/test_crash_log.py +76 -0
  88. refrain-0.5.2/tests/test_desktop_entries.py +60 -0
  89. refrain-0.5.2/tests/test_history.py +555 -0
  90. refrain-0.5.2/tests/test_history_window.py +413 -0
  91. refrain-0.5.2/tests/test_mpris_controls.py +57 -0
  92. {refrain-0.4.6 → refrain-0.5.2}/tests/test_mpris_dispatch.py +138 -1
  93. {refrain-0.4.6 → refrain-0.5.2}/tests/test_position_resolver.py +223 -8
  94. refrain-0.5.2/tests/test_scrobbler.py +567 -0
  95. refrain-0.5.2/tests/test_settings_github_icon.py +62 -0
  96. refrain-0.5.2/tests/test_settings_history.py +130 -0
  97. refrain-0.5.2/tests/test_song_lengths.py +164 -0
  98. refrain-0.5.2/tests/test_translations.py +81 -0
  99. {refrain-0.4.6 → refrain-0.5.2}/tests/test_tray_init.py +40 -0
  100. refrain-0.4.6/docs/screenshots/discord-rpc.png +0 -0
  101. refrain-0.4.6/docs/screenshots/legal.png +0 -0
  102. refrain-0.4.6/docs/screenshots/live-log.png +0 -0
  103. refrain-0.4.6/docs/screenshots/notification.png +0 -0
  104. refrain-0.4.6/docs/screenshots/settings-advanced.png +0 -0
  105. refrain-0.4.6/docs/screenshots/settings-general.png +0 -0
  106. refrain-0.4.6/docs/screenshots/settings-lastfm.png +0 -0
  107. refrain-0.4.6/docs/screenshots/settings-sources.png +0 -0
  108. refrain-0.4.6/docs/screenshots/settings-updates.png +0 -0
  109. refrain-0.4.6/docs/screenshots/tray-menu.png +0 -0
  110. refrain-0.4.6/docs/screenshots/update-dialog.png +0 -0
  111. refrain-0.4.6/src/refrain/__init__.py +0 -1
  112. refrain-0.4.6/src/refrain/cover_art.py +0 -222
  113. refrain-0.4.6/src/refrain/i18n/refrain_de.qm +0 -0
  114. refrain-0.4.6/src/refrain/i18n/refrain_es.qm +0 -0
  115. refrain-0.4.6/src/refrain/i18n/refrain_fr.qm +0 -0
  116. refrain-0.4.6/src/refrain/i18n/refrain_pl.qm +0 -0
  117. refrain-0.4.6/src/refrain/i18n/refrain_pt.qm +0 -0
  118. refrain-0.4.6/tests/conftest.py +0 -37
  119. refrain-0.4.6/tests/test_cover_art.py +0 -151
  120. refrain-0.4.6/tests/test_scrobbler.py +0 -267
  121. {refrain-0.4.6 → refrain-0.5.2}/.editorconfig +0 -0
  122. {refrain-0.4.6 → refrain-0.5.2}/.gitattributes +0 -0
  123. {refrain-0.4.6 → refrain-0.5.2}/.github/FUNDING.yml +0 -0
  124. {refrain-0.4.6 → refrain-0.5.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  125. {refrain-0.4.6 → refrain-0.5.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  126. {refrain-0.4.6 → refrain-0.5.2}/.github/codeql/codeql-config.yml +0 -0
  127. {refrain-0.4.6 → refrain-0.5.2}/.github/dependabot.yml +0 -0
  128. {refrain-0.4.6 → refrain-0.5.2}/.github/pull_request_template.md +0 -0
  129. {refrain-0.4.6 → refrain-0.5.2}/.github/workflows/codeql.yml +0 -0
  130. {refrain-0.4.6 → refrain-0.5.2}/.github/workflows/release.yml +0 -0
  131. {refrain-0.4.6 → refrain-0.5.2}/.github/workflows/tests.yml +0 -0
  132. {refrain-0.4.6 → refrain-0.5.2}/CODE_OF_CONDUCT.md +0 -0
  133. {refrain-0.4.6 → refrain-0.5.2}/LEGAL.md +0 -0
  134. {refrain-0.4.6 → refrain-0.5.2}/LICENSE +0 -0
  135. {refrain-0.4.6 → refrain-0.5.2}/SECURITY.md +0 -0
  136. {refrain-0.4.6 → refrain-0.5.2}/docs/screenshots/welcome.png +0 -0
  137. {refrain-0.4.6 → refrain-0.5.2}/packaging/flatpak/python-deps.json +0 -0
  138. {refrain-0.4.6 → refrain-0.5.2}/requirements-dev.txt +0 -0
  139. {refrain-0.4.6 → refrain-0.5.2}/requirements.txt +0 -0
  140. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/__main__.py +0 -0
  141. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/github-mark.svg +0 -0
  142. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/menu-quit.svg +0 -0
  143. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/menu-update.svg +0 -0
  144. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/refrain.png +0 -0
  145. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/refrain.svg +0 -0
  146. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/tray-paused-dark.svg +0 -0
  147. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/tray-paused.svg +0 -0
  148. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/tray-playing-dark.svg +0 -0
  149. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/tray-playing.svg +0 -0
  150. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/tray-stopped-dark.svg +0 -0
  151. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/assets/icons/tray-stopped.svg +0 -0
  152. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/cover_fetcher.py +0 -0
  153. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/discord_app.py +0 -0
  154. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/discord_rpc.py +0 -0
  155. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/logging_setup.py +0 -0
  156. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/paths.py +0 -0
  157. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/secrets_store.py +0 -0
  158. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/single_instance.py +0 -0
  159. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/sources/__init__.py +0 -0
  160. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/startup_check.py +0 -0
  161. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/ui/__init__.py +0 -0
  162. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/ui/cursors.py +0 -0
  163. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/ui/legal_dialog.py +0 -0
  164. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/ui/log_window.py +0 -0
  165. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/ui/update_dialog.py +0 -0
  166. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/ui/welcome_dialog.py +0 -0
  167. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/uninstall.py +0 -0
  168. {refrain-0.4.6 → refrain-0.5.2}/src/refrain/updater.py +0 -0
  169. {refrain-0.4.6 → refrain-0.5.2}/tests/__init__.py +0 -0
  170. {refrain-0.4.6 → refrain-0.5.2}/tests/test_config.py +0 -0
  171. {refrain-0.4.6 → refrain-0.5.2}/tests/test_config_preserve.py +0 -0
  172. {refrain-0.4.6 → refrain-0.5.2}/tests/test_cover_fetcher.py +0 -0
  173. {refrain-0.4.6 → refrain-0.5.2}/tests/test_daemon_album_format.py +0 -0
  174. {refrain-0.4.6 → refrain-0.5.2}/tests/test_daemon_idle.py +0 -0
  175. {refrain-0.4.6 → refrain-0.5.2}/tests/test_dbus_dispatch.py +0 -0
  176. {refrain-0.4.6 → refrain-0.5.2}/tests/test_discord_app.py +0 -0
  177. {refrain-0.4.6 → refrain-0.5.2}/tests/test_discord_ipc_scan.py +0 -0
  178. {refrain-0.4.6 → refrain-0.5.2}/tests/test_discord_listening.py +0 -0
  179. {refrain-0.4.6 → refrain-0.5.2}/tests/test_discord_multi_client.py +0 -0
  180. {refrain-0.4.6 → refrain-0.5.2}/tests/test_discord_rpc.py +0 -0
  181. {refrain-0.4.6 → refrain-0.5.2}/tests/test_imports.py +0 -0
  182. {refrain-0.4.6 → refrain-0.5.2}/tests/test_legal_notice.py +0 -0
  183. {refrain-0.4.6 → refrain-0.5.2}/tests/test_notify_argv.py +0 -0
  184. {refrain-0.4.6 → refrain-0.5.2}/tests/test_paths.py +0 -0
  185. {refrain-0.4.6 → refrain-0.5.2}/tests/test_qt_plugin_path.py +0 -0
  186. {refrain-0.4.6 → refrain-0.5.2}/tests/test_scrobble.py +0 -0
  187. {refrain-0.4.6 → refrain-0.5.2}/tests/test_scrobble_queue.py +0 -0
  188. {refrain-0.4.6 → refrain-0.5.2}/tests/test_secrets_store.py +0 -0
  189. {refrain-0.4.6 → refrain-0.5.2}/tests/test_settings_lastfm.py +0 -0
  190. {refrain-0.4.6 → refrain-0.5.2}/tests/test_settings_reset.py +0 -0
  191. {refrain-0.4.6 → refrain-0.5.2}/tests/test_source_selection.py +0 -0
  192. {refrain-0.4.6 → refrain-0.5.2}/tests/test_sources_base.py +0 -0
  193. {refrain-0.4.6 → refrain-0.5.2}/tests/test_startup_check.py +0 -0
  194. {refrain-0.4.6 → refrain-0.5.2}/tests/test_timing.py +0 -0
  195. {refrain-0.4.6 → refrain-0.5.2}/tests/test_ui_cursors.py +0 -0
  196. {refrain-0.4.6 → refrain-0.5.2}/tests/test_ui_locale.py +0 -0
  197. {refrain-0.4.6 → refrain-0.5.2}/tests/test_uninstall.py +0 -0
  198. {refrain-0.4.6 → refrain-0.5.2}/tests/test_update_dialog_body.py +0 -0
  199. {refrain-0.4.6 → refrain-0.5.2}/tests/test_updater.py +0 -0
  200. {refrain-0.4.6 → refrain-0.5.2}/tests/test_welcome_dialog_growth.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.4.6"
16
+ placeholder: "0.5.2"
17
17
  validations:
18
18
  required: true
19
19
 
@@ -78,7 +78,7 @@ jobs:
78
78
  fetch-depth: 0
79
79
 
80
80
  - name: Run TruffleHog
81
- uses: trufflesecurity/trufflehog@bcfcf73aaf4759d4dadc2783177c245a02792318 # v3.97.0
81
+ uses: trufflesecurity/trufflehog@363923b901c911a9164f50b6c423f47c15372b1c # v3.97.4
82
82
  with:
83
83
  # --only-verified: a secret only counts as flagged when TruffleHog
84
84
  # successfully validates it against the live service. Drops the
@@ -31,11 +31,6 @@ env/
31
31
  *.swo
32
32
  .DS_Store
33
33
 
34
- # AI assistants
35
- .claude*/
36
- .cursor/
37
- .aider*
38
-
39
34
  # Local config / runtime
40
35
  *.log
41
36
  .refrain.local.toml
@@ -7,6 +7,199 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.2] - 2026-09-18
11
+
12
+ A release about the gaps. Songs the iTunes catalog doesn't know — about
13
+ one in ten here — were never scrobbled from the browser and had no end
14
+ time on Discord; Refrain now measures their length from whole plays. A
15
+ stream or video playing over Bluetooth no longer passes for a song,
16
+ Discord no longer shows nothing when the tab's address has a space in
17
+ it, and Last.fm neither scrobbles a long song twice nor loses one around
18
+ a restart, an invalid session or an account switch.
19
+
20
+ ### Fixed
21
+
22
+ - **The desktop entries named invalid categories.** `Audio` requires
23
+ `AudioVideo`, which was missing — `desktop-file-validate` reports that
24
+ as an error set to become fatal — and `Audio` next to `Network` put
25
+ Refrain in two main categories, so a menu could list it twice. The
26
+ menu entry, the Flatpak entry and the autostart entry now all say
27
+ `AudioVideo;Audio;Music;`, and an autostart entry written by an
28
+ earlier version gets the new categories on the next start. Nothing
29
+ else in it changes — not the launcher it points at, and not a switch
30
+ the desktop set to keep Refrain from starting with the session.
31
+ - **The GitHub logo in the Settings footer was hard to see on a dark
32
+ theme.** Qt draws the logo's `currentColor` as black whatever the
33
+ theme; it now takes the theme's text colour, like the version beside
34
+ it, and follows a theme change while the window is open.
35
+ - **A long song could be scrobbled twice after a restart.** If the first
36
+ poll after starting saw no song yet, the play saved on quit was thrown
37
+ away, the song counted again from zero and was queued a second time —
38
+ or, not having counted yet, was never scrobbled at all. Quitting
39
+ before that first poll also deleted the saved play.
40
+ - **Pausing on an iPad could split the play in two.** The iPad reports
41
+ 0:00 for a moment when paused; that was passed to the history and to
42
+ Last.fm as the player's position, which read as the song starting over
43
+ — a second history entry, and on songs over eight minutes a second
44
+ scrobble.
45
+ - **The elapsed time could run ahead or fall behind for a whole song.**
46
+ A stream position that stalled and then caught up was taken for a
47
+ seek; a single poll that saw no song made a browser source's next
48
+ segment look like the song's start; and an album that arrived a few
49
+ seconds after the title restarted the clock at 0:00.
50
+ - **Scrobbles were lost while the Last.fm session was invalid.** The log
51
+ promised they would submit after reconnecting, but songs played in the
52
+ meantime were never queued. They are now.
53
+ - **Queued scrobbles could go to the wrong account.** After connecting a
54
+ different Last.fm account, plays still in the offline queue were sent
55
+ to it; they now stay with the account they were heard under.
56
+ - **Quitting could wait on Last.fm for up to ten seconds**, and the
57
+ scrobble queue was written readable by other users — it is owner-only
58
+ now, like the history.
59
+ - **Other apps couldn't read what Refrain was playing when the title had
60
+ an umlaut** — or any other letter beyond ASCII. Refrain's MPRIS track
61
+ ID kept it, which a D-Bus object path doesn't allow, and every read of
62
+ the metadata failed.
63
+ - **Songs the iTunes catalog doesn't know were never scrobbled from the
64
+ browser.** Since 0.5.1 the media segments plasma reports no longer pass
65
+ for a song's length, and without a catalog match that left none at all —
66
+ no scrobble, no end time on Discord, and nothing in the history under
67
+ four minutes. Refrain now measures such a song itself: a play it watched
68
+ from the start to an end the player reached on its own is as long as the
69
+ song, and two plays that agree settle it. Kept under a hash of the
70
+ title, artist and album, so the file holds no song titles.
71
+ - **Discord showed nothing while a song without a catalog match played
72
+ from Apple Music's search page.** Plasma reports the tab's address with
73
+ plain spaces in it, the "Listen on Apple Music" button took it as its
74
+ link, and Discord refused the whole activity over the malformed link.
75
+ The link is encoded now, and one that still isn't valid drops only the
76
+ button.
77
+ - **A stream or video playing over Bluetooth passed for a song.** A Twitch
78
+ stream on an iPad went to Discord and, four minutes in, into the history
79
+ under its channel's name. Refrain now reads which app is playing and
80
+ takes music apps only; an app it doesn't know counts when its track has
81
+ a length, which a live stream doesn't.
82
+ - **A browser tab with "Apple Music" anywhere in its title counted as the
83
+ Apple Music tab**, so a video about Apple Music could decide whether
84
+ Refrain showed the music as playing or paused.
85
+
86
+ ## [0.5.1] - 2026-09-15
87
+
88
+ A release about what you played. A new *Recently played* window lists the
89
+ last songs Refrain saw, and every play is now one row and one scrobble —
90
+ on repeat, across a restart of Refrain, or through a crash. Plasma's
91
+ browser integration turned out to report Apple Music's looping artwork
92
+ video rather than the music; Refrain now asks the Apple Music tab itself
93
+ whether it is playing, which also makes Play work again after a pause.
94
+
95
+ Also: the crash behind Refrain simply vanishing, Plasma's Stop starting
96
+ playback, Settings → Apply restarting a song's Last.fm count, and covers
97
+ and lengths for far more songs. (0.5.0 was never released.)
98
+
99
+ ### Added
100
+
101
+ - **Recently played.** A new window, opened from the tray menu or from
102
+ Settings → History, lists the last songs Refrain saw: cover, title,
103
+ artist and album, the time and day each one started, where it came
104
+ from — Apple Music in which browser, or which Bluetooth device — its
105
+ length, and whether it went to Last.fm. Every song is a link to its
106
+ Apple Music page — or, where Refrain never learned the page, to an
107
+ Apple Music search for it — and opens in the browser it played in
108
+ while that browser is still running; a right-click takes a single song
109
+ out of the list. A song shows up the moment it
110
+ starts playing and stays once it counts as listened: half its length
111
+ or four minutes, the rule Last.fm uses, so skipping through a playlist
112
+ leaves nothing behind. The list lives on this machine only
113
+ (`$XDG_STATE_HOME/refrain/history.json`, owner-only), which is why it
114
+ keeps running with the privacy mode set to *Off*. Settings → History
115
+ turns it off entirely — deleting the stored list — and sets how many
116
+ songs to keep, from 10 to 100; 30 by default. The song playing when
117
+ Refrain stops — or crashes — is saved with how much of it was heard
118
+ and where it was, and carries on as the same row if it is still on,
119
+ further along, when Refrain comes back. A song heard through and
120
+ played again is listed once per play.
121
+
122
+ ### Fixed
123
+
124
+ - **After a restart mid-song, the elapsed time fell back every ten
125
+ seconds.** Plasma's browser integration reports the media segment the
126
+ page has buffered — position 0 to 10 s, over and over — and at a
127
+ constant segment length nothing gave it away. A source length under
128
+ 30 s on a song the catalog knows to be longer is now never taken as the
129
+ song's position: the time counts on from the song's start, or stays
130
+ hidden when Refrain came up in the middle of it.
131
+ - **Paused in the browser, Apple Music still counted as playing — and
132
+ Play didn't start it again.** KDE's browser integration takes the
133
+ song's title from the page but its state from whatever video plays on
134
+ it: Apple Music's looping artwork. That kept "playing" through a pause,
135
+ so the time ran on, the history and Last.fm counted the pause as
136
+ listened, and its Play/Pause toggle could only pause. The browser's own
137
+ entry for the Apple Music tab now decides playing or paused, and gets
138
+ Play/Pause first.
139
+ - **Settings → Apply started the song's Last.fm count over.** Apply
140
+ sends the whole configuration, and the Scrobbler dropped the play in
141
+ progress on every one of them — for a change on any tab. Applied near
142
+ the end of a song, the song was never scrobbled. The play in progress
143
+ is now only dropped when the Last.fm account itself changes.
144
+ - **Restarting Refrain mid-song could scrobble the song twice.** A play
145
+ that counted was queued on quit, and after the restart the Scrobbler
146
+ counted the same song again from zero — a long song got a second
147
+ scrobble for what was heard afterwards, and a shorter one was
148
+ scrobbled under the time of the restart. The play in progress is now
149
+ kept across a restart (`scrobble_current.json`, like the history's):
150
+ the same play carries on, under the time it began, and is never
151
+ queued twice. One that counted before a crash is still scrobbled.
152
+ - **"Apple Music – Webplayer" could pass for a song.** With no song
153
+ loaded, KDE's browser integration reports the page's title as the
154
+ track — no artist — and calls it playing. A title like that is no
155
+ longer taken for a song, so it can't reach Discord or the history.
156
+ - **A song on repeat was scrobbled only once, and lost its time.**
157
+ Last.fm counts every play, but the Scrobbler saw one long play for as
158
+ long as the title stayed the same. A song that has counted and begins
159
+ again is now a new play, with a scrobble of its own. The player shows
160
+ it: an ordinary one jumps back to zero; Apple Music in the browser,
161
+ through Plasma, reports the song's own length at position zero for a
162
+ moment; a phone over Bluetooth set to repeat-one kept counting its
163
+ position on across the loop (5:13 into a 3:36 song), which hid the
164
+ elapsed time after the first play — it now counts from the start of
165
+ each loop.
166
+
167
+ - **The tray icon flashed to "paused" at every song change.** Apple
168
+ Music's web player reports a pause for a moment between two songs.
169
+ The icon and the Play/Pause entry now only follow a pause that lasts
170
+ two seconds; going back to playing still shows at once.
171
+ - **Plasma's "Stop" started the music when it was paused.** Right-click
172
+ Refrain in the task manager and Plasma offers its media controls;
173
+ Play, Pause and Stop all toggled playback blindly, so each one did the
174
+ opposite of what it said whenever the music was already in the state
175
+ it asked for. They now only act when that gets them there.
176
+ - **Refrain could die out of nowhere.** The daemon thread polled the
177
+ players over the same D-Bus connection the main thread dispatches for
178
+ the published MPRIS player, and dbus-glib keeps that connection's
179
+ bookkeeping without locks. Two threads on it at once corrupted the
180
+ heap, and the process aborted with `malloc(): unaligned tcache chunk
181
+ detected` — the "Refrain was simply gone" that the log shows as a
182
+ start without a stop before it. The sources now keep private
183
+ connections of their own, the MPRIS player is only touched from the
184
+ thread that dispatches it, and Plasma's play/pause/skip requests are
185
+ handed to the daemon thread instead of running the sources from the
186
+ main one. Should a crash still happen, `crash.log` next to the log
187
+ now holds every thread's Python stack.
188
+ - **About one song in ten had no cover and no length.** The catalog
189
+ lookup asked the US store only, with the title as given — "feat."
190
+ credits, remaster tags and all — and took the first result without
191
+ checking it. German releases missing from the US store, songs
192
+ crediting several artists and titles with a "(feat. …)" found
193
+ nothing; others got a stranger's cover (a Chinese ballad on a German
194
+ rap track). The lookup now asks the store of the desktop's own
195
+ country first, strips those tags, falls back to the first of several
196
+ artists, and only takes a result whose artist *and* title match. A
197
+ lookup that fails because iTunes can't be reached is retried a minute
198
+ later instead of leaving the song without a cover for the whole
199
+ session, a song the catalog doesn't have is asked about again after
200
+ three days instead of never, and covers cached by the old lookup are
201
+ looked up once more.
202
+
10
203
  ## [0.4.6] - 2026-08-26
11
204
 
12
205
  A release about the clock. Apple Music's web player and Plasma's browser
@@ -1576,7 +1769,9 @@ with a proper, installable Linux app.
1576
1769
  pip-audit, trufflehog, release), Dependabot, issue + PR templates,
1577
1770
  `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`.
1578
1771
 
1579
- [Unreleased]: https://github.com/Rockykln/refrain/compare/v0.4.6...HEAD
1772
+ [Unreleased]: https://github.com/Rockykln/refrain/compare/v0.5.2...HEAD
1773
+ [0.5.2]: https://github.com/Rockykln/refrain/compare/v0.5.1...v0.5.2
1774
+ [0.5.1]: https://github.com/Rockykln/refrain/compare/v0.4.6...v0.5.1
1580
1775
  [0.4.6]: https://github.com/Rockykln/refrain/compare/v0.4.5...v0.4.6
1581
1776
  [0.4.5]: https://github.com/Rockykln/refrain/compare/v0.4.4...v0.4.5
1582
1777
  [0.4.4]: https://github.com/Rockykln/refrain/compare/v0.4.3...v0.4.4
@@ -124,8 +124,19 @@ descriptions instead.
124
124
 
125
125
  User-visible strings live wrapped in `tr()` calls; translations are
126
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.
127
+ English is the source language; every shipped language is complete.
128
+
129
+ Strings that contain a number use Qt's plural forms —
130
+ `self.tr("Last %n song(s)", "", count)` — never a sentence assembled
131
+ around the number, because Russian and Polish change the noun's ending
132
+ with it. Two things follow:
133
+
134
+ - Only `self.tr(…)` with the count as the third argument is extracted
135
+ as a plural. `QCoreApplication.translate(…, n)` isn't, and neither is
136
+ any call whose arguments contain `//`.
137
+ - `refrain_en.ts` holds only the English plural forms (`lupdate
138
+ -pluralonly`, which `make i18n-update` runs for it) — without it
139
+ English would read "Last 12 song(s)".
129
140
 
130
141
  To add a new language end-to-end:
131
142
 
@@ -54,11 +54,18 @@ i18n:
54
54
 
55
55
  # Refresh the .ts source-string list from the current Python sources,
56
56
  # preserving existing translations. Use after wrapping new strings in tr().
57
+ # English is the source language, so refrain_en.ts holds only the plural
58
+ # forms of the %n strings (-pluralonly) — without it English would read
59
+ # "Last 12 song(s)".
57
60
  i18n-update:
58
61
  @"$(LUPDATE)" -no-obsolete \
59
62
  src/refrain/app.py \
60
63
  src/refrain/ui/*.py \
61
- -ts $(wildcard src/refrain/i18n/refrain_*.ts)
64
+ -ts $(filter-out %_en.ts,$(wildcard src/refrain/i18n/refrain_*.ts))
65
+ @"$(LUPDATE)" -no-obsolete -pluralonly \
66
+ src/refrain/app.py \
67
+ src/refrain/ui/*.py \
68
+ -ts src/refrain/i18n/refrain_en.ts
62
69
  @$(MAKE) i18n
63
70
 
64
71
  # Remove anything that pytest, ruff, hatch, pip, or coverage might leave
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: refrain
3
- Version: 0.4.6
3
+ Version: 0.5.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
@@ -58,6 +58,8 @@ streaming from your phone over Bluetooth.
58
58
  - Optionally **scrobbles to Last.fm** alongside Discord (opt-in, with a
59
59
  crash-safe offline queue).
60
60
  - Lives in your **system tray** with Play/Pause/Next/Previous controls.
61
+ - Keeps a **recently played** list — cover, time, source — of the last
62
+ 30 songs by default, on your machine only.
61
63
  - Provides a **settings window** (PySide6) for everything users typically want
62
64
  to tweak — privacy mode, sources, autostart, Bluetooth device picker.
63
65
 
@@ -212,6 +214,12 @@ scrobble_now_playing = true # also send the ephemeral "now playing" indic
212
214
  # NOTE: the Last.fm shared secret and session key are credentials and
213
215
  # are deliberately NOT stored here. They live in your OS keyring
214
216
  # (KWallet / GNOME Keyring), encrypted at rest — see "Last.fm" below.
217
+
218
+ [history]
219
+ enabled = true # the "Recently played" list; false also deletes what it stored
220
+ max_entries = 30 # songs kept, 1–100 (Settings offers 10, 20, 30, 50, 75, 100)
221
+ window_width = 0 # the history window's size when last closed; 0 = default
222
+ window_height = 0
215
223
  ```
216
224
 
217
225
  Per-source `client_id_*` fields let Apple Music render under one Discord
@@ -224,7 +232,10 @@ another (with a generic Bluetooth glyph). Empty falls back to the default
224
232
  Refrain publishes itself as `org.mpris.MediaPlayer2.refrain` on the session
225
233
  bus, so KDE Plasma's panel media-controls applet (and KDE Connect, GNOME
226
234
  Shell, Mako, …) drive the same Play/Pause/Next/Previous as the tray and
227
- render the same track Discord renders.
235
+ render the same track Discord renders. Plasma also offers these controls
236
+ when you right-click Refrain in the task manager, including a *Stop*
237
+ that it adds for every player — Apple Music has no stop, so there it
238
+ pauses, and does nothing when the music is already paused.
228
239
 
229
240
  ## Tray
230
241
 
@@ -248,6 +259,7 @@ no unicode-glyph prefixes.
248
259
  | Play / Pause | Toggle on the active source (label follows playback state)|
249
260
  | Next | Skip forward on the active source |
250
261
  | Update available — vX.Y.Z | Only visible when a newer release exists |
262
+ | Recently played… | Open the history window (hidden while the history is off) |
251
263
  | Settings… | Open the settings window |
252
264
  | Live log… | Open the live-log window |
253
265
  | Restart Refrain | Cleanly stop and re-launch (release D-Bus name + RPC, exec the same binary) |
@@ -284,26 +296,65 @@ the background.
284
296
  <img src="docs/screenshots/settings-lastfm.png" alt="Settings — Last.fm" width="420"/>
285
297
  <br/><sub>Opt-in scrobbling, API key + secret, connect / disconnect an account</sub>
286
298
  </td>
299
+ <td align="center">
300
+ <b>History</b><br/>
301
+ <img src="docs/screenshots/settings-history.png" alt="Settings — History" width="420"/>
302
+ <br/><sub>Recently played on or off, how many songs to keep</sub>
303
+ </td>
304
+ </tr>
305
+ <tr>
287
306
  <td align="center">
288
307
  <b>Updates</b><br/>
289
308
  <img src="docs/screenshots/settings-updates.png" alt="Settings — Updates" width="420"/>
290
309
  <br/><sub>Auto-check, last-checked, manual <i>Check for updates now</i></sub>
291
310
  </td>
292
- </tr>
293
- <tr>
294
311
  <td align="center">
295
312
  <b>Advanced</b><br/>
296
313
  <img src="docs/screenshots/settings-advanced.png" alt="Settings — Advanced" width="420"/>
297
314
  <br/><sub>Poll interval, notification delay, cover cache size, language, log level, restart, reset, uninstall</sub>
298
315
  </td>
316
+ </tr>
317
+ <tr>
299
318
  <td align="center">
300
319
  <b>Legal</b><br/>
301
320
  <img src="docs/screenshots/legal.png" alt="Legal notice" width="420"/>
302
321
  <br/><sub>Licence, trademark and affiliation notices — the <i>Legal</i> button in the footer</sub>
303
322
  </td>
323
+ <td></td>
304
324
  </tr>
305
325
  </table>
306
326
 
327
+ ## Recently played
328
+
329
+ <p align="center">
330
+ <img src="docs/screenshots/history.png" alt="Recently played" width="520"/>
331
+ </p>
332
+
333
+ *Tray → Recently played…* lists the last songs Refrain saw — 30 by
334
+ default, up to 100 — with cover, length, when each started, where it
335
+ came from (which browser, which Bluetooth device) and whether it went to
336
+ Last.fm. A song shows up the moment it plays and stays once it counts
337
+ as listened: half its length or four minutes, the rule Last.fm uses, so
338
+ skipping through a playlist leaves nothing behind. Restarting Refrain
339
+ mid-song doesn't lose or duplicate it; a song heard through and played
340
+ again is listed twice, once per play.
341
+
342
+ - Click a song to open it in Apple Music — in the browser it played in,
343
+ while that one is still open; a search when the song's page isn't
344
+ known.
345
+ - Right-click to copy artist and title or to remove the song from the
346
+ list; *Clear history…* empties it.
347
+ - From ten songs on there's a search over title, artist and album —
348
+ blind to case and accents, found words marked — and with more than
349
+ one source a filter by source. <kbd>Ctrl</kbd>+<kbd>F</kbd> jumps into
350
+ the search, <kbd>Esc</kbd> clears it.
351
+ - The window opens at the size you last left it.
352
+
353
+ The list stays on your machine (`history.json`, readable only by you)
354
+ and is never sent anywhere, so the privacy mode doesn't affect it.
355
+ *Settings → History* turns it off — which deletes the file — or changes
356
+ how many songs it keeps.
357
+
307
358
  ## Notifications
308
359
 
309
360
  When a track changes, Refrain fires a desktop notification with the album
@@ -355,7 +406,9 @@ A track is scrobbled once you've played at least half of it, or four
355
406
  minutes (Last.fm's rule), and only if it's longer than 30 s. Scrobbles
356
407
  are queued to disk the instant they qualify, so being offline, a
357
408
  Last.fm outage, or quitting mid-song never loses them — they submit on
358
- the next opportunity. Privacy mode `Off` silences scrobbling too.
409
+ the next opportunity. Restarting Refrain mid-song carries the play on
410
+ instead of counting it again, and a song on repeat is scrobbled once per
411
+ play. Privacy mode `Off` silences scrobbling too.
359
412
 
360
413
  Full walkthrough + troubleshooting: [`docs/lastfm.md`](docs/lastfm.md).
361
414
 
@@ -387,7 +440,11 @@ entry + icon.)
387
440
  |---------------|---------------------------------------------|
388
441
  | Config | `$XDG_CONFIG_HOME/refrain/config.toml` |
389
442
  | Scrobble queue| `$XDG_STATE_HOME/refrain/scrobble_queue.jsonl` |
443
+ | Scrobble in progress | `$XDG_STATE_HOME/refrain/scrobble_current.json` |
444
+ | Recently played | `$XDG_STATE_HOME/refrain/history.json` |
445
+ | Measured song lengths | `$XDG_STATE_HOME/refrain/song_lengths.txt` |
390
446
  | Logs | `$XDG_STATE_HOME/refrain/refrain.log` (rotates) |
447
+ | Crash stacks | `$XDG_STATE_HOME/refrain/crash.log` (written only if Refrain crashes) |
391
448
  | Cover cache | `$XDG_CACHE_HOME/refrain/covers/*.txt` |
392
449
  | Autostart | `$XDG_CONFIG_HOME/autostart/refrain.desktop` (when enabled) |
393
450
 
@@ -420,7 +477,9 @@ directly to that provider:
420
477
  your **OS keyring** (encrypted at rest), never in `config.toml`.
421
478
 
422
479
  `Privacy → Off` is the global kill switch (no Discord status, no
423
- scrobbling) while keeping the tray + controls running.
480
+ scrobbling) while keeping the tray + controls running. The *Recently
481
+ played* list never leaves your machine, so it has its own switch in
482
+ *Settings → History* instead.
424
483
 
425
484
  Full data-flow, retention and erasure details — written to GDPR
426
485
  transparency expectations — are in [`PRIVACY.md`](PRIVACY.md).
@@ -37,8 +37,12 @@ receiving service's own privacy policy and your account there.
37
37
  |---|---|---|
38
38
  | Now-playing metadata (title, artist, album, position) read from your media player via D-Bus / BlueZ AVRCP | Drive the tray, notifications, the published MPRIS player | In memory; reflected in the tray |
39
39
  | Listening history lines (track changes, scrobble events) | Diagnostics / live log | `$XDG_STATE_HOME/refrain/refrain.log` — rotating, ≤ 4 × 1 MB, plaintext, local only |
40
+ | Start stamps, and Python stack traces if Refrain ever crashes | Diagnosing crashes inside Qt or D-Bus | `$XDG_STATE_HOME/refrain/crash.log` — ≤ 256 KB, then started afresh; written **owner-only (`0600`)**; local only |
40
41
  | Album-cover images + iTunes URL/duration cache | Avoid re-fetching covers | `$XDG_CACHE_HOME/refrain/` — capped (default 200), oldest pruned |
41
- | Pending scrobbles (artist/track/album/timestamp) | Survive offline / restart until submitted to Last.fm | `$XDG_STATE_HOME/refrain/scrobble_queue.jsonl` — capped at 1000, `0600` semantics, removed once submitted |
42
+ | Pending scrobbles (artist/track/album/timestamp, and the Last.fm username they were heard under) | Survive offline / restart until submitted to Last.fm — and never to another account | `$XDG_STATE_HOME/refrain/scrobble_queue.jsonl` — capped at 1000, written **owner-only (`0600`)**, removed once submitted |
43
+ | The song being scrobbled right now (artist/track/album, when it began, how much of it was heard, where the player had it) | Carry that play on across a restart of Refrain instead of counting it — or scrobbling it — twice | `$XDG_STATE_HOME/refrain/scrobble_current.json` — one song, written **owner-only (`0600`)**, removed when the song ends; only while Last.fm scrobbling is on |
44
+ | Recently played (title, artist, album, cover URL, source + browser/device name, start time, length, whether it was scrobbled) | The *Recently played* window | `$XDG_STATE_HOME/refrain/history.json` — the last 30 songs by default (10–100), written **owner-only (`0600`)**; *Settings → History* turns it off and deletes the file |
45
+ | How long a song ran, under a hash of its title, artist and album | Give songs the iTunes catalog doesn't know a length, so they can still be scrobbled | `$XDG_STATE_HOME/refrain/song_lengths.txt` — at most 1000 songs, written **owner-only (`0600`)**; holds **no titles**, only hashes and seconds |
42
46
  | Preferences + the *public* Discord/Last.fm application IDs | Your settings | `$XDG_CONFIG_HOME/refrain/config.toml` — written **owner-only (`0600`)**; contains **no secrets** |
43
47
  | Last.fm **shared secret** and **session token** | Authenticate scrobbling | **OS keyring** (KWallet / GNOME Keyring), encrypted at rest; or, only if no keyring exists, a `0600` owner-only `secrets.json`. **Never** in `config.toml`; never logged |
44
48
 
@@ -24,6 +24,8 @@ streaming from your phone over Bluetooth.
24
24
  - Optionally **scrobbles to Last.fm** alongside Discord (opt-in, with a
25
25
  crash-safe offline queue).
26
26
  - Lives in your **system tray** with Play/Pause/Next/Previous controls.
27
+ - Keeps a **recently played** list — cover, time, source — of the last
28
+ 30 songs by default, on your machine only.
27
29
  - Provides a **settings window** (PySide6) for everything users typically want
28
30
  to tweak — privacy mode, sources, autostart, Bluetooth device picker.
29
31
 
@@ -178,6 +180,12 @@ scrobble_now_playing = true # also send the ephemeral "now playing" indic
178
180
  # NOTE: the Last.fm shared secret and session key are credentials and
179
181
  # are deliberately NOT stored here. They live in your OS keyring
180
182
  # (KWallet / GNOME Keyring), encrypted at rest — see "Last.fm" below.
183
+
184
+ [history]
185
+ enabled = true # the "Recently played" list; false also deletes what it stored
186
+ max_entries = 30 # songs kept, 1–100 (Settings offers 10, 20, 30, 50, 75, 100)
187
+ window_width = 0 # the history window's size when last closed; 0 = default
188
+ window_height = 0
181
189
  ```
182
190
 
183
191
  Per-source `client_id_*` fields let Apple Music render under one Discord
@@ -190,7 +198,10 @@ another (with a generic Bluetooth glyph). Empty falls back to the default
190
198
  Refrain publishes itself as `org.mpris.MediaPlayer2.refrain` on the session
191
199
  bus, so KDE Plasma's panel media-controls applet (and KDE Connect, GNOME
192
200
  Shell, Mako, …) drive the same Play/Pause/Next/Previous as the tray and
193
- render the same track Discord renders.
201
+ render the same track Discord renders. Plasma also offers these controls
202
+ when you right-click Refrain in the task manager, including a *Stop*
203
+ that it adds for every player — Apple Music has no stop, so there it
204
+ pauses, and does nothing when the music is already paused.
194
205
 
195
206
  ## Tray
196
207
 
@@ -214,6 +225,7 @@ no unicode-glyph prefixes.
214
225
  | Play / Pause | Toggle on the active source (label follows playback state)|
215
226
  | Next | Skip forward on the active source |
216
227
  | Update available — vX.Y.Z | Only visible when a newer release exists |
228
+ | Recently played… | Open the history window (hidden while the history is off) |
217
229
  | Settings… | Open the settings window |
218
230
  | Live log… | Open the live-log window |
219
231
  | Restart Refrain | Cleanly stop and re-launch (release D-Bus name + RPC, exec the same binary) |
@@ -250,26 +262,65 @@ the background.
250
262
  <img src="docs/screenshots/settings-lastfm.png" alt="Settings — Last.fm" width="420"/>
251
263
  <br/><sub>Opt-in scrobbling, API key + secret, connect / disconnect an account</sub>
252
264
  </td>
265
+ <td align="center">
266
+ <b>History</b><br/>
267
+ <img src="docs/screenshots/settings-history.png" alt="Settings — History" width="420"/>
268
+ <br/><sub>Recently played on or off, how many songs to keep</sub>
269
+ </td>
270
+ </tr>
271
+ <tr>
253
272
  <td align="center">
254
273
  <b>Updates</b><br/>
255
274
  <img src="docs/screenshots/settings-updates.png" alt="Settings — Updates" width="420"/>
256
275
  <br/><sub>Auto-check, last-checked, manual <i>Check for updates now</i></sub>
257
276
  </td>
258
- </tr>
259
- <tr>
260
277
  <td align="center">
261
278
  <b>Advanced</b><br/>
262
279
  <img src="docs/screenshots/settings-advanced.png" alt="Settings — Advanced" width="420"/>
263
280
  <br/><sub>Poll interval, notification delay, cover cache size, language, log level, restart, reset, uninstall</sub>
264
281
  </td>
282
+ </tr>
283
+ <tr>
265
284
  <td align="center">
266
285
  <b>Legal</b><br/>
267
286
  <img src="docs/screenshots/legal.png" alt="Legal notice" width="420"/>
268
287
  <br/><sub>Licence, trademark and affiliation notices — the <i>Legal</i> button in the footer</sub>
269
288
  </td>
289
+ <td></td>
270
290
  </tr>
271
291
  </table>
272
292
 
293
+ ## Recently played
294
+
295
+ <p align="center">
296
+ <img src="docs/screenshots/history.png" alt="Recently played" width="520"/>
297
+ </p>
298
+
299
+ *Tray → Recently played…* lists the last songs Refrain saw — 30 by
300
+ default, up to 100 — with cover, length, when each started, where it
301
+ came from (which browser, which Bluetooth device) and whether it went to
302
+ Last.fm. A song shows up the moment it plays and stays once it counts
303
+ as listened: half its length or four minutes, the rule Last.fm uses, so
304
+ skipping through a playlist leaves nothing behind. Restarting Refrain
305
+ mid-song doesn't lose or duplicate it; a song heard through and played
306
+ again is listed twice, once per play.
307
+
308
+ - Click a song to open it in Apple Music — in the browser it played in,
309
+ while that one is still open; a search when the song's page isn't
310
+ known.
311
+ - Right-click to copy artist and title or to remove the song from the
312
+ list; *Clear history…* empties it.
313
+ - From ten songs on there's a search over title, artist and album —
314
+ blind to case and accents, found words marked — and with more than
315
+ one source a filter by source. <kbd>Ctrl</kbd>+<kbd>F</kbd> jumps into
316
+ the search, <kbd>Esc</kbd> clears it.
317
+ - The window opens at the size you last left it.
318
+
319
+ The list stays on your machine (`history.json`, readable only by you)
320
+ and is never sent anywhere, so the privacy mode doesn't affect it.
321
+ *Settings → History* turns it off — which deletes the file — or changes
322
+ how many songs it keeps.
323
+
273
324
  ## Notifications
274
325
 
275
326
  When a track changes, Refrain fires a desktop notification with the album
@@ -321,7 +372,9 @@ A track is scrobbled once you've played at least half of it, or four
321
372
  minutes (Last.fm's rule), and only if it's longer than 30 s. Scrobbles
322
373
  are queued to disk the instant they qualify, so being offline, a
323
374
  Last.fm outage, or quitting mid-song never loses them — they submit on
324
- the next opportunity. Privacy mode `Off` silences scrobbling too.
375
+ the next opportunity. Restarting Refrain mid-song carries the play on
376
+ instead of counting it again, and a song on repeat is scrobbled once per
377
+ play. Privacy mode `Off` silences scrobbling too.
325
378
 
326
379
  Full walkthrough + troubleshooting: [`docs/lastfm.md`](docs/lastfm.md).
327
380
 
@@ -353,7 +406,11 @@ entry + icon.)
353
406
  |---------------|---------------------------------------------|
354
407
  | Config | `$XDG_CONFIG_HOME/refrain/config.toml` |
355
408
  | Scrobble queue| `$XDG_STATE_HOME/refrain/scrobble_queue.jsonl` |
409
+ | Scrobble in progress | `$XDG_STATE_HOME/refrain/scrobble_current.json` |
410
+ | Recently played | `$XDG_STATE_HOME/refrain/history.json` |
411
+ | Measured song lengths | `$XDG_STATE_HOME/refrain/song_lengths.txt` |
356
412
  | Logs | `$XDG_STATE_HOME/refrain/refrain.log` (rotates) |
413
+ | Crash stacks | `$XDG_STATE_HOME/refrain/crash.log` (written only if Refrain crashes) |
357
414
  | Cover cache | `$XDG_CACHE_HOME/refrain/covers/*.txt` |
358
415
  | Autostart | `$XDG_CONFIG_HOME/autostart/refrain.desktop` (when enabled) |
359
416
 
@@ -386,7 +443,9 @@ directly to that provider:
386
443
  your **OS keyring** (encrypted at rest), never in `config.toml`.
387
444
 
388
445
  `Privacy → Off` is the global kill switch (no Discord status, no
389
- scrobbling) while keeping the tray + controls running.
446
+ scrobbling) while keeping the tray + controls running. The *Recently
447
+ played* list never leaves your machine, so it has its own switch in
448
+ *Settings → History* instead.
390
449
 
391
450
  Full data-flow, retention and erasure details — written to GDPR
392
451
  transparency expectations — are in [`PRIVACY.md`](PRIVACY.md).
@@ -536,16 +536,45 @@ were tracked internally as "v0.2.8" but never separately tagged.
536
536
  connected"; the Connect button no longer treats an incomplete
537
537
  leftover as Disconnect. +13 tests (pure + offscreen).
538
538
 
539
+ ## Done — v0.5.1
540
+
541
+ - **Recently played.** A history window (tray → *Recently played…*)
542
+ with cover, length, start time, source and Last.fm state per song;
543
+ Last.fm's rule for what counts; a click opens the song in Apple
544
+ Music in the browser that played it; search and a source filter;
545
+ remove single songs; survives restarts and crashes mid-song without
546
+ losing or duplicating the song. Own settings tab: on/off (off
547
+ deletes) and 10–100 songs.
548
+ - **The crash behind "Refrain was simply gone".** Two threads shared
549
+ one dbus-glib connection; the daemon's sources now keep private
550
+ connections, the published MPRIS player is only touched from its
551
+ dispatching thread, and `crash.log` records every thread's Python
552
+ stack if anything still crashes.
553
+ - **Cover and length for far more songs.** The catalog lookup asks the
554
+ local store first, strips "feat." and remaster tags, falls back to
555
+ the first artist, checks artist *and* title, retries network
556
+ failures and lets misses expire.
557
+ - **Plasma's Stop no longer starts playback**, and Play/Pause only act
558
+ towards the state they ask for.
559
+ - **One play, one row, one scrobble.** A song on repeat counts once per
560
+ play — in the browser, over Bluetooth, and on any player that jumps
561
+ back to the start. A restart or a crash mid-song carries the play on,
562
+ for the history and Last.fm alike (`scrobble_current.json`), and
563
+ Settings → Apply no longer starts a song's Last.fm count over.
564
+ - **Pausing in the browser is seen as a pause.** Plasma's browser
565
+ integration follows Apple Music's looping artwork video, not the
566
+ music; the tab's own MPRIS entry now decides playing or paused and
567
+ takes Play/Pause. Its segment positions are no longer shown, which
568
+ stops the elapsed time falling back every ten seconds.
569
+ - **Every shipped translation complete**, plural forms included
570
+ (English now carries its own plural catalog).
571
+
539
572
  ## Up next — v0.5.x
540
573
 
541
574
  - **Stable-release AUR build** that doesn't rely on the GitHub
542
575
  release tarball — switch to a `git`-source PKGBUILD pinned to the
543
576
  signed tag, so AUR users get the exact same commit the release
544
577
  workflow ships.
545
- - **Complete the translation catalogs** for the newly-wrapped
546
- strings: regenerate the `.ts` / `.qm` files and bring every
547
- shipped language back to 100% coverage via community PRs (the
548
- code-side `tr()` wrapping landed in v0.3.0).
549
578
 
550
579
  ## Maybe — v0.5+
551
580