refrain 0.5.2__tar.gz → 0.5.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. {refrain-0.5.2 → refrain-0.5.4}/.github/ISSUE_TEMPLATE/bug_report.yml +12 -3
  2. {refrain-0.5.2 → refrain-0.5.4}/.github/codeql/codeql-config.yml +1 -1
  3. {refrain-0.5.2 → refrain-0.5.4}/.github/dependabot.yml +4 -0
  4. {refrain-0.5.2 → refrain-0.5.4}/.github/pull_request_template.md +1 -0
  5. {refrain-0.5.2 → refrain-0.5.4}/.github/workflows/codeql.yml +3 -3
  6. refrain-0.5.4/.github/workflows/release.yml +351 -0
  7. {refrain-0.5.2 → refrain-0.5.4}/.github/workflows/security.yml +15 -10
  8. refrain-0.5.4/.github/workflows/tests.yml +157 -0
  9. {refrain-0.5.2 → refrain-0.5.4}/CHANGELOG.md +464 -10
  10. {refrain-0.5.2 → refrain-0.5.4}/CONTRIBUTING.md +44 -14
  11. {refrain-0.5.2 → refrain-0.5.4}/LEGAL.md +42 -14
  12. refrain-0.5.4/LICENSE +109 -0
  13. {refrain-0.5.2 → refrain-0.5.4}/Makefile +7 -1
  14. refrain-0.5.4/PKG-INFO +283 -0
  15. {refrain-0.5.2 → refrain-0.5.4}/PRIVACY.md +67 -28
  16. refrain-0.5.4/README.md +247 -0
  17. refrain-0.5.4/SECURITY.md +119 -0
  18. {refrain-0.5.2 → refrain-0.5.4}/docs/architecture.md +89 -46
  19. refrain-0.5.4/docs/bluetooth.md +152 -0
  20. refrain-0.5.4/docs/configuration.md +70 -0
  21. refrain-0.5.4/docs/developer-mode.md +139 -0
  22. {refrain-0.5.2 → refrain-0.5.4}/docs/faq.md +114 -47
  23. refrain-0.5.4/docs/install.md +113 -0
  24. refrain-0.5.4/docs/interface.md +140 -0
  25. {refrain-0.5.2 → refrain-0.5.4}/docs/lastfm.md +25 -10
  26. refrain-0.5.4/docs/releases.md +40 -0
  27. refrain-0.5.2/ROADMAP.md → refrain-0.5.4/docs/roadmap.md +47 -7
  28. refrain-0.5.4/docs/settings.md +101 -0
  29. refrain-0.5.4/docs/stats.py +245 -0
  30. {refrain-0.5.2 → refrain-0.5.4}/docs/test-matrix.md +41 -22
  31. refrain-0.5.4/docs/troubleshooting.md +395 -0
  32. refrain-0.5.4/docs/updates.md +34 -0
  33. refrain-0.5.4/packaging/README.md +294 -0
  34. refrain-0.5.4/packaging/appimage/AppImageBuilder.yml +64 -0
  35. refrain-0.5.4/packaging/appimage/prune_qt.py +95 -0
  36. refrain-0.5.4/packaging/appimage/third_party_notices.py +240 -0
  37. {refrain-0.5.2 → refrain-0.5.4}/packaging/aur/refrain/.SRCINFO +8 -5
  38. {refrain-0.5.2 → refrain-0.5.4}/packaging/aur/refrain/PKGBUILD +15 -9
  39. {refrain-0.5.2 → refrain-0.5.4}/packaging/aur/refrain-git/.SRCINFO +5 -3
  40. {refrain-0.5.2 → refrain-0.5.4}/packaging/aur/refrain-git/PKGBUILD +7 -3
  41. {refrain-0.5.2 → refrain-0.5.4}/packaging/flatpak/io.github.Rockykln.Refrain.metainfo.xml +37 -26
  42. {refrain-0.5.2 → refrain-0.5.4}/packaging/flatpak/io.github.Rockykln.Refrain.yaml +8 -3
  43. {refrain-0.5.2 → refrain-0.5.4}/packaging/flatpak/python-deps.json +3 -3
  44. refrain-0.5.4/packaging/release_key.py +263 -0
  45. {refrain-0.5.2 → refrain-0.5.4}/pyproject.toml +61 -20
  46. refrain-0.5.4/requirements-dev.lock +1148 -0
  47. refrain-0.5.4/requirements-lowest.txt +4 -0
  48. refrain-0.5.4/requirements.txt +3 -0
  49. refrain-0.5.4/src/refrain/__init__.py +1 -0
  50. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/app.py +436 -109
  51. refrain-0.5.4/src/refrain/assets/icons/chronometer.svg +6 -0
  52. refrain-0.5.4/src/refrain/assets/icons/tray-offline-dark.svg +4 -0
  53. refrain-0.5.4/src/refrain/assets/icons/tray-offline.svg +4 -0
  54. refrain-0.5.4/src/refrain/assets/icons/tray-paused-dark.svg +4 -0
  55. refrain-0.5.4/src/refrain/assets/icons/tray-paused.svg +4 -0
  56. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/assets/icons/tray-playing-dark.svg +1 -1
  57. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/assets/icons/tray-playing.svg +1 -1
  58. refrain-0.5.4/src/refrain/assets/icons/tray-private-dark.svg +8 -0
  59. refrain-0.5.4/src/refrain/assets/icons/tray-private.svg +8 -0
  60. refrain-0.5.4/src/refrain/assets/icons/tray-stopped-dark.svg +3 -0
  61. refrain-0.5.4/src/refrain/assets/icons/tray-stopped.svg +3 -0
  62. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/autostart.py +19 -7
  63. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/browser.py +2 -9
  64. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/config.py +291 -50
  65. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/cover_art.py +124 -45
  66. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/cover_fetcher.py +165 -65
  67. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/daemon.py +723 -221
  68. refrain-0.5.4/src/refrain/dev_metrics.py +559 -0
  69. refrain-0.5.4/src/refrain/diagnostics.py +170 -0
  70. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/discord_app.py +44 -37
  71. refrain-0.5.4/src/refrain/discord_rpc.py +771 -0
  72. refrain-0.5.4/src/refrain/ed25519.py +106 -0
  73. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/history.py +158 -69
  74. refrain-0.5.4/src/refrain/https_only.py +17 -0
  75. refrain-0.5.4/src/refrain/i18n/refrain_cs.qm +0 -0
  76. refrain-0.5.4/src/refrain/i18n/refrain_cs.ts +2237 -0
  77. refrain-0.5.4/src/refrain/i18n/refrain_de.qm +0 -0
  78. refrain-0.5.4/src/refrain/i18n/refrain_de.ts +2227 -0
  79. refrain-0.5.4/src/refrain/i18n/refrain_en.qm +0 -0
  80. refrain-0.5.4/src/refrain/i18n/refrain_en.ts +96 -0
  81. refrain-0.5.4/src/refrain/i18n/refrain_es.qm +0 -0
  82. refrain-0.5.4/src/refrain/i18n/refrain_es.ts +2227 -0
  83. refrain-0.5.4/src/refrain/i18n/refrain_fr.qm +0 -0
  84. refrain-0.5.4/src/refrain/i18n/refrain_fr.ts +2227 -0
  85. refrain-0.5.4/src/refrain/i18n/refrain_it.qm +0 -0
  86. refrain-0.5.4/src/refrain/i18n/refrain_it.ts +2227 -0
  87. refrain-0.5.4/src/refrain/i18n/refrain_ja.qm +0 -0
  88. refrain-0.5.4/src/refrain/i18n/refrain_ja.ts +2217 -0
  89. refrain-0.5.4/src/refrain/i18n/refrain_ko.qm +0 -0
  90. refrain-0.5.4/src/refrain/i18n/refrain_ko.ts +2217 -0
  91. refrain-0.5.4/src/refrain/i18n/refrain_nl.qm +0 -0
  92. refrain-0.5.4/src/refrain/i18n/refrain_nl.ts +2227 -0
  93. refrain-0.5.4/src/refrain/i18n/refrain_pl.qm +0 -0
  94. refrain-0.5.4/src/refrain/i18n/refrain_pl.ts +2237 -0
  95. refrain-0.5.4/src/refrain/i18n/refrain_pt.qm +0 -0
  96. refrain-0.5.4/src/refrain/i18n/refrain_pt.ts +2227 -0
  97. refrain-0.5.4/src/refrain/i18n/refrain_ru.qm +0 -0
  98. refrain-0.5.4/src/refrain/i18n/refrain_ru.ts +2237 -0
  99. refrain-0.5.4/src/refrain/i18n/refrain_sv.qm +0 -0
  100. refrain-0.5.4/src/refrain/i18n/refrain_sv.ts +2227 -0
  101. refrain-0.5.4/src/refrain/i18n/refrain_tr.qm +0 -0
  102. refrain-0.5.4/src/refrain/i18n/refrain_tr.ts +2217 -0
  103. refrain-0.5.4/src/refrain/i18n/refrain_uk.qm +0 -0
  104. refrain-0.5.4/src/refrain/i18n/refrain_uk.ts +2237 -0
  105. refrain-0.5.4/src/refrain/i18n/refrain_zh_CN.qm +0 -0
  106. refrain-0.5.4/src/refrain/i18n/refrain_zh_CN.ts +2217 -0
  107. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/logging_setup.py +29 -3
  108. refrain-0.5.4/src/refrain/paths.py +104 -0
  109. refrain-0.5.4/src/refrain/qt_libraries.py +115 -0
  110. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/scrobble.py +156 -71
  111. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/scrobble_queue.py +78 -40
  112. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/secrets_store.py +30 -41
  113. refrain-0.5.4/src/refrain/service_status.py +185 -0
  114. refrain-0.5.4/src/refrain/single_instance.py +114 -0
  115. refrain-0.5.4/src/refrain/song_lengths.py +222 -0
  116. refrain-0.5.4/src/refrain/sources/base.py +78 -0
  117. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/sources/bluetooth.py +113 -83
  118. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/sources/mpris.py +288 -120
  119. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/sources/mpris_server.py +34 -32
  120. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/startup_check.py +3 -17
  121. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/timing.py +127 -72
  122. refrain-0.5.4/src/refrain/ui/clock.py +43 -0
  123. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/ui/cursors.py +33 -20
  124. refrain-0.5.4/src/refrain/ui/external_link.py +33 -0
  125. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/ui/history_window.py +222 -95
  126. refrain-0.5.4/src/refrain/ui/icons.py +52 -0
  127. refrain-0.5.4/src/refrain/ui/layout_check.py +280 -0
  128. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/ui/legal_dialog.py +58 -36
  129. refrain-0.5.4/src/refrain/ui/log_window.py +494 -0
  130. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/ui/settings_window.py +877 -341
  131. refrain-0.5.4/src/refrain/ui/status_window.py +999 -0
  132. refrain-0.5.4/src/refrain/ui/tooltips.py +57 -0
  133. refrain-0.5.4/src/refrain/ui/tray.py +525 -0
  134. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/ui/update_dialog.py +51 -20
  135. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/ui/welcome_dialog.py +131 -54
  136. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/uninstall.py +11 -27
  137. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/updater.py +250 -67
  138. refrain-0.5.4/tests/conftest.py +115 -0
  139. refrain-0.5.4/tests/daemon_fakes.py +364 -0
  140. refrain-0.5.4/tests/integration/__init__.py +0 -0
  141. refrain-0.5.4/tests/integration/conftest.py +241 -0
  142. refrain-0.5.4/tests/integration/fake_mpris_player.py +189 -0
  143. refrain-0.5.4/tests/integration/test_mpris_live_dbus.py +129 -0
  144. refrain-0.5.4/tests/release_signing.py +18 -0
  145. refrain-0.5.4/tests/test_album_fill_in.py +152 -0
  146. refrain-0.5.4/tests/test_app_cli.py +184 -0
  147. refrain-0.5.4/tests/test_app_edges.py +260 -0
  148. refrain-0.5.4/tests/test_app_helpers.py +247 -0
  149. refrain-0.5.4/tests/test_app_main.py +1326 -0
  150. {refrain-0.5.2 → refrain-0.5.4}/tests/test_autostart.py +31 -0
  151. refrain-0.5.4/tests/test_autostart_more.py +107 -0
  152. refrain-0.5.4/tests/test_bluetooth_apps.py +105 -0
  153. refrain-0.5.4/tests/test_bluetooth_source.py +518 -0
  154. refrain-0.5.4/tests/test_browser.py +128 -0
  155. refrain-0.5.4/tests/test_browser_edges.py +28 -0
  156. {refrain-0.5.2 → refrain-0.5.4}/tests/test_button_url.py +5 -6
  157. refrain-0.5.4/tests/test_clock.py +57 -0
  158. {refrain-0.5.2 → refrain-0.5.4}/tests/test_config.py +106 -23
  159. refrain-0.5.4/tests/test_config_edges.py +163 -0
  160. refrain-0.5.4/tests/test_config_notifications.py +40 -0
  161. {refrain-0.5.2 → refrain-0.5.4}/tests/test_config_preserve.py +2 -6
  162. refrain-0.5.4/tests/test_config_validation.py +230 -0
  163. {refrain-0.5.2 → refrain-0.5.4}/tests/test_cover_art.py +77 -39
  164. refrain-0.5.4/tests/test_cover_art_edges.py +79 -0
  165. refrain-0.5.4/tests/test_cover_art_more.py +214 -0
  166. {refrain-0.5.2 → refrain-0.5.4}/tests/test_cover_fetcher.py +18 -34
  167. refrain-0.5.4/tests/test_cover_fetcher_cache.py +225 -0
  168. refrain-0.5.4/tests/test_cover_fetcher_edges.py +105 -0
  169. refrain-0.5.4/tests/test_cover_history.py +150 -0
  170. {refrain-0.5.2 → refrain-0.5.4}/tests/test_crash_log.py +2 -2
  171. {refrain-0.5.2 → refrain-0.5.4}/tests/test_daemon_album_format.py +3 -8
  172. refrain-0.5.4/tests/test_daemon_discord_artwork.py +108 -0
  173. refrain-0.5.4/tests/test_daemon_edges.py +131 -0
  174. {refrain-0.5.2 → refrain-0.5.4}/tests/test_daemon_idle.py +8 -26
  175. refrain-0.5.4/tests/test_daemon_lastfm_length.py +214 -0
  176. refrain-0.5.4/tests/test_daemon_length_rule.py +116 -0
  177. refrain-0.5.4/tests/test_daemon_lifecycle.py +513 -0
  178. refrain-0.5.4/tests/test_daemon_paused_rule.py +94 -0
  179. refrain-0.5.4/tests/test_daemon_pipeline.py +736 -0
  180. refrain-0.5.4/tests/test_daemon_restart_estimate.py +123 -0
  181. refrain-0.5.4/tests/test_daemon_service_states.py +143 -0
  182. refrain-0.5.4/tests/test_daemon_song_identity.py +136 -0
  183. refrain-0.5.4/tests/test_daemon_stop.py +45 -0
  184. refrain-0.5.4/tests/test_daemon_tick_lookups.py +120 -0
  185. {refrain-0.5.2 → refrain-0.5.4}/tests/test_dbus_dispatch.py +8 -35
  186. {refrain-0.5.2 → refrain-0.5.4}/tests/test_desktop_entries.py +19 -0
  187. refrain-0.5.4/tests/test_dev_metrics.py +440 -0
  188. refrain-0.5.4/tests/test_developer_mode_ui.py +386 -0
  189. refrain-0.5.4/tests/test_diagnostics.py +247 -0
  190. {refrain-0.5.2 → refrain-0.5.4}/tests/test_discord_app.py +87 -50
  191. refrain-0.5.4/tests/test_discord_app_edges.py +26 -0
  192. {refrain-0.5.2 → refrain-0.5.4}/tests/test_discord_ipc_scan.py +2 -17
  193. {refrain-0.5.2 → refrain-0.5.4}/tests/test_discord_listening.py +1 -6
  194. {refrain-0.5.2 → refrain-0.5.4}/tests/test_discord_multi_client.py +15 -33
  195. refrain-0.5.4/tests/test_discord_rpc.py +285 -0
  196. refrain-0.5.4/tests/test_discord_rpc_connect.py +442 -0
  197. refrain-0.5.4/tests/test_discord_rpc_fd_leak.py +90 -0
  198. refrain-0.5.4/tests/test_discord_rpc_robustness.py +699 -0
  199. refrain-0.5.4/tests/test_ed25519.py +183 -0
  200. refrain-0.5.4/tests/test_ed25519_edge_cases.py +145 -0
  201. refrain-0.5.4/tests/test_external_link.py +58 -0
  202. refrain-0.5.4/tests/test_foreign_locales.py +88 -0
  203. {refrain-0.5.2 → refrain-0.5.4}/tests/test_history.py +148 -11
  204. refrain-0.5.4/tests/test_history_edges.py +261 -0
  205. {refrain-0.5.2 → refrain-0.5.4}/tests/test_history_window.py +236 -22
  206. refrain-0.5.4/tests/test_history_window_edges.py +218 -0
  207. refrain-0.5.4/tests/test_icons.py +273 -0
  208. {refrain-0.5.2 → refrain-0.5.4}/tests/test_imports.py +2 -7
  209. refrain-0.5.4/tests/test_layout_check.py +341 -0
  210. refrain-0.5.4/tests/test_layout_languages.py +113 -0
  211. {refrain-0.5.2 → refrain-0.5.4}/tests/test_legal_notice.py +6 -14
  212. refrain-0.5.4/tests/test_log_window_filter.py +47 -0
  213. refrain-0.5.4/tests/test_logging_setup.py +128 -0
  214. refrain-0.5.4/tests/test_logging_setup_edges.py +59 -0
  215. refrain-0.5.4/tests/test_mpris_controls.py +137 -0
  216. {refrain-0.5.2 → refrain-0.5.4}/tests/test_mpris_dispatch.py +28 -37
  217. refrain-0.5.4/tests/test_mpris_edges.py +120 -0
  218. refrain-0.5.4/tests/test_mpris_server_props.py +381 -0
  219. refrain-0.5.4/tests/test_mpris_source_more.py +700 -0
  220. refrain-0.5.4/tests/test_notify_argv.py +126 -0
  221. {refrain-0.5.2 → refrain-0.5.4}/tests/test_paths.py +21 -0
  222. {refrain-0.5.2 → refrain-0.5.4}/tests/test_position_resolver.py +159 -84
  223. refrain-0.5.4/tests/test_private_files.py +89 -0
  224. refrain-0.5.4/tests/test_private_writes.py +107 -0
  225. refrain-0.5.4/tests/test_qt_libraries.py +95 -0
  226. refrain-0.5.4/tests/test_qt_libraries_more.py +85 -0
  227. {refrain-0.5.2 → refrain-0.5.4}/tests/test_qt_plugin_path.py +5 -23
  228. refrain-0.5.4/tests/test_readme_stats.py +101 -0
  229. refrain-0.5.4/tests/test_release_key.py +214 -0
  230. {refrain-0.5.2 → refrain-0.5.4}/tests/test_scrobble.py +1 -5
  231. refrain-0.5.4/tests/test_scrobble_edges.py +50 -0
  232. refrain-0.5.4/tests/test_scrobble_more.py +309 -0
  233. refrain-0.5.4/tests/test_scrobble_queue_edges.py +75 -0
  234. refrain-0.5.4/tests/test_scrobble_queue_file.py +194 -0
  235. {refrain-0.5.2 → refrain-0.5.4}/tests/test_scrobbler.py +59 -32
  236. refrain-0.5.4/tests/test_sdist_contents.py +45 -0
  237. {refrain-0.5.2 → refrain-0.5.4}/tests/test_secrets_store.py +118 -20
  238. refrain-0.5.4/tests/test_secrets_store_edges.py +32 -0
  239. refrain-0.5.4/tests/test_secrets_store_more.py +234 -0
  240. refrain-0.5.4/tests/test_service_status.py +122 -0
  241. refrain-0.5.4/tests/test_settings_app_name.py +104 -0
  242. refrain-0.5.4/tests/test_settings_apply.py +352 -0
  243. {refrain-0.5.2 → refrain-0.5.4}/tests/test_settings_github_icon.py +18 -0
  244. {refrain-0.5.2 → refrain-0.5.4}/tests/test_settings_history.py +3 -6
  245. refrain-0.5.4/tests/test_settings_lastfm.py +226 -0
  246. refrain-0.5.4/tests/test_settings_more.py +395 -0
  247. {refrain-0.5.2 → refrain-0.5.4}/tests/test_settings_reset.py +5 -16
  248. refrain-0.5.4/tests/test_settings_unsaved.py +607 -0
  249. refrain-0.5.4/tests/test_settings_window_edges.py +273 -0
  250. refrain-0.5.4/tests/test_single_instance.py +117 -0
  251. refrain-0.5.4/tests/test_single_instance_errors.py +41 -0
  252. refrain-0.5.4/tests/test_song_lengths.py +320 -0
  253. refrain-0.5.4/tests/test_song_lengths_edges.py +33 -0
  254. refrain-0.5.4/tests/test_song_lengths_pins.py +410 -0
  255. refrain-0.5.4/tests/test_source_field_limits.py +56 -0
  256. {refrain-0.5.2 → refrain-0.5.4}/tests/test_source_selection.py +2 -7
  257. {refrain-0.5.2 → refrain-0.5.4}/tests/test_sources_base.py +28 -0
  258. {refrain-0.5.2 → refrain-0.5.4}/tests/test_startup_check.py +6 -19
  259. refrain-0.5.4/tests/test_status_window.py +705 -0
  260. refrain-0.5.4/tests/test_system_certificates.py +57 -0
  261. refrain-0.5.4/tests/test_theme_colors.py +69 -0
  262. {refrain-0.5.2 → refrain-0.5.4}/tests/test_timing.py +33 -31
  263. {refrain-0.5.2 → refrain-0.5.4}/tests/test_translations.py +17 -7
  264. {refrain-0.5.2 → refrain-0.5.4}/tests/test_tray_init.py +18 -34
  265. refrain-0.5.4/tests/test_tray_states.py +510 -0
  266. {refrain-0.5.2 → refrain-0.5.4}/tests/test_ui_cursors.py +70 -21
  267. refrain-0.5.4/tests/test_ui_edges.py +279 -0
  268. {refrain-0.5.2 → refrain-0.5.4}/tests/test_ui_locale.py +6 -13
  269. {refrain-0.5.2 → refrain-0.5.4}/tests/test_uninstall.py +41 -14
  270. refrain-0.5.4/tests/test_update_dialog_cancel.py +133 -0
  271. refrain-0.5.4/tests/test_update_dialog_flows.py +210 -0
  272. refrain-0.5.4/tests/test_update_orchestrator.py +212 -0
  273. refrain-0.5.4/tests/test_updater.py +791 -0
  274. refrain-0.5.4/tests/test_updater_download_limits.py +186 -0
  275. refrain-0.5.4/tests/test_updater_paths.py +495 -0
  276. refrain-0.5.4/tests/test_updater_subprocess_calls.py +149 -0
  277. {refrain-0.5.2 → refrain-0.5.4}/tests/test_welcome_dialog_growth.py +2 -13
  278. refrain-0.5.4/tests/test_welcome_probes.py +387 -0
  279. refrain-0.5.2/.github/workflows/release.yml +0 -181
  280. refrain-0.5.2/.github/workflows/tests.yml +0 -68
  281. refrain-0.5.2/LICENSE +0 -71
  282. refrain-0.5.2/PKG-INFO +0 -539
  283. refrain-0.5.2/README.md +0 -505
  284. refrain-0.5.2/SECURITY.md +0 -72
  285. refrain-0.5.2/docs/bluetooth.md +0 -141
  286. refrain-0.5.2/docs/screenshots/README.md +0 -75
  287. refrain-0.5.2/docs/screenshots/demo-cover.png +0 -0
  288. refrain-0.5.2/docs/screenshots/discord-rpc.png +0 -0
  289. refrain-0.5.2/docs/screenshots/history.png +0 -0
  290. refrain-0.5.2/docs/screenshots/legal.png +0 -0
  291. refrain-0.5.2/docs/screenshots/live-log.png +0 -0
  292. refrain-0.5.2/docs/screenshots/notification.png +0 -0
  293. refrain-0.5.2/docs/screenshots/settings-advanced.png +0 -0
  294. refrain-0.5.2/docs/screenshots/settings-general.png +0 -0
  295. refrain-0.5.2/docs/screenshots/settings-history.png +0 -0
  296. refrain-0.5.2/docs/screenshots/settings-lastfm.png +0 -0
  297. refrain-0.5.2/docs/screenshots/settings-sources.png +0 -0
  298. refrain-0.5.2/docs/screenshots/settings-updates.png +0 -0
  299. refrain-0.5.2/docs/screenshots/tray-menu.png +0 -0
  300. refrain-0.5.2/docs/screenshots/update-dialog.png +0 -0
  301. refrain-0.5.2/docs/screenshots/welcome.png +0 -0
  302. refrain-0.5.2/packaging/README.md +0 -192
  303. refrain-0.5.2/packaging/appimage/AppImageBuilder.yml +0 -80
  304. refrain-0.5.2/requirements.txt +0 -3
  305. refrain-0.5.2/src/refrain/__init__.py +0 -1
  306. refrain-0.5.2/src/refrain/assets/icons/tray-paused-dark.svg +0 -4
  307. refrain-0.5.2/src/refrain/assets/icons/tray-paused.svg +0 -4
  308. refrain-0.5.2/src/refrain/assets/icons/tray-stopped-dark.svg +0 -3
  309. refrain-0.5.2/src/refrain/assets/icons/tray-stopped.svg +0 -3
  310. refrain-0.5.2/src/refrain/discord_rpc.py +0 -399
  311. refrain-0.5.2/src/refrain/i18n/refrain_de.qm +0 -0
  312. refrain-0.5.2/src/refrain/i18n/refrain_de.ts +0 -1274
  313. refrain-0.5.2/src/refrain/i18n/refrain_en.qm +0 -0
  314. refrain-0.5.2/src/refrain/i18n/refrain_en.ts +0 -47
  315. refrain-0.5.2/src/refrain/i18n/refrain_es.qm +0 -0
  316. refrain-0.5.2/src/refrain/i18n/refrain_es.ts +0 -1274
  317. refrain-0.5.2/src/refrain/i18n/refrain_fr.qm +0 -0
  318. refrain-0.5.2/src/refrain/i18n/refrain_fr.ts +0 -1274
  319. refrain-0.5.2/src/refrain/i18n/refrain_it.qm +0 -0
  320. refrain-0.5.2/src/refrain/i18n/refrain_it.ts +0 -1274
  321. refrain-0.5.2/src/refrain/i18n/refrain_ja.qm +0 -0
  322. refrain-0.5.2/src/refrain/i18n/refrain_ja.ts +0 -1269
  323. refrain-0.5.2/src/refrain/i18n/refrain_pl.qm +0 -0
  324. refrain-0.5.2/src/refrain/i18n/refrain_pl.ts +0 -1279
  325. refrain-0.5.2/src/refrain/i18n/refrain_pt.qm +0 -0
  326. refrain-0.5.2/src/refrain/i18n/refrain_pt.ts +0 -1274
  327. refrain-0.5.2/src/refrain/i18n/refrain_ru.qm +0 -0
  328. refrain-0.5.2/src/refrain/i18n/refrain_ru.ts +0 -1279
  329. refrain-0.5.2/src/refrain/i18n/refrain_zh_CN.qm +0 -0
  330. refrain-0.5.2/src/refrain/i18n/refrain_zh_CN.ts +0 -1269
  331. refrain-0.5.2/src/refrain/paths.py +0 -46
  332. refrain-0.5.2/src/refrain/single_instance.py +0 -53
  333. refrain-0.5.2/src/refrain/song_lengths.py +0 -144
  334. refrain-0.5.2/src/refrain/sources/base.py +0 -43
  335. refrain-0.5.2/src/refrain/ui/log_window.py +0 -122
  336. refrain-0.5.2/src/refrain/ui/tray.py +0 -390
  337. refrain-0.5.2/tests/conftest.py +0 -69
  338. refrain-0.5.2/tests/test_bluetooth_apps.py +0 -70
  339. refrain-0.5.2/tests/test_browser.py +0 -57
  340. refrain-0.5.2/tests/test_discord_rpc.py +0 -180
  341. refrain-0.5.2/tests/test_mpris_controls.py +0 -57
  342. refrain-0.5.2/tests/test_notify_argv.py +0 -62
  343. refrain-0.5.2/tests/test_settings_lastfm.py +0 -133
  344. refrain-0.5.2/tests/test_song_lengths.py +0 -164
  345. refrain-0.5.2/tests/test_updater.py +0 -320
  346. {refrain-0.5.2 → refrain-0.5.4}/.editorconfig +0 -0
  347. {refrain-0.5.2 → refrain-0.5.4}/.gitattributes +0 -0
  348. {refrain-0.5.2 → refrain-0.5.4}/.github/FUNDING.yml +0 -0
  349. {refrain-0.5.2 → refrain-0.5.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  350. {refrain-0.5.2 → refrain-0.5.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  351. {refrain-0.5.2 → refrain-0.5.4}/.gitignore +0 -0
  352. {refrain-0.5.2 → refrain-0.5.4}/CODE_OF_CONDUCT.md +0 -0
  353. {refrain-0.5.2 → refrain-0.5.4}/packaging/flatpak/io.github.Rockykln.Refrain.desktop +0 -0
  354. {refrain-0.5.2 → refrain-0.5.4}/requirements-dev.txt +0 -0
  355. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/__main__.py +0 -0
  356. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/assets/icons/github-mark.svg +0 -0
  357. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/assets/icons/menu-quit.svg +0 -0
  358. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/assets/icons/menu-update.svg +0 -0
  359. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/assets/icons/refrain.png +0 -0
  360. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/assets/icons/refrain.svg +0 -0
  361. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/assets/refrain.desktop +0 -0
  362. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/sources/__init__.py +0 -0
  363. {refrain-0.5.2 → refrain-0.5.4}/src/refrain/ui/__init__.py +0 -0
  364. {refrain-0.5.2 → refrain-0.5.4}/tests/__init__.py +0 -0
  365. {refrain-0.5.2 → refrain-0.5.4}/tests/test_scrobble_queue.py +0 -0
  366. {refrain-0.5.2 → refrain-0.5.4}/tests/test_update_dialog_body.py +0 -0
@@ -13,7 +13,7 @@ body:
13
13
  attributes:
14
14
  label: Refrain version
15
15
  description: Run `refrain --version` or check the bottom-right of the settings window.
16
- placeholder: "0.5.2"
16
+ placeholder: "0.5.4"
17
17
  validations:
18
18
  required: true
19
19
 
@@ -61,14 +61,23 @@ body:
61
61
  validations:
62
62
  required: true
63
63
 
64
+ - type: markdown
65
+ attributes:
66
+ value: |
67
+ **Before you paste a log:** issues are public. The log lists the songs you
68
+ played (your listening history) and, in debug mode, the address of your
69
+ Bluetooth device. Read it first and remove anything you don't want public,
70
+ or mail the log to [contact@rockykln.com](mailto:contact@rockykln.com) instead
71
+ and mention your issue number.
72
+
64
73
  - type: textarea
65
74
  id: logs
66
75
  attributes:
67
76
  label: Refrain log
68
77
  description: |
69
78
  Paste the relevant portion of `~/.local/state/refrain/refrain.log`.
70
- Tracks playing at the time of the bug are useful context but no secrets
71
- live in the log.
79
+ It holds no passwords or tokens, but it does contain song titles and,
80
+ in debug mode, Bluetooth device addresses — check it before posting.
72
81
  render: shell
73
82
  validations:
74
83
  required: false
@@ -9,7 +9,7 @@ queries:
9
9
  - uses: security-and-quality
10
10
 
11
11
  query-filters:
12
- # Empty `except` blocks are deliberate in best-effort cleanup paths
12
+ # Empty `except` blocks are deliberate in cleanup paths
13
13
  # (DBus disconnects, Discord IPC, tray icon teardown). Each site has
14
14
  # context in surrounding code; the CodeQL note here would be repeated
15
15
  # ~10× without adding signal.
@@ -4,6 +4,10 @@ updates:
4
4
  # Python dependencies (pip — reads requirements*.txt + pyproject.toml)
5
5
  - package-ecosystem: "pip"
6
6
  directory: "/"
7
+ # The lowest supported versions, tested as such by the "lowest" job:
8
+ # raising them would stop testing the floor pyproject.toml promises.
9
+ exclude-paths:
10
+ - "requirements-lowest.txt"
7
11
  schedule:
8
12
  interval: "weekly"
9
13
  day: "monday"
@@ -34,6 +34,7 @@ whatever's relevant and delete the rest.
34
34
 
35
35
  ## Checklist
36
36
 
37
+ - [ ] I agree that my contribution is licensed as described in the "Contributions" section of LICENSE (summary in CONTRIBUTING.md)
37
38
  - [ ] `pytest` passes locally
38
39
  - [ ] `ruff check .` and `ruff format --check .` are clean
39
40
  - [ ] New / changed behavior is covered by tests (or unreachable from tests due to system integration — explain)
@@ -27,15 +27,15 @@ jobs:
27
27
  language: [python]
28
28
 
29
29
  steps:
30
- - uses: actions/checkout@v7
30
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
31
31
 
32
32
  - name: Initialize CodeQL
33
- uses: github/codeql-action/init@v4
33
+ uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
34
34
  with:
35
35
  languages: ${{ matrix.language }}
36
36
  config-file: ./.github/codeql/codeql-config.yml
37
37
 
38
38
  - name: Perform CodeQL analysis
39
- uses: github/codeql-action/analyze@v4
39
+ uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
40
40
  with:
41
41
  category: "/language:${{ matrix.language }}"
@@ -0,0 +1,351 @@
1
+ name: release
2
+
3
+ # Triggered when a semver tag is pushed: e.g. `git tag v0.2.0 && git push --tags`
4
+ on:
5
+ push:
6
+ tags:
7
+ - "v*.*.*"
8
+
9
+ # The AppImage build tools are installed unpinned, so the job that runs them
10
+ # only gets read access; the write and OIDC permissions sit on the small
11
+ # attestation and publishing jobs below.
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-24.04
18
+
19
+ steps:
20
+ - name: Checkout (full history for changelog)
21
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
+ with:
23
+ fetch-depth: 0
24
+
25
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
26
+ with:
27
+ python-version: "3.12"
28
+ cache: pip
29
+ cache-dependency-path: requirements-dev.lock
30
+
31
+ - name: Install + run tests (gate the release)
32
+ run: |
33
+ # System libs: dbus-python needs the *-dev headers at build
34
+ # time; PySide6.QtWidgets needs the EGL/xkbcommon/GL/fontconfig
35
+ # runtime libs to import (even under QT_QPA_PLATFORM=offscreen).
36
+ sudo apt-get update
37
+ sudo apt-get install -y \
38
+ libdbus-1-dev libglib2.0-dev pkg-config \
39
+ libegl1 libxkbcommon0 libfontconfig1 libgl1
40
+ # The pinned, hash-checked tool and runtime versions, then
41
+ # Refrain itself on top.
42
+ pip install --require-hashes -r requirements-dev.lock
43
+ pip install --no-deps -e .
44
+ pytest -q
45
+
46
+ - name: Verify tag matches package version
47
+ run: |
48
+ TAG="${GITHUB_REF#refs/tags/v}"
49
+ INIT_VERSION=$(python -c "import sys; sys.path.insert(0,'src'); import refrain; print(refrain.__version__)")
50
+ PYPROJECT_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
51
+ if [ "$TAG" != "$INIT_VERSION" ] || [ "$TAG" != "$PYPROJECT_VERSION" ]; then
52
+ echo "::error::Version mismatch — tag=v$TAG, __init__.py=$INIT_VERSION, pyproject.toml=$PYPROJECT_VERSION"
53
+ exit 1
54
+ fi
55
+ echo "Releasing v$TAG"
56
+
57
+ - name: Verify CHANGELOG has a matching entry for this tag
58
+ run: |
59
+ TAG="${GITHUB_REF#refs/tags/v}"
60
+ if ! grep -qE "^## \[${TAG}\]" CHANGELOG.md; then
61
+ echo "::error::CHANGELOG.md is missing a '## [${TAG}]' section"
62
+ exit 1
63
+ fi
64
+
65
+ - name: Verify Flatpak manifest pins this tag
66
+ # The Flatpak build source is a pinned tag — it isn't
67
+ # rebuilt/published by this workflow (no live Flathub channel),
68
+ # so nothing else catches it going stale. It silently sat on
69
+ # v0.1.4 from then until v0.4.2. This gate makes a stale pin a
70
+ # hard release failure, like the version/CHANGELOG gates above.
71
+ run: |
72
+ TAG="${GITHUB_REF#refs/tags/v}"
73
+ MANIFEST=packaging/flatpak/io.github.Rockykln.Refrain.yaml
74
+ if ! grep -qE "^ tag: v${TAG}$" "$MANIFEST"; then
75
+ echo "::error::$MANIFEST is not pinned to tag v${TAG}"
76
+ exit 1
77
+ fi
78
+
79
+ - name: Verify the metainfo lists this release
80
+ # Flathub's own CI rejects a build without it, but there is no live
81
+ # Flathub channel yet, so nothing else would notice this going stale.
82
+ run: |
83
+ TAG="${GITHUB_REF#refs/tags/v}"
84
+ METAINFO=packaging/flatpak/io.github.Rockykln.Refrain.metainfo.xml
85
+ if ! grep -qE "<release version=\"${TAG}\" date=" "$METAINFO"; then
86
+ echo "::error::$METAINFO has no <release> entry for ${TAG}"
87
+ exit 1
88
+ fi
89
+
90
+ - name: Build wheel + sdist
91
+ run: |
92
+ python -m build
93
+ # Keep a clean copy of just the Python distributions for PyPI;
94
+ # the AppImage step writes additional files into dist/ later.
95
+ mkdir -p pypi-dist
96
+ cp dist/*.whl dist/*.tar.gz pypi-dist/
97
+ ls -la pypi-dist/
98
+
99
+ # pip-audit is already installed from requirements-dev.lock (dev extra) by
100
+ # the test step above, and doubles as a CycloneDX generator when pointed
101
+ # at a directory instead of the live environment — no new dependency.
102
+ # The target dir is a plain `pip install`, not the test venv, so the
103
+ # SBOM lists exactly what `pip install refrain` pulls in, not the dev
104
+ # tooling installed for the test run.
105
+ - name: Generate SBOM (CycloneDX) for the PyPI package
106
+ run: |
107
+ TAG="${GITHUB_REF#refs/tags/v}"
108
+ rm -rf /tmp/refrain-sbom-pypi
109
+ pip install --target /tmp/refrain-sbom-pypi dist/*.whl
110
+ sbom="dist/Refrain-${TAG}.cdx.json"
111
+ # Don't let a found (not induced) vulnerability fail the release build —
112
+ # that gate already exists as the pip-audit job in security.yml.
113
+ set +e
114
+ pip-audit --path /tmp/refrain-sbom-pypi --format=cyclonedx-json -o "$sbom"
115
+ rc=$?
116
+ set -e
117
+ if [ ! -s "$sbom" ]; then
118
+ echo "::error::SBOM generation produced no output"
119
+ exit 1
120
+ fi
121
+ if [ "$rc" -ne 0 ]; then
122
+ echo "::warning::pip-audit found known vulnerabilities while building the SBOM"
123
+ fi
124
+ rm -rf /tmp/refrain-sbom-pypi
125
+
126
+ # Single-file AppImage so end users can grab one binary from the
127
+ # Releases page and run it without installing anything else.
128
+ # The recipe installs Refrain with the runner's /usr/bin/python3,
129
+ # which must match the Python it bundles from Ubuntu 24.04.
130
+ - name: Install AppImage build deps
131
+ run: |
132
+ sudo apt-get update
133
+ sudo apt-get install -y \
134
+ python3-pip libfuse2t64 desktop-file-utils \
135
+ zsync appstream patchelf squashfs-tools file
136
+ # appimage-builder can't parse Ubuntu package versions with newer
137
+ # packaging. Both are pinned: whatever they produce is signed by
138
+ # hand afterwards, so the build must not float.
139
+ pip install "appimage-builder==1.1.0" "packaging==21.3"
140
+
141
+ - name: Build AppImage
142
+ id: appimage
143
+ run: |
144
+ TAG="${GITHUB_REF#refs/tags/v}"
145
+ # The recipe pins a literal `version:` value, which determines the
146
+ # output filename (Refrain-<version>-x86_64.AppImage). Rewrite it
147
+ # to match the tag so we don't hand-edit the recipe per release.
148
+ sed -i -E "s/^( version: ).*/\1${TAG}/" packaging/appimage/AppImageBuilder.yml
149
+ grep " version:" packaging/appimage/AppImageBuilder.yml
150
+ cd packaging/appimage
151
+ appimage-builder --recipe AppImageBuilder.yml --skip-test
152
+ mkdir -p ../../dist
153
+ mv *.AppImage ../../dist/ 2>/dev/null || true
154
+ mv *.AppImage.zsync ../../dist/ 2>/dev/null || true
155
+ ls -la ../../dist/
156
+
157
+ # Every AppImage up to 0.5.2 was published without ever starting.
158
+ - name: Start the AppImage once
159
+ run: |
160
+ TAG="${GITHUB_REF#refs/tags/v}"
161
+ out="$(./dist/Refrain-${TAG}-x86_64.AppImage --appimage-extract-and-run --version)"
162
+ echo "$out"
163
+ [ "$out" = "refrain ${TAG}" ]
164
+
165
+ - name: Check licence notices and Qt modules in the AppImage
166
+ run: |
167
+ TAG="${GITHUB_REF#refs/tags/v}"
168
+ tmp="$(mktemp -d)"
169
+ (cd "$tmp" && "$GITHUB_WORKSPACE/dist/Refrain-${TAG}-x86_64.AppImage" --appimage-extract >/dev/null)
170
+ doc="$tmp/squashfs-root/usr/share/doc/refrain"
171
+ grep -q "^ PySide6_Essentials " "$doc/THIRD-PARTY-NOTICES"
172
+ grep -q "^ python3.12 " "$doc/THIRD-PARTY-NOTICES"
173
+ test -s "$doc/LICENSE" && test -s "$doc/LICENSES/LGPL-3" && test -s "$doc/LICENSES/GPL-3"
174
+ gpl_only="$(find "$tmp/squashfs-root" -iregex '.*\(Lottie\|WaylandCompositor\|VirtualKeyboard\|Charts\|DataVisualization\|Quick3D\|pyside6_addons\).*')"
175
+ if [ -n "$gpl_only" ]; then
176
+ echo "::error::GPL-only Qt parts in the AppImage:"; echo "$gpl_only"; exit 1
177
+ fi
178
+ # Bonus SBOM for the pip-installed half of the AppImage (dbus-python
179
+ # and PyGObject come from apt and have no dist-info, so they won't
180
+ # show up here). A failure only warns: the PyPI package's SBOM above
181
+ # is the one of record.
182
+ sbom="$GITHUB_WORKSPACE/dist/Refrain-${TAG}-x86_64.AppImage.cdx.json"
183
+ set +e
184
+ pip-audit --path "$tmp/squashfs-root/usr/lib/python3/dist-packages" \
185
+ --format=cyclonedx-json -o "$sbom"
186
+ rc=$?
187
+ set -e
188
+ if [ ! -s "$sbom" ]; then
189
+ echo "::warning::AppImage SBOM generation produced no output — skipping this bonus asset"
190
+ rm -f "$sbom"
191
+ elif [ "$rc" -ne 0 ]; then
192
+ echo "::warning::pip-audit found known vulnerabilities while building the AppImage SBOM"
193
+ fi
194
+ rm -rf "$tmp"
195
+
196
+ # The in-app updater only installs an AppImage listed here, and only once
197
+ # the owner has signed this file (SHA256SUMS.sig, see packaging/README.md).
198
+ - name: Checksums for the in-app updater
199
+ # *.cdx.json (the SBOMs above) rides along in the same file: the
200
+ # updater only ever looks up the one line matching the AppImage's
201
+ # own name (see _expected_sha256 in src/refrain/updater.py), so
202
+ # extra lines for other assets are inert as far as it's concerned.
203
+ run: |
204
+ cd dist
205
+ sha256sum *.AppImage *.cdx.json > SHA256SUMS
206
+ cat SHA256SUMS
207
+
208
+ - name: Publish the AppImage's third-party notices next to it
209
+ run: |
210
+ cd dist
211
+ ./Refrain-*-x86_64.AppImage --appimage-extract usr/share/doc/refrain/THIRD-PARTY-NOTICES > /dev/null
212
+ mv squashfs-root/usr/share/doc/refrain/THIRD-PARTY-NOTICES .
213
+ rm -rf squashfs-root
214
+
215
+ # Extract the matching `## [X.Y.Z]` section from CHANGELOG.md
216
+ # and feed it into the GitHub release body. Without this we'd
217
+ # fall back to GitHub's auto-generated "Full Changelog: …"
218
+ # one-liner — which is also what the in-app
219
+ # Settings → Updates → "Latest release notes" pane renders, so
220
+ # users would only ever see the bare compare-link there.
221
+ - name: Extract CHANGELOG section for the release body
222
+ run: |
223
+ TAG="${GITHUB_REF#refs/tags/v}"
224
+ # awk: print everything between `## [TAG]` and the next
225
+ # top-level ## section, excluding the heading lines themselves.
226
+ awk -v ver="${TAG}" '
227
+ $0 ~ "^## \\[" ver "\\]" { p=1; next }
228
+ p && /^## \[/ { exit }
229
+ p { print }
230
+ ' CHANGELOG.md > release-notes.md
231
+ echo "::group::release-notes.md"
232
+ cat release-notes.md
233
+ echo "::endgroup::"
234
+ if [ ! -s release-notes.md ]; then
235
+ echo "::error::CHANGELOG section for v${TAG} extracted to nothing"
236
+ exit 1
237
+ fi
238
+ cat >> release-notes.md <<'NOTES'
239
+
240
+ ---
241
+
242
+ The AppImage bundles third-party software under its own licences; the
243
+ list, with versions and where to get the source, is in `THIRD-PARTY-NOTICES`
244
+ below and inside the AppImage. Versions that were taken down, and why, are
245
+ listed in [docs/releases.md](https://github.com/Rockykln/refrain/blob/main/docs/releases.md).
246
+ NOTES
247
+
248
+ - name: Hand the release files to the publishing jobs
249
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
250
+ with:
251
+ name: release
252
+ path: |
253
+ dist/
254
+ release-notes.md
255
+ if-no-files-found: error
256
+
257
+ - name: Hand the PyPI files to the publishing job
258
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
259
+ with:
260
+ name: pypi-dist
261
+ path: pypi-dist/
262
+ if-no-files-found: error
263
+
264
+ attest:
265
+ needs: build
266
+ runs-on: ubuntu-24.04
267
+ permissions:
268
+ contents: read
269
+ id-token: write # Sigstore signing certificate for the attestation
270
+ attestations: write # store the attestation with the repository
271
+
272
+ steps:
273
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
274
+ with:
275
+ name: release
276
+
277
+ # Verify with: gh attestation verify <file> --repo Rockykln/refrain
278
+ - name: Attest build provenance of the release files
279
+ uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
280
+ with:
281
+ subject-path: |
282
+ dist/*.whl
283
+ dist/*.tar.gz
284
+ dist/*.AppImage
285
+ dist/*.AppImage.zsync
286
+ dist/*.cdx.json
287
+ dist/SHA256SUMS
288
+ dist/THIRD-PARTY-NOTICES
289
+
290
+ github-release:
291
+ needs: [build, attest]
292
+ runs-on: ubuntu-24.04
293
+ permissions:
294
+ contents: write # required to create the release
295
+
296
+ steps:
297
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
298
+ with:
299
+ name: release
300
+
301
+ # A draft: the owner signs SHA256SUMS on their own machine and only then
302
+ # publishes it (packaging/release_key.py release <tag>).
303
+ - name: Create GitHub release (draft)
304
+ uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
305
+ with:
306
+ # Use our CHANGELOG section (extracted above), not GitHub's
307
+ # auto-generated notes — the in-app update dialog renders
308
+ # this body verbatim so it has to actually be the changelog.
309
+ body_path: release-notes.md
310
+ generate_release_notes: false
311
+ draft: true
312
+ prerelease: ${{ contains(github.ref, '-') }} # e.g. v0.2.0-rc1
313
+ name: "Refrain ${{ github.ref_name }}"
314
+ files: |
315
+ dist/*.whl
316
+ dist/*.tar.gz
317
+ dist/*.AppImage
318
+ dist/*.AppImage.zsync
319
+ dist/*.cdx.json
320
+ dist/SHA256SUMS
321
+ dist/THIRD-PARTY-NOTICES
322
+
323
+ pypi:
324
+ # After the GitHub release, as before the jobs were split.
325
+ needs: github-release
326
+ runs-on: ubuntu-24.04
327
+ environment:
328
+ name: pypi
329
+ url: https://pypi.org/project/refrain/
330
+ permissions:
331
+ id-token: write # required by PyPA Trusted Publishers (OIDC) for PyPI upload
332
+
333
+ steps:
334
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
335
+ with:
336
+ name: pypi-dist
337
+ path: pypi-dist/
338
+
339
+ # PyPI upload via Trusted Publishers (OIDC). No API token needed; the
340
+ # one-time setup is on pypi.org → Manage → Publishing → Add a new
341
+ # pending publisher (project: refrain, owner: Rockykln,
342
+ # repository: refrain, workflow: release.yml, environment: pypi).
343
+ # Skips silently if the version already exists on PyPI so re-running
344
+ # a release for AppImage-only fixes doesn't fail the workflow.
345
+ - name: Publish to PyPI
346
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
347
+ with:
348
+ packages-dir: pypi-dist/
349
+ skip-existing: true
350
+ verbose: true
351
+ attestations: true
@@ -15,7 +15,6 @@ on:
15
15
 
16
16
  permissions:
17
17
  contents: read
18
- security-events: write
19
18
 
20
19
  jobs:
21
20
 
@@ -23,15 +22,16 @@ jobs:
23
22
  name: bandit (python SAST)
24
23
  runs-on: ubuntu-latest
25
24
  steps:
26
- - uses: actions/checkout@v7
25
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
27
26
 
28
- - uses: actions/setup-python@v7
27
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
29
28
  with:
30
29
  python-version: "3.12"
31
30
  cache: pip
31
+ cache-dependency-path: requirements-dev.lock
32
32
 
33
33
  - name: Install bandit
34
- run: pip install bandit
34
+ run: pip install --require-hashes -c requirements-dev.lock bandit
35
35
 
36
36
  - name: Run bandit (medium + high severity only)
37
37
  # Config (skips, exclude_dirs) lives in pyproject.toml [tool.bandit].
@@ -42,12 +42,13 @@ jobs:
42
42
  name: pip-audit (dependency CVEs)
43
43
  runs-on: ubuntu-latest
44
44
  steps:
45
- - uses: actions/checkout@v7
45
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
46
 
47
- - uses: actions/setup-python@v7
47
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
48
48
  with:
49
49
  python-version: "3.12"
50
50
  cache: pip
51
+ cache-dependency-path: requirements-dev.lock
51
52
 
52
53
  - name: Install system headers for dbus-python build
53
54
  # dbus-python is a C-extension module; pip-audit's resolver tries
@@ -60,8 +61,7 @@ jobs:
60
61
 
61
62
  - name: Install pip-audit
62
63
  run: |
63
- python -m pip install --upgrade pip
64
- pip install pip-audit
64
+ pip install --require-hashes -c requirements-dev.lock pip-audit
65
65
 
66
66
  - name: Run pip-audit on requirements.txt
67
67
  # --strict makes the job fail on any known vulnerability in the
@@ -69,16 +69,21 @@ jobs:
69
69
  # noise is minimal.
70
70
  run: pip-audit -r requirements.txt --strict
71
71
 
72
+ - name: Run pip-audit on the pinned dev/CI lock
73
+ # Nothing updates requirements-dev.lock on its own; a finding here
74
+ # means: regenerate it (command at the top of the file).
75
+ run: pip-audit -r requirements-dev.lock --require-hashes --disable-pip --strict
76
+
72
77
  trufflehog:
73
78
  name: trufflehog (secret detection)
74
79
  runs-on: ubuntu-latest
75
80
  steps:
76
- - uses: actions/checkout@v7
81
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
77
82
  with:
78
83
  fetch-depth: 0
79
84
 
80
85
  - name: Run TruffleHog
81
- uses: trufflesecurity/trufflehog@363923b901c911a9164f50b6c423f47c15372b1c # v3.97.4
86
+ uses: trufflesecurity/trufflehog@f714bf454f350590f4a24c3ddb1aef02c35bf5b6 # v3.97.5
82
87
  with:
83
88
  # --only-verified: a secret only counts as flagged when TruffleHog
84
89
  # successfully validates it against the live service. Drops the
@@ -0,0 +1,157 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ matrix:
17
+ python-version: ["3.11", "3.12", "3.13"]
18
+ fail-fast: false
19
+
20
+ steps:
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
+
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ cache: pip
28
+ cache-dependency-path: requirements-dev.lock
29
+
30
+ - name: Install runtime + test dependencies
31
+ # The newer tests import refrain modules that pull PySide6 /
32
+ # pypresence / dbus-python at module load, so the runtime deps
33
+ # have to be present in CI too. PySide6.QtWidgets requires the
34
+ # EGL / xkbcommon / GL / fontconfig system libs even when run
35
+ # under QT_QPA_PLATFORM=offscreen — without them the import
36
+ # itself fails before the test can opt out. dbus-daemon/dbus-bin
37
+ # give tests/integration a real dbus-run-session to spin up its
38
+ # own private bus; it skips itself if either is missing.
39
+ run: |
40
+ sudo apt-get update
41
+ sudo apt-get install -y \
42
+ libdbus-1-dev libglib2.0-dev pkg-config \
43
+ libegl1 libxkbcommon0 libfontconfig1 libgl1 \
44
+ dbus-daemon dbus-bin
45
+ pip install --require-hashes -r requirements-dev.lock
46
+ pip install --no-deps -e .
47
+
48
+ - name: Compile-check every module
49
+ run: python -m compileall -q src/
50
+
51
+ - name: Run tests with coverage
52
+ run: PYTHONPATH=src pytest --cov --cov-report=term-missing --cov-fail-under=99
53
+
54
+ - name: Coverage in the job summary
55
+ if: ${{ !cancelled() && hashFiles('.coverage') != '' }}
56
+ run: |
57
+ {
58
+ echo "### Coverage (Python ${{ matrix.python-version }}): $(coverage report --format=total)%"
59
+ echo
60
+ coverage report --format=markdown
61
+ } >> "$GITHUB_STEP_SUMMARY"
62
+
63
+ lowest:
64
+ # The lower bounds in pyproject.toml, on the oldest supported Python.
65
+ runs-on: ubuntu-latest
66
+ steps:
67
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
68
+
69
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
70
+ with:
71
+ python-version: "3.11"
72
+ cache: pip
73
+ cache-dependency-path: |
74
+ requirements-dev.lock
75
+ requirements-lowest.txt
76
+
77
+ - name: Install the oldest supported dependencies
78
+ run: |
79
+ sudo apt-get update
80
+ sudo apt-get install -y \
81
+ libdbus-1-dev libglib2.0-dev pkg-config \
82
+ libegl1 libxkbcommon0 libfontconfig1 libgl1 \
83
+ dbus-daemon dbus-bin
84
+ pip install --require-hashes -c requirements-dev.lock pytest
85
+ pip install -r requirements-lowest.txt
86
+ pip install --no-deps -e .
87
+ pip check
88
+
89
+ - name: Run tests
90
+ run: PYTHONPATH=src pytest
91
+
92
+ mpris_integration:
93
+ # Real dbus-daemon, real dbus-python, no mocks: drives MPRISSource
94
+ # against a fake MPRIS player over a private session bus. Split out
95
+ # from `test` so its result is its own signal; it also runs as part
96
+ # of `test`/`lowest` above, which collect all of tests/.
97
+ runs-on: ubuntu-latest
98
+ steps:
99
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
100
+
101
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
102
+ with:
103
+ python-version: "3.12"
104
+ cache: pip
105
+ cache-dependency-path: requirements-dev.lock
106
+
107
+ - name: Install runtime + test dependencies
108
+ run: |
109
+ sudo apt-get update
110
+ sudo apt-get install -y \
111
+ libdbus-1-dev libglib2.0-dev pkg-config \
112
+ libegl1 libxkbcommon0 libfontconfig1 libgl1 \
113
+ dbus-daemon dbus-bin
114
+ pip install --require-hashes -r requirements-dev.lock
115
+ pip install --no-deps -e .
116
+
117
+ - name: Run the MPRIS D-Bus integration tests
118
+ run: QT_QPA_PLATFORM=offscreen PYTHONPATH=src pytest -q tests/integration
119
+
120
+ lint:
121
+ runs-on: ubuntu-latest
122
+ steps:
123
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
124
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
125
+ with:
126
+ python-version: "3.12"
127
+ cache: pip
128
+ cache-dependency-path: requirements-dev.lock
129
+
130
+ - name: Install ruff
131
+ # The version comes from requirements-dev.lock, like everywhere else.
132
+ run: pip install --require-hashes -c requirements-dev.lock ruff
133
+
134
+ - name: Lint with ruff
135
+ run: ruff check . --output-format=github
136
+
137
+ - name: Format check with ruff
138
+ run: ruff format --check .
139
+
140
+ typecheck:
141
+ # The oldest supported Python, the version [tool.mypy] checks against.
142
+ runs-on: ubuntu-latest
143
+ steps:
144
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
145
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
146
+ with:
147
+ python-version: "3.11"
148
+ cache: pip
149
+ cache-dependency-path: requirements-dev.lock
150
+
151
+ - name: Install mypy and the typed runtime dependencies
152
+ # PySide6 and pypresence ship their own type information and are
153
+ # checked against it; dbus-python has none (see [tool.mypy]).
154
+ run: pip install --require-hashes -c requirements-dev.lock mypy pypresence PySide6
155
+
156
+ - name: Type-check with mypy
157
+ run: mypy