refrain 0.2.7__tar.gz → 0.3.0__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.
- {refrain-0.2.7 → refrain-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +1 -1
- {refrain-0.2.7 → refrain-0.3.0}/.github/workflows/security.yml +1 -1
- {refrain-0.2.7 → refrain-0.3.0}/CHANGELOG.md +89 -1
- {refrain-0.2.7 → refrain-0.3.0}/PKG-INFO +66 -13
- refrain-0.3.0/PRIVACY.md +148 -0
- {refrain-0.2.7 → refrain-0.3.0}/README.md +64 -11
- {refrain-0.2.7 → refrain-0.3.0}/ROADMAP.md +179 -97
- {refrain-0.2.7 → refrain-0.3.0}/SECURITY.md +16 -3
- {refrain-0.2.7 → refrain-0.3.0}/docs/architecture.md +17 -2
- refrain-0.3.0/docs/lastfm.md +103 -0
- {refrain-0.2.7 → refrain-0.3.0}/packaging/appimage/AppImageBuilder.yml +1 -1
- {refrain-0.2.7 → refrain-0.3.0}/packaging/aur/refrain/.SRCINFO +3 -3
- {refrain-0.2.7 → refrain-0.3.0}/packaging/aur/refrain/PKGBUILD +6 -2
- {refrain-0.2.7 → refrain-0.3.0}/packaging/flatpak/io.github.Rockykln.Refrain.metainfo.xml +23 -0
- {refrain-0.2.7 → refrain-0.3.0}/packaging/flatpak/io.github.Rockykln.Refrain.yaml +5 -0
- {refrain-0.2.7 → refrain-0.3.0}/pyproject.toml +2 -2
- refrain-0.3.0/src/refrain/__init__.py +1 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/app.py +6 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/config.py +137 -3
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/daemon.py +257 -35
- refrain-0.3.0/src/refrain/scrobble.py +537 -0
- refrain-0.3.0/src/refrain/scrobble_queue.py +216 -0
- refrain-0.3.0/src/refrain/secrets_store.py +356 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/ui/log_window.py +4 -1
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/ui/settings_window.py +363 -33
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/ui/welcome_dialog.py +1 -1
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_config.py +10 -0
- refrain-0.3.0/tests/test_config_preserve.py +106 -0
- refrain-0.3.0/tests/test_notify_argv.py +64 -0
- refrain-0.3.0/tests/test_scrobble.py +185 -0
- refrain-0.3.0/tests/test_scrobble_queue.py +140 -0
- refrain-0.3.0/tests/test_scrobbler.py +203 -0
- refrain-0.3.0/tests/test_secrets_store.py +135 -0
- refrain-0.3.0/tests/test_source_selection.py +80 -0
- refrain-0.2.7/src/refrain/__init__.py +0 -1
- {refrain-0.2.7 → refrain-0.3.0}/.editorconfig +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.gitattributes +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.github/FUNDING.yml +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.github/codeql/codeql-config.yml +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.github/dependabot.yml +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.github/pull_request_template.md +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.github/workflows/codeql.yml +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.github/workflows/release.yml +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.github/workflows/tests.yml +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/.gitignore +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/CONTRIBUTING.md +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/LICENSE +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/Makefile +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/bluetooth.md +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/faq.md +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/README.md +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/discord-rpc.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/live-log.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/notification.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/settings-advanced.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/settings-general.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/settings-sources.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/settings-updates.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/tray-menu.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/update-dialog.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/screenshots/welcome.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/docs/test-matrix.md +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/packaging/README.md +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/packaging/aur/refrain-git/PKGBUILD +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/packaging/flatpak/io.github.Rockykln.Refrain.desktop +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/packaging/flatpak/python-deps.json +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/requirements-dev.txt +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/requirements.txt +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/__main__.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/github-mark.svg +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/menu-quit.svg +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/menu-update.svg +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/refrain.png +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/refrain.svg +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/tray-paused-dark.svg +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/tray-paused.svg +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/tray-playing-dark.svg +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/tray-playing.svg +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/tray-stopped-dark.svg +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/icons/tray-stopped.svg +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/assets/refrain.desktop +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/autostart.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/cover_art.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/cover_fetcher.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/discord_rpc.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_de.qm +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_de.ts +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_es.qm +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_es.ts +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_fr.qm +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_fr.ts +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_it.qm +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_it.ts +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_ja.qm +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_ja.ts +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_pl.qm +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_pl.ts +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_pt.qm +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_pt.ts +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_ru.qm +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_ru.ts +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_zh_CN.qm +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/i18n/refrain_zh_CN.ts +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/logging_setup.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/paths.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/single_instance.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/sources/__init__.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/sources/base.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/sources/bluetooth.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/sources/mpris.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/sources/mpris_server.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/timing.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/ui/__init__.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/ui/tray.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/ui/update_dialog.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/src/refrain/updater.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/__init__.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/conftest.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_autostart.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_cover_art.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_cover_fetcher.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_daemon_album_format.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_daemon_idle.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_discord_listening.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_discord_rpc.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_imports.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_mpris_dispatch.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_paths.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_sources_base.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_timing.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_tray_init.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_update_dialog_body.py +0 -0
- {refrain-0.2.7 → refrain-0.3.0}/tests/test_updater.py +0 -0
|
@@ -78,7 +78,7 @@ jobs:
|
|
|
78
78
|
fetch-depth: 0
|
|
79
79
|
|
|
80
80
|
- name: Run TruffleHog
|
|
81
|
-
uses: trufflesecurity/trufflehog@
|
|
81
|
+
uses: trufflesecurity/trufflehog@37b77001d0174ebec2fcca2bd83ff83a6d45a3ab # v3.95.3
|
|
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
|
|
@@ -7,6 +7,93 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-05-19
|
|
11
|
+
|
|
12
|
+
Last.fm scrobbling (opt-in, alongside the Discord status), with
|
|
13
|
+
credentials kept in the OS keyring instead of plaintext, a
|
|
14
|
+
crash-safe offline scrobble queue, the source-priority fix, the
|
|
15
|
+
cover-replace notifications, a comment-preserving config writer, and
|
|
16
|
+
a Settings reshuffle (Last.fm on its own tab, per-source Discord IDs
|
|
17
|
+
behind an opt-in toggle). Folds in the accumulated v0.2.8 fixes
|
|
18
|
+
(never separately tagged). A full `PRIVACY.md` documents every data
|
|
19
|
+
flow.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Last.fm scrobbling** — opt-in, *alongside* the Discord Rich
|
|
24
|
+
Presence (never a replacement), off by default. Register your own
|
|
25
|
+
free Last.fm API account (same bring-your-own-credentials model as
|
|
26
|
+
the Discord Application ID), enter the key + shared secret in
|
|
27
|
+
*Settings → General → Last.fm scrobbling*, and **Connect** via the
|
|
28
|
+
in-app browser auth flow. A track is scrobbled at Last.fm's standard
|
|
29
|
+
threshold (half the track or four minutes, whichever first; > 30 s
|
|
30
|
+
only), with pause/seek-aware play-time accounting. Optional
|
|
31
|
+
`track.updateNowPlaying`. No new dependency — the signed API surface
|
|
32
|
+
is hand-rolled on `urllib` + `hashlib`. Walkthrough:
|
|
33
|
+
[`docs/lastfm.md`](docs/lastfm.md).
|
|
34
|
+
- **Crash-safe persistent scrobble queue**
|
|
35
|
+
(`$XDG_STATE_HOME/refrain/scrobble_queue.jsonl`). A qualifying track
|
|
36
|
+
is queued the instant it earns it, so being offline, a Last.fm
|
|
37
|
+
outage, or quitting Refrain mid-song never loses a scrobble — it's
|
|
38
|
+
submitted (≤ 50-item batches) on the next opportunity. SHA-256
|
|
39
|
+
dedup, 1000-entry cap, atomic writes, corrupt-line tolerant. All
|
|
40
|
+
Last.fm network runs on a worker executor so the poll tick never
|
|
41
|
+
blocks; Privacy `Off` silences scrobbling too.
|
|
42
|
+
- **Cover-art replacement notifications.** When iTunes is slow and the
|
|
43
|
+
~2 s cover-retry window times out, Refrain now fires the
|
|
44
|
+
brand-fallback notification immediately, then re-issues it with
|
|
45
|
+
`notify-send --replace-id` once the cover finishes downloading — the
|
|
46
|
+
album art swaps into the *same* bubble instead of never appearing
|
|
47
|
+
(or a second popup).
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **Per-source Discord Client IDs are now opt-in.** The General tab
|
|
52
|
+
showed three Client ID fields; most users need one. The two
|
|
53
|
+
per-source overrides (Apple Music / Bluetooth) are hidden behind a
|
|
54
|
+
*"Use a separate Discord application per source (advanced)"* toggle
|
|
55
|
+
(off by default, auto-enabled when an override is already
|
|
56
|
+
configured so existing setups are untouched).
|
|
57
|
+
- **Source priority now prefers the actively-playing source.** A
|
|
58
|
+
stale *paused* Apple Music browser tab no longer masks music
|
|
59
|
+
actively playing over Bluetooth headphones (idle detection is
|
|
60
|
+
PLAYING-only, so the paused tab never cleared either). MPRIS keeps
|
|
61
|
+
the tie-break when neither is playing.
|
|
62
|
+
- **Config saves preserve comments and unknown keys.** `Config.save()`
|
|
63
|
+
now rewrites only the `key = value` lines Refrain owns; hand-added
|
|
64
|
+
comments, ordering, and keys written by a newer Refrain survive the
|
|
65
|
+
daily silent update-check that stamps `last_check_ts`.
|
|
66
|
+
- **More UI strings localised** — the Updates "last checked"
|
|
67
|
+
timestamp now formats in the active locale (`QLocale`) instead of a
|
|
68
|
+
hard-coded ISO string; the Bluetooth picker labels and the
|
|
69
|
+
live-log "ALL" sentinel are now translatable.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- `welcome_dialog.py` relied on `urllib.error` transitively via
|
|
74
|
+
`urllib.request`; an `except urllib.error.URLError` could have
|
|
75
|
+
raised `AttributeError` during handler matching. Imported
|
|
76
|
+
explicitly; dropped the unused `urllib.parse` import.
|
|
77
|
+
- ROADMAP section ordering corrected — forward-looking sections no
|
|
78
|
+
longer sit above shipped releases.
|
|
79
|
+
- Settings dialog no longer crushes/overlaps form rows: the Last.fm
|
|
80
|
+
controls moved to their own tab and every tab page is wrapped in a
|
|
81
|
+
scroll area; the default window size fits the tallest tab with no
|
|
82
|
+
visible scrollbar in English and German.
|
|
83
|
+
|
|
84
|
+
### Security
|
|
85
|
+
|
|
86
|
+
- **Last.fm credentials are never written to `config.toml`.** The
|
|
87
|
+
shared secret and session token are stored in the OS keyring
|
|
88
|
+
(freedesktop Secret Service — KWallet / GNOME Keyring), encrypted
|
|
89
|
+
at rest, hand-rolled on `dbus-python` (no new dependency). When no
|
|
90
|
+
keyring is reachable they fall back to a `0600`, owner-only file
|
|
91
|
+
separate from the config. `config.toml` itself is now written
|
|
92
|
+
`0600`, and a legacy plaintext secret left by an earlier build is
|
|
93
|
+
auto-migrated into the keyring and scrubbed from disk on the next
|
|
94
|
+
save. Secrets are never logged and only ever leave the machine to
|
|
95
|
+
Last.fm over HTTPS (which is scrobbling itself).
|
|
96
|
+
|
|
10
97
|
## [0.2.7] - 2026-05-07
|
|
11
98
|
|
|
12
99
|
Tray-menu polish + Discord-RPC reliability fixes. Every tray action
|
|
@@ -1069,7 +1156,8 @@ with a proper, installable Linux app.
|
|
|
1069
1156
|
pip-audit, trufflehog, release), Dependabot, issue + PR templates,
|
|
1070
1157
|
`CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`.
|
|
1071
1158
|
|
|
1072
|
-
[Unreleased]: https://github.com/Rockykln/refrain/compare/v0.
|
|
1159
|
+
[Unreleased]: https://github.com/Rockykln/refrain/compare/v0.3.0...HEAD
|
|
1160
|
+
[0.3.0]: https://github.com/Rockykln/refrain/compare/v0.2.7...v0.3.0
|
|
1073
1161
|
[0.2.7]: https://github.com/Rockykln/refrain/compare/v0.2.6...v0.2.7
|
|
1074
1162
|
[0.2.6]: https://github.com/Rockykln/refrain/compare/v0.2.5...v0.2.6
|
|
1075
1163
|
[0.2.5]: https://github.com/Rockykln/refrain/compare/v0.2.4...v0.2.5
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refrain
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -9,7 +9,7 @@ Project-URL: Changelog, https://github.com/Rockykln/refrain/blob/main/CHANGELOG.
|
|
|
9
9
|
Author: Rockykln
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Keywords: apple-music,bluetooth,discord,linux,mpris,rich-presence
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Environment :: X11 Applications
|
|
14
14
|
Classifier: Intended Audience :: End Users/Desktop
|
|
15
15
|
Classifier: License :: Other/Proprietary License
|
|
@@ -55,6 +55,8 @@ streaming from your iPhone over Bluetooth.
|
|
|
55
55
|
- Reads playback metadata from **MPRIS** (Apple Music in any major Linux
|
|
56
56
|
browser) and **BlueZ AVRCP** (any AVRCP-capable Bluetooth source).
|
|
57
57
|
- Forwards track + cover art to Discord via the local IPC socket.
|
|
58
|
+
- Optionally **scrobbles to Last.fm** alongside Discord (opt-in, with a
|
|
59
|
+
crash-safe offline queue).
|
|
58
60
|
- Lives in your **system tray** with Play/Pause/Next/Previous controls.
|
|
59
61
|
- Provides a **settings window** (PySide6) for everything users typically want
|
|
60
62
|
to tweak — privacy mode, sources, autostart, Bluetooth device picker.
|
|
@@ -197,6 +199,15 @@ log_level = "INFO"
|
|
|
197
199
|
cover_cache_size = 200 # disk cap for cached covers
|
|
198
200
|
idle_grace_s = 30 # clear status when same track plays past duration + grace; 0 disables
|
|
199
201
|
language = "system" # "system" follows QLocale; "en", "de", "es", "fr", "pt", "it", "ru", "pl", "ja", "zh_CN" force a translation
|
|
202
|
+
|
|
203
|
+
[lastfm]
|
|
204
|
+
enabled = false # opt-in, alongside (never replacing) the Discord RPC
|
|
205
|
+
api_key = "" # register your own at last.fm/api/account/create
|
|
206
|
+
username = "" # display only
|
|
207
|
+
scrobble_now_playing = true # also send the ephemeral "now playing" indicator
|
|
208
|
+
# NOTE: the Last.fm shared secret and session key are credentials and
|
|
209
|
+
# are deliberately NOT stored here. They live in your OS keyring
|
|
210
|
+
# (KWallet / GNOME Keyring), encrypted at rest — see "Last.fm" below.
|
|
200
211
|
```
|
|
201
212
|
|
|
202
213
|
Per-source `client_id_*` fields let Apple Music render under one Discord
|
|
@@ -295,14 +306,40 @@ Behavior is install-type-aware:
|
|
|
295
306
|
own upgrade command (`flatpak update …` / `yay -Syu refrain`) so the
|
|
296
307
|
package manager stays in charge.
|
|
297
308
|
|
|
309
|
+
## Last.fm scrobbling
|
|
310
|
+
|
|
311
|
+
Refrain can scrobble to [Last.fm](https://www.last.fm) **alongside** the
|
|
312
|
+
Discord status — a second, independent channel, never a replacement.
|
|
313
|
+
It's **opt-in** and off by default.
|
|
314
|
+
|
|
315
|
+
Register your own free [API account](https://www.last.fm/api/account/create),
|
|
316
|
+
then open the *Settings → Last.fm* tab: tick **Enable**, paste the
|
|
317
|
+
**API key** + **shared secret**, click **Connect…** and approve the
|
|
318
|
+
browser prompt. Scrobbling starts on the next track — no restart.
|
|
319
|
+
|
|
320
|
+
The **shared secret and session token are stored in your OS keyring**
|
|
321
|
+
(KWallet / GNOME Keyring), encrypted at rest — never in `config.toml`
|
|
322
|
+
(which is itself written owner-only, `0600`). On a system with no
|
|
323
|
+
keyring they fall back to a `0600` file. Credentials only ever leave
|
|
324
|
+
the machine to Last.fm over HTTPS, which is what scrobbling *is*.
|
|
325
|
+
|
|
326
|
+
A track is scrobbled once you've played at least half of it, or four
|
|
327
|
+
minutes (Last.fm's rule), and only if it's longer than 30 s. Scrobbles
|
|
328
|
+
are queued to disk the instant they qualify, so being offline, a
|
|
329
|
+
Last.fm outage, or quitting mid-song never loses them — they submit on
|
|
330
|
+
the next opportunity. Privacy mode `Off` silences scrobbling too.
|
|
331
|
+
|
|
332
|
+
Full walkthrough + troubleshooting: [`docs/lastfm.md`](docs/lastfm.md).
|
|
333
|
+
|
|
298
334
|
## File locations
|
|
299
335
|
|
|
300
|
-
| What
|
|
301
|
-
|
|
302
|
-
| Config
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
|
|
|
336
|
+
| What | Where |
|
|
337
|
+
|---------------|---------------------------------------------|
|
|
338
|
+
| Config | `$XDG_CONFIG_HOME/refrain/config.toml` |
|
|
339
|
+
| Scrobble queue| `$XDG_STATE_HOME/refrain/scrobble_queue.jsonl` |
|
|
340
|
+
| Logs | `$XDG_STATE_HOME/refrain/refrain.log` (rotates) |
|
|
341
|
+
| Cover cache | `$XDG_CACHE_HOME/refrain/covers/*.txt` |
|
|
342
|
+
| Autostart | `$XDG_CONFIG_HOME/autostart/refrain.desktop` (when enabled) |
|
|
306
343
|
|
|
307
344
|
## Diagnostics — live log
|
|
308
345
|
|
|
@@ -317,22 +354,38 @@ without tailing it from a terminal.
|
|
|
317
354
|
|
|
318
355
|
## Privacy
|
|
319
356
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
357
|
+
Refrain is **local-first**: no Refrain server, no account, **no
|
|
358
|
+
telemetry**, and the author receives nothing. Data leaves your machine
|
|
359
|
+
**only** when *you* enable an optional integration, and then it goes
|
|
360
|
+
directly to that provider:
|
|
361
|
+
|
|
362
|
+
- **Discord** — only if you set a Discord Application ID; track
|
|
363
|
+
metadata goes to the *local* Discord IPC socket (the Discord client
|
|
364
|
+
then broadcasts it under your account).
|
|
365
|
+
- **Apple iTunes Search** (HTTPS) — artist + track name, only while
|
|
366
|
+
cover-art is enabled, to fetch album art. Untick it for zero egress.
|
|
367
|
+
- **GitHub** (HTTPS) — a daily update check sends only the Refrain
|
|
368
|
+
version + your IP. Disable in *Updates*.
|
|
369
|
+
- **Last.fm** (HTTPS) — opt-in scrobbling only; credentials live in
|
|
370
|
+
your **OS keyring** (encrypted at rest), never in `config.toml`.
|
|
371
|
+
|
|
372
|
+
`Privacy → Off` is the global kill switch (no Discord status, no
|
|
373
|
+
scrobbling) while keeping the tray + controls running.
|
|
323
374
|
|
|
324
|
-
|
|
325
|
-
|
|
375
|
+
Full data-flow, retention and erasure details — written to GDPR
|
|
376
|
+
transparency expectations — are in [`PRIVACY.md`](PRIVACY.md).
|
|
326
377
|
|
|
327
378
|
## Documentation
|
|
328
379
|
|
|
329
380
|
- [Architecture overview](docs/architecture.md) — threads, D-Bus surface, file paths
|
|
330
381
|
- [FAQ](docs/faq.md)
|
|
331
382
|
- [Bluetooth quick-start](docs/bluetooth.md) — pair + AVRCP setup walkthrough
|
|
383
|
+
- [Last.fm scrobbling](docs/lastfm.md) — API account + connect walkthrough
|
|
332
384
|
- [Test matrix](docs/test-matrix.md) — supported distros, smoke-check checklist
|
|
333
385
|
- [Roadmap](ROADMAP.md)
|
|
334
386
|
- [Changelog](CHANGELOG.md)
|
|
335
387
|
- [Contributing](CONTRIBUTING.md) — dev setup, testing, code style
|
|
388
|
+
- [Privacy & data protection](PRIVACY.md) — every data flow, retention, erasure
|
|
336
389
|
- [Security policy](SECURITY.md)
|
|
337
390
|
- [Packaging guide](packaging/README.md) — AUR, Flatpak, AppImage build steps
|
|
338
391
|
|
refrain-0.3.0/PRIVACY.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Privacy & Data Protection
|
|
2
|
+
|
|
3
|
+
This document describes **every piece of data Refrain touches**, where it
|
|
4
|
+
goes, why, and how to erase it. It is written to satisfy the
|
|
5
|
+
transparency expectations of the EU GDPR (Arts. 13–14) and equivalent
|
|
6
|
+
regimes, and as an honest plain-language overview for everyone.
|
|
7
|
+
|
|
8
|
+
## TL;DR
|
|
9
|
+
|
|
10
|
+
- Refrain is a **local desktop application**. There is **no
|
|
11
|
+
Refrain-operated server, no account, and no telemetry**. The author
|
|
12
|
+
receives **nothing** and has no access to any of your data.
|
|
13
|
+
- Refrain does not profile you, does not make automated decisions about
|
|
14
|
+
you, shows no ads, and never sells or shares data.
|
|
15
|
+
- Data leaves your machine **only** when *you* enable an optional
|
|
16
|
+
feature, and then it goes **directly** to the third party that
|
|
17
|
+
feature integrates with (Apple, Discord, GitHub, Last.fm) under
|
|
18
|
+
*their* privacy policy and *your* relationship with them — never via
|
|
19
|
+
the author.
|
|
20
|
+
- Everything Refrain stores is on your own machine, in standard XDG
|
|
21
|
+
directories, and can be deleted at any time (see *Erasure*).
|
|
22
|
+
|
|
23
|
+
## Who is the "controller"?
|
|
24
|
+
|
|
25
|
+
Because Refrain runs entirely on your computer with no backend, **you**
|
|
26
|
+
are effectively the controller of your own data. The Refrain author is
|
|
27
|
+
not a processor of your data — the author never receives it. Each
|
|
28
|
+
external transfer below is initiated solely by your own configuration
|
|
29
|
+
choice (your consent / your own purposes) and is then subject to the
|
|
30
|
+
receiving service's own privacy policy and your account there.
|
|
31
|
+
|
|
32
|
+
## Data Refrain processes — and where it goes
|
|
33
|
+
|
|
34
|
+
### 1. Stays on your machine, never transmitted by Refrain
|
|
35
|
+
|
|
36
|
+
| Data | Purpose | Location |
|
|
37
|
+
|---|---|---|
|
|
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
|
+
| Listening history lines (track changes, scrobble events) | Diagnostics / live log | `$XDG_STATE_HOME/refrain/refrain.log` — rotating, ≤ 4 × 1 MB, plaintext, local only |
|
|
40
|
+
| 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
|
+
| Preferences + the *public* Discord/Last.fm application IDs | Your settings | `$XDG_CONFIG_HOME/refrain/config.toml` — written **owner-only (`0600`)**; contains **no secrets** |
|
|
43
|
+
| 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
|
+
|
|
45
|
+
Refrain never transmits any of the above over a network on its own.
|
|
46
|
+
|
|
47
|
+
### 2. Sent to a third party — only when you enable that feature
|
|
48
|
+
|
|
49
|
+
| Recipient | When | What is sent | Default | How to stop it |
|
|
50
|
+
|---|---|---|---|---|
|
|
51
|
+
| **Discord** (your local Discord client → Discord's servers) | Only if you set a Discord Application ID | Track title / artist / album, cover-art URL, optional "Listen on Apple Music" link | **Off** (no ID configured) | Leave the Client ID blank, or set *Privacy → Off*, or *Minimal* (only "Listening to music") |
|
|
52
|
+
| **Apple** (`itunes.apple.com` + artwork CDN, HTTPS) | If "Fetch album cover art" is on **and** a track with artist+title plays | The **artist + track name** (to look up cover/duration/song URL) and your IP / User-Agent; then an image fetch. No account, no auth, no cookies | **On** | Untick *Notifications → Fetch album cover art from iTunes* |
|
|
53
|
+
| **GitHub** (`api.github.com`, HTTPS) | If auto-update check is on: once per day on start, or a manual check | A `User-Agent` containing the Refrain version, and your IP. **No personal data, no account** | **On** | Untick *Updates → Automatically check on startup* |
|
|
54
|
+
| **Last.fm** (`ws.audioscrobbler.com` / `last.fm`, HTTPS) | Only if you enable scrobbling **and** connect an account | Artist / track / album / timestamp, your Last.fm API key + session token + a request signature | **Off** (opt-in) | Don't enable it; or *Disconnect*; or set *Privacy → Off* |
|
|
55
|
+
|
|
56
|
+
Notes:
|
|
57
|
+
|
|
58
|
+
- **Privacy modes** (*Settings → General → Privacy*): `Full` sends full
|
|
59
|
+
metadata to Discord; `Minimal` sends only "Listening to music";
|
|
60
|
+
`Off` is the global **no-external-broadcasting** kill switch — it
|
|
61
|
+
disables the Discord status **and** silences Last.fm scrobbling.
|
|
62
|
+
- The Apple cover-art lookup is gated by the *cover-art* toggle, which
|
|
63
|
+
is **independent of Privacy mode** — if you want zero network egress
|
|
64
|
+
even in `Off` mode, also untick "Fetch album cover art".
|
|
65
|
+
- Discord itself then displays/broadcasts your status to your Discord
|
|
66
|
+
contacts. That processing is Discord's, under your Discord account
|
|
67
|
+
and Discord's policy — Refrain only writes to the **local** Discord
|
|
68
|
+
IPC socket.
|
|
69
|
+
|
|
70
|
+
### 3. Never collected at all
|
|
71
|
+
|
|
72
|
+
No analytics, no telemetry, no crash reporting, no advertising
|
|
73
|
+
identifiers, no device/hardware fingerprinting, no machine ID, no
|
|
74
|
+
contacts, no email, no location, no special-category data, no data
|
|
75
|
+
about children. There is nothing to opt out of because none of it
|
|
76
|
+
exists.
|
|
77
|
+
|
|
78
|
+
## Legal basis (GDPR Art. 6)
|
|
79
|
+
|
|
80
|
+
Every external transfer above happens **only** because you enabled the
|
|
81
|
+
corresponding optional integration for your own purposes — i.e. your
|
|
82
|
+
**consent / your own legitimate use**. No processing relies on the
|
|
83
|
+
author having any legal basis, because the author processes nothing.
|
|
84
|
+
|
|
85
|
+
## Recipients & international transfers
|
|
86
|
+
|
|
87
|
+
When you enable an integration, data goes directly to that provider,
|
|
88
|
+
which may process it outside your country under its own framework and
|
|
89
|
+
policy. Refrain has no influence over this. Review their policies:
|
|
90
|
+
|
|
91
|
+
- Apple — <https://www.apple.com/legal/privacy/>
|
|
92
|
+
- Discord — <https://discord.com/privacy>
|
|
93
|
+
- GitHub — <https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement>
|
|
94
|
+
- Last.fm — <https://www.last.fm/legal/privacy>
|
|
95
|
+
|
|
96
|
+
## Security of processing (GDPR Art. 32)
|
|
97
|
+
|
|
98
|
+
- All network egress is **HTTPS** (the code refuses non-`https://`
|
|
99
|
+
endpoints for its lookups/updates).
|
|
100
|
+
- Credentials are stored in the **OS keyring, encrypted at rest**;
|
|
101
|
+
the fallback file and `config.toml` are written **owner-only
|
|
102
|
+
(`0600`)**. Secrets are **never written to logs** or to
|
|
103
|
+
`config.toml`, and a legacy plaintext secret from an older build is
|
|
104
|
+
auto-migrated into the keyring and scrubbed from disk.
|
|
105
|
+
- Refrain writes only inside `$XDG_CONFIG_HOME`, `$XDG_STATE_HOME`,
|
|
106
|
+
`$XDG_CACHE_HOME`.
|
|
107
|
+
- See [`SECURITY.md`](SECURITY.md) for the threat model and how to
|
|
108
|
+
report a vulnerability.
|
|
109
|
+
|
|
110
|
+
## Retention & your control
|
|
111
|
+
|
|
112
|
+
- **Logs** rotate automatically (≤ 4 files of 1 MB); old data ages out.
|
|
113
|
+
- **Cover cache** is capped and pruned oldest-first.
|
|
114
|
+
- **Scrobble queue** entries are deleted once submitted; the queue is
|
|
115
|
+
capped at 1000.
|
|
116
|
+
- Everything else persists until you change or delete it.
|
|
117
|
+
|
|
118
|
+
## Erasure ("right to be forgotten", locally)
|
|
119
|
+
|
|
120
|
+
To remove all data Refrain stored on your machine:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
rm -rf "${XDG_CONFIG_HOME:-$HOME/.config}/refrain" \
|
|
124
|
+
"${XDG_STATE_HOME:-$HOME/.local/state}/refrain" \
|
|
125
|
+
"${XDG_CACHE_HOME:-$HOME/.cache}/refrain"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- **Last.fm credentials in the keyring:** click *Disconnect* in
|
|
129
|
+
*Settings → Last.fm* (then Apply), or delete the
|
|
130
|
+
`io.github.Rockykln.Refrain` entries in your keyring tool
|
|
131
|
+
(KWallet Manager / Seahorse).
|
|
132
|
+
- **Data already sent to a third party** (your Discord activity
|
|
133
|
+
history, your Last.fm scrobbles, etc.) is held by *that* provider —
|
|
134
|
+
exercise your access/erasure rights with them directly via their
|
|
135
|
+
links above (e.g. Last.fm lets you delete individual scrobbles or
|
|
136
|
+
your account).
|
|
137
|
+
- Autostart entry (if enabled):
|
|
138
|
+
`${XDG_CONFIG_HOME:-$HOME/.config}/autostart/refrain.desktop`.
|
|
139
|
+
|
|
140
|
+
## Changes
|
|
141
|
+
|
|
142
|
+
Material changes to data handling are recorded in
|
|
143
|
+
[`CHANGELOG.md`](CHANGELOG.md) (notably under **Security**) and here.
|
|
144
|
+
|
|
145
|
+
## Contact
|
|
146
|
+
|
|
147
|
+
- General / privacy questions → **contact@rockykln.com**
|
|
148
|
+
- Security reports → **report@rockykln.com** (see [`SECURITY.md`](SECURITY.md))
|
|
@@ -21,6 +21,8 @@ streaming from your iPhone over Bluetooth.
|
|
|
21
21
|
- Reads playback metadata from **MPRIS** (Apple Music in any major Linux
|
|
22
22
|
browser) and **BlueZ AVRCP** (any AVRCP-capable Bluetooth source).
|
|
23
23
|
- Forwards track + cover art to Discord via the local IPC socket.
|
|
24
|
+
- Optionally **scrobbles to Last.fm** alongside Discord (opt-in, with a
|
|
25
|
+
crash-safe offline queue).
|
|
24
26
|
- Lives in your **system tray** with Play/Pause/Next/Previous controls.
|
|
25
27
|
- Provides a **settings window** (PySide6) for everything users typically want
|
|
26
28
|
to tweak — privacy mode, sources, autostart, Bluetooth device picker.
|
|
@@ -163,6 +165,15 @@ log_level = "INFO"
|
|
|
163
165
|
cover_cache_size = 200 # disk cap for cached covers
|
|
164
166
|
idle_grace_s = 30 # clear status when same track plays past duration + grace; 0 disables
|
|
165
167
|
language = "system" # "system" follows QLocale; "en", "de", "es", "fr", "pt", "it", "ru", "pl", "ja", "zh_CN" force a translation
|
|
168
|
+
|
|
169
|
+
[lastfm]
|
|
170
|
+
enabled = false # opt-in, alongside (never replacing) the Discord RPC
|
|
171
|
+
api_key = "" # register your own at last.fm/api/account/create
|
|
172
|
+
username = "" # display only
|
|
173
|
+
scrobble_now_playing = true # also send the ephemeral "now playing" indicator
|
|
174
|
+
# NOTE: the Last.fm shared secret and session key are credentials and
|
|
175
|
+
# are deliberately NOT stored here. They live in your OS keyring
|
|
176
|
+
# (KWallet / GNOME Keyring), encrypted at rest — see "Last.fm" below.
|
|
166
177
|
```
|
|
167
178
|
|
|
168
179
|
Per-source `client_id_*` fields let Apple Music render under one Discord
|
|
@@ -261,14 +272,40 @@ Behavior is install-type-aware:
|
|
|
261
272
|
own upgrade command (`flatpak update …` / `yay -Syu refrain`) so the
|
|
262
273
|
package manager stays in charge.
|
|
263
274
|
|
|
275
|
+
## Last.fm scrobbling
|
|
276
|
+
|
|
277
|
+
Refrain can scrobble to [Last.fm](https://www.last.fm) **alongside** the
|
|
278
|
+
Discord status — a second, independent channel, never a replacement.
|
|
279
|
+
It's **opt-in** and off by default.
|
|
280
|
+
|
|
281
|
+
Register your own free [API account](https://www.last.fm/api/account/create),
|
|
282
|
+
then open the *Settings → Last.fm* tab: tick **Enable**, paste the
|
|
283
|
+
**API key** + **shared secret**, click **Connect…** and approve the
|
|
284
|
+
browser prompt. Scrobbling starts on the next track — no restart.
|
|
285
|
+
|
|
286
|
+
The **shared secret and session token are stored in your OS keyring**
|
|
287
|
+
(KWallet / GNOME Keyring), encrypted at rest — never in `config.toml`
|
|
288
|
+
(which is itself written owner-only, `0600`). On a system with no
|
|
289
|
+
keyring they fall back to a `0600` file. Credentials only ever leave
|
|
290
|
+
the machine to Last.fm over HTTPS, which is what scrobbling *is*.
|
|
291
|
+
|
|
292
|
+
A track is scrobbled once you've played at least half of it, or four
|
|
293
|
+
minutes (Last.fm's rule), and only if it's longer than 30 s. Scrobbles
|
|
294
|
+
are queued to disk the instant they qualify, so being offline, a
|
|
295
|
+
Last.fm outage, or quitting mid-song never loses them — they submit on
|
|
296
|
+
the next opportunity. Privacy mode `Off` silences scrobbling too.
|
|
297
|
+
|
|
298
|
+
Full walkthrough + troubleshooting: [`docs/lastfm.md`](docs/lastfm.md).
|
|
299
|
+
|
|
264
300
|
## File locations
|
|
265
301
|
|
|
266
|
-
| What
|
|
267
|
-
|
|
268
|
-
| Config
|
|
269
|
-
|
|
|
270
|
-
|
|
|
271
|
-
|
|
|
302
|
+
| What | Where |
|
|
303
|
+
|---------------|---------------------------------------------|
|
|
304
|
+
| Config | `$XDG_CONFIG_HOME/refrain/config.toml` |
|
|
305
|
+
| Scrobble queue| `$XDG_STATE_HOME/refrain/scrobble_queue.jsonl` |
|
|
306
|
+
| Logs | `$XDG_STATE_HOME/refrain/refrain.log` (rotates) |
|
|
307
|
+
| Cover cache | `$XDG_CACHE_HOME/refrain/covers/*.txt` |
|
|
308
|
+
| Autostart | `$XDG_CONFIG_HOME/autostart/refrain.desktop` (when enabled) |
|
|
272
309
|
|
|
273
310
|
## Diagnostics — live log
|
|
274
311
|
|
|
@@ -283,22 +320,38 @@ without tailing it from a terminal.
|
|
|
283
320
|
|
|
284
321
|
## Privacy
|
|
285
322
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
323
|
+
Refrain is **local-first**: no Refrain server, no account, **no
|
|
324
|
+
telemetry**, and the author receives nothing. Data leaves your machine
|
|
325
|
+
**only** when *you* enable an optional integration, and then it goes
|
|
326
|
+
directly to that provider:
|
|
327
|
+
|
|
328
|
+
- **Discord** — only if you set a Discord Application ID; track
|
|
329
|
+
metadata goes to the *local* Discord IPC socket (the Discord client
|
|
330
|
+
then broadcasts it under your account).
|
|
331
|
+
- **Apple iTunes Search** (HTTPS) — artist + track name, only while
|
|
332
|
+
cover-art is enabled, to fetch album art. Untick it for zero egress.
|
|
333
|
+
- **GitHub** (HTTPS) — a daily update check sends only the Refrain
|
|
334
|
+
version + your IP. Disable in *Updates*.
|
|
335
|
+
- **Last.fm** (HTTPS) — opt-in scrobbling only; credentials live in
|
|
336
|
+
your **OS keyring** (encrypted at rest), never in `config.toml`.
|
|
337
|
+
|
|
338
|
+
`Privacy → Off` is the global kill switch (no Discord status, no
|
|
339
|
+
scrobbling) while keeping the tray + controls running.
|
|
289
340
|
|
|
290
|
-
|
|
291
|
-
|
|
341
|
+
Full data-flow, retention and erasure details — written to GDPR
|
|
342
|
+
transparency expectations — are in [`PRIVACY.md`](PRIVACY.md).
|
|
292
343
|
|
|
293
344
|
## Documentation
|
|
294
345
|
|
|
295
346
|
- [Architecture overview](docs/architecture.md) — threads, D-Bus surface, file paths
|
|
296
347
|
- [FAQ](docs/faq.md)
|
|
297
348
|
- [Bluetooth quick-start](docs/bluetooth.md) — pair + AVRCP setup walkthrough
|
|
349
|
+
- [Last.fm scrobbling](docs/lastfm.md) — API account + connect walkthrough
|
|
298
350
|
- [Test matrix](docs/test-matrix.md) — supported distros, smoke-check checklist
|
|
299
351
|
- [Roadmap](ROADMAP.md)
|
|
300
352
|
- [Changelog](CHANGELOG.md)
|
|
301
353
|
- [Contributing](CONTRIBUTING.md) — dev setup, testing, code style
|
|
354
|
+
- [Privacy & data protection](PRIVACY.md) — every data flow, retention, erasure
|
|
302
355
|
- [Security policy](SECURITY.md)
|
|
303
356
|
- [Packaging guide](packaging/README.md) — AUR, Flatpak, AppImage build steps
|
|
304
357
|
|