├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── filter-script-examples ├── basic.py ├── ignore_artists.py ├── ignore_genre.py ├── parse_artist_from_title.py └── shell_example.sh ├── rescrobbled.service └── src ├── config.rs ├── filter.rs ├── main.rs ├── mainloop.rs ├── player.rs ├── service.rs ├── service └── lastfm.rs └── track.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | target-branch: "development" 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install dependencies 16 | run: sudo apt-get install --no-install-recommends -y libdbus-1-dev dbus 17 | - name: Build 18 | run: cargo build --verbose 19 | - name: Run tests 20 | run: cargo test --verbose 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | .vscode -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.8.0 (2025-06-12) 4 | 5 | - Added the `use_track_start_timestamp` option, allowing tracks to be recorded with a timestamp 6 | of when the track started playing, instead of submission time 7 | - This currently only works for Last.fm, but may be added for ListenBrainz in in the future as well 8 | - Allow overriding config options with environment variables. Currently supported options: 9 | | Option | Environment variable | 10 | |---|---| 11 | | `lastfm-key`, `lastfm-secret` | `LASTFM_KEY`, `LASTFM_SECRET` | 12 | | `listenbrainz-token` | `LISTENBRAINZ_TOKEN` | 13 | | `min-play-time` | `MIN_PLAY_TIME` | 14 | | `filter-script` | `FILTER_SCRIPT` | 15 | | `use-track-start-timestamp` | `USE_TRACK_START_TIMESTAMP` | 16 | Not supported: `player-whitelist`, `[[listenbrainz]]` 17 | - Added the `config` subcommand to show the effective config 18 | - Updated dependencies 19 | 20 | ## v0.7.2 (2025-03-04) 21 | 22 | - Updated dependencies 23 | 24 | ## v0.7.1 (2023-07-13) 25 | 26 | - Made album name optional when submitting to Last.fm 27 | - Updated dependencies 28 | 29 | ## v0.7.0 (2023-01-20) 30 | 31 | - Removed notification functionality 32 | - As an alternative, I recommend [mpris-notifier](https://github.com/l1na-forever/mpris-notifier). 33 | - Made album name optional when submitting to ListenBrainz 34 | - ListenBrainz does not require album names for submissions, so they are now optional. 35 | This means tracks without `xesam:album` will still be submitted. 36 | - The Last.fm library used by rescrobbled still requires the album, but this restriction could 37 | be lifted in the future. 38 | - This does have the side effect of now treating empty album names (i.e. "") 39 | the same as if they were missing from the MPRIS metadata. 40 | - Updated player finding logic to be more resilient to players that cause errors 41 | - Moved to OpenSSL/`libssl` version 3 42 | 43 | ## v0.6.2 (2022-11-16) 44 | 45 | - Fixed scrobbling from applications that report a single string value for `xesam:artist` 46 | 47 | ## v0.6.1 (2022-10-11) 48 | 49 | - Fixed builds of version 0.6.0 breaking 50 | - Dependency mpris released a breaking change in version 2.0.0-rc3, 51 | but Rust/Cargo's semver resolution does not see this as a breaking change, 52 | leading to builds of rescrobbled 0.6.0 breaking that were previously fine 53 | with mpris 2.0.0-rc2. 54 | 55 | ## v0.6.0 (2022-07-20) 56 | 57 | - Fixed scrobbling behind a HTTP/HTTPS proxy 58 | - Replaced the rustfm-scrobble dependency with a fork that automatically picks up proxy settings 59 | - Filter scripts now receive the `xesam:genre` (song genre) MPRIS property in addition to artist, 60 | title and album name 61 | - Note: you may have to update your filter script to take this into account. For example, the 62 | following Python code now raises an error because the additional line (genre) is not unpacked: 63 | ```python 64 | artist, title, album = (l.rstrip() for l in sys.stdin.readlines()) 65 | ``` 66 | This can be fixed by reading and ignoring the additional line: 67 | ```python 68 | artist, title, album, _ = (l.rstrip() for l in sys.stdin.readlines()) 69 | ``` 70 | Or, alternatively: 71 | ```python 72 | artist = sys.stdin.readline().rstrip() 73 | title = sys.stdin.readline().rstrip() 74 | album = sys.stdin.readline().rstrip() 75 | ``` 76 | - Moved to Rust 2021 edition 77 | 78 | ## v0.5.3 (2022-06-16) 79 | 80 | - Entered Last.fm passwords are no longer displayed in plaintext 81 | - Updated dependencies 82 | 83 | ## v0.5.2 (2022-03-04) 84 | 85 | - Improved error handling 86 | - More consistent error messages 87 | - Causes of errors are now always included 88 | - Fixed `basic.py` and `ignore_artists.py` filter script examples 89 | - Updated dependencies 90 | 91 | ## v0.5.1 (2022-01-23) 92 | 93 | - Fixed the way player D-Bus bus names are checked against the player whitelist 94 | 95 | ## v0.5.0 (2022-01-04) 96 | 97 | - Added support for multiple ListenBrainz instances 98 | - You can now specify multiple ListenBrainz instances, supporting custom installs 99 | and other scrobbling services that use a ListenBrainz compatible API 100 | - Added a number of example filter scripts 101 | - The auto-generated config file and session token file are now created with 102 | more restrictive permissions (`0600`) 103 | - Added fallback behavior when a player does not report track length: 104 | - Tracks will scrobble after the default minimum track length (30 seconds) 105 | - Tracks will only scrobble once, unless paused and then unpaused 106 | - Internal refactoring 107 | - Cleaned up the README 108 | - Documented where the session token is stored 109 | 110 | ## 0.4.0 (2021-05-07) 111 | 112 | - Added ignore functionality for filter scripts: 113 | - Filter scripts that return nothing will cause the current track to be ignored/not scrobbled 114 | - This can be used to, for example, filter certain artists or songs entirely 115 | 116 | ## v0.3.3 (2021-05-06) 117 | 118 | - Added `-v` (`--version`) command-line switch to get the program's version 119 | - Released on crates.io 120 | 121 | ## v0.3.2 (2021-04-19) 122 | 123 | - Fixed config template typos (`min_play_time` => `min-play-time`, `player_whitelist` => `player-whitelist`) 124 | 125 | ## v0.3.1 (2021-03-29) 126 | 127 | - Fixed a typo in the config file template (`lastfm-token` => `lastfm-key`) 128 | 129 | ## v0.3.0 (2021-02-18) 130 | 131 | - Fixed a bug where a single song on repeat only scrobbled once 132 | - Rescrobbled now creates the config file if it doesn't exist 133 | - Added the `filter-script` config option: 134 | - Rescrobbled will run this script to filter metadata before 135 | submitting it to Last.fm and/or ListenBrainz 136 | - The script receives artist, song title and album name on 137 | consecutive lines of its standard input (in that order) 138 | - It should produce filtered metadata on the corresponding 139 | lines of its standard output 140 | - Format might change in future updates, eg. to provide 141 | additional metadata 142 | 143 | ## v0.2.0 (2020-08-12) 144 | 145 | - Improved usage instructions 146 | - Renamed config options (old names still supported) 147 | - `api-key` => `lastfm-key` 148 | - `api-secret` => `lastfm-secret` 149 | - `lb-token` => `listenbrainz-token` 150 | - Added music player whitelisting (by MPRIS identity or D-Bus bus name) 151 | - Made Last.fm scrobbling optional 152 | 153 | ## v0.1.0 (2019-09-15) 154 | 155 | Initial release 156 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 | 11 | [[package]] 12 | name = "anyhow" 13 | version = "1.0.98" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 16 | 17 | [[package]] 18 | name = "attohttpc" 19 | version = "0.25.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "7e57d6e7a84f33ff3316e97af3180fe7f86597a6a60161c0be70c0e45f382620" 22 | dependencies = [ 23 | "flate2", 24 | "http 0.2.12", 25 | "log", 26 | "native-tls", 27 | "serde", 28 | "serde_urlencoded", 29 | "url", 30 | ] 31 | 32 | [[package]] 33 | name = "attohttpc" 34 | version = "0.28.5" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "07a9b245ba0739fc90935094c29adbaee3f977218b5fb95e822e261cda7f56a3" 37 | dependencies = [ 38 | "flate2", 39 | "http 1.3.1", 40 | "log", 41 | "native-tls", 42 | "serde", 43 | "serde_json", 44 | "url", 45 | ] 46 | 47 | [[package]] 48 | name = "bitflags" 49 | version = "2.9.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 52 | 53 | [[package]] 54 | name = "bytes" 55 | version = "1.10.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 58 | 59 | [[package]] 60 | name = "cc" 61 | version = "1.2.26" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" 64 | dependencies = [ 65 | "shlex", 66 | ] 67 | 68 | [[package]] 69 | name = "cfg-if" 70 | version = "1.0.1" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 73 | 74 | [[package]] 75 | name = "core-foundation" 76 | version = "0.9.4" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 79 | dependencies = [ 80 | "core-foundation-sys", 81 | "libc", 82 | ] 83 | 84 | [[package]] 85 | name = "core-foundation-sys" 86 | version = "0.8.7" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 89 | 90 | [[package]] 91 | name = "crc32fast" 92 | version = "1.4.2" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 95 | dependencies = [ 96 | "cfg-if", 97 | ] 98 | 99 | [[package]] 100 | name = "darling" 101 | version = "0.14.4" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" 104 | dependencies = [ 105 | "darling_core", 106 | "darling_macro", 107 | ] 108 | 109 | [[package]] 110 | name = "darling_core" 111 | version = "0.14.4" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" 114 | dependencies = [ 115 | "fnv", 116 | "ident_case", 117 | "proc-macro2", 118 | "quote 1.0.40", 119 | "strsim", 120 | "syn 1.0.109", 121 | ] 122 | 123 | [[package]] 124 | name = "darling_macro" 125 | version = "0.14.4" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" 128 | dependencies = [ 129 | "darling_core", 130 | "quote 1.0.40", 131 | "syn 1.0.109", 132 | ] 133 | 134 | [[package]] 135 | name = "dbus" 136 | version = "0.9.7" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" 139 | dependencies = [ 140 | "libc", 141 | "libdbus-sys", 142 | "winapi", 143 | ] 144 | 145 | [[package]] 146 | name = "derive_is_enum_variant" 147 | version = "0.1.1" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "d0ac8859845146979953797f03cc5b282fb4396891807cdb3d04929a88418197" 150 | dependencies = [ 151 | "heck", 152 | "quote 0.3.15", 153 | "syn 0.11.11", 154 | ] 155 | 156 | [[package]] 157 | name = "dirs" 158 | version = "6.0.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 161 | dependencies = [ 162 | "dirs-sys", 163 | ] 164 | 165 | [[package]] 166 | name = "dirs-sys" 167 | version = "0.5.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 170 | dependencies = [ 171 | "libc", 172 | "option-ext", 173 | "redox_users", 174 | "windows-sys 0.59.0", 175 | ] 176 | 177 | [[package]] 178 | name = "displaydoc" 179 | version = "0.2.5" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 182 | dependencies = [ 183 | "proc-macro2", 184 | "quote 1.0.40", 185 | "syn 2.0.102", 186 | ] 187 | 188 | [[package]] 189 | name = "enum-kinds" 190 | version = "0.5.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462" 193 | dependencies = [ 194 | "proc-macro2", 195 | "quote 1.0.40", 196 | "syn 1.0.109", 197 | ] 198 | 199 | [[package]] 200 | name = "equivalent" 201 | version = "1.0.2" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 204 | 205 | [[package]] 206 | name = "errno" 207 | version = "0.3.12" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 210 | dependencies = [ 211 | "libc", 212 | "windows-sys 0.59.0", 213 | ] 214 | 215 | [[package]] 216 | name = "fastrand" 217 | version = "2.3.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 220 | 221 | [[package]] 222 | name = "flate2" 223 | version = "1.1.2" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 226 | dependencies = [ 227 | "crc32fast", 228 | "miniz_oxide", 229 | ] 230 | 231 | [[package]] 232 | name = "fnv" 233 | version = "1.0.7" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 236 | 237 | [[package]] 238 | name = "foreign-types" 239 | version = "0.3.2" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 242 | dependencies = [ 243 | "foreign-types-shared", 244 | ] 245 | 246 | [[package]] 247 | name = "foreign-types-shared" 248 | version = "0.1.1" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 251 | 252 | [[package]] 253 | name = "form_urlencoded" 254 | version = "1.2.1" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 257 | dependencies = [ 258 | "percent-encoding", 259 | ] 260 | 261 | [[package]] 262 | name = "from_variants" 263 | version = "1.0.2" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "4e859c8f2057687618905dbe99fc76e836e0a69738865ef90e46fc214a41bbf2" 266 | dependencies = [ 267 | "from_variants_impl", 268 | ] 269 | 270 | [[package]] 271 | name = "from_variants_impl" 272 | version = "1.0.2" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "55a5e644a80e6d96b2b4910fa7993301d7b7926c045b475b62202b20a36ce69e" 275 | dependencies = [ 276 | "darling", 277 | "proc-macro2", 278 | "quote 1.0.40", 279 | "syn 1.0.109", 280 | ] 281 | 282 | [[package]] 283 | name = "getrandom" 284 | version = "0.2.16" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 287 | dependencies = [ 288 | "cfg-if", 289 | "libc", 290 | "wasi 0.11.1+wasi-snapshot-preview1", 291 | ] 292 | 293 | [[package]] 294 | name = "getrandom" 295 | version = "0.3.3" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 298 | dependencies = [ 299 | "cfg-if", 300 | "libc", 301 | "r-efi", 302 | "wasi 0.14.2+wasi-0.2.4", 303 | ] 304 | 305 | [[package]] 306 | name = "hashbrown" 307 | version = "0.15.4" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 310 | 311 | [[package]] 312 | name = "heck" 313 | version = "0.3.3" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 316 | dependencies = [ 317 | "unicode-segmentation", 318 | ] 319 | 320 | [[package]] 321 | name = "http" 322 | version = "0.2.12" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 325 | dependencies = [ 326 | "bytes", 327 | "fnv", 328 | "itoa", 329 | ] 330 | 331 | [[package]] 332 | name = "http" 333 | version = "1.3.1" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 336 | dependencies = [ 337 | "bytes", 338 | "fnv", 339 | "itoa", 340 | ] 341 | 342 | [[package]] 343 | name = "icu_collections" 344 | version = "2.0.0" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 347 | dependencies = [ 348 | "displaydoc", 349 | "potential_utf", 350 | "yoke", 351 | "zerofrom", 352 | "zerovec", 353 | ] 354 | 355 | [[package]] 356 | name = "icu_locale_core" 357 | version = "2.0.0" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 360 | dependencies = [ 361 | "displaydoc", 362 | "litemap", 363 | "tinystr", 364 | "writeable", 365 | "zerovec", 366 | ] 367 | 368 | [[package]] 369 | name = "icu_normalizer" 370 | version = "2.0.0" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 373 | dependencies = [ 374 | "displaydoc", 375 | "icu_collections", 376 | "icu_normalizer_data", 377 | "icu_properties", 378 | "icu_provider", 379 | "smallvec", 380 | "zerovec", 381 | ] 382 | 383 | [[package]] 384 | name = "icu_normalizer_data" 385 | version = "2.0.0" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 388 | 389 | [[package]] 390 | name = "icu_properties" 391 | version = "2.0.1" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 394 | dependencies = [ 395 | "displaydoc", 396 | "icu_collections", 397 | "icu_locale_core", 398 | "icu_properties_data", 399 | "icu_provider", 400 | "potential_utf", 401 | "zerotrie", 402 | "zerovec", 403 | ] 404 | 405 | [[package]] 406 | name = "icu_properties_data" 407 | version = "2.0.1" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 410 | 411 | [[package]] 412 | name = "icu_provider" 413 | version = "2.0.0" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 416 | dependencies = [ 417 | "displaydoc", 418 | "icu_locale_core", 419 | "stable_deref_trait", 420 | "tinystr", 421 | "writeable", 422 | "yoke", 423 | "zerofrom", 424 | "zerotrie", 425 | "zerovec", 426 | ] 427 | 428 | [[package]] 429 | name = "ident_case" 430 | version = "1.0.1" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 433 | 434 | [[package]] 435 | name = "idna" 436 | version = "1.0.3" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 439 | dependencies = [ 440 | "idna_adapter", 441 | "smallvec", 442 | "utf8_iter", 443 | ] 444 | 445 | [[package]] 446 | name = "idna_adapter" 447 | version = "1.2.1" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 450 | dependencies = [ 451 | "icu_normalizer", 452 | "icu_properties", 453 | ] 454 | 455 | [[package]] 456 | name = "indexmap" 457 | version = "2.9.0" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 460 | dependencies = [ 461 | "equivalent", 462 | "hashbrown", 463 | ] 464 | 465 | [[package]] 466 | name = "itoa" 467 | version = "1.0.15" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 470 | 471 | [[package]] 472 | name = "libc" 473 | version = "0.2.172" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 476 | 477 | [[package]] 478 | name = "libdbus-sys" 479 | version = "0.2.5" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" 482 | dependencies = [ 483 | "pkg-config", 484 | ] 485 | 486 | [[package]] 487 | name = "libredox" 488 | version = "0.1.3" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 491 | dependencies = [ 492 | "bitflags", 493 | "libc", 494 | ] 495 | 496 | [[package]] 497 | name = "linux-raw-sys" 498 | version = "0.9.4" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 501 | 502 | [[package]] 503 | name = "listenbrainz" 504 | version = "0.8.1" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "1fd685ff956be01182a38cb22fb4ba75646caa3ae226befdb7fd31d379cebcc7" 507 | dependencies = [ 508 | "attohttpc 0.28.5", 509 | "serde", 510 | "serde_json", 511 | "thiserror 2.0.12", 512 | ] 513 | 514 | [[package]] 515 | name = "litemap" 516 | version = "0.8.0" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 519 | 520 | [[package]] 521 | name = "log" 522 | version = "0.4.27" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 525 | 526 | [[package]] 527 | name = "md5" 528 | version = "0.7.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 531 | 532 | [[package]] 533 | name = "memchr" 534 | version = "2.7.5" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 537 | 538 | [[package]] 539 | name = "miniz_oxide" 540 | version = "0.8.9" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 543 | dependencies = [ 544 | "adler2", 545 | ] 546 | 547 | [[package]] 548 | name = "mpris" 549 | version = "2.0.1" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "55cef955a7826b1e00e901a3652e7a895abd221fb4ab61547e7d0e4c235d7feb" 552 | dependencies = [ 553 | "dbus", 554 | "derive_is_enum_variant", 555 | "enum-kinds", 556 | "from_variants", 557 | "thiserror 1.0.69", 558 | ] 559 | 560 | [[package]] 561 | name = "native-tls" 562 | version = "0.2.14" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 565 | dependencies = [ 566 | "libc", 567 | "log", 568 | "openssl", 569 | "openssl-probe", 570 | "openssl-sys", 571 | "schannel", 572 | "security-framework", 573 | "security-framework-sys", 574 | "tempfile", 575 | ] 576 | 577 | [[package]] 578 | name = "once_cell" 579 | version = "1.21.3" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 582 | 583 | [[package]] 584 | name = "openssl" 585 | version = "0.10.73" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 588 | dependencies = [ 589 | "bitflags", 590 | "cfg-if", 591 | "foreign-types", 592 | "libc", 593 | "once_cell", 594 | "openssl-macros", 595 | "openssl-sys", 596 | ] 597 | 598 | [[package]] 599 | name = "openssl-macros" 600 | version = "0.1.1" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 603 | dependencies = [ 604 | "proc-macro2", 605 | "quote 1.0.40", 606 | "syn 2.0.102", 607 | ] 608 | 609 | [[package]] 610 | name = "openssl-probe" 611 | version = "0.1.6" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 614 | 615 | [[package]] 616 | name = "openssl-sys" 617 | version = "0.9.109" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 620 | dependencies = [ 621 | "cc", 622 | "libc", 623 | "pkg-config", 624 | "vcpkg", 625 | ] 626 | 627 | [[package]] 628 | name = "option-ext" 629 | version = "0.2.0" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 632 | 633 | [[package]] 634 | name = "percent-encoding" 635 | version = "2.3.1" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 638 | 639 | [[package]] 640 | name = "pkg-config" 641 | version = "0.3.32" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 644 | 645 | [[package]] 646 | name = "potential_utf" 647 | version = "0.1.2" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 650 | dependencies = [ 651 | "zerovec", 652 | ] 653 | 654 | [[package]] 655 | name = "proc-macro2" 656 | version = "1.0.95" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 659 | dependencies = [ 660 | "unicode-ident", 661 | ] 662 | 663 | [[package]] 664 | name = "quote" 665 | version = "0.3.15" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" 668 | 669 | [[package]] 670 | name = "quote" 671 | version = "1.0.40" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 674 | dependencies = [ 675 | "proc-macro2", 676 | ] 677 | 678 | [[package]] 679 | name = "r-efi" 680 | version = "5.2.0" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 683 | 684 | [[package]] 685 | name = "redox_users" 686 | version = "0.5.0" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 689 | dependencies = [ 690 | "getrandom 0.2.16", 691 | "libredox", 692 | "thiserror 2.0.12", 693 | ] 694 | 695 | [[package]] 696 | name = "rescrobbled" 697 | version = "0.8.0" 698 | dependencies = [ 699 | "anyhow", 700 | "dirs", 701 | "listenbrainz", 702 | "mpris", 703 | "rpassword", 704 | "rustfm-scrobble-proxy", 705 | "serde", 706 | "tempfile", 707 | "toml", 708 | ] 709 | 710 | [[package]] 711 | name = "rpassword" 712 | version = "7.4.0" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" 715 | dependencies = [ 716 | "libc", 717 | "rtoolbox", 718 | "windows-sys 0.59.0", 719 | ] 720 | 721 | [[package]] 722 | name = "rtoolbox" 723 | version = "0.0.3" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" 726 | dependencies = [ 727 | "libc", 728 | "windows-sys 0.52.0", 729 | ] 730 | 731 | [[package]] 732 | name = "rustfm-scrobble-proxy" 733 | version = "2.0.0" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "fe604e5881fb980b570695795440cdba1abdbe13d1cc4858923ce11b4fc912be" 736 | dependencies = [ 737 | "attohttpc 0.25.0", 738 | "md5", 739 | "serde", 740 | "serde_json", 741 | "wrapped-vec", 742 | ] 743 | 744 | [[package]] 745 | name = "rustix" 746 | version = "1.0.7" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 749 | dependencies = [ 750 | "bitflags", 751 | "errno", 752 | "libc", 753 | "linux-raw-sys", 754 | "windows-sys 0.59.0", 755 | ] 756 | 757 | [[package]] 758 | name = "ryu" 759 | version = "1.0.20" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 762 | 763 | [[package]] 764 | name = "schannel" 765 | version = "0.1.27" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 768 | dependencies = [ 769 | "windows-sys 0.59.0", 770 | ] 771 | 772 | [[package]] 773 | name = "security-framework" 774 | version = "2.11.1" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 777 | dependencies = [ 778 | "bitflags", 779 | "core-foundation", 780 | "core-foundation-sys", 781 | "libc", 782 | "security-framework-sys", 783 | ] 784 | 785 | [[package]] 786 | name = "security-framework-sys" 787 | version = "2.14.0" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 790 | dependencies = [ 791 | "core-foundation-sys", 792 | "libc", 793 | ] 794 | 795 | [[package]] 796 | name = "serde" 797 | version = "1.0.219" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 800 | dependencies = [ 801 | "serde_derive", 802 | ] 803 | 804 | [[package]] 805 | name = "serde_derive" 806 | version = "1.0.219" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 809 | dependencies = [ 810 | "proc-macro2", 811 | "quote 1.0.40", 812 | "syn 2.0.102", 813 | ] 814 | 815 | [[package]] 816 | name = "serde_json" 817 | version = "1.0.140" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 820 | dependencies = [ 821 | "itoa", 822 | "memchr", 823 | "ryu", 824 | "serde", 825 | ] 826 | 827 | [[package]] 828 | name = "serde_spanned" 829 | version = "0.6.9" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 832 | dependencies = [ 833 | "serde", 834 | ] 835 | 836 | [[package]] 837 | name = "serde_urlencoded" 838 | version = "0.7.1" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 841 | dependencies = [ 842 | "form_urlencoded", 843 | "itoa", 844 | "ryu", 845 | "serde", 846 | ] 847 | 848 | [[package]] 849 | name = "shlex" 850 | version = "1.3.0" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 853 | 854 | [[package]] 855 | name = "smallvec" 856 | version = "1.15.1" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 859 | 860 | [[package]] 861 | name = "stable_deref_trait" 862 | version = "1.2.0" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 865 | 866 | [[package]] 867 | name = "strsim" 868 | version = "0.10.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 871 | 872 | [[package]] 873 | name = "syn" 874 | version = "0.11.11" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" 877 | dependencies = [ 878 | "quote 0.3.15", 879 | "synom", 880 | "unicode-xid", 881 | ] 882 | 883 | [[package]] 884 | name = "syn" 885 | version = "1.0.109" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 888 | dependencies = [ 889 | "proc-macro2", 890 | "quote 1.0.40", 891 | "unicode-ident", 892 | ] 893 | 894 | [[package]] 895 | name = "syn" 896 | version = "2.0.102" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462" 899 | dependencies = [ 900 | "proc-macro2", 901 | "quote 1.0.40", 902 | "unicode-ident", 903 | ] 904 | 905 | [[package]] 906 | name = "synom" 907 | version = "0.11.3" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" 910 | dependencies = [ 911 | "unicode-xid", 912 | ] 913 | 914 | [[package]] 915 | name = "synstructure" 916 | version = "0.13.2" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 919 | dependencies = [ 920 | "proc-macro2", 921 | "quote 1.0.40", 922 | "syn 2.0.102", 923 | ] 924 | 925 | [[package]] 926 | name = "tempfile" 927 | version = "3.20.0" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 930 | dependencies = [ 931 | "fastrand", 932 | "getrandom 0.3.3", 933 | "once_cell", 934 | "rustix", 935 | "windows-sys 0.59.0", 936 | ] 937 | 938 | [[package]] 939 | name = "thiserror" 940 | version = "1.0.69" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 943 | dependencies = [ 944 | "thiserror-impl 1.0.69", 945 | ] 946 | 947 | [[package]] 948 | name = "thiserror" 949 | version = "2.0.12" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 952 | dependencies = [ 953 | "thiserror-impl 2.0.12", 954 | ] 955 | 956 | [[package]] 957 | name = "thiserror-impl" 958 | version = "1.0.69" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 961 | dependencies = [ 962 | "proc-macro2", 963 | "quote 1.0.40", 964 | "syn 2.0.102", 965 | ] 966 | 967 | [[package]] 968 | name = "thiserror-impl" 969 | version = "2.0.12" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 972 | dependencies = [ 973 | "proc-macro2", 974 | "quote 1.0.40", 975 | "syn 2.0.102", 976 | ] 977 | 978 | [[package]] 979 | name = "tinystr" 980 | version = "0.8.1" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 983 | dependencies = [ 984 | "displaydoc", 985 | "zerovec", 986 | ] 987 | 988 | [[package]] 989 | name = "toml" 990 | version = "0.8.23" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 993 | dependencies = [ 994 | "serde", 995 | "serde_spanned", 996 | "toml_datetime", 997 | "toml_edit", 998 | ] 999 | 1000 | [[package]] 1001 | name = "toml_datetime" 1002 | version = "0.6.11" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 1005 | dependencies = [ 1006 | "serde", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "toml_edit" 1011 | version = "0.22.27" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 1014 | dependencies = [ 1015 | "indexmap", 1016 | "serde", 1017 | "serde_spanned", 1018 | "toml_datetime", 1019 | "toml_write", 1020 | "winnow", 1021 | ] 1022 | 1023 | [[package]] 1024 | name = "toml_write" 1025 | version = "0.1.2" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" 1028 | 1029 | [[package]] 1030 | name = "unicode-ident" 1031 | version = "1.0.18" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1034 | 1035 | [[package]] 1036 | name = "unicode-segmentation" 1037 | version = "1.12.0" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1040 | 1041 | [[package]] 1042 | name = "unicode-xid" 1043 | version = "0.0.4" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" 1046 | 1047 | [[package]] 1048 | name = "url" 1049 | version = "2.5.4" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1052 | dependencies = [ 1053 | "form_urlencoded", 1054 | "idna", 1055 | "percent-encoding", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "utf8_iter" 1060 | version = "1.0.4" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1063 | 1064 | [[package]] 1065 | name = "vcpkg" 1066 | version = "0.2.15" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1069 | 1070 | [[package]] 1071 | name = "wasi" 1072 | version = "0.11.1+wasi-snapshot-preview1" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1075 | 1076 | [[package]] 1077 | name = "wasi" 1078 | version = "0.14.2+wasi-0.2.4" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1081 | dependencies = [ 1082 | "wit-bindgen-rt", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "winapi" 1087 | version = "0.3.9" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1090 | dependencies = [ 1091 | "winapi-i686-pc-windows-gnu", 1092 | "winapi-x86_64-pc-windows-gnu", 1093 | ] 1094 | 1095 | [[package]] 1096 | name = "winapi-i686-pc-windows-gnu" 1097 | version = "0.4.0" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1100 | 1101 | [[package]] 1102 | name = "winapi-x86_64-pc-windows-gnu" 1103 | version = "0.4.0" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1106 | 1107 | [[package]] 1108 | name = "windows-sys" 1109 | version = "0.52.0" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1112 | dependencies = [ 1113 | "windows-targets", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "windows-sys" 1118 | version = "0.59.0" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1121 | dependencies = [ 1122 | "windows-targets", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "windows-targets" 1127 | version = "0.52.6" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1130 | dependencies = [ 1131 | "windows_aarch64_gnullvm", 1132 | "windows_aarch64_msvc", 1133 | "windows_i686_gnu", 1134 | "windows_i686_gnullvm", 1135 | "windows_i686_msvc", 1136 | "windows_x86_64_gnu", 1137 | "windows_x86_64_gnullvm", 1138 | "windows_x86_64_msvc", 1139 | ] 1140 | 1141 | [[package]] 1142 | name = "windows_aarch64_gnullvm" 1143 | version = "0.52.6" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1146 | 1147 | [[package]] 1148 | name = "windows_aarch64_msvc" 1149 | version = "0.52.6" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1152 | 1153 | [[package]] 1154 | name = "windows_i686_gnu" 1155 | version = "0.52.6" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1158 | 1159 | [[package]] 1160 | name = "windows_i686_gnullvm" 1161 | version = "0.52.6" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1164 | 1165 | [[package]] 1166 | name = "windows_i686_msvc" 1167 | version = "0.52.6" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1170 | 1171 | [[package]] 1172 | name = "windows_x86_64_gnu" 1173 | version = "0.52.6" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1176 | 1177 | [[package]] 1178 | name = "windows_x86_64_gnullvm" 1179 | version = "0.52.6" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1182 | 1183 | [[package]] 1184 | name = "windows_x86_64_msvc" 1185 | version = "0.52.6" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1188 | 1189 | [[package]] 1190 | name = "winnow" 1191 | version = "0.7.11" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" 1194 | dependencies = [ 1195 | "memchr", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "wit-bindgen-rt" 1200 | version = "0.39.0" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1203 | dependencies = [ 1204 | "bitflags", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "wrapped-vec" 1209 | version = "0.3.0" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "b85e08702c1e919669e1e90213c9c75ea4bb689d0f3970347e2b37c04600b4e5" 1212 | dependencies = [ 1213 | "proc-macro2", 1214 | "quote 1.0.40", 1215 | "syn 1.0.109", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "writeable" 1220 | version = "0.6.1" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 1223 | 1224 | [[package]] 1225 | name = "yoke" 1226 | version = "0.8.0" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 1229 | dependencies = [ 1230 | "serde", 1231 | "stable_deref_trait", 1232 | "yoke-derive", 1233 | "zerofrom", 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "yoke-derive" 1238 | version = "0.8.0" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 1241 | dependencies = [ 1242 | "proc-macro2", 1243 | "quote 1.0.40", 1244 | "syn 2.0.102", 1245 | "synstructure", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "zerofrom" 1250 | version = "0.1.6" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1253 | dependencies = [ 1254 | "zerofrom-derive", 1255 | ] 1256 | 1257 | [[package]] 1258 | name = "zerofrom-derive" 1259 | version = "0.1.6" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1262 | dependencies = [ 1263 | "proc-macro2", 1264 | "quote 1.0.40", 1265 | "syn 2.0.102", 1266 | "synstructure", 1267 | ] 1268 | 1269 | [[package]] 1270 | name = "zerotrie" 1271 | version = "0.2.2" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 1274 | dependencies = [ 1275 | "displaydoc", 1276 | "yoke", 1277 | "zerofrom", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "zerovec" 1282 | version = "0.11.2" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 1285 | dependencies = [ 1286 | "yoke", 1287 | "zerofrom", 1288 | "zerovec-derive", 1289 | ] 1290 | 1291 | [[package]] 1292 | name = "zerovec-derive" 1293 | version = "0.11.1" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 1296 | dependencies = [ 1297 | "proc-macro2", 1298 | "quote 1.0.40", 1299 | "syn 2.0.102", 1300 | ] 1301 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rescrobbled" 3 | version = "0.8.0" 4 | authors = ["Koen Bolhuis "] 5 | edition = "2021" 6 | license = "GPL-3.0" 7 | repository = "https://github.com/InputUsername/rescrobbled.git" 8 | homepage = "https://github.com/InputUsername/rescrobbled" 9 | readme = "README.md" 10 | description = "MPRIS music scrobbler daemon" 11 | keywords = ["mpris", "music", "scrobble", "daemon"] 12 | categories = ["multimedia", "command-line-utilities"] 13 | publish = false 14 | 15 | [dependencies] 16 | mpris = "2.0.1" 17 | rustfm-scrobble-proxy = "2.0.0" 18 | listenbrainz = "0.8.1" 19 | serde = { version = "1.0.219", features = ["derive"] } 20 | toml = "0.8.23" 21 | dirs = "6.0.0" 22 | anyhow = "1.0.98" 23 | rpassword = "7.4.0" 24 | 25 | [dev-dependencies] 26 | tempfile = "3.20.0" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rescrobbled 2 | 3 | [![License](https://img.shields.io/github/license/InputUsername/rescrobbled)](https://github.com/InputUsername/rescrobbled/blob/master/LICENSE) 4 | [![Crates.io](https://img.shields.io/crates/v/rescrobbled)](https://crates.io/crates/rescrobbled) 5 | [![CI](https://github.com/InputUsername/rescrobbled/actions/workflows/ci.yml/badge.svg)](https://github.com/InputUsername/rescrobbled/actions/workflows/ci.yml) 6 | 7 | Rescrobbled is a music scrobbler daemon. It detects active media players running on D-Bus using [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/), automatically updates "now playing" status, and scrobbles songs to [Last.fm](https://last.fm) or [ListenBrainz](https://listenbrainz.org)-compatible services as they play. 8 | 9 | Among other things, due to sharing a Spotify account (I know, I know), I needed a way to scrobble to Last.fm without connecting the Spotify account to my Last.fm account. Rescrobbled offers a simple solution for this. 10 | 11 | ## Installation 12 | 13 | You can download one of the prebuilt binaries [here](https://github.com/InputUsername/rescrobbled/releases). The binary can be placed anywhere you like. 14 | 15 | Rescrobbled is available on [crates.io](https://crates.io/crates/rescrobbled): 16 | ``` 17 | cargo install rescrobbled 18 | ``` 19 | 20 | Alternatively you can install from source using `cargo install --path .` from the crate root. 21 | 22 | There is also an [AUR package](https://aur.archlinux.org/packages/rescrobbled-git/) by [brycied00d](https://github.com/brycied00d), which should always build the latest version of rescrobbled from this repository. 23 | 24 | ## Configuration 25 | 26 | Rescrobbled expects a configuration file at `~/.config/rescrobbled/config.toml` with the following format: 27 | ```toml 28 | lastfm-key = "Last.fm API key" 29 | lastfm-secret = "Last.fm API secret" 30 | min-play-time = 0 31 | player-whitelist = [ "Player MPRIS identity or bus name" ] 32 | filter-script = "path/to/script" 33 | use-track-start-timestamp = false 34 | 35 | [[listenbrainz]] 36 | url = "Custom API URL" 37 | token = "User token" 38 | ``` 39 | 40 | All settings are optional, although rescrobbled isn't very useful without Last.fm or ListenBrainz credentials. ;-) 41 | 42 | If the config file doesn't exist, rescrobbled will generate an example config for you when you run it for the first time. 43 | 44 | - `lastfm-key`, `lastfm-secret` 45 | 46 | To use rescrobbled with Last.fm, you'll need a Last.fm API key and secret. These can be obtained [here](https://www.last.fm/api/account/create). 47 | 48 | - `min-play-time` 49 | 50 | Minimum play time in seconds before a song is scrobbled. 51 | 52 | By default, track submission respects Last.fm's recommended behavior: songs should only be scrobbled if they have been playing for at least half their duration, or for 4 minutes, whichever comes first. Using `min-play-time` you can override this. 53 | 54 | - `player-whitelist` 55 | 56 | If empty or ommitted, music from all players will be scrobbled; otherwise, rescrobbled will only listen to players in this list. 57 | 58 | A CLI application like [playerctl](https://github.com/altdesktop/playerctl) can be used to determine a player's name for the whitelist. To do so, start playing a song and run the following command: 59 | ``` 60 | playerctl --list-all 61 | ``` 62 | 63 | - `filter-script` 64 | 65 | The `filter-script` will be run before updating status and before submitting tracks. 66 | It receives the following properties on consecutive lines of its standard input (separated by `\n`): 67 | - artist; 68 | - song title; 69 | - album name; 70 | - zero or more comma-separated (`,`) genre(s) 71 | 72 | The script should write the filtered artist, song title and album name on corresponding lines of 73 | its standard output. 74 | This can be used to clean up song names, for example removing "remastered" and similar suffixes. 75 | If the filter script does not return any output, the current track will be ignored. 76 | 77 | A number of example scripts can be found in the [`filter-script-examples`](https://github.com/InputUsername/rescrobbled/tree/master/filter-script-examples) directory. 78 | 79 | - `use-track-start-timestamp` 80 | 81 | By default, tracks are submitted with a timestamp of the submission time. By setting `use-track-start-timestamp` to `true`, tracks are instead submitted with the time the track originally started playing. This is currently Last.fm-only. 82 | 83 | - `[[listenbrainz]]` 84 | 85 | You can specify one or more ListenBrainz instances by repeating this option. Each definition needs at least a `token`. You can set `url` to use a custom API URL (eg. for use with custom ListenBrainz instances or services like [Maloja](https://github.com/krateng/maloja)). If the URL is not provided, it defaults to the ListenBrainz.org instance. 86 | 87 | If you only want to use ListenBrainz.org, you can set the `listenbrainz-token` option as a shorthand instead. 88 | 89 | For ListenBrainz.org, the user token can be found [here](https://listenbrainz.org/profile/). Other services might do this differently, refer to their documentation for more info. 90 | 91 | > [!NOTE] 92 | > Due to the way TOML works, the `[[listenbrainz]]` definitions need to be the last thing in your config file. 93 | 94 | Options can also be overridden using environment variables. The following variables are supported: 95 | | Option | Environment variable | 96 | |---|---| 97 | | `lastfm-key`, `lastfm-secret` | `LASTFM_KEY`, `LASTFM_SECRET` | 98 | | `listenbrainz-token` | `LISTENBRAINZ_TOKEN` | 99 | | `min-play-time` | `MIN_PLAY_TIME` | 100 | | `filter-script` | `FILTER_SCRIPT` | 101 | | `use-track-start-timestamp` | `USE_TRACK_START_TIMESTAMP` | 102 | 103 | ## Usage 104 | 105 | To make sure that rescrobbled can scrobble to Last.fm, you need to run the program in a terminal. This will prompt you for your Last.fm username and password, and authenticate with Last.fm. A long-lasting session token is then obtained, which will be used on subsequent runs instead of your username/password. The session token is stored in `~/.config/rescrobbled/session`. 106 | 107 | If you want to run rescrobbled as a daemon, you can put the provided [systemd unit file](https://github.com/InputUsername/rescrobbled/blob/master/rescrobbled.service) in the `~/.config/systemd/user/` directory. 108 | Change `ExecStart` to point to the location of the binary, as necessary. Then, to enable the program to run at startup, use: 109 | ``` 110 | systemctl --user enable rescrobbled.service 111 | ``` 112 | You can run it in the current session using: 113 | ``` 114 | systemctl --user start rescrobbled.service 115 | ``` 116 | 117 | ## Project resources 118 | 119 | - [Issues](https://github.com/InputUsername/rescrobbled/issues) 120 | - [Changelog](https://github.com/InputUsername/rescrobbled/blob/master/CHANGELOG.md) 121 | - [Releases](https://github.com/InputUsername/rescrobbled/releases) 122 | 123 | Issues and pull requests are more than welcome! Development happens on the [`development`](https://github.com/InputUsername/rescrobbled/tree/development) branch, so please create pull requests against that. 124 | All contributions will be licensed under GPLv3. 125 | 126 | ## License 127 | 128 | GPL-3.0, see [`LICENSE`](https://github.com/InputUsername/rescrobbled/blob/master/LICENSE). 129 | -------------------------------------------------------------------------------- /filter-script-examples/basic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | # Filter scripts receive the track artist, title, album and comma-separated list of genre(s) 6 | # on separate lines of their standard input... 7 | 8 | artist, title, album, genres = (l.rstrip() for l in sys.stdin.readlines()) 9 | genres = genres.split(',') 10 | 11 | # ...and should provide artist, title and album on the corresponding lines of the 12 | # standard output 13 | 14 | print(artist, title, album, sep='\n') 15 | -------------------------------------------------------------------------------- /filter-script-examples/ignore_artists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | artist, title, album, _ = (l.rstrip() for l in sys.stdin.readlines()) 6 | 7 | # Ignore all tracks by specific artists 8 | 9 | IGNORED_ARTISTS = {'Justin Bieber', 'The Beatles', 'Michael Jackson'} 10 | 11 | if artist not in IGNORED_ARTISTS: 12 | print(artist, title, album, sep='\n') 13 | -------------------------------------------------------------------------------- /filter-script-examples/ignore_genre.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | artist, title, album, genres = (l.rstrip() for l in sys.stdin.readlines()) 6 | genres = genres.lower().split(',') 7 | 8 | IGNORED_GENRES = {'country', 'idm'} 9 | 10 | # Only output if none of the genres are in the IGNORED_GENRES 11 | if all(genre not in IGNORED_GENRES for genre in genres): 12 | print(artist, title, album, sep='\n') 13 | -------------------------------------------------------------------------------- /filter-script-examples/parse_artist_from_title.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | artist, title, album, _ = (l.rstrip() for l in sys.stdin.readlines()) 6 | 7 | # Parse the artist from the track title if the artist is empty 8 | 9 | if len(artist) == 0: 10 | artist, title = title.split(' - ', maxsplit=1) 11 | 12 | print(artist, title, album, sep='\n') 13 | -------------------------------------------------------------------------------- /filter-script-examples/shell_example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | read artist 4 | read title 5 | read album 6 | 7 | echo "Artist=$artist" 8 | echo "Title=$title" 9 | echo "Album=$album" 10 | -------------------------------------------------------------------------------- /rescrobbled.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=An MPRIS scrobbler 3 | Documentation=https://github.com/InputUsername/rescrobbled 4 | Wants=network-online.target 5 | After=network-online.target 6 | 7 | [Service] 8 | ExecStart=%h/.cargo/bin/rescrobbled 9 | 10 | [Install] 11 | WantedBy=default.target 12 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Koen Bolhuis 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use std::collections::HashSet; 17 | use std::env::{self, VarError}; 18 | use std::fs::{self, Permissions}; 19 | use std::os::unix::fs::PermissionsExt; 20 | use std::path::PathBuf; 21 | use std::str::FromStr; 22 | use std::time::Duration; 23 | 24 | use anyhow::{anyhow, bail, Context, Result}; 25 | 26 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 27 | 28 | const CONFIG_DIR: &str = "rescrobbled"; 29 | const CONFIG_FILE: &str = "config.toml"; 30 | 31 | fn deserialize_duration_seconds<'de, D: Deserializer<'de>>( 32 | de: D, 33 | ) -> Result, D::Error> { 34 | Ok(Some(Duration::from_secs(u64::deserialize(de)?))) 35 | } 36 | 37 | fn serialize_duration_seconds( 38 | value: &Option, 39 | se: S, 40 | ) -> Result { 41 | if let Some(d) = value { 42 | se.serialize_some(&d.as_secs()) 43 | } else { 44 | se.serialize_none() 45 | } 46 | } 47 | 48 | #[derive(Deserialize, Serialize, Default, Debug)] 49 | pub struct ListenBrainzConfig { 50 | pub url: Option, 51 | pub token: String, 52 | } 53 | 54 | #[derive(Deserialize, Serialize, Default, Debug)] 55 | #[serde(rename_all = "kebab-case")] 56 | pub struct Config { 57 | #[serde(alias = "api-key")] 58 | pub lastfm_key: Option, 59 | 60 | #[serde(alias = "api-secret")] 61 | pub lastfm_secret: Option, 62 | 63 | #[serde(alias = "lb-token")] 64 | pub listenbrainz_token: Option, 65 | 66 | #[serde( 67 | default, 68 | deserialize_with = "deserialize_duration_seconds", 69 | serialize_with = "serialize_duration_seconds" 70 | )] 71 | pub min_play_time: Option, 72 | 73 | pub player_whitelist: Option>, 74 | 75 | pub filter_script: Option, 76 | 77 | pub use_track_start_timestamp: Option, 78 | 79 | pub listenbrainz: Option>, 80 | } 81 | 82 | impl Config { 83 | pub fn template() -> String { 84 | let template = Config { 85 | lastfm_key: Some(String::new()), 86 | lastfm_secret: Some(String::new()), 87 | listenbrainz_token: None, 88 | min_play_time: Some(Duration::from_secs(0)), 89 | player_whitelist: Some(HashSet::new()), 90 | filter_script: Some(PathBuf::new()), 91 | use_track_start_timestamp: Some(false), 92 | listenbrainz: Some(vec![ListenBrainzConfig { 93 | url: Some(String::new()), 94 | token: String::new(), 95 | }]), 96 | }; 97 | toml::to_string(&template) 98 | .unwrap() 99 | .lines() 100 | .map(|l| format!("# {}\n", l)) 101 | .collect() 102 | } 103 | 104 | fn normalize(&mut self) { 105 | // Turn `listenbrainz-token` into a `[[listenbrainz]]` definition 106 | if self.listenbrainz_token.is_some() { 107 | if self.listenbrainz.is_none() { 108 | self.listenbrainz = Some(vec![ListenBrainzConfig { 109 | url: None, 110 | token: self.listenbrainz_token.take().unwrap(), 111 | }]) 112 | } else { 113 | eprintln!("Warning: both listenbrainz-token and [[listenbrainz]] config options are defined (listenbrainz-token will be ignored)"); 114 | } 115 | 116 | self.listenbrainz_token.take(); 117 | } 118 | } 119 | } 120 | 121 | pub fn config_dir() -> Result { 122 | let mut path = 123 | dirs::config_dir().ok_or_else(|| anyhow!("User config directory does not exist"))?; 124 | 125 | path.push(CONFIG_DIR); 126 | 127 | if !path.exists() { 128 | fs::create_dir_all(&path).context("Failed to create config directory")?; 129 | } 130 | 131 | Ok(path) 132 | } 133 | 134 | fn get_envvar(name: &str) -> Result> 135 | where 136 | T: FromStr, 137 | ::Err: std::fmt::Display, 138 | { 139 | match env::var(name) { 140 | Ok(value) => value.parse().map(Some).map_err(|err| anyhow!("{err}")), 141 | Err(VarError::NotPresent) => Ok(None), 142 | Err(err) => Err(anyhow!("{err}")), 143 | } 144 | } 145 | 146 | fn replace_if_some(option: &mut Option, replacement: Option) { 147 | if replacement.is_some() { 148 | *option = replacement; 149 | } 150 | } 151 | 152 | fn override_from_environment(config: &mut Config) -> Result<()> { 153 | replace_if_some(&mut config.lastfm_key, get_envvar("LASTFM_KEY")?); 154 | replace_if_some(&mut config.lastfm_secret, get_envvar("LASTFM_SECRET")?); 155 | replace_if_some( 156 | &mut config.listenbrainz_token, 157 | get_envvar("LISTENBRAINZ_TOKEN")?, 158 | ); 159 | replace_if_some( 160 | &mut config.min_play_time, 161 | get_envvar::("MIN_PLAY_TIME").map(|t| t.map(Duration::from_secs))?, 162 | ); 163 | replace_if_some(&mut config.filter_script, get_envvar("FILTER_SCRIPT")?); 164 | replace_if_some( 165 | &mut config.use_track_start_timestamp, 166 | get_envvar("USE_TRACK_START_TIMESTAMP")?, 167 | ); 168 | 169 | Ok(()) 170 | } 171 | 172 | pub fn load_config() -> Result { 173 | let mut path = config_dir()?; 174 | 175 | path.push(CONFIG_FILE); 176 | 177 | if !path.exists() { 178 | fs::write(&path, Config::template()).context("Failed to create config template")?; 179 | fs::set_permissions(&path, Permissions::from_mode(0o600)) 180 | .context("Failed to set permissions for config file")?; 181 | 182 | bail!( 183 | "Config file did not exist, created it at {}", 184 | path.display() 185 | ); 186 | } 187 | 188 | let buffer = fs::read_to_string(&path).context("Failed to open config file")?; 189 | 190 | let mut config: Config = toml::from_str(&buffer).context("Failed to parse config file")?; 191 | 192 | override_from_environment(&mut config)?; 193 | 194 | config.normalize(); 195 | 196 | Ok(config) 197 | } 198 | 199 | #[cfg(test)] 200 | mod tests { 201 | use std::path::Path; 202 | 203 | use super::*; 204 | 205 | #[test] 206 | fn test_normalize_empty_config() { 207 | let mut config = Config::default(); 208 | config.normalize(); 209 | 210 | assert!(config.listenbrainz_token.is_none()); 211 | assert!(config.listenbrainz.is_none()); 212 | } 213 | 214 | #[test] 215 | fn test_normalize_listenbrainz_token() { 216 | let mut config = Config::default(); 217 | config.listenbrainz_token = Some("TEST TOKEN".to_string()); 218 | config.normalize(); 219 | 220 | assert!(config.listenbrainz_token.is_none()); 221 | assert!(config.listenbrainz.is_some()); 222 | assert!(matches!( 223 | &config.listenbrainz.unwrap()[..], 224 | [ListenBrainzConfig { url: None, token }] if token == "TEST TOKEN" 225 | )); 226 | } 227 | 228 | #[test] 229 | fn test_normalize_listenbrainz_double() { 230 | let mut config = Config::default(); 231 | config.listenbrainz_token = Some("TEST TOKEN".to_string()); 232 | config.listenbrainz = Some(vec![ListenBrainzConfig { 233 | url: None, 234 | token: "SECOND TEST TOKEN".to_string(), 235 | }]); 236 | config.normalize(); 237 | 238 | assert!(config.listenbrainz_token.is_none()); 239 | assert!(config.listenbrainz.is_some()); 240 | } 241 | 242 | #[test] 243 | fn test_override_from_environment() { 244 | let mut config = Config::default(); 245 | 246 | std::env::set_var("LASTFM_KEY", "lastfm_key_123"); 247 | std::env::set_var("LASTFM_SECRET", "lastfm_secret_456"); 248 | std::env::set_var("LISTENBRAINZ_TOKEN", "listenbrainz_token_xyz"); 249 | std::env::set_var("MIN_PLAY_TIME", "30"); 250 | std::env::set_var("FILTER_SCRIPT", "/tmp/filter.sh"); 251 | std::env::set_var("USE_TRACK_START_TIMESTAMP", "true"); 252 | 253 | override_from_environment(&mut config).unwrap(); 254 | 255 | assert_eq!(config.lastfm_key.as_deref(), Some("lastfm_key_123")); 256 | assert_eq!(config.lastfm_secret.as_deref(), Some("lastfm_secret_456")); 257 | assert_eq!( 258 | config.listenbrainz_token.as_deref(), 259 | Some("listenbrainz_token_xyz") 260 | ); 261 | assert_eq!(config.min_play_time, Some(Duration::from_secs(30))); 262 | assert_eq!( 263 | config.filter_script.as_deref(), 264 | Some(Path::new("/tmp/filter.sh")) 265 | ); 266 | assert_eq!(config.use_track_start_timestamp, Some(true)); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/filter.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Koen Bolhuis 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use std::fmt::Write as _; 17 | use std::io::Write as _; 18 | use std::process::{Command, Stdio}; 19 | 20 | use anyhow::{anyhow, bail, Context, Result}; 21 | 22 | use mpris::Metadata; 23 | 24 | use crate::config::Config; 25 | use crate::track::Track; 26 | 27 | #[derive(Debug, PartialEq)] 28 | pub enum FilterResult { 29 | Filtered(Track), 30 | NotFiltered(Track), 31 | Ignored, 32 | } 33 | 34 | pub fn filter_metadata(config: &Config, track: Track, metadata: &Metadata) -> Result { 35 | if config.filter_script.is_none() { 36 | return Ok(FilterResult::NotFiltered(track)); 37 | } 38 | 39 | let path = config.filter_script.as_ref().unwrap(); 40 | 41 | let mut child = Command::new(config.filter_script.as_ref().unwrap()) 42 | .stdin(Stdio::piped()) 43 | .stdout(Stdio::piped()) 44 | .spawn() 45 | .with_context(|| format!("Failed to run filter script at {}", path.display()))?; 46 | 47 | let mut stdin = child 48 | .stdin 49 | .take() 50 | .ok_or_else(|| anyhow!("Failed to get an stdin handle for the filter script"))?; 51 | 52 | // Write metadata to filter script stdin 53 | 54 | let genre = metadata 55 | .get("xesam:genre") 56 | .and_then(|value| value.as_str_array()) 57 | .unwrap_or_default(); 58 | 59 | let buffer = format!( 60 | "{}\n{}\n{}\n{}\n", 61 | track.artist(), 62 | track.title(), 63 | track.album().unwrap_or(""), 64 | genre.join(","), 65 | ); 66 | stdin 67 | .write_all(buffer.as_bytes()) 68 | .context("Failed to write track metadata to filter script stdin")?; 69 | 70 | // Close child's stdin to prevent endless waiting 71 | drop(stdin); 72 | 73 | let output = child 74 | .wait_with_output() 75 | .context("Failed to retrieve output from filter script")?; 76 | 77 | if !output.status.success() { 78 | let mut message = "Filter script returned unsuccessully ".to_owned(); 79 | if let Some(status) = output.status.code() { 80 | writeln!(message, "with status: {status}").unwrap(); 81 | } else { 82 | message += "without status\n"; 83 | } 84 | 85 | match String::from_utf8(output.stderr) { 86 | Ok(output) => write!(message, "Stderr: {output}").unwrap(), 87 | Err(err) => write!(message, "Stderr is not valid UTF-8: {err}").unwrap(), 88 | } 89 | 90 | bail!(message); 91 | } 92 | 93 | let output = 94 | String::from_utf8(output.stdout).context("Filter script stdout is not valid UTF-8")?; 95 | 96 | let mut output = output.split('\n'); 97 | match (output.next(), output.next(), output.next()) { 98 | (Some(artist), Some(title), album) => { 99 | Ok(FilterResult::Filtered(Track::new(artist, title, album))) 100 | } 101 | _ => Ok(FilterResult::Ignored), 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use std::fs; 108 | use std::os::unix::fs::PermissionsExt; 109 | use std::path::Path; 110 | 111 | use super::*; 112 | 113 | fn write_test_script(path: &Path, contents: &str) { 114 | fs::write(path, contents).unwrap(); 115 | fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap(); 116 | } 117 | 118 | #[test] 119 | fn test_filter_script() { 120 | let mut config = Config::default(); 121 | let temp_dir = tempfile::tempdir().unwrap(); 122 | 123 | let path = temp_dir.path().join("filter.sh"); 124 | const FILTER_SCRIPT: &str = "#!/usr/bin/env sh 125 | read artist 126 | read title 127 | read album 128 | echo \"Artist=$artist\" 129 | echo \"Title=$title\" 130 | echo \"Album=$album\" 131 | "; 132 | 133 | write_test_script(&path, FILTER_SCRIPT); 134 | 135 | config.filter_script = Some(path); 136 | 137 | assert_eq!( 138 | filter_metadata( 139 | &config, 140 | Track::new("lorem", "ipsum", Some("dolor")), 141 | &Metadata::new("track_id"), 142 | ) 143 | .unwrap(), 144 | FilterResult::Filtered(Track::new( 145 | "Artist=lorem", 146 | "Title=ipsum", 147 | Some("Album=dolor") 148 | )) 149 | ); 150 | 151 | // Script that produces no output should result in `FilterResult::Ignored` 152 | 153 | let path_ignore = temp_dir.path().join("filter_ignore.sh"); 154 | const FILTER_SCRIPT_IGNORE: &str = "#!/usr/bin/env sh 155 | true 156 | "; 157 | 158 | write_test_script(&path_ignore, FILTER_SCRIPT_IGNORE); 159 | 160 | config.filter_script = Some(path_ignore); 161 | 162 | assert_eq!( 163 | filter_metadata( 164 | &config, 165 | Track::new("lorem", "ipsum", Some("dolor")), 166 | &Metadata::new("track_id"), 167 | ) 168 | .unwrap(), 169 | FilterResult::Ignored 170 | ); 171 | 172 | // Not using a filter script should result in `FilterResult::NotFiltered` 173 | 174 | config.filter_script = None; 175 | 176 | assert_eq!( 177 | filter_metadata( 178 | &config, 179 | Track::new("lorem", "ipsum", Some("dolor")), 180 | &Metadata::new("track_id"), 181 | ) 182 | .unwrap(), 183 | FilterResult::NotFiltered(Track::new("lorem", "ipsum", Some("dolor"))) 184 | ); 185 | 186 | // Album should be optional, empty album should still result in `FilterResult::Filtered` 187 | 188 | let path_no_album = temp_dir.path().join("filter_no_album.sh"); 189 | const FILTER_SCRIPT_NO_ALBUM: &str = "#!/usr/bin/env sh 190 | read artist 191 | read title 192 | read album 193 | read genre 194 | echo \"$artist\" 195 | echo \"$title\" 196 | echo \"$album\" 197 | "; 198 | 199 | write_test_script(&path_no_album, FILTER_SCRIPT_NO_ALBUM); 200 | 201 | config.filter_script = Some(path_no_album); 202 | 203 | assert_eq!( 204 | filter_metadata( 205 | &config, 206 | Track::new("lorem", "ipsum", None), 207 | &Metadata::new("track_id"), 208 | ) 209 | .unwrap(), 210 | FilterResult::Filtered(Track::new("lorem", "ipsum", None)), 211 | ) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Rescrobbled is an MPRIS music scrobbler daemon. 2 | // 3 | // Copyright (C) 2025 Koen Bolhuis 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | 18 | use anyhow::Result; 19 | 20 | mod config; 21 | mod filter; 22 | mod mainloop; 23 | mod player; 24 | mod service; 25 | mod track; 26 | 27 | use config::load_config; 28 | use service::Service; 29 | 30 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 31 | 32 | fn main() -> Result<()> { 33 | let arg = std::env::args().nth(1); 34 | 35 | if let Some("-v" | "--version") = arg.as_deref() { 36 | println!("rescrobbled v{VERSION}"); 37 | return Ok(()); 38 | } 39 | 40 | let config = load_config()?; 41 | 42 | if let Some("config") = arg.as_deref() { 43 | println!("{:#?}", config); 44 | return Ok(()); 45 | } 46 | 47 | let services = Service::initialize_all(&config); 48 | 49 | mainloop::run(config, services) 50 | } 51 | -------------------------------------------------------------------------------- /src/mainloop.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Koen Bolhuis 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use std::thread; 17 | use std::time::{Duration, Instant, SystemTime}; 18 | 19 | use anyhow::{anyhow, Context, Result}; 20 | 21 | use mpris::{PlaybackStatus, PlayerFinder}; 22 | 23 | use crate::config::Config; 24 | use crate::filter::{filter_metadata, FilterResult}; 25 | use crate::player; 26 | use crate::service::Service; 27 | use crate::track::Track; 28 | 29 | const POLL_INTERVAL: Duration = Duration::from_millis(500); 30 | 31 | const MIN_LENGTH: Duration = Duration::from_secs(30); 32 | const MIN_PLAY_TIME: Duration = Duration::from_secs(4 * 60); 33 | 34 | fn get_min_play_time(config: &Config, track_length: Duration) -> Duration { 35 | config.min_play_time.unwrap_or_else(|| { 36 | if (track_length / 2) < MIN_PLAY_TIME { 37 | track_length / 2 38 | } else { 39 | MIN_PLAY_TIME 40 | } 41 | }) 42 | } 43 | 44 | pub fn run(config: Config, services: Vec) -> Result<()> { 45 | let finder = PlayerFinder::new() 46 | .map_err(|err| anyhow!("{}", err)) 47 | .context("Failed to connect to D-Bus")?; 48 | 49 | println!("Looking for an active MPRIS player..."); 50 | 51 | let mut player = player::wait_for_player(&config, &finder); 52 | 53 | println!("Found active player {}", player.identity()); 54 | 55 | let mut previous_track = Track::default(); 56 | 57 | let mut timer = Instant::now(); 58 | let mut current_play_time = Duration::from_secs(0); 59 | let mut scrobbled_current_song = false; 60 | let mut track_start = SystemTime::now(); 61 | 62 | loop { 63 | if !player::is_active(&player) { 64 | println!( 65 | "----\n\ 66 | Player {} stopped, looking for a new MPRIS player...", 67 | player.identity() 68 | ); 69 | 70 | player = player::wait_for_player(&config, &finder); 71 | 72 | println!("Found active player {}", player.identity()); 73 | 74 | previous_track.clear(); 75 | 76 | timer = Instant::now(); 77 | current_play_time = Duration::from_secs(0); 78 | scrobbled_current_song = false; 79 | } 80 | 81 | let status = player 82 | .get_playback_status() 83 | .map_err(|err| anyhow!("{}", err)) 84 | .context("Failed to retrieve playback status"); 85 | 86 | match status { 87 | Ok(PlaybackStatus::Playing) => {} 88 | Ok(_) => { 89 | thread::sleep(POLL_INTERVAL); 90 | continue; 91 | } 92 | Err(err) => { 93 | eprintln!("{:?}", err); 94 | 95 | thread::sleep(POLL_INTERVAL); 96 | continue; 97 | } 98 | } 99 | 100 | let metadata = player 101 | .get_metadata() 102 | .map_err(|err| anyhow!("{}", err)) 103 | .context("Failed to get metadata"); 104 | 105 | let metadata = match metadata { 106 | Ok(metadata) => metadata, 107 | Err(err) => { 108 | eprintln!("{:?}", err); 109 | 110 | thread::sleep(POLL_INTERVAL); 111 | continue; 112 | } 113 | }; 114 | 115 | let current_track = Track::from_metadata(&metadata); 116 | 117 | let length = metadata 118 | .length() 119 | .and_then(|length| if length.is_zero() { None } else { Some(length) }); 120 | 121 | if current_track == previous_track { 122 | if !scrobbled_current_song { 123 | let min_play_time = get_min_play_time(&config, length.unwrap_or(MIN_LENGTH)); 124 | 125 | if length.map(|length| length > MIN_LENGTH).unwrap_or(true) 126 | && current_play_time > min_play_time 127 | { 128 | let track_start = config 129 | .use_track_start_timestamp 130 | .unwrap_or(false) 131 | .then_some(&track_start); 132 | 133 | match filter_metadata(&config, current_track, &metadata) { 134 | Ok(FilterResult::Filtered(track)) 135 | | Ok(FilterResult::NotFiltered(track)) => { 136 | for service in services.iter() { 137 | match service.submit(&track, track_start) { 138 | Ok(()) => { 139 | println!("Track submitted to {} successfully", service) 140 | } 141 | Err(err) => eprintln!("{:?}", err), 142 | } 143 | } 144 | } 145 | Ok(FilterResult::Ignored) => {} 146 | Err(err) => eprintln!("{:?}", err), 147 | } 148 | 149 | scrobbled_current_song = true; 150 | } 151 | } else if length 152 | .map(|length| current_play_time >= length) 153 | .unwrap_or(false) 154 | { 155 | current_play_time = Duration::from_secs(0); 156 | scrobbled_current_song = false; 157 | track_start = SystemTime::now(); 158 | } 159 | 160 | current_play_time += timer.elapsed(); 161 | timer = Instant::now(); 162 | } else { 163 | previous_track.clone_from(¤t_track); 164 | 165 | timer = Instant::now(); 166 | current_play_time = Duration::from_secs(0); 167 | scrobbled_current_song = false; 168 | track_start = SystemTime::now(); 169 | 170 | print!( 171 | "----\n\ 172 | Now playing: {} - {}", 173 | current_track.artist(), 174 | current_track.title(), 175 | ); 176 | if let Some(album) = current_track.album() { 177 | println!(" ({album})"); 178 | } 179 | 180 | match filter_metadata(&config, current_track, &metadata) { 181 | Ok(FilterResult::Filtered(track)) | Ok(FilterResult::NotFiltered(track)) => { 182 | for service in services.iter() { 183 | match service.now_playing(&track) { 184 | Ok(()) => println!("Status updated on {} successfully", service), 185 | Err(err) => eprintln!("{:?}", err), 186 | } 187 | } 188 | } 189 | Ok(FilterResult::Ignored) => println!("Track ignored"), 190 | Err(err) => eprintln!("{:?}", err), 191 | } 192 | } 193 | 194 | thread::sleep(POLL_INTERVAL); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/player.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 Koen Bolhuis 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use std::collections::HashSet; 17 | use std::thread; 18 | use std::time::Duration; 19 | 20 | use mpris::{PlaybackStatus, Player, PlayerFinder}; 21 | 22 | use crate::config::Config; 23 | 24 | const INIT_WAIT_TIME: Duration = Duration::from_secs(1); 25 | 26 | const BUS_NAME_PREFIX: &str = "org.mpris.MediaPlayer2."; 27 | 28 | /// Determine if a player is running and actually playing music. 29 | pub fn is_active(player: &Player) -> bool { 30 | if !player.is_running() { 31 | return false; 32 | } 33 | 34 | matches!(player.get_playback_status(), Ok(PlaybackStatus::Playing)) 35 | } 36 | 37 | /// Determine if the unique part of the D-Bus bus name, ie. the part 38 | /// after `org.mpris.MediaPlayer2.`, is whitelisted. 39 | /// 40 | /// This takes into account the possibility of multiple player instances: 41 | /// it checks both the name, and the name with the instance part 42 | /// (something like `.instance123`) stripped off. 43 | fn is_bus_name_whitelisted(player: &Player, whitelist: &HashSet) -> bool { 44 | let bus_name = player.bus_name().trim_start_matches(BUS_NAME_PREFIX); 45 | 46 | let without_instance = bus_name 47 | .rsplit_once('.') 48 | .map(|(name, _instance)| name) 49 | .unwrap_or(bus_name); 50 | 51 | whitelist.contains(bus_name) || whitelist.contains(without_instance) 52 | } 53 | 54 | /// Determine if a player's MPRIS identity or the unique part 55 | /// of its D-Bus bus name are whitelisted. 56 | fn is_whitelisted(config: &Config, player: &Player) -> bool { 57 | if let Some(ref whitelist) = config.player_whitelist { 58 | if !whitelist.is_empty() { 59 | return whitelist.contains(player.identity()) 60 | || is_bus_name_whitelisted(player, whitelist); 61 | } 62 | } 63 | true 64 | } 65 | 66 | /// Wait for any (whitelisted) player to become active again. 67 | pub fn wait_for_player(config: &Config, finder: &PlayerFinder) -> Player { 68 | loop { 69 | let players = match finder.iter_players() { 70 | Ok(players) => players, 71 | _ => { 72 | thread::sleep(INIT_WAIT_TIME); 73 | continue; 74 | } 75 | }; 76 | 77 | #[allow(clippy::manual_flatten)] 78 | for player in players { 79 | if let Ok(player) = player { 80 | if is_active(&player) && is_whitelisted(config, &player) { 81 | return player; 82 | } 83 | } 84 | } 85 | 86 | thread::sleep(INIT_WAIT_TIME); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Koen Bolhuis 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use std::fmt::{self, Write}; 17 | use std::time::{SystemTime, UNIX_EPOCH}; 18 | 19 | use anyhow::{anyhow, Context, Result}; 20 | 21 | use listenbrainz::ListenBrainz; 22 | 23 | use rustfm_scrobble_proxy::{Scrobble, Scrobbler}; 24 | 25 | mod lastfm; 26 | 27 | use crate::config::{Config, ListenBrainzConfig}; 28 | use crate::track::Track; 29 | 30 | /// Represents a music scrobbling service. 31 | pub enum Service { 32 | LastFM(Scrobbler), 33 | ListenBrainz { 34 | client: ListenBrainz, 35 | is_default: bool, 36 | }, 37 | } 38 | 39 | impl Service { 40 | /// Try to connect to Last.fm. 41 | fn lastfm(config: &Config) -> Result> { 42 | match (&config.lastfm_key, &config.lastfm_secret) { 43 | (Some(key), Some(secret)) => { 44 | let mut scrobbler = Scrobbler::new(key, secret); 45 | 46 | lastfm::authenticate(&mut scrobbler) 47 | .context("Failed to authenticate with Last.fm")?; 48 | 49 | Ok(Some(Self::LastFM(scrobbler))) 50 | } 51 | (None, None) => Ok(None), 52 | _ => Err(anyhow!("Last.fm API key or API secret are missing")), 53 | } 54 | } 55 | 56 | /// Try to connect to a ListenBrainz instance. 57 | fn listenbrainz(lb: &ListenBrainzConfig) -> Result { 58 | let mut client = match lb.url { 59 | Some(ref url) => ListenBrainz::new_with_url(url), 60 | None => ListenBrainz::new(), 61 | }; 62 | 63 | client.authenticate(&lb.token).with_context(|| { 64 | let mut err = "Failed to authenticate with ListenBrainz".to_owned(); 65 | if let Some(ref url) = lb.url { 66 | write!(err, " ({url})").unwrap(); 67 | } 68 | err 69 | })?; 70 | 71 | Ok(Self::ListenBrainz { 72 | is_default: lb.url.is_none(), 73 | client, 74 | }) 75 | } 76 | 77 | /// Initialize all services specified in the config. 78 | pub fn initialize_all(config: &Config) -> Vec { 79 | let mut services = Vec::new(); 80 | 81 | match Self::lastfm(config) { 82 | Ok(Some(lastfm)) => { 83 | println!("Authenticated with {} successfully!", lastfm); 84 | services.push(lastfm); 85 | } 86 | Err(err) => eprintln!("{:?}", err), 87 | _ => {} 88 | } 89 | 90 | for lb in config.listenbrainz.iter().flatten() { 91 | match Self::listenbrainz(lb) { 92 | Ok(service) => { 93 | println!("Authenticated with {} successfully!", service); 94 | services.push(service); 95 | } 96 | Err(err) => eprintln!("{:?}", err), 97 | } 98 | } 99 | 100 | if services.is_empty() { 101 | eprintln!("Warning: no scrobbling services defined"); 102 | } 103 | 104 | services 105 | } 106 | 107 | /// Submit a "now playing" request. 108 | pub fn now_playing(&self, track: &Track) -> Result<()> { 109 | match self { 110 | Self::LastFM(scrobbler) => { 111 | let scrobble = Scrobble::new(track.artist(), track.title(), track.album()); 112 | 113 | scrobbler 114 | .now_playing(&scrobble) 115 | .with_context(|| format!("Failed to update status on {}", self))?; 116 | } 117 | Self::ListenBrainz { client, .. } => { 118 | client 119 | .playing_now(track.artist(), track.title(), track.album()) 120 | .with_context(|| format!("Failed to update status on {}", self))?; 121 | } 122 | } 123 | Ok(()) 124 | } 125 | 126 | /// Scrobble a track. 127 | pub fn submit(&self, track: &Track, track_start: Option<&SystemTime>) -> Result<()> { 128 | match self { 129 | Self::LastFM(scrobbler) => { 130 | let mut scrobble = Scrobble::new(track.artist(), track.title(), track.album()); 131 | 132 | if let Some(track_start) = track_start { 133 | let timestamp = track_start 134 | .duration_since(UNIX_EPOCH) 135 | .context("Track started before UNIX epoch")?; 136 | 137 | scrobble.with_timestamp(timestamp.as_secs()); 138 | } 139 | 140 | scrobbler 141 | .scrobble(&scrobble) 142 | .with_context(|| format!("Failed to submit track to {}", self))?; 143 | } 144 | Self::ListenBrainz { client, .. } => { 145 | client 146 | .listen(track.artist(), track.title(), track.album()) 147 | .with_context(|| format!("Failed to submit track to {}", self))?; 148 | } 149 | } 150 | Ok(()) 151 | } 152 | } 153 | 154 | impl fmt::Display for Service { 155 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 156 | match self { 157 | Self::LastFM(_) => write!(f, "Last.fm"), 158 | Self::ListenBrainz { client, is_default } => { 159 | write!(f, "ListenBrainz")?; 160 | if !is_default { 161 | write!(f, " ({})", client.api_url())?; 162 | } 163 | Ok(()) 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/service/lastfm.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 Koen Bolhuis 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | use std::fs::{self, Permissions}; 16 | use std::io::{self, Write}; 17 | use std::os::unix::fs::PermissionsExt; 18 | 19 | use anyhow::{Context, Result}; 20 | 21 | use rustfm_scrobble_proxy::Scrobbler; 22 | 23 | use rpassword::read_password; 24 | 25 | use crate::config::config_dir; 26 | 27 | const SESSION_FILE: &str = "session"; 28 | 29 | /// Authenticate with Last.fm either using an existing 30 | /// session file or by logging in. 31 | pub fn authenticate(scrobbler: &mut Scrobbler) -> Result<()> { 32 | let mut path = config_dir()?; 33 | path.push(SESSION_FILE); 34 | 35 | if let Ok(session_key) = fs::read_to_string(&path) { 36 | // TODO: validate session 37 | scrobbler.authenticate_with_session_key(&session_key); 38 | } else { 39 | let mut input = String::new(); 40 | 41 | print!( 42 | "Log in to Last.fm\n\ 43 | Username: " 44 | ); 45 | io::stdout().flush()?; 46 | 47 | io::stdin().read_line(&mut input)?; 48 | input.pop(); 49 | let username = input.clone(); 50 | 51 | input.clear(); 52 | 53 | print!("Password: "); 54 | io::stdout().flush()?; 55 | 56 | let password = read_password().context("Failed to read password")?; 57 | 58 | let session_response = scrobbler.authenticate_with_password(&username, &password)?; 59 | 60 | let _ = fs::write(&path, session_response.key); 61 | let _ = fs::set_permissions(&path, Permissions::from_mode(0o600)); 62 | } 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /src/track.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Koen Bolhuis 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use mpris::Metadata; 17 | 18 | #[derive(Debug, Default, PartialEq)] 19 | pub struct Track { 20 | artist: String, 21 | title: String, 22 | album: Option, 23 | } 24 | 25 | impl Track { 26 | pub fn artist(&self) -> &str { 27 | &self.artist 28 | } 29 | 30 | pub fn title(&self) -> &str { 31 | &self.title 32 | } 33 | 34 | pub fn album(&self) -> Option<&str> { 35 | self.album.as_deref() 36 | } 37 | 38 | pub fn new(artist: &str, title: &str, album: Option<&str>) -> Self { 39 | Self { 40 | artist: artist.to_owned(), 41 | title: title.to_owned(), 42 | album: album.and_then(|album| { 43 | if !album.is_empty() { 44 | Some(album.to_owned()) 45 | } else { 46 | None 47 | } 48 | }), 49 | } 50 | } 51 | 52 | pub fn clear(&mut self) { 53 | self.artist.clear(); 54 | self.title.clear(); 55 | self.album.take(); 56 | } 57 | 58 | pub fn clone_from(&mut self, other: &Self) { 59 | self.artist.clone_from(&other.artist); 60 | self.title.clone_from(&other.title); 61 | self.album.clone_from(&other.album); 62 | } 63 | 64 | pub fn from_metadata(metadata: &Metadata) -> Self { 65 | let artist = metadata 66 | .artists() 67 | .as_ref() 68 | .and_then(|artists| artists.first().copied()) 69 | .unwrap_or("") 70 | .to_owned(); 71 | 72 | let title = metadata.title().unwrap_or("").to_owned(); 73 | 74 | let album = metadata.album_name().and_then(|album| { 75 | if !album.is_empty() { 76 | Some(album.to_owned()) 77 | } else { 78 | None 79 | } 80 | }); 81 | 82 | Self { 83 | artist, 84 | title, 85 | album, 86 | } 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | use mpris::MetadataValue; 94 | use std::collections::HashMap; 95 | 96 | #[test] 97 | fn test_new() { 98 | // Constructing a track with an empty album should result in `None` for `Track::album()` 99 | 100 | assert_eq!( 101 | Track::new("Enter Shikari", "Live Outside", None).album(), 102 | None 103 | ); 104 | 105 | // Constructing a track with a nonempty album should result in `Some` for `Track::album()` 106 | 107 | assert_eq!( 108 | Track::new("Dimension", "Psycho", Some("Organ")).album(), 109 | Some("Organ") 110 | ); 111 | } 112 | 113 | #[test] 114 | fn test_from_metadata() { 115 | // Metadata without an album should result in a `None` for `Track::album()` 116 | 117 | let mut metadata_without_album = HashMap::new(); 118 | metadata_without_album.insert( 119 | "xesam:artists".to_owned(), 120 | MetadataValue::Array(vec![MetadataValue::String("Billy Joel".to_owned())]), 121 | ); 122 | metadata_without_album.insert( 123 | "xesam:title".to_owned(), 124 | MetadataValue::String("We didn't start the fire".to_owned()), 125 | ); 126 | let metadata_without_album = Metadata::from(metadata_without_album); 127 | let track_without_album = Track::from_metadata(&metadata_without_album); 128 | 129 | assert_eq!(track_without_album.album(), None); 130 | 131 | // Metadata with an empty album should result in a `None` for `Track::album()` 132 | 133 | let mut metadata_empty_album = HashMap::new(); 134 | metadata_empty_album.insert( 135 | "xesam:artist".to_owned(), 136 | MetadataValue::Array(vec![MetadataValue::String("The Prodigy".to_owned())]), 137 | ); 138 | metadata_empty_album.insert( 139 | "xesam:title".to_owned(), 140 | MetadataValue::String("Wild Frontier".to_owned()), 141 | ); 142 | metadata_empty_album.insert( 143 | "xesam:album".to_owned(), 144 | MetadataValue::String("".to_owned()), 145 | ); 146 | let metadata_empty_album = Metadata::from(metadata_empty_album); 147 | let track_empty_album = Track::from_metadata(&metadata_empty_album); 148 | 149 | assert_eq!(track_empty_album.album(), None); 150 | 151 | // Metadata with a nonempty album should result in a `Some` for `Track::album()` 152 | 153 | let mut metadata_with_album = HashMap::new(); 154 | metadata_with_album.insert( 155 | "xesam:artist".to_owned(), 156 | MetadataValue::Array(vec![MetadataValue::String("Men At Work".to_owned())]), 157 | ); 158 | metadata_with_album.insert( 159 | "xesam:title".to_owned(), 160 | MetadataValue::String("Who Can It Be Now?".to_owned()), 161 | ); 162 | metadata_with_album.insert( 163 | "xesam:album".to_owned(), 164 | MetadataValue::String("Business As Usual".to_owned()), 165 | ); 166 | let metadata_with_album = Metadata::from(metadata_with_album); 167 | let track_with_album = Track::from_metadata(&metadata_with_album); 168 | 169 | assert_eq!(track_with_album.album(), Some("Business As Usual")); 170 | } 171 | } 172 | --------------------------------------------------------------------------------