├── .env.example ├── .gitignore ├── .github ├── release-notes.md ├── assets │ ├── demo.png │ ├── demo.xcf │ ├── social_preview.png │ └── social_preview.xcf ├── CONTRIBUTING.md └── workflows │ ├── build.yml │ └── release.yml ├── xbps ├── update └── template ├── music-discord-rpc.service ├── nfpm.yaml ├── PKGBUILD ├── LICENSE ├── Formula └── music-discord-rpc.rb ├── Cargo.toml ├── config.yaml ├── CHANGELOG.md ├── src ├── settings.rs ├── utils.rs └── main.rs ├── README.md └── Cargo.lock /.env.example: -------------------------------------------------------------------------------- 1 | LASTFM_API_KEY=key_here 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | icon.png 4 | libs 5 | -------------------------------------------------------------------------------- /.github/release-notes.md: -------------------------------------------------------------------------------- 1 | ## Changes: 2 | 3 | - Fix: Prevent playerctld from breaking allow list (#41). 4 | -------------------------------------------------------------------------------- /.github/assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patryk-ku/music-discord-rpc/HEAD/.github/assets/demo.png -------------------------------------------------------------------------------- /.github/assets/demo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patryk-ku/music-discord-rpc/HEAD/.github/assets/demo.xcf -------------------------------------------------------------------------------- /.github/assets/social_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patryk-ku/music-discord-rpc/HEAD/.github/assets/social_preview.png -------------------------------------------------------------------------------- /.github/assets/social_preview.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patryk-ku/music-discord-rpc/HEAD/.github/assets/social_preview.xcf -------------------------------------------------------------------------------- /xbps/update: -------------------------------------------------------------------------------- 1 | site='https://github.com/patryk-ku/music-discord-rpc/tags' 2 | pattern='/archive/refs/tags/v\K[0-9.]+(?=.tar.gz)' 3 | -------------------------------------------------------------------------------- /music-discord-rpc.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Cross-platform Discord rich presence for music with album cover and progress bar support. 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/usr/bin/music-discord-rpc 7 | Restart=always 8 | RestartSec=10 9 | StandardOutput=journal 10 | StandardError=journal 11 | 12 | [Install] 13 | WantedBy=default.target 14 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to music-discord-rpc 2 | 3 | ## Submitting an Issue (Bugs, Feature Requests, etc) 4 | 5 | Before creating an issue, please read the [FAQ](https://github.com/patryk-ku/music-discord-rpc?tab=readme-ov-file#faq). 6 | 7 | ## Contributing Code (Pull Requests) 8 | 9 | To save your own time, please open an issue first to discuss your idea before you start writing code. I may not accept pull requests that are submitted without prior discussion in an issue, and I don't want to see your work go to waste. 10 | -------------------------------------------------------------------------------- /nfpm.yaml: -------------------------------------------------------------------------------- 1 | name: music-discord-rpc 2 | arch: amd64 3 | platform: linux 4 | version: "0.6.2" 5 | release: "1" 6 | section: default 7 | maintainer: "Patryk Kurdziel " 8 | description: | 9 | Cross-platform Discord rich presence for music with album cover and progress bar support. 10 | homepage: https://github.com/patryk-ku/music-discord-rpc 11 | license: MIT 12 | 13 | contents: 14 | - src: target/release/music-discord-rpc 15 | dst: /usr/bin/music-discord-rpc 16 | - src: ./music-discord-rpc.service 17 | dst: /usr/lib/systemd/user/music-discord-rpc.service 18 | -------------------------------------------------------------------------------- /xbps/template: -------------------------------------------------------------------------------- 1 | # Template file for 'music-discord-rpc' 2 | pkgname=music-discord-rpc 3 | version=0.6.2 4 | revision=1 5 | build_style=cargo 6 | short_desc="Cross-platform Discord rich presence for music with album cover and progress bar support." 7 | maintainer="JkktBkkt " 8 | license="MIT" 9 | makedepends="dbus-devel pkg-config openssl-devel" 10 | homepage="https://github.com/patryk-ku/music-discord-rpc" 11 | distfiles="https://github.com/patryk-ku/music-discord-rpc/archive/refs/tags/v${version}.tar.gz" 12 | checksum=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb 13 | post_install() { 14 | vlicense LICENSE 15 | } 16 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Patryk Kurdziel 2 | 3 | _pkgname=music-discord-rpc 4 | pkgname="${_pkgname}-bin" 5 | pkgver=0.6.2 6 | pkgrel=1 7 | pkgdesc='Cross-platform Discord rich presence for music with album cover and progress bar support.' 8 | url="https://github.com/patryk-ku/${_pkgname}" 9 | license=('MIT') 10 | arch=('x86_64') 11 | provides=("${_pkgname}") 12 | conflicts=("${_pkgname}") 13 | source=("${_pkgname}-v${pkgver}::${url}/releases/download/v${pkgver}/${_pkgname}" 14 | "${_pkgname}-v${pkgver}.service::https://raw.githubusercontent.com/patryk-ku/${_pkgname}/refs/tags/v${pkgver}/${_pkgname}.service" 15 | "LICENSE-v${pkgver}::https://raw.githubusercontent.com/patryk-ku/${_pkgname}/refs/tags/v${pkgver}/LICENSE") 16 | sha512sums=('SKIP' 'SKIP' 'SKIP') 17 | 18 | package() { 19 | install -Dm755 "${_pkgname}-v${pkgver}" "${pkgdir}/usr/bin/${_pkgname}" 20 | install -Dm644 "${_pkgname}-v${pkgver}.service" "${pkgdir}/usr/lib/systemd/user/${_pkgname}.service" 21 | install -Dm644 "LICENSE-v${pkgver}" "${pkgdir}/usr/share/licenses/${_pkgname}/LICENSE" 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-2025 Patryk Kurdziel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Formula/music-discord-rpc.rb: -------------------------------------------------------------------------------- 1 | class MusicDiscordRpc < Formula 2 | desc "Cross-platform Discord rich presence for music with album cover and progress bar" 3 | homepage "https://github.com/patryk-ku/music-discord-rpc" 4 | version "0.6.2" 5 | license "MIT" 6 | 7 | depends_on "media-control" 8 | 9 | on_intel do 10 | url "https://github.com/patryk-ku/music-discord-rpc/releases/download/v#{version}/music-discord-rpc-macos-amd64.tar.gz" 11 | sha256 "25074bf3eae5fafe5405f38d87c65e4aa0b5a7c92574f7379b429a46ce5bb422" 12 | end 13 | 14 | on_arm do 15 | url "https://github.com/patryk-ku/music-discord-rpc/releases/download/v#{version}/music-discord-rpc-macos-arm64.tar.gz" 16 | sha256 "9fe07e593b1c66872590a5488c93e1dc2078a990f57aff6f79a9e9bd2fd356f6" 17 | end 18 | 19 | def install 20 | bin.install "music-discord-rpc" 21 | end 22 | 23 | service do 24 | run [opt_bin/"music-discord-rpc"] 25 | keep_alive true 26 | environment_variables PATH: "#{HOMEBREW_PREFIX}/bin:/usr/bin:/bin" 27 | log_path var/"log/music-discord-rpc.log" 28 | error_log_path var/"log/music-discord-rpc.error.log" 29 | end 30 | 31 | test do 32 | assert_match version.to_s, shell_output("#{bin}/music-discord-rpc --version") 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "music-discord-rpc" 3 | version = "0.6.2" 4 | edition = "2021" 5 | authors = ["Patryk Kurdziel "] 6 | description = "Cross-platform Discord rich presence for music with album cover and progress bar support." 7 | repository = "https://github.com/patryk-ku/music-discord-rpc" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | discord-rich-presence = { git = "https://github.com/vionya/discord-rich-presence", branch = "main" } 12 | reqwest = { version = "0.12", features = ["blocking", "json"] } 13 | url-escape = "0.1.1" 14 | serde_json = "1.0.141" 15 | clap = { version = "4.5.42", features = ["derive"] } 16 | pickledb = "0.5.1" 17 | clap-serde-derive = "0.2.1" 18 | serde = { version = "1.0.219", features = ["derive"] } 19 | serde_yaml = "0.9.34" 20 | 21 | # Linux dependencies 22 | [target.'cfg(target_os = "linux")'.dependencies] 23 | mpris = "2.0.1" 24 | 25 | [profile.release] 26 | strip = true 27 | codegen-units = 1 28 | panic = "abort" 29 | 30 | [build-dependencies] 31 | dotenvy = "0.15.7" 32 | 33 | [package.metadata.appimage] 34 | auto_link = true 35 | auto_link_exclude_list = [ 36 | "libc.so*", 37 | "libdl.so*", 38 | "libpthread.so*", 39 | "libm.so*", 40 | "libgcc_s.so*", 41 | "ld-linux*.so*", 42 | "libsystemd.so*", 43 | "libcap.so*", 44 | ] 45 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # music-discord-rpc configuration file 2 | 3 | # You can reset this file using the command: 4 | # music-discord-rpc --reset-config 5 | # Or you can manually copy the example config from repo: 6 | # https://github.com/patryk-ku/music-discord-rpc/blob/main/config.yaml 7 | 8 | # If you compiled binary by yourself, you may need to provide your Last.fm API key here. 9 | # Or if you use precompiled binary, you can override the default Last.fm API key. 10 | # You can easily get it from: https://www.last.fm/pl/api 11 | # lastfm_api_key: key_here 12 | 13 | # You can also disable Last.fm as a cover source by providing an empty string as the key. 14 | # lastfm_api_key: "" 15 | 16 | # Activity refresh rate in seconds (min 5) 17 | interval: 10 18 | 19 | # Select visible activity buttons (max 2) [possible values: yt, lastfm, listenbrainz, mprisUrl, shamelessAd] 20 | # button: 21 | # - yt 22 | # - lastfm 23 | 24 | # Uncomment and enter your nicknames for activity buttons 25 | # lastfm_name: "nickname" 26 | # listenbrainz_name: "nickname" 27 | 28 | # Select what will be displayed after "Listening to" (default: artist) [possible values: artist, track, none] 29 | # rpc_name: artist 30 | 31 | # Select the icon displayed next to the album cover (default playPause) [possible values: playPause, player, lastfmAvatar, none] 32 | small_image: playPause 33 | 34 | # Force a different player id and name to be displayed than the one actually used. "force_player_id" changes icon and "force_player_name" changes displayed text while hovering over the icon. 35 | # List of available icons: https://github.com/patryk-ku/music-discord-rpc?tab=readme-ov-file#the-icon-next-to-the-album-cover 36 | # force_player_id: "custom_player_id" 37 | # force_player_name: "Custom Player Name" 38 | 39 | # Prevent MPRIS artUrl to be used as album cover if cover is not available on Last.fm. Mainly for working with thumbnails from YouTube and other video sites. 40 | # Additionally, it also disables icon and player name replacement on YouTube if it detects a YouTube thumbnail link. 41 | disable_mpris_art_url: false 42 | 43 | # Only use the status from the following music players 44 | # Use -l, --list-players to get player exact name to use with this option 45 | # The order matters and the first is the most important. 46 | # allowlist: 47 | # - "VLC Media Player" 48 | # - "Chrome" 49 | # - "Any other player" 50 | 51 | # Will use the "watching" activity 52 | # Use -l, --list-players to get player exact name to use with this option 53 | # video_players: 54 | # - "VLC Media Player" 55 | # - "Chrome" 56 | 57 | # Hide the album name to decrease activity height 58 | hide_album_name: false 59 | 60 | # Only send activity when media is playing 61 | only_when_playing: false 62 | 63 | # Prevent MusicBrainz to be used as source of album cover if cover is not available on Last.fm 64 | disable_musicbrainz_cover: false 65 | 66 | # Disable cache (not recommended) 67 | disable_cache: false 68 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | include: 14 | - os: ubuntu-latest 15 | target: music-discord-rpc 16 | - os: macos-13 17 | target: music-discord-rpc-macos-amd64 # Intel macOS 18 | - os: macos-14 19 | target: music-discord-rpc-macos-arm64 # Apple Silicon macOS 20 | 21 | permissions: 22 | contents: write 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - uses: dtolnay/rust-toolchain@stable 28 | 29 | - name: Install Ubuntu dependencies 30 | if: runner.os == 'Linux' 31 | run: | 32 | echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list 33 | sudo add-apt-repository -y universe 34 | sudo apt-get -y update 35 | sudo apt-get install -y libdbus-1-dev pkg-config nfpm libfuse2 36 | env: 37 | DEBIAN_FRONTEND: noninteractive 38 | 39 | - name: Install appimagetool 40 | if: runner.os == 'Linux' 41 | run: | 42 | wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage 43 | chmod +x appimagetool-x86_64.AppImage 44 | sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool 45 | 46 | - name: "Create env file" 47 | run: | 48 | touch .env 49 | echo LASTFM_API_KEY=${{ secrets.LASTFM_API_KEY }} >> .env 50 | 51 | - name: Build 52 | run: | 53 | cargo build --release 54 | 55 | - name: Create .deb package 56 | if: runner.os == 'Linux' 57 | run: | 58 | nfpm package --packager deb --config nfpm.yaml --target target/release/music-discord-rpc.deb 59 | 60 | - name: Create .rpm package 61 | if: runner.os == 'Linux' 62 | run: | 63 | nfpm package --packager rpm --config nfpm.yaml --target target/release/music-discord-rpc.rpm 64 | 65 | - name: Create .AppImage package 66 | if: runner.os == 'Linux' 67 | run: | 68 | cargo install cargo-appimage 69 | cargo appimage 70 | 71 | - name: Package tar.gz (macOS only) 72 | if: runner.os == 'macOS' 73 | run: | 74 | tar -czf target/release/${{ matrix.target }}.tar.gz -C target/release music-discord-rpc 75 | rm target/release/music-discord-rpc 76 | 77 | - name: Upload binaries and packages 78 | uses: actions/upload-artifact@v4 79 | with: 80 | name: artifacts-${{ matrix.target }} 81 | path: | 82 | target/release/music-discord-rpc 83 | target/release/music-discord-rpc.deb 84 | target/release/music-discord-rpc.rpm 85 | target/appimage/music-discord-rpc.AppImage 86 | target/release/${{ matrix.target }}.tar.gz 87 | if-no-files-found: ignore 88 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "**" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | release: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | include: 17 | - os: ubuntu-latest 18 | target: music-discord-rpc 19 | - os: macos-13 20 | target: music-discord-rpc-macos-amd64 # Intel macOS 21 | - os: macos-14 22 | target: music-discord-rpc-macos-arm64 # Apple Silicon macOS 23 | 24 | permissions: 25 | contents: write 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: dtolnay/rust-toolchain@stable 31 | 32 | - name: Install Ubuntu dependencies 33 | if: runner.os == 'Linux' 34 | run: | 35 | echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list 36 | sudo add-apt-repository -y universe 37 | sudo apt-get -y update 38 | sudo apt-get install -y libdbus-1-dev pkg-config nfpm libfuse2 39 | env: 40 | DEBIAN_FRONTEND: noninteractive 41 | 42 | - name: Install appimagetool 43 | if: runner.os == 'Linux' 44 | run: | 45 | wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage 46 | chmod +x appimagetool-x86_64.AppImage 47 | sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool 48 | 49 | - name: "Create env file" 50 | run: | 51 | touch .env 52 | echo LASTFM_API_KEY=${{ secrets.LASTFM_API_KEY }} >> .env 53 | 54 | - name: Build 55 | run: | 56 | cargo build --release 57 | 58 | - name: Create .deb package 59 | if: runner.os == 'Linux' 60 | run: | 61 | nfpm package --packager deb --config nfpm.yaml --target target/release/music-discord-rpc.deb 62 | 63 | - name: Create .rpm package 64 | if: runner.os == 'Linux' 65 | run: | 66 | nfpm package --packager rpm --config nfpm.yaml --target target/release/music-discord-rpc.rpm 67 | 68 | - name: Create .AppImage package 69 | if: runner.os == 'Linux' 70 | run: | 71 | cargo install cargo-appimage 72 | cargo appimage 73 | 74 | - name: Package tar.gz (macOS only) 75 | if: runner.os == 'macOS' 76 | run: | 77 | tar -czf target/release/${{ matrix.target }}.tar.gz -C target/release music-discord-rpc 78 | rm target/release/music-discord-rpc 79 | 80 | - name: GH Release (Linux) 81 | if: startsWith(github.ref, 'refs/tags/') || runner.os == 'Linux' 82 | uses: softprops/action-gh-release@v2 83 | with: 84 | body_path: .github/release-notes.md 85 | files: | 86 | target/release/music-discord-rpc 87 | target/release/music-discord-rpc.deb 88 | target/release/music-discord-rpc.rpm 89 | target/appimage/music-discord-rpc.AppImage 90 | 91 | - name: GH Release (macOS) 92 | if: startsWith(github.ref, 'refs/tags/') || runner.os == 'macOS' 93 | uses: softprops/action-gh-release@v2 94 | with: 95 | body_path: .github/release-notes.md 96 | files: | 97 | target/release/${{ matrix.target }}.tar.gz 98 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | - Use HTTPS for all external API requests (Last.fm, Cover Art Archive). 4 | 5 | ## v0.6.2 6 | 7 | - Fix: Prevent playerctld from breaking allow list (#41). 8 | 9 | ## v0.6.1 10 | 11 | - Fix the allowlist on macOS (#38). 12 | - Add fallback cover search for albums with ` - EP` and ` - Single` suffixes added by Apple Music (#39). 13 | - Minor changes to how the player ID and name are determined (mainly on macOS). 14 | 15 | ## v0.6.0 16 | 17 | - Ported to MacOS. 18 | - Renamed project from **mpris-discord-rpc** to **music-discord-rpc**. 19 | - Added `--get-player-id` flag for easier retrieval of Player ID when requesting missing icons. 20 | 21 | ## v0.5.1 22 | 23 | - Added `--only-when-playing` argument and `only_when_playing` option in the config file. When enabled, RPC activity is only sent to Discord when media is playing, similar to Spotify. Thanks to @aritsune for the contribution. 24 | 25 | ## v0.5.0 26 | 27 | - Added option to customize "Listening to" text on the Discord user list. It now looks the same as the Spotify RPC, but you can also choose to show either the artist name or the song title. 28 | - The song title is now a link that searches for the song on YouTube. 29 | - Minor bug fixes. 30 | - Updated dependencies to latest versions. 31 | 32 | ## v0.4.0 33 | 34 | - Added the ability to provide your own Last.fm API key via arguments and config file. 35 | - Providing a Last.fm API key during compilation is now optional. 36 | - Added MusicBrainz as a fallback source for album art when Last.fm doesn't provide one or API key is not set. This can be disabled with an argument or in the config. 37 | - Added `--xdg` flag to `enable` and `disable` subcommands that creates or removes a .desktop file for XDG Autostart as an alternative to systemd for distributions without it. 38 | - Now respects `$XDG_CONFIG_HOME` when choosing where to store and read the config file. 39 | - Also available as an `.AppImage` format. 40 | 41 | ## v0.3.0 42 | 43 | - If no album art is found on Last.fm, use the `artUrl` provided by MPRIS if it exists. This is especially useful for movies played in a browser, e.g., YouTube. If a YouTube thumbnail URL is detected, replace the player icon with the YouTube icon. This can be disabled with an argument or in the config. 44 | - Added the option to mark players as video players, which will display the status "Watching Video" and make the RPC more appropriate for movies. A video thumbnail will be displayed if available as `artUrl` in MPRIS. 45 | - Added a new `mprisUrl` button that can link to the currently playing content if MPRIS provides such information. 46 | - The systemd unit file is now installed by the package manager instead of manually by the program. 47 | - Added the ability to force a different player icon and name than is actually used. 48 | 49 | ## v0.2.2 50 | 51 | - Fixed: Incorrect album art display when the album artist differed from the track artist. 52 | - Updated dependencies to latest versions. 53 | 54 | ## v0.2.1 55 | 56 | - Added option to hide album title in activity. 57 | - Added the ability to customize displayed icon adjacent to album artwork. Available options: none, play/pause icons, music player icon, Last.fm avatar. 58 | 59 | ## v0.2.0 60 | 61 | - (⚠️ **Breaking**) The `-n`, `--player-name` argument has been removed. Use `-a`, `--allowlist-add` instead. 62 | - (⚠️ **Breaking**) The previous button arguments (`-p`, `--profile-button`, `-y`, `--yt-button`) have been consolidated into a single new argument: `-b, --button` with options: `yt`, `lastfm`, `listenbrainz` and separate arguments for setting service usernames. More additional buttons coming in the future. 63 | - Added support for configuring the program via a configuration file. 64 | - Added commands for easy setup of autostart using systemd. 65 | - Now available as `.deb` and `.rpm` packages, and in the AUR. 66 | - Print debug logs with `--debug-log`. 67 | 68 | ## v0.1.5 69 | 70 | - Set Discord RPC activity type to "Listening". 71 | - Listening progress bar similar to Spofity. 72 | - Album name is now displayed. 73 | - Fixed detection of track duration, current position and start/end time calculation. 74 | - From now on, the program checks if the directory to which it tries to save the cache file exists. The cache should now work properly. 75 | 76 | ## v0.1.4 77 | 78 | - Allowlist of music players (`-a` or `--allowlist-add`). 79 | 80 | ## v0.1.3 81 | 82 | - Dependencies update. 83 | 84 | ## v0.1.2 85 | 86 | - Dependencies update. 87 | 88 | ## v0.1.1 89 | 90 | - List active MPRIS2 players with `-l` or `--list-players` arguments. 91 | - Select only one specific player for music status with `-n` or `--player-name` arguments. 92 | - Fix: skip setting status when unknown metadata. 93 | - Better log messages. 94 | - Switched to different cache library. 95 | 96 | ## v0.1.0 97 | 98 | - Initial release. 99 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | use clap_serde_derive::{ 2 | clap::{self, Parser, Subcommand}, 3 | serde::Serialize, 4 | ClapSerde, 5 | }; 6 | use std::fs; 7 | use std::path::PathBuf; 8 | use std::process; 9 | 10 | use crate::debug_log; 11 | use crate::utils::get_config_path; 12 | 13 | #[derive(Parser, ClapSerde, Serialize, Debug)] 14 | #[command(author, version, about, long_about = None)] 15 | pub struct Cli { 16 | /// Activity refresh rate (min: 5, default: 10) 17 | #[arg(short, long, value_name = "seconds", value_parser = clap::value_parser!(u64).range(5..))] 18 | pub interval: Option, 19 | 20 | /// Select visible buttons 21 | #[arg(short, long, value_name = "name", value_parser = ["yt", "lastfm", "listenbrainz", "mprisUrl", "shamelessAd"])] 22 | pub button: Vec, 23 | 24 | /// Your Last.fm nickname 25 | #[arg(long, value_name = "nickname", value_parser = clap::value_parser!(String))] 26 | pub lastfm_name: Option, 27 | 28 | /// Your Listenbrainz nickname 29 | #[arg(long, value_name = "nickname", value_parser = clap::value_parser!(String))] 30 | pub listenbrainz_name: Option, 31 | 32 | /// Select what will be displayed after "Listening to" (default: artist) 33 | #[arg(short, long, value_name = "value", value_parser = ["artist", "track", "none"])] 34 | pub rpc_name: Option, 35 | 36 | /// Select the icon displayed next to the album cover (default: playPause) 37 | #[arg(short, long, value_name = "name", value_parser = ["playPause", "player", "lastfmAvatar", "none"])] 38 | pub small_image: Option, 39 | 40 | /// Force a different player id to be displayed than the one actually used 41 | #[arg(long, value_name = "player_id", value_parser = clap::value_parser!(String))] 42 | pub force_player_id: Option, 43 | 44 | /// Force a different player name to be displayed than the one actually used 45 | #[arg(long, value_name = "player name", value_parser = clap::value_parser!(String))] 46 | pub force_player_name: Option, 47 | 48 | /// Prevent MPRIS artUrl to be used as album cover if cover is not available on Last.fm 49 | #[arg(long)] 50 | pub disable_mpris_art_url: bool, 51 | 52 | /// Displays all available music player names and exits. Use to get your player name for -a argument 53 | #[arg(short, long)] 54 | #[serde(skip_deserializing)] 55 | pub list_players: bool, 56 | 57 | /// Show ID of currently detected player. Use when requesting missing icon. 58 | #[arg(long)] 59 | #[serde(skip_deserializing)] 60 | pub get_player_id: bool, 61 | 62 | /// Get status only from given player. Use multiple times to add several players. 63 | #[arg(short = 'a', long = "allowlist-add", value_name = "Player Name", value_parser = clap::value_parser!(String))] 64 | pub allowlist: Vec, 65 | 66 | /// Will use the "watching" activity. Use multiple times to add several players. 67 | #[arg(short = 'w', long = "video-players", value_name = "Player Name", value_parser = clap::value_parser!(String))] 68 | pub video_players: Vec, 69 | 70 | /// Hide album name 71 | #[arg(long)] 72 | pub hide_album_name: bool, 73 | 74 | /// Only send activity when media is playing 75 | #[arg(long)] 76 | pub only_when_playing: bool, 77 | 78 | /// Disable cache (not recommended) 79 | #[arg(short, long)] 80 | pub disable_cache: bool, 81 | 82 | /// Your Last.fm API key 83 | #[arg(long, value_name = "api_key", value_parser = clap::value_parser!(String))] 84 | pub lastfm_api_key: Option, 85 | 86 | /// Do not use MusicBrainz as a fallback source of album covers 87 | #[arg(long)] 88 | pub disable_musicbrainz_cover: bool, 89 | 90 | /// Show debug log 91 | #[arg(long)] 92 | #[serde(skip_deserializing)] 93 | pub debug_log: bool, 94 | 95 | /// Reset config file (overwrites the old file if exists) 96 | #[arg(long)] 97 | #[serde(skip_deserializing)] 98 | pub reset_config: bool, 99 | 100 | /// Recursive fields 101 | #[serde(skip_deserializing)] 102 | #[command(flatten)] 103 | pub suboptions: SubConfig, 104 | } 105 | 106 | #[derive(Debug, Parser, Default, Serialize)] 107 | pub struct SubConfig { 108 | #[command(subcommand)] 109 | pub command: Option, 110 | } 111 | 112 | #[derive(Subcommand, Debug, Serialize)] 113 | pub enum Commands { 114 | /// Start RPC in the background and enable autostart 115 | Enable { 116 | /// Use XDG Autostart instead of systemd 117 | #[arg(long)] 118 | #[serde(skip_deserializing)] 119 | xdg: bool, 120 | }, 121 | /// Stop RPC and disable autostart 122 | Disable { 123 | /// Use XDG Autostart instead of systemd 124 | #[arg(long)] 125 | #[serde(skip_deserializing)] 126 | xdg: bool, 127 | }, 128 | /// Use to restart the service and reload the changed configuration file. 129 | Restart {}, 130 | } 131 | 132 | // Use to get config path, create new config or reset existing 133 | fn create_config_file(force: bool) -> (bool, PathBuf) { 134 | let mut config_file = match get_config_path() { 135 | Some(path) => path, 136 | None => { 137 | println!("\x1b[31mWARNING: Failed to determine user config directory.\x1b[0m"); 138 | return (false, PathBuf::new()); 139 | } 140 | }; 141 | config_file.push("music-discord-rpc"); 142 | 143 | let config_dir = config_file.clone(); 144 | config_file.push("config.yaml"); 145 | 146 | if config_file.exists() && !force { 147 | return (true, config_file); 148 | } 149 | 150 | let config_text = r#"# music-discord-rpc configuration file 151 | 152 | # You can reset this file using the command: 153 | # music-discord-rpc --reset-config 154 | # Or you can manually copy the example config from repo: 155 | # https://github.com/patryk-ku/music-discord-rpc/blob/main/config.yaml 156 | 157 | # If you compiled binary by yourself, you may need to provide your Last.fm API key here. 158 | # Or if you use precompiled binary, you can override the default Last.fm API key. 159 | # You can easily get it from: https://www.last.fm/pl/api 160 | # lastfm_api_key: key_here 161 | 162 | # You can also disable Last.fm as a cover source by providing an empty string as the key. 163 | # lastfm_api_key: "" 164 | 165 | # Activity refresh rate in seconds (min 5) 166 | interval: 10 167 | 168 | # Select visible activity buttons (max 2) [possible values: yt, lastfm, listenbrainz, mprisUrl, shamelessAd] 169 | # button: 170 | # - yt 171 | # - lastfm 172 | 173 | # Uncomment and enter your nicknames for activity buttons 174 | # lastfm_name: "nickname" 175 | # listenbrainz_name: "nickname" 176 | 177 | # Select what will be displayed after "Listening to" (default: artist) [possible values: artist, track, none] 178 | # rpc_name: artist 179 | 180 | # Select the icon displayed next to the album cover (default playPause) [possible values: playPause, player, lastfmAvatar, none] 181 | small_image: playPause 182 | 183 | # Force a different player id and name to be displayed than the one actually used. "force_player_id" changes icon and "force_player_name" changes displayed text while hovering over the icon. 184 | # List of available icons: https://github.com/patryk-ku/music-discord-rpc?tab=readme-ov-file#the-icon-next-to-the-album-cover 185 | # force_player_id: "custom_player_id" 186 | # force_player_name: "Custom Player Name" 187 | 188 | # Prevent MPRIS artUrl to be used as album cover if cover is not available on Last.fm. Mainly for working with thumbnails from YouTube and other video sites. 189 | # Additionally, it also disables icon and player name replacement on YouTube if it detects a YouTube thumbnail link. 190 | disable_mpris_art_url: false 191 | 192 | # Only use the status from the following music players 193 | # Use -l, --list-players to get player exact name to use with this option 194 | # The order matters and the first is the most important. 195 | # allowlist: 196 | # - "VLC Media Player" 197 | # - "Chrome" 198 | # - "Any other player" 199 | 200 | # Will use the "watching" activity 201 | # Use -l, --list-players to get player exact name to use with this option 202 | # video_players: 203 | # - "VLC Media Player" 204 | # - "Chrome" 205 | 206 | # Hide the album name to decrease activity height 207 | hide_album_name: false 208 | 209 | # Only send activity when media is playing 210 | only_when_playing: false 211 | 212 | # Prevent MusicBrainz to be used as source of album cover if cover is not available on Last.fm 213 | disable_musicbrainz_cover: false 214 | 215 | # Disable cache (not recommended) 216 | disable_cache: false 217 | "#; 218 | 219 | match fs::create_dir_all(&config_dir) { 220 | Err(_) => { 221 | println!("[config] Failed to create config directory."); 222 | return (false, config_file); 223 | } 224 | Ok(_) => match fs::write(&config_file, config_text) { 225 | Ok(_) => println!( 226 | "[config] Created new config file: {}", 227 | config_file.display() 228 | ), 229 | Err(_) => { 230 | println!("[config] Error: Failed to create config file."); 231 | return (false, config_file); 232 | } 233 | }, 234 | } 235 | 236 | return (true, config_file); 237 | } 238 | 239 | // Used to get settings merged from args and config file 240 | pub fn load_settings() -> Cli { 241 | let args = Cli::parse(); 242 | debug_log!(args.debug_log, "Debug logs: enabled."); 243 | debug_log!(args.debug_log, "args: {:#?}", args); 244 | 245 | // Reset config file is user used --reset-config and exit 246 | if args.reset_config { 247 | create_config_file(true); 248 | process::exit(0); 249 | } 250 | 251 | let (mut config_exists, config_file) = create_config_file(false); 252 | if !config_exists { 253 | return args; 254 | } 255 | 256 | // Read user config file 257 | let mut config = match fs::read_to_string(&config_file) { 258 | Ok(yaml_str) => match serde_yaml::from_str::<::Opt>(&yaml_str) { 259 | Ok(yaml_args) => Cli::from(yaml_args), 260 | Err(error) => { 261 | println!("Failed to parse config file: {}", error); 262 | config_exists = false; 263 | Cli::from_clap() 264 | } 265 | }, 266 | Err(_) => { 267 | println!("Failed to read config file."); 268 | config_exists = false; 269 | Cli::from_clap() 270 | } 271 | }; 272 | 273 | if !config_exists { 274 | return args; 275 | } 276 | println!("Configuration loaded from file: {}", config_file.display()); 277 | debug_log!(args.debug_log, "config: {:#?}", config); 278 | 279 | // Logic of merging config with args 280 | if args.interval != config.interval && args.interval.is_some() { 281 | config.interval = args.interval; 282 | } 283 | 284 | if args.button != config.button && args.button.len() > 0 { 285 | config.button = args.button; 286 | } 287 | 288 | if args.lastfm_name != config.lastfm_name && args.lastfm_name.is_some() { 289 | config.lastfm_name = args.lastfm_name; 290 | } 291 | 292 | if args.listenbrainz_name != config.listenbrainz_name && args.listenbrainz_name.is_some() { 293 | config.listenbrainz_name = args.listenbrainz_name; 294 | } 295 | 296 | if args.rpc_name != config.rpc_name && args.rpc_name.is_some() { 297 | config.rpc_name = args.rpc_name; 298 | } 299 | 300 | if args.small_image != config.small_image && args.small_image.is_some() { 301 | config.small_image = args.small_image; 302 | } 303 | 304 | if args.force_player_id != config.force_player_id && args.force_player_id.is_some() { 305 | config.force_player_id = args.force_player_id; 306 | } 307 | 308 | if args.force_player_name != config.force_player_name && args.force_player_name.is_some() { 309 | config.force_player_name = args.force_player_name; 310 | } 311 | 312 | if args.disable_musicbrainz_cover { 313 | config.disable_musicbrainz_cover = args.disable_musicbrainz_cover; 314 | } 315 | 316 | if args.hide_album_name { 317 | config.hide_album_name = args.hide_album_name; 318 | } 319 | 320 | if args.only_when_playing { 321 | config.only_when_playing = args.only_when_playing; 322 | } 323 | 324 | if args.disable_cache { 325 | config.disable_cache = args.disable_cache; 326 | } 327 | 328 | if args.list_players { 329 | config.list_players = args.list_players; 330 | } 331 | 332 | if args.get_player_id { 333 | config.get_player_id = args.get_player_id; 334 | } 335 | 336 | if args.allowlist != config.allowlist && args.allowlist.len() > 0 { 337 | config.allowlist = args.allowlist; 338 | } 339 | 340 | if args.video_players != config.video_players && args.video_players.len() > 0 { 341 | config.video_players = args.video_players; 342 | } 343 | 344 | if args.lastfm_api_key != config.lastfm_api_key && args.lastfm_api_key.is_some() { 345 | config.lastfm_api_key = args.lastfm_api_key; 346 | } 347 | 348 | if args.disable_mpris_art_url { 349 | config.disable_mpris_art_url = args.disable_mpris_art_url; 350 | } 351 | 352 | if args.debug_log { 353 | config.debug_log = args.debug_log; 354 | } 355 | 356 | if args.reset_config { 357 | config.reset_config = args.reset_config; 358 | } 359 | 360 | config.suboptions = args.suboptions; 361 | 362 | return config; 363 | } 364 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use discord_rich_presence::{DiscordIpc, DiscordIpcClient}; 2 | use pickledb::PickleDb; 3 | use reqwest; 4 | use reqwest::blocking::Client; 5 | use reqwest::header::USER_AGENT; 6 | use serde_json; 7 | use std::env; 8 | 9 | #[cfg(target_os = "linux")] 10 | use mpris::Player; 11 | #[cfg(target_os = "linux")] 12 | use std::time::Duration; 13 | #[cfg(target_os = "linux")] 14 | use std::{fs, process}; 15 | 16 | // A common struct to hold song information, ensuring a consistent 17 | // return type regardless of the platform. 18 | #[derive(Debug)] 19 | pub struct MediaInfo { 20 | pub title: String, 21 | pub artist: String, 22 | pub album_artist: String, 23 | pub album: String, 24 | pub is_playing: bool, 25 | pub duration: u64, 26 | pub position: u64, 27 | pub is_track_position: bool, 28 | pub art_url: String, // Link to cover art on the internet 29 | pub url: String, // Link to the currently playing media on the internet 30 | #[cfg(target_os = "macos")] 31 | pub player_id: String, 32 | } 33 | 34 | // Use a Result to handle potential errors, like no media playing. 35 | type NowPlayingResult = Result>; 36 | 37 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 38 | 39 | // Use to print debug log if enabled with argument 40 | #[macro_export] 41 | macro_rules! debug_log { 42 | ($cond:expr, $($arg:tt)*) => { 43 | if $cond { 44 | println!("\x1b[34;1m[debug]\x1b[0m {}", format!($($arg)*)); 45 | } 46 | }; 47 | } 48 | 49 | #[cfg(target_os = "linux")] 50 | fn is_systemd_present() { 51 | match process::Command::new("ps") 52 | .arg("-p") 53 | .arg("1") 54 | .arg("-o") 55 | .arg("comm=") 56 | .output() 57 | { 58 | Ok(output) => { 59 | let init_process = String::from_utf8_lossy(&output.stdout); 60 | if init_process.trim() == "systemd" { 61 | return; 62 | } 63 | println!("\x1b[33;1mINFO: Most likely systemd is not available on your system.\x1b[0m"); 64 | } 65 | Err(_) => { 66 | println!( 67 | "\x1b[33;1mINFO: Could not detect if systemd is available on your system.\x1b[0m" 68 | ) 69 | } 70 | } 71 | println!( 72 | "You can try using XDG Autostart instead to add the application to autostart without systemd." 73 | ); 74 | println!("Use the \x1b[32;1m--xdg\x1b[0m flag with the subcommands like this: \x1b[32;1mmusic-discord-rpc enable --xdg\x1b[0m."); 75 | } 76 | 77 | #[cfg(target_os = "linux")] 78 | pub fn enable_service() { 79 | match process::Command::new("systemctl") 80 | .arg("--user") 81 | .arg("daemon-reload") 82 | .status() 83 | { 84 | Ok(_) => println!("Reloaded user systemd services."), 85 | Err(_) => { 86 | println!("Failed to reload user systemd services."); 87 | is_systemd_present(); 88 | process::exit(1); 89 | } 90 | } 91 | 92 | match process::Command::new("systemctl") 93 | .arg("--user") 94 | .arg("enable") 95 | .arg("--now") 96 | .arg("music-discord-rpc.service") 97 | .status() 98 | { 99 | Ok(_) => println!("Enabled and started user systemd service."), 100 | Err(_) => { 101 | println!("Failed to enable and start user systemd service."); 102 | is_systemd_present(); 103 | process::exit(1); 104 | } 105 | } 106 | 107 | process::exit(0); 108 | } 109 | 110 | #[cfg(target_os = "linux")] 111 | pub fn disable_service() { 112 | match process::Command::new("systemctl") 113 | .arg("--user") 114 | .arg("disable") 115 | .arg("--now") 116 | .arg("music-discord-rpc.service") 117 | .status() 118 | { 119 | Ok(_) => println!("Stopped and disabled user systemd service."), 120 | Err(_) => { 121 | println!("Failed to stop and disable user systemd service."); 122 | is_systemd_present(); 123 | process::exit(1); 124 | } 125 | } 126 | 127 | process::exit(0); 128 | } 129 | 130 | #[cfg(target_os = "linux")] 131 | pub fn restart_service() { 132 | match process::Command::new("systemctl") 133 | .arg("--user") 134 | .arg("restart") 135 | .arg("music-discord-rpc.service") 136 | .status() 137 | { 138 | Ok(_) => println!("Restarted user systemd service."), 139 | Err(_) => { 140 | println!("Failed to restart user systemd service."); 141 | process::exit(1); 142 | } 143 | } 144 | process::exit(0); 145 | } 146 | 147 | pub fn get_config_path() -> Option { 148 | if let Some(config_home) = env::var_os("XDG_CONFIG_HOME") { 149 | Some(std::path::PathBuf::from(config_home)) 150 | } else if let Some(home_dir) = env::var_os("HOME") { 151 | let mut path = std::path::PathBuf::from(home_dir); 152 | path.push(".config"); 153 | Some(path) 154 | } else { 155 | None 156 | } 157 | } 158 | 159 | #[cfg(target_os = "linux")] 160 | pub fn add_xdg_autostart() { 161 | let mut desktopt_file_path = match get_config_path() { 162 | Some(path) => path, 163 | None => { 164 | println!("\x1b[31mWARNING: Failed to determine user config directory.\x1b[0m"); 165 | process::exit(1); 166 | } 167 | }; 168 | desktopt_file_path.push("autostart"); 169 | desktopt_file_path.push("music-discord-rpc.desktop"); 170 | 171 | let desktop_file_content = r#"[Desktop Entry] 172 | Name=music-discord-rpc 173 | Type=Application 174 | Exec=music-discord-rpc 175 | X-GNOME-Autostart-enabled=true 176 | Hidden=false 177 | StartupNotify=false 178 | Terminal=false 179 | "#; 180 | 181 | match fs::write(&desktopt_file_path, desktop_file_content) { 182 | Ok(_) => { 183 | println!( 184 | "Created file: \x1b[32;1m{}\x1b[0m ", 185 | desktopt_file_path.display() 186 | ); 187 | println!("This RPC should now start automatically with your system if your DE/WM supports XDG Autostart."); 188 | } 189 | Err(_) => { 190 | println!("\x1b[31mERROR: Failed to create autostart .desktop file.\x1b[0m"); 191 | process::exit(1); 192 | } 193 | } 194 | 195 | process::exit(0); 196 | } 197 | 198 | #[cfg(target_os = "linux")] 199 | pub fn remove_xdg_autostart() { 200 | let mut desktopt_file_path = match get_config_path() { 201 | Some(path) => path, 202 | None => { 203 | println!("\x1b[31mWARNING: Failed to determine user config directory.\x1b[0m"); 204 | process::exit(1); 205 | } 206 | }; 207 | desktopt_file_path.push("autostart"); 208 | desktopt_file_path.push("music-discord-rpc.desktop"); 209 | 210 | match fs::remove_file(&desktopt_file_path) { 211 | Ok(_) => println!( 212 | "Removed file: \x1b[32;1m{}\x1b[0m ", 213 | desktopt_file_path.display() 214 | ), 215 | Err(_) => { 216 | println!("\x1b[31mERROR: Failed to remove autostart .desktop file.\x1b[0m"); 217 | process::exit(1); 218 | } 219 | } 220 | 221 | process::exit(0); 222 | } 223 | 224 | pub fn clear_activity(is_activity_set: &mut bool, client: &mut DiscordIpcClient) { 225 | if *is_activity_set { 226 | let is_activity_cleared = client.clear_activity().is_ok(); 227 | 228 | if is_activity_cleared { 229 | *is_activity_set = false; 230 | return; 231 | } 232 | 233 | let is_reconnected = client.reconnect().is_ok(); 234 | if !is_reconnected { 235 | return; 236 | } 237 | 238 | if client.clear_activity().is_ok() { 239 | *is_activity_set = false; 240 | } 241 | } 242 | } 243 | 244 | pub fn get_cover_url( 245 | album_id: &str, 246 | album: &str, 247 | mut _cover_url: String, 248 | cache_enabled: bool, 249 | album_cache: &mut PickleDb, 250 | artist: &str, 251 | lastfm_api_key: &str, 252 | ) -> String { 253 | // If no album or Unknown Album 254 | if album.eq("Unknown Album") { 255 | println!("Missing album name or Unknown Album."); 256 | 257 | return String::from("missing-cover"); 258 | } 259 | 260 | // Load from cache if enabled 261 | if cache_enabled { 262 | let cache_url = if album_cache.exists(&album_id) { 263 | match album_cache.get(&album_id) { 264 | Some(url) => url, 265 | None => String::new(), 266 | } 267 | } else { 268 | String::new() 269 | }; 270 | 271 | if (!cache_url.is_empty()) && (cache_url.len() > 5) { 272 | return String::from(cache_url); 273 | } 274 | } 275 | 276 | let request_url = format!( 277 | "https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={}&artist={}&album={}&autocorrect=0&format=json", 278 | lastfm_api_key, 279 | url_escape::encode_component(artist), 280 | url_escape::encode_component(album) 281 | ); 282 | 283 | let mut url: String = match reqwest::blocking::get(request_url) { 284 | Ok(res) => match res.json::() { 285 | Ok(data) => data["album"]["image"][3]["#text"].to_string(), 286 | Err(_) => String::new(), 287 | }, 288 | Err(_) => String::new(), 289 | }; 290 | 291 | if !url.is_empty() && (url.len() > 5) { 292 | url.pop(); 293 | url.remove(0); 294 | println!("[last.fm] fetched image link: {}", url); 295 | 296 | // Save cover url to cache 297 | if cache_enabled { 298 | match album_cache.set(&album_id, &url) { 299 | Ok(_) => { 300 | println!("[cache] saved image url for: {}.", album_id) 301 | } 302 | Err(_) => { 303 | println!("[cache] error, unable to write to cache file.") 304 | } 305 | } 306 | } 307 | 308 | return url; 309 | } 310 | 311 | return String::from("missing-cover"); 312 | } 313 | 314 | pub fn get_cover_url_musicbrainz( 315 | album_id: &str, 316 | album: &str, 317 | mut _cover_url: String, 318 | cache_enabled: bool, 319 | album_cache: &mut PickleDb, 320 | artist: &str, 321 | ) -> String { 322 | // If no album or Unknown Album 323 | if album.eq("Unknown Album") { 324 | println!("Missing album name or Unknown Album."); 325 | 326 | return String::from("missing-cover"); 327 | } 328 | 329 | // Load from cache if enabled 330 | if cache_enabled { 331 | let cache_url = if album_cache.exists(&album_id) { 332 | match album_cache.get(&album_id) { 333 | Some(url) => url, 334 | None => String::new(), 335 | } 336 | } else { 337 | String::new() 338 | }; 339 | 340 | if (!cache_url.is_empty()) && (cache_url.len() > 5) { 341 | return String::from(cache_url); 342 | } 343 | } 344 | 345 | let user_agent = format!( 346 | "music-discord-rpc/{} (patryk.kurdziel@protonmail.com)", 347 | VERSION 348 | ); 349 | 350 | let request_url = format!( 351 | "https://musicbrainz.org/ws/2/release/?query=artist:\"{}\"ANDrelease:\"{}\"&fmt=json&limit=1", 352 | url_escape::encode_component(artist), 353 | url_escape::encode_component(album) 354 | ); 355 | 356 | let client = Client::new(); 357 | let mut mbid: String = match client 358 | .get(request_url) 359 | .header(USER_AGENT, &user_agent) 360 | .send() 361 | { 362 | Ok(res) => match res.json::() { 363 | Ok(data) => data["releases"][0]["id"].to_string(), 364 | Err(_) => String::new(), 365 | }, 366 | Err(_) => String::new(), 367 | }; 368 | 369 | if !mbid.is_empty() && (mbid.len() > 5) { 370 | mbid.pop(); 371 | mbid.remove(0); 372 | } 373 | 374 | let mut url: String = match client 375 | .get(format!("https://coverartarchive.org/release/{}/", mbid)) 376 | .header(USER_AGENT, &user_agent) 377 | .send() 378 | { 379 | Ok(res) => match res.json::() { 380 | Ok(data) => data["images"][0]["thumbnails"]["small"].to_string(), 381 | Err(_) => String::new(), 382 | }, 383 | Err(_) => String::new(), 384 | }; 385 | 386 | if !url.is_empty() && (url.len() > 5) { 387 | url.pop(); 388 | url.remove(0); 389 | println!("[musicbrainz] fetched image link: {}", url); 390 | 391 | // Save cover url to cache 392 | if cache_enabled { 393 | match album_cache.set(&album_id, &url) { 394 | Ok(_) => { 395 | println!("[cache] saved image url for: {}.", album_id) 396 | } 397 | Err(_) => { 398 | println!("[cache] error, unable to write to cache file.") 399 | } 400 | } 401 | } 402 | 403 | return url; 404 | } 405 | 406 | return String::from("missing-cover"); 407 | } 408 | 409 | pub fn get_lastfm_avatar(username: &str, lastfm_api_key: &str) -> String { 410 | let request_url = format!( 411 | "https://ws.audioscrobbler.com/2.0/?method=user.getinfo&api_key={}&user={}&format=json", 412 | lastfm_api_key, 413 | url_escape::encode_component(username) 414 | ); 415 | 416 | let mut url: String = match reqwest::blocking::get(request_url) { 417 | Ok(res) => match res.json::() { 418 | Ok(data) => data["user"]["image"][3]["#text"].to_string(), 419 | Err(_) => String::new(), 420 | }, 421 | Err(_) => String::new(), 422 | }; 423 | 424 | if !url.is_empty() && (url.len() > 15) { 425 | url.pop(); 426 | url.remove(0); 427 | println!("[last.fm] fetched avatar link: {}", url); 428 | return url; 429 | } 430 | 431 | return String::new(); 432 | } 433 | 434 | pub fn sanitize_name(input: &str) -> String { 435 | input 436 | .to_lowercase() 437 | .chars() 438 | .map(|c| if c.is_alphanumeric() { c } else { '_' }) 439 | .collect::() 440 | .split('_') 441 | .filter(|s| !s.is_empty()) 442 | .collect::>() 443 | .join("_") 444 | } 445 | 446 | #[cfg(target_os = "linux")] 447 | pub fn get_currently_playing(player: &Player, debug_log: bool) -> NowPlayingResult { 448 | let metadata = match player.get_metadata() { 449 | Ok(metadata) => metadata, 450 | Err(err) => return Err(format!("Could not get metadata from player: {}", err).into()), 451 | }; 452 | debug_log!(debug_log, "{:#?}", metadata); 453 | 454 | let playback_status = match player.get_playback_status() { 455 | Ok(status) => status, 456 | Err(err) => { 457 | return Err(format!("Could not get playback status from player: {}", err).into()) 458 | } 459 | }; 460 | 461 | let is_playing: bool = match playback_status { 462 | mpris::PlaybackStatus::Playing => true, 463 | mpris::PlaybackStatus::Paused => false, 464 | mpris::PlaybackStatus::Stopped => false, 465 | }; 466 | debug_log!(debug_log, "playback_status: {:#?}", playback_status); 467 | 468 | // Parse metadata 469 | let title = metadata.title().unwrap_or("Unknown Title").to_string(); 470 | let mut album = metadata.album_name().unwrap_or("Unknown Album").to_string(); 471 | if album.is_empty() { 472 | album = "Unknown Album".to_string(); 473 | } 474 | let artist = match metadata.artists() { 475 | Some(artists) => { 476 | if artists.is_empty() { 477 | "Unknown Artist".to_string() 478 | } else { 479 | artists[0].to_string() 480 | } 481 | } 482 | None => "Unknown Artist".to_string(), 483 | }; 484 | let mut album_artist = match metadata.album_artists() { 485 | Some(artists) => { 486 | if artists.is_empty() { 487 | "Unknown Artist".to_string() 488 | } else { 489 | artists[0].to_string() 490 | } 491 | } 492 | None => "Unknown Artist".to_string(), 493 | }; 494 | if album_artist.is_empty() || album_artist == "Unknown Artist" { 495 | album_artist = artist.clone(); 496 | } 497 | 498 | // Get track duration if supported by player else return 0 499 | let duration = metadata.length().unwrap_or(Duration::new(0, 0)).as_secs(); 500 | 501 | // Get track position if supported by player else return 0 secs 502 | let mut is_track_position: bool = false; 503 | let position = match player.get_position() { 504 | Ok(position) => { 505 | is_track_position = true; 506 | position.as_secs() 507 | } 508 | Err(_) => Duration::new(0, 0).as_secs(), 509 | }; 510 | 511 | let art_url = match metadata.art_url() { 512 | Some(url) => url.to_string(), 513 | _ => String::new(), 514 | }; 515 | 516 | let url = match metadata.url() { 517 | Some(url) => { 518 | let url_string = url.to_string(); 519 | if url_string.starts_with("http://") || url_string.starts_with("https://") { 520 | url_string 521 | } else { 522 | String::new() 523 | } 524 | } 525 | _ => String::new(), 526 | }; 527 | 528 | Ok(MediaInfo { 529 | title, 530 | artist, 531 | album_artist, 532 | album, 533 | is_playing, 534 | duration, 535 | position, 536 | is_track_position, 537 | art_url, 538 | url, 539 | }) 540 | } 541 | 542 | #[cfg(target_os = "macos")] 543 | pub fn get_currently_playing() -> NowPlayingResult { 544 | // PREREQUISITE: You must install this tool first! 545 | // ==> brew install media-control 546 | use std::process::Command; 547 | 548 | let output = Command::new("media-control").args(["get"]).output(); 549 | 550 | match output { 551 | Ok(output) => { 552 | let result_str = String::from_utf8(output.stdout)?; 553 | if result_str.eq("null\n") { 554 | return Err("No media is currently playing. Waiting for any player...".into()); 555 | } 556 | let json_result: serde_json::Value = serde_json::from_str(&result_str)?; 557 | 558 | let title = json_result["title"] 559 | .as_str() 560 | .unwrap_or("Unknown Title") 561 | .to_string(); 562 | let artist = json_result["artist"] 563 | .as_str() 564 | .unwrap_or("Unknown Artist") 565 | .to_string(); 566 | let album = json_result["album"] 567 | .as_str() 568 | .unwrap_or("Unknown Album") 569 | .to_string(); 570 | let album_artist = artist.clone(); // Assuming album artist is the same as artist 571 | let is_playing = json_result["playing"].as_bool().unwrap_or(false); 572 | let duration = json_result["duration"].as_f64().unwrap_or(0.0) as u64; 573 | let position = json_result["elapsedTime"].as_f64().unwrap_or(0.0) as u64; 574 | let player_id = json_result["bundleIdentifier"] 575 | .as_str() 576 | .unwrap_or("Unknown Player") 577 | .to_string(); 578 | let art_url = String::new(); // For now cant get artwork remote url like with mpris 579 | let is_track_position = true; 580 | let url = String::new(); 581 | 582 | Ok(MediaInfo { 583 | title, 584 | artist, 585 | album_artist, 586 | album, 587 | is_playing, 588 | duration, 589 | position, 590 | is_track_position, 591 | art_url, 592 | url, 593 | player_id, 594 | }) 595 | } 596 | Err(e) => { 597 | // This error usually means media-control is not installed or not in PATH. 598 | Err(format!( 599 | "Failed to execute 'media-control'. Is it installed? (brew install media-control) Error: {}", 600 | e 601 | ).into()) 602 | } 603 | } 604 | } 605 | 606 | // Translate bundle id to app name for some players 607 | #[cfg(target_os = "macos")] 608 | pub fn app_name_from_bundle_id(bundle_id: &str) -> String { 609 | let name = match bundle_id { 610 | "com.apple.Music" => "Apple Music", 611 | "com.apple.iTunes" => "iTunes", 612 | "com.spotify.client" => "Spotify", 613 | "org.videolan.vlc" => "VLC media player", 614 | "com.colliderli.iina" => "IINA", 615 | "org.cogx.cog" => "Cog", 616 | "com.foobar2000.mac" => "foobar2000", 617 | "org.clementine-player.Clementine" => "Clementine", 618 | "com.jriver.MediaCenter" => "JRiver Media Center", 619 | "org.quodlibet.quodlibet" => "Quod Libet", 620 | "com.swinsian.Swinsian" => "Swinsian", 621 | "app.ytmdesktop.ytmdesktop" => "YouTube Music", 622 | "com.tidal.desktop" => "TIDAL", 623 | "com.deezer.desktop" => "Deezer", 624 | 625 | // Browsers 626 | "com.apple.Safari" => "Safari", 627 | "com.google.Chrome" => "Chrome", 628 | "org.chromium.Chromium" => "Chromium", 629 | "org.mozilla.firefox" => "Firefox", 630 | "com.microsoft.edgemac" => "Microsoft Edge", 631 | "com.brave.Browser" => "Brave", 632 | "com.opera.Opera" => "Opera", 633 | "com.vivaldi.Vivaldi" => "Vivaldi", 634 | 635 | // fallback 636 | other => other, 637 | }; 638 | 639 | name.to_string() 640 | } 641 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # music-discord-rpc ![GitHub Release](https://img.shields.io/github/v/release/patryk-ku/music-discord-rpc?label=%20) ![License](https://img.shields.io/badge/MIT-blue) ![Rust](https://img.shields.io/badge/Rust-%23000000.svg?e&logo=rust&logoColor=white&color=CE412B) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) 2 | 3 |

4 | 5 |

6 | 7 | Cross-platform Discord rich presence for music with **album cover and progress bar support**. You can customize additional buttons, such as linking to your Last.fm profile or searching for the current song on YouTube. There's also an option to display either the music player's icon or your Last.fm avatar next to the album cover. Album covers are fetched from Last.fm, with MusicBrainz used as a fallback. The application is written in Rust. 8 | 9 | ## Supported players 10 | 11 | ### Linux 12 | 13 | Any player or app with [MPRIS](https://wiki.archlinux.org/title/MPRIS) support. Basically nearly every music application on Linux supports MRPIS in some way so there are plenty of compatible players. Web browsers also support MPRIS so this will work even with music streaming services playing in Google Chrome or Firefox. 14 | 15 | ### MacOS 16 | 17 | The macOS version uses [media-control](https://github.com/ungive/media-control) to retrieve information about the currently playing song. From what I understand, media-control can extract information from most if not all music players, but I cannot guarantee this 100%. 18 | 19 | ## Requirements 20 | 21 | ### Linux 22 | 23 | Any fairly new 64-bit Linux distribution. It will probably also work on older versions of Linux but would have to be manually compiled on an older system. The optional background service and automatic startup capabilities rely on systemd or XDG Autostart. 24 | 25 | ### MacOS 26 | 27 | It works both on Intel-based Macs and the newer ones with Apple Silicon. 28 | 29 | ## Installation 30 | 31 | ### MacOS 32 | 33 |
34 | Instructions 35 | 36 | Install using Homebrew: 37 | 38 | ```sh 39 | brew tap patryk-ku/music-discord-rpc https://github.com/patryk-ku/music-discord-rpc 40 | brew install patryk-ku/music-discord-rpc/music-discord-rpc 41 | ``` 42 | 43 |
44 | 45 | ### Debian, Ubuntu, Mint and derivatives (.deb) 46 | 47 |
48 | Instructions 49 | 50 | Download the latest .deb file from the [Releases](https://github.com/patryk-ku/music-discord-rpc/releases) page. 51 | 52 | Now double-click it from file explorer to install it using Software Manager or use this command: 53 | 54 | ```sh 55 | sudo dpkg -i music-discord-rpc.deb 56 | ``` 57 | 58 |
59 | 60 | ### Fedora, openSUSE and derivatives (.rpm) 61 | 62 |
63 | Instructions 64 | 65 | Download the latest .rpm file from the [Releases](https://github.com/patryk-ku/music-discord-rpc/releases) page. 66 | 67 | Double-click the file in your file explorer to install it using your distribution's software manager. 68 | 69 | Alternatively, you can use the following commands: 70 | 71 | Fedora 72 | 73 | ```sh 74 | sudo dnf install ./music-discord-rpc.rpm 75 | ``` 76 | 77 | openSUSE 78 | 79 | ```sh 80 | sudo zypper install music-discord-rpc.rpm 81 | ``` 82 | 83 |
84 | 85 | ### Arch, Manjaro, EndeavourOS and derivatives (AUR) 86 | 87 |
88 | Instructions 89 | 90 | Available in the [AUR](https://aur.archlinux.org/packages/music-discord-rpc-bin). Install with your favorite AUR helper: 91 | 92 | ```sh 93 | yay -S music-discord-rpc-bin 94 | ``` 95 | 96 |
97 | 98 | ### Void 99 | 100 |
101 | Instructions 102 | 103 | This assumes using pre-cloned [xbps-src](https://github.com/void-linux/void-packages/) and installed [xtools](https://github.com/leahneukirchen/xtools) (shell helpers for xbps), available in official repos. 104 | If you don't want to build it locally, use the 'Other Distributions' option (below) 105 | 106 | ```sh 107 | #1. Change directory to your local clone of void-packages repo 108 | cd /path/to/void-packages 109 | # 2. Create package template folder for music-discord-rpc 110 | mkdir -p srcpkgs/music-discord-rpc 111 | # 3. Fetch template 112 | curl https://raw.githubusercontent.com/patryk-ku/music-discord-rpc/refs/heads/main/xbps/template > srcpkgs/music-discord-rpc/template 113 | # 4. And version checking pattern 114 | curl https://raw.githubusercontent.com/patryk-ku/music-discord-rpc/refs/heads/main/xbps/update > srcpkgs/music-discord-rpc/update 115 | # 5. Update checksum of music-discord-rpc with newest release 116 | xgensum -i music-discord-rpc 117 | # 6. Build and package 118 | ./xbps-src pkg music-discord-rpc 119 | # 7. Install 120 | xi music-discord-rpc 121 | ``` 122 | To update repeat the above steps. You can use `./xbps-src update-check music-discord-rpc` to check for availability of new version(s). 123 | 124 | See the [Intro](https://xbps-src-tutorials.github.io/) to or [Manual](https://github.com/void-linux/void-packages/blob/master/Manual.md) for `xbps-src` for details about how it builds and operates. 125 | 126 | Ping @JkktBkkt if you're experiencing an issue with building on Void. 127 | Note that officially supported platform is x86_64 on glibc only, the rest aren't tested. 128 | 129 |
130 | 131 | ### Other Distributions 132 | 133 |
134 | Instructions 135 | 136 | Download the latest executable from the [Releases](https://github.com/patryk-ku/music-discord-rpc/releases) page (just a `music-discord-rpc` file) and grant execute permissions: 137 | 138 | ```sh 139 | curl -L -o music-discord-rpc 'https://github.com/patryk-ku/music-discord-rpc/releases/latest/download/music-discord-rpc' 140 | chmod +x music-discord-rpc 141 | ``` 142 | 143 | This binary has no additional dependencies and should work on most distributions. So you can simply run the file in the terminal like this: 144 | 145 | ```sh 146 | ./music-discord-rpc 147 | ``` 148 | 149 | You can add the binary to your PATH or create an alias. Now the only thing left is to set it to launch automatically on startup. There are several ways to do that. If your distribution uses systemd, you can download a ready-to-use [music-discord-rpc.service](music-discord-rpc.service) file and save it to `~/.config/systemd/user/`. Then, edit the `ExecStart=/usr/bin/music-discord-rpc` line so it points to the location where you keep the binary. Once that's done, you can control the app's autostart behavior using the `enable`, `disable`, or `restart` subcommands. However, if your distribution doesn’t use systemd, you’ll need to create a service unit manually for your process manager. Alternatively, you can use [XDG Autostart](https://wiki.archlinux.org/title/XDG_Autostart) or configure it in your [desktop environment](https://wiki.archlinux.org/title/Autostarting#On_desktop_environment_startup) or [window manager’s](https://wiki.archlinux.org/title/Autostarting#On_window_manager_startup) config file, depending on what you’re using. The command `music-discord-rpc enable --xdg` will create a `.desktop` file for XDG Autostart in `$XDG_CONFIG_HOME/autostart` for you. Feel free to customize it to fit your needs. 150 | 151 | If somehow the binary doesn't work on your distribution, there is also an `.AppImage` package available. 152 | 153 |
154 | 155 | > [!NOTE] 156 | > If there is no package for your distribution and you have experience creating packages for it, you can open an Issue so we can work together to add it. 157 | 158 | ## Configuration and usage 159 | 160 | Use this command to start the service in the background and enable autostart. See the [Autostart](#autostart) section for more information. 161 | 162 | ```sh 163 | # On Linux 164 | music-discord-rpc enable 165 | 166 | # On MacOS 167 | brew services start music-discord-rpc 168 | ``` 169 | 170 | Or just directly run it in the terminal: 171 | 172 | ```sh 173 | music-discord-rpc 174 | ``` 175 | 176 | You can change the default settings using arguments or by editing config file. Launch executable with `-h` or `--help` for aditional info: 177 | 178 | ``` 179 | music-discord-rpc --help 180 | 181 | Usage: music-discord-rpc [OPTIONS] [COMMAND] 182 | 183 | Commands: 184 | enable Start RPC in the background and enable autostart 185 | disable Stop RPC and disable autostart 186 | restart Use to restart the service and reload the changed configuration file 187 | help Print this message or the help of the given subcommand(s) 188 | 189 | Options: 190 | -i, --interval 191 | Activity refresh rate (min: 5, default: 10) 192 | -b, --button 193 | Select visible buttons [possible values: yt, lastfm, listenbrainz, mprisUrl, shamelessAd] 194 | --lastfm-name 195 | Your Last.fm nickname 196 | --listenbrainz-name 197 | Your Listenbrainz nickname 198 | -r, --rpc-name 199 | Select what will be displayed after "Listening to" (default: artist) [possible values: artist, track, none] 200 | -s, --small-image 201 | Select the icon displayed next to the album cover (default: playPause) [possible values: playPause, player, lastfmAvatar, none] 202 | --force-player-id 203 | Force a different player id to be displayed than the one actually used 204 | --force-player-name 205 | Force a different player name to be displayed than the one actually used 206 | --disable-mpris-art-url 207 | Prevent MPRIS artUrl to be used as album cover if cover is not available on Last.fm 208 | -l, --list-players 209 | Displays all available music player names and exits. Use to get your player name for -a argument 210 | --get-player-id 211 | Show ID of currently detected player. Use when requesting missing icon 212 | -a, --allowlist-add 213 | Get status only from given player. Use multiple times to add several players 214 | -w, --video-players 215 | Will use the "watching" activity. Use multiple times to add several players 216 | --hide-album-name 217 | Hide album name 218 | --only-when-playing 219 | Only send activity when media is playing 220 | -d, --disable-cache 221 | Disable cache (not recommended) 222 | --lastfm-api-key 223 | Your Last.fm API key 224 | --disable-musicbrainz-cover 225 | Do not use MusicBrainz as a fallback source of album covers 226 | --debug-log 227 | Show debug log 228 | --reset-config 229 | Reset config file (overwrites the old file if exists) 230 | -h, --help 231 | Print help 232 | -V, --version 233 | Print version 234 | ``` 235 | 236 | ### Autostart 237 | 238 | #### Linux 239 | 240 | There are 2 built-in ways to autostart this app: systemd and XDG Autostart: 241 | 242 | ```sh 243 | # Systemd distributions (Ubuntu, Fedora, Arch, etc) 244 | music-discord-rpc enable 245 | 246 | # XDG Autostart for distrubutions without systemd (Void and others) 247 | music-discord-rpc enable --xdg 248 | 249 | ``` 250 | 251 | The `enable` subcommand automatically reloads the systemd daemon and enables the service, `disable` will disable the service, and `restart` will restart it. 252 | 253 | The `--xdg` flag is available for the `enable` and `disable` subcommands and creates/removes a `.desktop` file from `$XDG_CONFIG_HOME/autostart` instead. 254 | 255 | With systemd you can check the service status with: 256 | 257 | ```sh 258 | systemctl --user status music-discord-rpc.service 259 | ``` 260 | 261 | And check the logs with: 262 | 263 | ```sh 264 | journalctl --user -u music-discord-rpc.service 265 | ``` 266 | 267 | Or monitor the logs in real-time with: 268 | 269 | ```sh 270 | journalctl --user -u music-discord-rpc.service -f 271 | ``` 272 | 273 | #### MacOS 274 | 275 | Enable app and autostart: 276 | 277 | ```sh 278 | brew services start music-discord-rpc 279 | ``` 280 | 281 | Disable app and autostart: 282 | 283 | ```sh 284 | brew services stop music-discord-rpc 285 | ``` 286 | 287 | Restart app: 288 | 289 | ```sh 290 | brew services restart music-discord-rpc 291 | ``` 292 | 293 | ### Config 294 | 295 | The application will generate a configuration file at `~/.config/music-discord-rpc/config.yaml` when you run it for the first time. You can reset or regenerate it with `--reset-config`. You can also check default config file here: [config.yaml](config.yaml). 296 | 297 | After editing the file, run this command to reload service and apply the changes: 298 | 299 | ```sh 300 | # On Linux 301 | music-discord-rpc restart 302 | 303 | # On MacOS 304 | brew services restart music-discord-rpc 305 | ``` 306 | 307 | Keep in mind that when using XDG Autostart, there's no built-in way to restart the service after changing the config. Config updates will only take effect after reboot. You can manually kill the process and restart it in the background as a workaround. 308 | 309 | ### Allowlist 310 | 311 | To select the music players, use the `-a`,`--allowlist-add` argument or `allowlist` in the config file. This argument can be used multiple times to add more players. The order matters and the first is the most important. 312 | 313 | arguments: 314 | 315 | ```sh 316 | music-discord-rpc -a "VLC Media Player" -a "Chrome" -a "Any other player" 317 | ``` 318 | 319 | config: 320 | 321 | ```yaml 322 | allowlist: 323 | - "VLC Media Player" 324 | - "Chrome" 325 | - "Any other player" 326 | ``` 327 | 328 | Use the `-l`, `--list-players` to get your player name. 329 | 330 | ### "Watching Video" activity 331 | 332 | You can mark players as video players using the `-w`,`--video-players` argument or `video_players` in the config file. Then the status will be "Watching Video" and the RPC will be more suitable for videos. This argument can be used multiple times to add more players. 333 | 334 | arguments: 335 | 336 | ```sh 337 | music-discord-rpc -w "VLC Media Player" -w "Chrome" -w "Any other player" 338 | ``` 339 | 340 | config: 341 | 342 | ```yaml 343 | video_players: 344 | - "VLC Media Player" 345 | - "Chrome" 346 | - "Any other player" 347 | ``` 348 | 349 | It's also possible to display a thumbnail or cover of the video you're watching (e.g., from YouTube), but this requires a player that provides the URL via MPRIS. There aren't many players that do this natively, but `mpv` with the `mpv-mpris` plugin will share the thumbnail of a video piped to it from `yt-dlp`. Other custom YouTube players sometimes have similar functionality. Streaming apps like Jellyfin should work too. Additionally, Chromium-based browsers or Firefox (and forks) can achieve similar functionality using a browser extension. 350 | 351 | KDE Plasma: 352 | 353 | - Google Chrome, Chromium, and Vivaldi: https://chromewebstore.google.com/detail/plasma-integration/cimiefiiaegbelhefglklhhakcgmhkai 354 | - Mozilla Firefox: https://addons.mozilla.org/en-US/firefox/addon/plasma-integration/ 355 | - Microsoft Edge: https://microsoftedge.microsoft.com/addons/detail/plasma-integration/dnnckbejblnejeabhcmhklcaljjpdjeh 356 | 357 | GNOME (not tested): 358 | 359 | - Google Chrome, Chromium, and Vivaldi: https://chromewebstore.google.com/detail/integracja-z-gnome-shell/gphhapmejobijbbhgpjhcjognlahblep 360 | - Mozilla Firefox: https://addons.mozilla.org/en-US/firefox/addon/gnome-shell-integration/ 361 | 362 | > [!CAUTION] 363 | > Using this RPC with browser extensions can potentially compromise your privacy. Most videos played in the browser will be displayed as your activity, including content from sites like Instagram, FB, Twitter, etc. Even NSFW content might be displayed with thumbnails, which could result in a ban from Discord or removal from servers. You can disable thumbnail display using the `--disable-mpris-art-url` argument or by setting `disable_mpris_art_url` to true in the config file. 364 | 365 | ### "Listening to ..." 366 | 367 | You can choose what shows up after "Listening to" on the Discord user list: artist name, song title, or just "Music" (default: artist). 368 | 369 | arguments: 370 | 371 | ```sh 372 | music-discord-rpc -r artist 373 | ``` 374 | 375 | config: 376 | 377 | ```yaml 378 | rpc_name: artist 379 | ``` 380 | 381 | example: 382 | 383 | | value | displayed RPC | 384 | | --------- | ------------------------------------------------- | 385 | | `artist` | Listening to **Rick Astley** | 386 | | `track` | Listening to **Never Gonna Give You Up** | 387 | | `none` | Listening to **Music** | 388 | 389 | ### Buttons 390 | 391 | You can choose from available options (max 2): 392 | 393 | - `yt` - Search this song on YouTube. 394 | - `lasfm` - Last.fm profile. 395 | - `listenbrainz` - Listenbrainz profile. 396 | - `mprisUrl` - Some custom YT players, Jellyfin, mpv or browsers with extension may provide a URL to the currently playing content (see the "Watching Video" activity section for more details). When available, a "Play Now" button will be displayed for music and a "Watch Now" button for video. If the URL is not available, this button will be replaced with a `yt` button. 397 | - `shamelessAd` - Link to the repository of this RPC. 398 | 399 | Remember to provide your usernames for the services you want to add as buttons. 400 | 401 | arguments: 402 | 403 | ```sh 404 | music-discord-rpc -b yt -b lastfm --lastfm-name nickname 405 | ``` 406 | 407 | config: 408 | 409 | ```yaml 410 | allowlist: 411 | - "yt" 412 | - "lastfm" 413 | 414 | lastfm_name: "nickname" 415 | ``` 416 | 417 | > [!IMPORTANT] 418 | > After Discord recent profile layout update, users cannot see their activity buttons anymore, BUT other users can see them. This is not a bug but a feature from Discord. You can make sure the buttons work by logging into an alternative account in your browser, or just by asking a friend :) 419 | 420 | > You can request more buttons by opening an Issue. 421 | 422 | ### The icon next to the album cover 423 | 424 | You can choose from available options: `playPause`, `player`, `lastfmAvatar`, `none`. 425 | 426 | arguments: 427 | 428 | ```sh 429 | music-discord-rpc -s player 430 | ``` 431 | 432 | config: 433 | 434 | ```yaml 435 | small_image: player 436 | ``` 437 | 438 | Available music player icons: `Amberol`, `Audacious`, `Elisa`, `Finamp`, `Firefox`, `fooyin`, `GNOME Music`, `Google Chrome`, `Lollypop`, `Mozilla Firefox`, `mpv`, `Plexamp`, `Spotify`, `Strawberry`, `Tauon`, `TIDAL Hi-Fi`, `VLC Media Player`, `YouTube`, `Zen Browser`. 439 | 440 | You can also force a different player icon and name to be displayed than the one actually used. 441 | 442 | arguments: 443 | 444 | ```sh 445 | music-discord-rpc --force-player-id "vlc_media_player" --force-player-name "VLC media player" 446 | ``` 447 | 448 | config: 449 | 450 | ```yaml 451 | force_player_id: "vlc_media_player" 452 | force_player_name: "VLC media player" 453 | ``` 454 | 455 | Icons are available for these ids: `amberol`, `audacious`, `chrome`, `elisa`, `finamp`, `firefox`, `fooyin`, `lollypop`, `mozilla_firefox`, `mozilla_zen`, `mpv`, `music`, `plexamp`, `spotify`, `strawberry`, `tauon`, `tidalhifi`, `vlc_media_player`, `youtube`. 456 | 457 | **Missing your player icon?** Open an Issue with: 458 | 459 | - Icon link (png, min. 512x512 resolution - Discord requirement) 460 | - Player ID (obtainable by running `music-discord-rpc --get-player-id`) 461 | 462 | Icons are managed through Discord Developer Portal, so no app update is needed after adding new ones. 463 | 464 | ### Flatpak Discord fix 465 | 466 | **This fix is likely no longer necessary**, as the application typically works with Flatpak Discord without any additional steps. However, if you experience issues with Discord not detecting the rich presence, you can try this solution: 467 | 468 | ```sh 469 | ln -sf {app/com.discordapp.Discord,$XDG_RUNTIME_DIR}/discord-ipc-0 470 | ``` 471 | 472 | If you do need to use this fix, note that it will need to be done **every reboot**. So I would also recommend adding this command to the autostart. 473 | 474 | ## System usage 475 | 476 | As it is a very simple program its impact on computer performance is unnoticeable. Normaly it uses around **12 MiB** of RAM but even less than **6 MiB** when fetching album covers only from cache. 477 | 478 | If not disabled, the program stores the cache in `$XDG_CACHE_HOME/music-discord-rpc/` or `$HOME/.cache/music-discord-rpc/`. The application caches only image URLs from last.fm, not the images themselves, keeping the cache size small. 479 | 480 | ## Compile from source 481 | 482 | 1. Install Rust and Cargo using instructions from [Rust site](https://www.rust-lang.org/). 483 | 2. Clone the repository. 484 | ```sh 485 | git clone 'https://github.com/patryk-ku/music-discord-rpc' 486 | cd music-discord-rpc 487 | ``` 488 | 3. (Optional) Rename `.env.example` to `.env` and insert here your last.fm API key. You can easily get it [here](https://www.last.fm/pl/api). Do this if you want to embed the API key in the binary. If you don't, you can provide it later via argument or config file. 489 | ```sh 490 | mv .env.example .env 491 | echo LASTFM_API_KEY=key_here > .env 492 | ``` 493 | 4. Compile executable using Cargo. 494 | ```sh 495 | cargo build --release 496 | ``` 497 | 5. The compiled executable file location is: `target/release/music-discord-rpc`. 498 | 499 | ## Changelog 500 | 501 | [CHANGELOG.md](CHANGELOG.md) 502 | 503 | ## FAQ 504 | 505 |
506 | Can I Use local image files for RPC cover art instead of Last.fm/MusicBrainz? 507 | 508 | No. It's not possible to use local images for Discord RPC, only URLs to images available on the internet are supported by Discord for now. That's why I have to use cover arts from Last.fm and MusicBrainz. 509 | 510 |
511 | 512 |
513 | An incorrect album image is being displayed. 514 | 515 | MusicBrainz often returns incorrect images, but it is only used if finding a cover on Last.fm fails. So, you can either disable MusicBrainz as a cover source in the config and wait for someone to upload the correct album cover to Last.fm, or you can upload it yourself. 516 | 517 | Then, clear the album cache. 518 | 519 |
520 | 521 |
522 | How to clear album cache? 523 | 524 | If not disabled, the program stores the cache in `$XDG_CACHE_HOME/music-discord-rpc/` or `$HOME/.cache/music-discord-rpc/`. Just delete file inside this directory and restart service. 525 | 526 | For most users, the following commands should work: 527 | 528 | ```sh 529 | rm ~/.cache/music-discord-rpc/album_cache.db 530 | music-discord-rpc restart 531 | ``` 532 | 533 |
534 | 535 |
536 | There is no icon for my music player/video player/streaming service, etc. 537 | 538 | Open an Issue with: 539 | - Icon link (png, min. 512x512 resolution - Discord requirement) 540 | - Player ID (obtainable by running `music-discord-rpc --get-player-id`) 541 | 542 |
543 | 544 |
545 | How are new player icons added without releasing a new binary? 546 | 547 | Icons are managed through Discord Developer Portal, so no app update is needed after adding new ones. See [here](https://github.com/patryk-ku/music-discord-rpc/issues/29#issuecomment-2936507734). 548 | 549 |
550 | 551 |
552 | Can you create a package for my Linux distribution? 553 | 554 | Of course! If you're willing to help, feel free to open a new Issue so we can work together on adding it. 555 | 556 |
557 | 558 |
559 | I have an idea for a feature. 560 | 561 | Feel free to open a new Issue and share your idea! I'm generally open to adding new features, and we can discuss what is possible to implement. 562 | 563 |
564 | 565 |
566 | The macOS version doesn't work correctly / displays data incorrectly. 567 | 568 | Before opening an issue, please check if `media-control get -h` returns data correctly, as the macOS version relies on it and my program only reads its output. If there's a problem with [media-control](https://github.com/ungive/media-control), open a new issue in its repository, if not, open one here. 569 | 570 |
571 | 572 | ## Credits 573 | 574 | I wouldn't have been able to create this without two fantastic crates: [mpris-rs](https://github.com/Mange/mpris-rs) and [discord-rich-presence](https://github.com/vionya/discord-rich-presence). The macOS version wouldn't be possible without [media-control](https://github.com/ungive/media-control). Implementing these features myself would have been beyond my current skills. A huge thank you to their creators. 575 | 576 | Any trademarks, featured track metadata, artwork and coverart in banner, music player icons and streaming service logos belong to their respective owners. 577 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use discord_rich_presence::activity::StatusDisplayType; 2 | use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient}; 3 | use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod}; 4 | use url_escape; 5 | 6 | #[cfg(target_os = "linux")] 7 | use mpris::PlayerFinder; 8 | 9 | use std::env; 10 | use std::fs; 11 | use std::ops::Sub; 12 | use std::path::PathBuf; 13 | use std::thread::sleep; 14 | use std::time::{Duration, SystemTime}; 15 | 16 | mod settings; 17 | mod utils; 18 | 19 | // Load api key from .env file durning compilation 20 | const LASTFM_API_KEY: &'static str = match option_env!("LASTFM_API_KEY") { 21 | Some(key) => key, 22 | None => "", 23 | }; 24 | 25 | fn main() -> Result<(), Box> { 26 | // Set home path, If $HOME is not set, do not write or read anything from the user's disk 27 | let (home_exists, home_dir) = match env::var("HOME") { 28 | Ok(val) => (true, PathBuf::from(val)), 29 | Err(_) => (false, PathBuf::from("/")), 30 | }; 31 | 32 | let settings = settings::load_settings(); 33 | 34 | debug_log!(settings.debug_log, "Settings: {:#?}", settings); 35 | debug_log!(settings.debug_log, "home_exists: {}", home_exists); 36 | debug_log!(settings.debug_log, "home_dir: {}", home_dir.display()); 37 | 38 | // Exec subcommands 39 | #[cfg(target_os = "linux")] 40 | match settings.suboptions.command { 41 | Some(settings::Commands::Enable { xdg }) => { 42 | if xdg { 43 | utils::add_xdg_autostart() 44 | } else { 45 | utils::enable_service() 46 | } 47 | } 48 | Some(settings::Commands::Disable { xdg }) => { 49 | if xdg { 50 | utils::remove_xdg_autostart() 51 | } else { 52 | utils::disable_service() 53 | } 54 | } 55 | Some(settings::Commands::Restart {}) => utils::restart_service(), 56 | None => {} 57 | } 58 | #[cfg(target_os = "macos")] 59 | match settings.suboptions.command { 60 | Some(_) => { 61 | println!("Subcommands to manage the daemon are not available on macOS."); 62 | println!( 63 | "Check: https://github.com/patryk-ku/music-discord-rpc?tab=readme-ov-file#macos-3" 64 | ); 65 | std::process::exit(0); 66 | } 67 | None => {} 68 | } 69 | 70 | // User settings 71 | 72 | // Use api key provided by user 73 | let lastfm_api_key = settings.lastfm_api_key.unwrap_or(LASTFM_API_KEY.into()); 74 | if lastfm_api_key.is_empty() { 75 | println!("\x1b[31mWARNING: Last.fm API key is not set. Album covers from Last.fm will not be available.\x1b[0m"); 76 | } 77 | 78 | // Main loop interval 79 | let mut interval = settings.interval.unwrap_or(10); 80 | if interval < 5 { 81 | interval = 5 82 | } 83 | debug_log!(settings.debug_log, "interval: {}", interval); 84 | 85 | // Nicknames for buttons 86 | let lastfm_name = settings.lastfm_name.unwrap_or_default(); 87 | let listenbrainz_name = settings.listenbrainz_name.unwrap_or_default(); 88 | 89 | // "Listening to ..." 90 | let rpc_name = settings.rpc_name.unwrap_or(String::from("artist")); 91 | 92 | // Icon displayed next to the album cover 93 | let small_image = settings.small_image.unwrap_or(String::from("playPause")); 94 | let mut lastfm_avatar = String::new(); 95 | if small_image == "lastfmAvatar" && !lastfm_name.is_empty() { 96 | lastfm_avatar = utils::get_lastfm_avatar(&lastfm_name, &lastfm_api_key); 97 | debug_log!(settings.debug_log, "lastfm_avatar: {}", lastfm_avatar); 98 | } 99 | let lastfm_icon_text = if !lastfm_name.is_empty() { 100 | lastfm_name.to_string() + " on Last.fm" 101 | } else { 102 | String::new() 103 | }; 104 | 105 | // Force player id and name 106 | let force_player_name = settings.force_player_name.unwrap_or_default(); 107 | let force_player_id = settings.force_player_id.unwrap_or_default(); 108 | 109 | // Enable/disable use of cache 110 | let mut cache_enabled: bool = !settings.disable_cache; 111 | if !home_exists { 112 | cache_enabled = false; 113 | } 114 | 115 | // Allowlist of music players 116 | let allowlist_enabled: bool = match settings.allowlist.len() { 117 | 0 => false, 118 | _ => true, 119 | }; 120 | 121 | // Vars for activity update detection 122 | let mut last_title: String = String::new(); 123 | let mut last_album: String = String::new(); 124 | let mut last_artist: String = String::new(); 125 | let mut last_album_artist: String = String::new(); 126 | let mut last_album_id: String = String::new(); 127 | let mut last_track_position: u64 = 0; 128 | let mut last_is_playing: bool = false; 129 | 130 | let mut _cover_url: String = "".to_string(); 131 | let mut is_first_time_audio: bool = true; 132 | let mut is_first_time_video: bool = true; 133 | let mut is_interrupted: bool = false; 134 | let mut is_activity_set: bool = false; 135 | 136 | // Preventing stdout spam while waiting for player or discord 137 | #[cfg(target_os = "linux")] 138 | let mut dbus_notif: bool = false; 139 | let mut player_notif: u8 = 0; 140 | let mut discord_notif: bool = false; 141 | 142 | let mut client_audio = DiscordIpcClient::new("1129859263741837373"); 143 | let mut client_video = DiscordIpcClient::new("1356756023813210293"); 144 | let mut client: &mut DiscordIpcClient = &mut client_audio; 145 | 146 | // Set cache path 147 | let cache_dir = match env::var("XDG_CACHE_HOME") { 148 | Ok(xgd_cache_home) => PathBuf::from(xgd_cache_home).join("music-discord-rpc"), 149 | Err(_) => home_dir.join(".cache/music-discord-rpc"), 150 | }; 151 | 152 | if cache_enabled { 153 | debug_log!( 154 | settings.debug_log, 155 | "Cache location: {}", 156 | &cache_dir.display() 157 | ); 158 | if let Err(err) = fs::create_dir_all(&cache_dir) { 159 | println!("Could not create cache directory: {}", err); 160 | } 161 | } 162 | 163 | // Cache file 164 | let db_path = cache_dir.join("album_cache.db"); 165 | let mut album_cache = match PickleDb::load( 166 | &db_path, 167 | PickleDbDumpPolicy::AutoDump, 168 | SerializationMethod::Json, 169 | ) { 170 | Ok(db) => { 171 | if cache_enabled { 172 | println!("Cache loaded from file: {}", &db_path.display()); 173 | } 174 | db 175 | } 176 | Err(_) => { 177 | if cache_enabled { 178 | println!("Generated new cache file: {}", &db_path.display()); 179 | } 180 | PickleDb::new( 181 | &db_path, 182 | PickleDbDumpPolicy::AutoDump, 183 | SerializationMethod::Json, 184 | ) 185 | } 186 | }; 187 | 188 | loop { 189 | debug_log!( 190 | settings.debug_log, 191 | "───────────────────────────────Loop─1───────────────────────────────────" 192 | ); 193 | 194 | // On Linux try to connect to MPRIS 195 | #[cfg(target_os = "linux")] 196 | let player = match PlayerFinder::new() { 197 | Ok(player) => { 198 | dbus_notif = false; 199 | player 200 | } 201 | Err(err) => { 202 | if !dbus_notif { 203 | println!("Could not connect to D-Bus: {}", err); 204 | dbus_notif = true; 205 | } 206 | sleep(Duration::from_secs(interval)); 207 | continue; 208 | } 209 | }; 210 | 211 | // List available players and exit 212 | if settings.list_players { 213 | #[cfg(target_os = "linux")] 214 | match player.find_all() { 215 | Ok(player_list) => { 216 | if player_list.is_empty() { 217 | println!("Could not find any player with MPRIS support."); 218 | } else { 219 | println!(""); 220 | println!("────────────────────────────────────────────────────"); 221 | println!("List of available music players with MPRIS support:"); 222 | for music_player in &player_list { 223 | if music_player.bus_name() != "org.mpris.MediaPlayer2.playerctld" { 224 | println!(" * {}", music_player.identity()); 225 | } 226 | } 227 | println!(""); 228 | println!("Use the name to choose from which source the script should take data for the Discord status."); 229 | println!("Usage instructions:"); 230 | println!(""); 231 | println!(r#" music-discord-rpc -a "{}""#, player_list[0].identity()); 232 | println!(""); 233 | println!("You can use the -a argument multiple times to add more than one player to the allowlist:"); 234 | println!(""); 235 | println!( 236 | r#" music-discord-rpc -a "{}" -a "Second Player" -a "Any other player""#, 237 | player_list[0].identity() 238 | ); 239 | } 240 | } 241 | Err(_) => { 242 | println!("Could not find any player with MPRIS support."); 243 | } 244 | }; 245 | 246 | #[cfg(target_os = "macos")] 247 | { 248 | println!(""); 249 | println!("Displaying the list of players is not supported on macOS."); 250 | println!( 251 | "However, it's possible to show the name of the currently detected player." 252 | ); 253 | 254 | match utils::get_currently_playing() { 255 | Ok(player) => { 256 | println!("Player name: {}", player.player_id); 257 | println!(""); 258 | println!( 259 | "You can use this name together with the -a flag to add this player to the allowlist:" 260 | ); 261 | println!(r#" music-discord-rpc -a "{}""#, player.player_id); 262 | println!(""); 263 | println!("You can use the -a argument multiple times to add more than one player to the allowlist:"); 264 | println!( 265 | r#" music-discord-rpc -a "{}" -a "Second Player" -a "Any other player""#, 266 | player.player_id 267 | ); 268 | } 269 | Err(_) => { 270 | println!("No player detected."); 271 | } 272 | }; 273 | } 274 | 275 | return Ok(()); 276 | } 277 | 278 | // Find active player (and filter them by name if enabled) 279 | #[cfg(target_os = "linux")] 280 | let player_finder = if allowlist_enabled { 281 | let mut allowlist_finder = Err(mpris::FindingError::NoPlayerFound); 282 | 283 | // Find all players and sort them by allow list order then return the first one 284 | if let Ok(all_players) = player.find_all() { 285 | let mut found_players: Vec<_> = all_players 286 | .into_iter() 287 | .filter(|p| { 288 | !p.bus_name().eq("org.mpris.MediaPlayer2.playerctld") 289 | && settings.allowlist.contains(&p.identity().to_string()) 290 | }) 291 | .collect(); 292 | 293 | if !found_players.is_empty() { 294 | found_players.sort_by_key(|p| { 295 | settings 296 | .allowlist 297 | .iter() 298 | .position(|allowlisted_name| allowlisted_name == p.identity()) 299 | .unwrap_or(usize::MAX) 300 | }); 301 | 302 | allowlist_finder = Ok(found_players.remove(0)); 303 | } 304 | } 305 | allowlist_finder 306 | } else { 307 | player.find_active() 308 | }; 309 | 310 | // Connect with player 311 | #[cfg(target_os = "linux")] 312 | let player = match player_finder { 313 | Ok(player) => { 314 | if player_notif != 1 { 315 | println!("Found active player with MPRIS support."); 316 | player_notif = 1; 317 | } 318 | player 319 | } 320 | Err(_) => { 321 | if player_notif != 2 { 322 | if allowlist_enabled { 323 | println!( 324 | "Could not find any active player from your allowlist with MPRIS support. Waiting for any player from your allowlist..." 325 | ); 326 | } else { 327 | println!( 328 | "Could not find any player with MPRIS support. Waiting for any player..." 329 | ); 330 | } 331 | 332 | player_notif = 2; 333 | discord_notif = false; 334 | } 335 | 336 | is_interrupted = true; 337 | utils::clear_activity(&mut is_activity_set, &mut client); 338 | sleep(Duration::from_secs(interval)); 339 | continue; 340 | } 341 | }; 342 | 343 | // On macOS use media info fetching function to determine if anything is playing now 344 | #[cfg(target_os = "macos")] 345 | let player = match utils::get_currently_playing() { 346 | Ok(player) => { 347 | if allowlist_enabled { 348 | let mut is_player_on_allowlist = false; 349 | for allowlist_entry in &settings.allowlist { 350 | if *allowlist_entry == player.player_id { 351 | is_player_on_allowlist = true; 352 | break; 353 | } 354 | } 355 | if !is_player_on_allowlist { 356 | if player_notif != 2 { 357 | println!( 358 | "Could not find any active player from your allowlist. Waiting for any player from your allowlist..." 359 | ); 360 | player_notif = 2; 361 | discord_notif = false; 362 | } 363 | 364 | is_interrupted = true; 365 | utils::clear_activity(&mut is_activity_set, &mut client); 366 | sleep(Duration::from_secs(interval)); 367 | continue; 368 | } 369 | } 370 | 371 | if player_notif != 1 { 372 | println!("Found active player using media-control."); 373 | player_notif = 1; 374 | } 375 | player 376 | } 377 | Err(e) => { 378 | if player_notif != 2 { 379 | println!("{}", e); 380 | 381 | player_notif = 2; 382 | discord_notif = false; 383 | } 384 | 385 | is_interrupted = true; 386 | utils::clear_activity(&mut is_activity_set, &mut client); 387 | sleep(Duration::from_secs(interval)); 388 | continue; 389 | } 390 | }; 391 | 392 | #[cfg(target_os = "linux")] 393 | let mut player_name = player.identity().to_string(); 394 | #[cfg(target_os = "macos")] 395 | let mut player_name = player.player_id.clone(); 396 | 397 | // Use video presence if player is in video_players list 398 | let is_video_player = settings 399 | .video_players 400 | .iter() 401 | .any(|video_player_name| video_player_name == &player_name); 402 | if is_video_player { 403 | client = &mut client_video; 404 | debug_log!(settings.debug_log, "Using video player presence"); 405 | } else { 406 | client = &mut client_audio; 407 | debug_log!(settings.debug_log, "Using audio player presence"); 408 | } 409 | 410 | #[cfg(target_os = "macos")] 411 | { 412 | player_name = utils::app_name_from_bundle_id(player_name.as_str()); 413 | } 414 | 415 | let mut player_id = utils::sanitize_name(&player_name); 416 | 417 | debug_log!(settings.debug_log, "player_name: {}", player_name); 418 | debug_log!(settings.debug_log, "player_id: {}", player_id); 419 | debug_log!( 420 | settings.debug_log, 421 | "force_player_name: {}", 422 | force_player_name 423 | ); 424 | debug_log!(settings.debug_log, "force_player_id: {}", force_player_id); 425 | 426 | // Display player ID and exit 427 | if settings.get_player_id { 428 | println!("\nplayer_id: {}", player_id); 429 | return Ok(()); 430 | } 431 | 432 | #[cfg(target_os = "macos")] 433 | let last_player_id = player.player_id.clone(); 434 | 435 | // Set different name and ID for RPC if enabled by argument 436 | if !force_player_name.is_empty() { 437 | player_name = force_player_name.to_string(); 438 | } 439 | if !force_player_id.is_empty() { 440 | player_id = force_player_id.to_string(); 441 | } 442 | 443 | // Connect with Discord 444 | if (is_first_time_audio && !is_video_player) || (is_first_time_video && is_video_player) { 445 | match client.connect() { 446 | Ok(_) => { 447 | println!("Connected to Discord."); 448 | discord_notif = false; 449 | } 450 | Err(_) => { 451 | if !discord_notif { 452 | println!("Could not connect to Discord. Waiting for discord to start..."); 453 | discord_notif = true; 454 | } 455 | sleep(Duration::from_secs(interval)); 456 | continue; 457 | } 458 | }; 459 | if is_video_player { 460 | is_first_time_video = false; 461 | } else { 462 | is_first_time_audio = false; 463 | } 464 | } else { 465 | match client.reconnect() { 466 | Ok(_) => { 467 | if discord_notif { 468 | println!("Reconnected to Discord."); 469 | } 470 | is_interrupted = true; 471 | discord_notif = false; 472 | } 473 | Err(_) => { 474 | if !discord_notif { 475 | println!("Could not reconnect to Discord. Waiting for discord to start..."); 476 | discord_notif = true; 477 | } 478 | sleep(Duration::from_secs(interval)); 479 | continue; 480 | } 481 | }; 482 | } 483 | 484 | loop { 485 | debug_log!( 486 | settings.debug_log, 487 | "───────────────────────────────Loop─2───────────────────────────────────" 488 | ); 489 | 490 | // Get metadata from player 491 | #[cfg(target_os = "linux")] 492 | let media_info = match utils::get_currently_playing(&player, settings.debug_log) { 493 | Ok(metadata) => metadata, 494 | Err(err) => { 495 | println!("Could not get metadata from player: {}", err); 496 | utils::clear_activity(&mut is_activity_set, &mut client); 497 | break; 498 | } 499 | }; 500 | #[cfg(target_os = "macos")] 501 | let media_info = match utils::get_currently_playing() { 502 | Ok(metadata) => metadata, 503 | Err(err) => { 504 | println!("Could not get metadata from player: {}", err); 505 | utils::clear_activity(&mut is_activity_set, &mut client); 506 | break; 507 | } 508 | }; 509 | debug_log!(settings.debug_log, "{:#?}", media_info); 510 | 511 | // Fix allowlist on macos, if player ID changes then break loop 512 | #[cfg(target_os = "macos")] 513 | if media_info.player_id != last_player_id { 514 | debug_log!(settings.debug_log, "Detected player change."); 515 | utils::clear_activity(&mut is_activity_set, client); 516 | break; 517 | } 518 | 519 | if settings.only_when_playing && !media_info.is_playing { 520 | is_interrupted = true; 521 | utils::clear_activity(&mut is_activity_set, client); 522 | sleep(Duration::from_secs(interval)); 523 | continue; 524 | } 525 | 526 | let album_id = format!("{} - {}", media_info.album_artist, media_info.album); 527 | 528 | // If all metadata values are unknown then break 529 | if (media_info.artist.to_lowercase() == "unknown artist") 530 | && (media_info.album.to_lowercase() == "unknown album") 531 | && (media_info.title.to_lowercase() == "unknown title") 532 | { 533 | debug_log!(settings.debug_log, "Unknown metadata, skipping..."); 534 | sleep(Duration::from_secs(interval)); 535 | break; 536 | } 537 | 538 | // If artist or track is empty then break 539 | if (media_info.artist.len() == 0) | (media_info.title.len() == 0) { 540 | debug_log!(settings.debug_log, "Unknown metadata, skipping..."); 541 | sleep(Duration::from_secs(interval)); 542 | break; 543 | } 544 | 545 | let mut metadata_changed: bool = false; 546 | debug_log!(settings.debug_log, "Checking if metadata changed:"); 547 | debug_log!(settings.debug_log, "{} - {last_title}", media_info.title); 548 | debug_log!(settings.debug_log, "{} - {last_album}", media_info.album); 549 | debug_log!(settings.debug_log, "{} - {last_artist}", media_info.artist); 550 | debug_log!( 551 | settings.debug_log, 552 | "{} - {last_album_artist}", 553 | media_info.album_artist 554 | ); 555 | debug_log!( 556 | settings.debug_log, 557 | "is_playing: {} - {}", 558 | media_info.is_playing, 559 | last_is_playing 560 | ); 561 | if (media_info.title != last_title) 562 | | (media_info.album != last_album) 563 | | (media_info.artist != last_artist) 564 | | (media_info.album_artist != last_album_artist) 565 | | (media_info.is_playing != last_is_playing) 566 | { 567 | metadata_changed = true; 568 | } 569 | 570 | debug_log!( 571 | settings.debug_log, 572 | "track_position: {} - {}", 573 | media_info.position, 574 | last_track_position 575 | ); 576 | 577 | // Check if song repeated 578 | if (media_info.position < last_track_position) && !metadata_changed { 579 | debug_log!(settings.debug_log, "Detected a potential song seek/replay"); 580 | metadata_changed = true; 581 | } 582 | last_track_position = media_info.position; // update it before loop continue 583 | debug_log!(settings.debug_log, "metadata_changed: {}", metadata_changed); 584 | 585 | if !metadata_changed && !is_interrupted { 586 | debug_log!( 587 | settings.debug_log, 588 | "The same metadata and status, skipping..." 589 | ); 590 | 591 | sleep(Duration::from_secs(interval)); 592 | continue; 593 | } 594 | 595 | // Get unix time of track start if supported, else return time now 596 | let time_start: u64 = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { 597 | Ok(n) => n.as_secs().sub(media_info.position), 598 | Err(_) => 0, 599 | }; 600 | 601 | // Fetch album cover 602 | if album_id != last_album_id { 603 | if lastfm_api_key.is_empty() { 604 | _cover_url = "missing-cover".to_string() 605 | } else { 606 | _cover_url = utils::get_cover_url( 607 | &album_id, 608 | media_info.album.as_str(), 609 | _cover_url, 610 | cache_enabled, 611 | &mut album_cache, 612 | media_info.album_artist.as_str(), 613 | &lastfm_api_key, 614 | ); 615 | 616 | // Fallback for Apple Music for album names with " - EP" and " - Single" 617 | if _cover_url.is_empty() || _cover_url == "missing-cover" { 618 | let album_name = media_info.album.trim(); 619 | let album_name_without_suffix = if album_name.ends_with(" - EP") { 620 | &album_name[..album_name.len() - 5] 621 | } else if album_name.ends_with(" - Single") { 622 | &album_name[..album_name.len() - 9] 623 | } else { 624 | "" 625 | }; 626 | 627 | if !album_name_without_suffix.is_empty() { 628 | debug_log!( 629 | settings.debug_log, 630 | "Album cover not found, attempting to use album name without the 'EP' or 'Single' suffix (Apple Music)." 631 | ); 632 | debug_log!( 633 | settings.debug_log, 634 | "{} => {}", 635 | album_name, 636 | album_name_without_suffix 637 | ); 638 | 639 | _cover_url = utils::get_cover_url( 640 | &album_id, 641 | album_name_without_suffix, 642 | _cover_url, 643 | cache_enabled, 644 | &mut album_cache, 645 | media_info.album_artist.as_str(), 646 | &lastfm_api_key, 647 | ); 648 | } 649 | } 650 | } 651 | 652 | // Use Musicbrainz cover if Last.fm fails 653 | if !settings.disable_musicbrainz_cover { 654 | if _cover_url.is_empty() || _cover_url == "missing-cover" { 655 | _cover_url = utils::get_cover_url_musicbrainz( 656 | &album_id, 657 | media_info.album.as_str(), 658 | _cover_url, 659 | cache_enabled, 660 | &mut album_cache, 661 | media_info.album_artist.as_str(), 662 | ); 663 | } 664 | } 665 | } 666 | 667 | let image: String = if _cover_url.is_empty() || _cover_url == "missing-cover" { 668 | match media_info.art_url.is_empty() { 669 | true => "missing-cover".to_string(), 670 | false => { 671 | if media_info.art_url.starts_with("http") && !settings.disable_mpris_art_url 672 | { 673 | media_info.art_url 674 | } else { 675 | "missing-cover".to_string() 676 | } 677 | } 678 | } 679 | } else { 680 | _cover_url.clone() 681 | }; 682 | 683 | // Save last refresh info 684 | last_title = media_info.title.clone(); 685 | last_album = media_info.album.clone(); 686 | last_artist = media_info.artist.clone(); 687 | last_album_artist = media_info.album_artist; 688 | last_album_id = album_id.to_string(); 689 | last_is_playing = media_info.is_playing; 690 | 691 | // Set activity 692 | let song_name: String = format!("{} - {}", media_info.artist, media_info.title); 693 | let title = if media_info.title.len() > 1 { 694 | media_info.title 695 | } else { 696 | format!("{} ", media_info.title) // Discord activity min 2 char len bug fix 697 | }; 698 | let artist = match rpc_name.as_str() { 699 | "artist" => { 700 | if media_info.artist.len() > 1 { 701 | media_info.artist 702 | } else { 703 | format!("{} ", media_info.artist) // Discord activity min 2 char len bug fix 704 | } 705 | } 706 | _ => format!("by: {}", media_info.artist), 707 | }; 708 | let album = format!("album: {}", media_info.album); 709 | let status_text: String = if media_info.is_playing { 710 | "playing".to_string() 711 | } else { 712 | "paused".to_string() 713 | }; 714 | 715 | let mut assets = activity::Assets::new().large_image(&image); 716 | 717 | if !settings.hide_album_name { 718 | assets = assets.large_text(&album); 719 | } 720 | 721 | // Icon displayed next to the album cover 722 | match small_image.as_str() { 723 | "player" => { 724 | if !settings.disable_mpris_art_url && image.contains("ytimg.com/") { 725 | assets = assets.small_image("youtube").small_text("YouTube") 726 | } else { 727 | assets = assets.small_image(&player_id).small_text(&player_name) 728 | } 729 | } 730 | "lastfmAvatar" => { 731 | if !lastfm_avatar.is_empty() { 732 | assets = assets 733 | .small_image(&lastfm_avatar) 734 | .small_text(&lastfm_icon_text); 735 | } 736 | } 737 | "none" => {} 738 | _ => assets = assets.small_image(&status_text).small_text(&status_text), 739 | } 740 | 741 | // Display paused icon anyway if playpack is paused or stopped 742 | if status_text != "playing" { 743 | assets = assets.small_image(&status_text).small_text(&status_text) 744 | } 745 | 746 | let mut payload = activity::Activity::new() 747 | .details(&title) 748 | .assets(assets) 749 | .activity_type(if is_video_player { 750 | activity::ActivityType::Watching 751 | } else { 752 | activity::ActivityType::Listening 753 | }); 754 | 755 | // "Listening to ..." 756 | match rpc_name.as_str() { 757 | "none" => payload = payload.status_display_type(StatusDisplayType::Name), 758 | "track" => payload = payload.status_display_type(StatusDisplayType::Details), 759 | "artist" | _ => payload = payload.status_display_type(StatusDisplayType::State), 760 | } 761 | 762 | // Don't display Unknown Artist for videos 763 | if !(is_video_player && (artist.to_lowercase() == "by: unknown artist") 764 | || artist.to_lowercase() == "unknown artist") 765 | { 766 | payload = payload.state(&artist); 767 | } 768 | 769 | payload = if media_info.is_track_position && (media_info.duration > 0) { 770 | let time_end = time_start + media_info.duration; 771 | if media_info.is_playing { 772 | payload.timestamps( 773 | activity::Timestamps::new() 774 | .start(time_start.try_into().unwrap()) 775 | .end(time_end.try_into().unwrap()), 776 | ) 777 | } else { 778 | payload.timestamps( 779 | activity::Timestamps::new().start(time_start.try_into().unwrap()), 780 | ) 781 | } 782 | } else { 783 | payload.timestamps(activity::Timestamps::new().end(time_start.try_into().unwrap())) 784 | }; 785 | 786 | // Create urls for activity links 787 | let yt_url: String = format!( 788 | "https://www.youtube.com/results?search_query={}", 789 | url_escape::encode_component(&song_name) 790 | ); 791 | let lastfm_url: String = format!( 792 | "https://www.last.fm/user/{}", 793 | url_escape::encode_component(&lastfm_name) 794 | ); 795 | let listenbrainz_url: String = format!( 796 | "https://listenbrainz.org/user/{}/", 797 | url_escape::encode_component(&listenbrainz_name) 798 | ); 799 | 800 | // Add YouTube URL to song title 801 | payload = payload.details_url(&yt_url); 802 | 803 | // Add activity buttons 804 | let mut buttons = Vec::new(); 805 | let mut first_button = ""; 806 | for button in &settings.button { 807 | let initial_len = buttons.len(); 808 | if initial_len == 2 { 809 | break; 810 | } 811 | 812 | // Make sure buttons wont repeat 813 | if initial_len > 0 { 814 | if first_button == button { 815 | continue; 816 | } 817 | } 818 | 819 | match button.as_str() { 820 | "yt" => { 821 | buttons.push(activity::Button::new( 822 | "Search this song on YouTube", 823 | &yt_url, 824 | )); 825 | } 826 | "lastfm" => { 827 | if lastfm_name.len() > 0 { 828 | buttons.push(activity::Button::new("Last.fm profile", &lastfm_url)); 829 | } 830 | } 831 | "listenbrainz" => { 832 | if listenbrainz_name.len() > 0 { 833 | buttons.push(activity::Button::new( 834 | "Listenbrainz profile", 835 | &listenbrainz_url, 836 | )); 837 | } 838 | } 839 | "mprisUrl" => { 840 | if media_info.url.is_empty() { 841 | // if mpris url is empty or not set convert button to yt button 842 | buttons.push(activity::Button::new( 843 | "Search this song on YouTube", 844 | &yt_url, 845 | )); 846 | } else { 847 | if is_video_player { 848 | buttons.push(activity::Button::new("Watch Now", &media_info.url)); 849 | } else { 850 | buttons.push(activity::Button::new("Play Now", &media_info.url)); 851 | } 852 | } 853 | } 854 | "shamelessAd" => { 855 | buttons.push(activity::Button::new( 856 | "Get This RPC", 857 | "https://github.com/patryk-ku/music-discord-rpc", 858 | )); 859 | } 860 | _ => continue, 861 | } 862 | 863 | // Make sure buttons wont repeat 864 | if initial_len < buttons.len() { 865 | first_button = button; 866 | } 867 | } 868 | 869 | payload = match buttons.is_empty() { 870 | false => payload.buttons(buttons), 871 | true => payload, 872 | }; 873 | 874 | match client.set_activity(payload) { 875 | Ok(_) => { 876 | is_interrupted = false; 877 | is_activity_set = true; 878 | println!("=> Set activity [{status_text}]: {song_name}"); 879 | } 880 | Err(_) => { 881 | println!("Could not set activity."); 882 | is_interrupted = true; 883 | is_activity_set = false; 884 | client.close()?; 885 | break; 886 | } 887 | }; 888 | 889 | sleep(Duration::from_secs(interval)); 890 | } 891 | 892 | sleep(Duration::from_secs(interval)); 893 | } 894 | } 895 | -------------------------------------------------------------------------------- /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 = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.20" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.11" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.7" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.4" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 55 | dependencies = [ 56 | "windows-sys 0.60.2", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.10" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell_polyfill", 67 | "windows-sys 0.60.2", 68 | ] 69 | 70 | [[package]] 71 | name = "atomic-waker" 72 | version = "1.1.2" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 75 | 76 | [[package]] 77 | name = "backtrace" 78 | version = "0.3.75" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 81 | dependencies = [ 82 | "addr2line", 83 | "cfg-if", 84 | "libc", 85 | "miniz_oxide", 86 | "object", 87 | "rustc-demangle", 88 | "windows-targets 0.52.6", 89 | ] 90 | 91 | [[package]] 92 | name = "base64" 93 | version = "0.22.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 96 | 97 | [[package]] 98 | name = "bitflags" 99 | version = "2.9.3" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" 102 | 103 | [[package]] 104 | name = "bumpalo" 105 | version = "3.19.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 108 | 109 | [[package]] 110 | name = "bytes" 111 | version = "1.10.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 114 | 115 | [[package]] 116 | name = "cc" 117 | version = "1.2.34" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" 120 | dependencies = [ 121 | "shlex", 122 | ] 123 | 124 | [[package]] 125 | name = "cfg-if" 126 | version = "1.0.3" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 129 | 130 | [[package]] 131 | name = "clap" 132 | version = "4.5.45" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" 135 | dependencies = [ 136 | "clap_builder", 137 | "clap_derive", 138 | ] 139 | 140 | [[package]] 141 | name = "clap-serde-derive" 142 | version = "0.2.1" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "c4b7d643cbdc3a4eb0b5db8b9844ab2002bc4be44c1244db5cd27df8e594c125" 145 | dependencies = [ 146 | "clap", 147 | "clap-serde-proc", 148 | "serde", 149 | ] 150 | 151 | [[package]] 152 | name = "clap-serde-proc" 153 | version = "0.2.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "f6725cfcf906f158cdad4ca9a2a426133b36a3f91b5da2b971f8b956823ef55e" 156 | dependencies = [ 157 | "proc-macro2", 158 | "quote 1.0.40", 159 | "syn 1.0.109", 160 | ] 161 | 162 | [[package]] 163 | name = "clap_builder" 164 | version = "4.5.44" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" 167 | dependencies = [ 168 | "anstream", 169 | "anstyle", 170 | "clap_lex", 171 | "strsim 0.11.1", 172 | ] 173 | 174 | [[package]] 175 | name = "clap_derive" 176 | version = "4.5.45" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" 179 | dependencies = [ 180 | "heck 0.5.0", 181 | "proc-macro2", 182 | "quote 1.0.40", 183 | "syn 2.0.106", 184 | ] 185 | 186 | [[package]] 187 | name = "clap_lex" 188 | version = "0.7.5" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 191 | 192 | [[package]] 193 | name = "colorchoice" 194 | version = "1.0.4" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 197 | 198 | [[package]] 199 | name = "core-foundation" 200 | version = "0.9.4" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 203 | dependencies = [ 204 | "core-foundation-sys", 205 | "libc", 206 | ] 207 | 208 | [[package]] 209 | name = "core-foundation-sys" 210 | version = "0.8.7" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 213 | 214 | [[package]] 215 | name = "darling" 216 | version = "0.14.4" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" 219 | dependencies = [ 220 | "darling_core", 221 | "darling_macro", 222 | ] 223 | 224 | [[package]] 225 | name = "darling_core" 226 | version = "0.14.4" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" 229 | dependencies = [ 230 | "fnv", 231 | "ident_case", 232 | "proc-macro2", 233 | "quote 1.0.40", 234 | "strsim 0.10.0", 235 | "syn 1.0.109", 236 | ] 237 | 238 | [[package]] 239 | name = "darling_macro" 240 | version = "0.14.4" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" 243 | dependencies = [ 244 | "darling_core", 245 | "quote 1.0.40", 246 | "syn 1.0.109", 247 | ] 248 | 249 | [[package]] 250 | name = "dbus" 251 | version = "0.9.8" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "6e8d0767bcb66eb101d5ab87b9f38542691185af14fa8a7026c2490e62b45cfc" 254 | dependencies = [ 255 | "libc", 256 | "libdbus-sys", 257 | "windows-sys 0.59.0", 258 | ] 259 | 260 | [[package]] 261 | name = "derive_is_enum_variant" 262 | version = "0.1.1" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "d0ac8859845146979953797f03cc5b282fb4396891807cdb3d04929a88418197" 265 | dependencies = [ 266 | "heck 0.3.3", 267 | "quote 0.3.15", 268 | "syn 0.11.11", 269 | ] 270 | 271 | [[package]] 272 | name = "discord-rich-presence" 273 | version = "0.2.5" 274 | source = "git+https://github.com/vionya/discord-rich-presence?branch=main#eab0b36bd35c5f58cdb8580ab4fdc01f8ff82414" 275 | dependencies = [ 276 | "log", 277 | "serde", 278 | "serde_derive", 279 | "serde_json", 280 | "serde_repr", 281 | "thiserror 2.0.16", 282 | "uuid", 283 | ] 284 | 285 | [[package]] 286 | name = "displaydoc" 287 | version = "0.2.5" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 290 | dependencies = [ 291 | "proc-macro2", 292 | "quote 1.0.40", 293 | "syn 2.0.106", 294 | ] 295 | 296 | [[package]] 297 | name = "dotenvy" 298 | version = "0.15.7" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 301 | 302 | [[package]] 303 | name = "encoding_rs" 304 | version = "0.8.35" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 307 | dependencies = [ 308 | "cfg-if", 309 | ] 310 | 311 | [[package]] 312 | name = "enum-kinds" 313 | version = "0.5.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462" 316 | dependencies = [ 317 | "proc-macro2", 318 | "quote 1.0.40", 319 | "syn 1.0.109", 320 | ] 321 | 322 | [[package]] 323 | name = "equivalent" 324 | version = "1.0.2" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 327 | 328 | [[package]] 329 | name = "errno" 330 | version = "0.3.13" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 333 | dependencies = [ 334 | "libc", 335 | "windows-sys 0.60.2", 336 | ] 337 | 338 | [[package]] 339 | name = "fastrand" 340 | version = "2.3.0" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 343 | 344 | [[package]] 345 | name = "fnv" 346 | version = "1.0.7" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 349 | 350 | [[package]] 351 | name = "foreign-types" 352 | version = "0.3.2" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 355 | dependencies = [ 356 | "foreign-types-shared", 357 | ] 358 | 359 | [[package]] 360 | name = "foreign-types-shared" 361 | version = "0.1.1" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 364 | 365 | [[package]] 366 | name = "form_urlencoded" 367 | version = "1.2.2" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 370 | dependencies = [ 371 | "percent-encoding", 372 | ] 373 | 374 | [[package]] 375 | name = "from_variants" 376 | version = "1.0.2" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "4e859c8f2057687618905dbe99fc76e836e0a69738865ef90e46fc214a41bbf2" 379 | dependencies = [ 380 | "from_variants_impl", 381 | ] 382 | 383 | [[package]] 384 | name = "from_variants_impl" 385 | version = "1.0.2" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "55a5e644a80e6d96b2b4910fa7993301d7b7926c045b475b62202b20a36ce69e" 388 | dependencies = [ 389 | "darling", 390 | "proc-macro2", 391 | "quote 1.0.40", 392 | "syn 1.0.109", 393 | ] 394 | 395 | [[package]] 396 | name = "futures-channel" 397 | version = "0.3.31" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 400 | dependencies = [ 401 | "futures-core", 402 | "futures-sink", 403 | ] 404 | 405 | [[package]] 406 | name = "futures-core" 407 | version = "0.3.31" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 410 | 411 | [[package]] 412 | name = "futures-io" 413 | version = "0.3.31" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 416 | 417 | [[package]] 418 | name = "futures-sink" 419 | version = "0.3.31" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 422 | 423 | [[package]] 424 | name = "futures-task" 425 | version = "0.3.31" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 428 | 429 | [[package]] 430 | name = "futures-util" 431 | version = "0.3.31" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 434 | dependencies = [ 435 | "futures-core", 436 | "futures-io", 437 | "futures-sink", 438 | "futures-task", 439 | "memchr", 440 | "pin-project-lite", 441 | "pin-utils", 442 | "slab", 443 | ] 444 | 445 | [[package]] 446 | name = "getrandom" 447 | version = "0.2.16" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 450 | dependencies = [ 451 | "cfg-if", 452 | "libc", 453 | "wasi 0.11.1+wasi-snapshot-preview1", 454 | ] 455 | 456 | [[package]] 457 | name = "getrandom" 458 | version = "0.3.3" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 461 | dependencies = [ 462 | "cfg-if", 463 | "libc", 464 | "r-efi", 465 | "wasi 0.14.2+wasi-0.2.4", 466 | ] 467 | 468 | [[package]] 469 | name = "gimli" 470 | version = "0.31.1" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 473 | 474 | [[package]] 475 | name = "h2" 476 | version = "0.4.12" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 479 | dependencies = [ 480 | "atomic-waker", 481 | "bytes", 482 | "fnv", 483 | "futures-core", 484 | "futures-sink", 485 | "http", 486 | "indexmap", 487 | "slab", 488 | "tokio", 489 | "tokio-util", 490 | "tracing", 491 | ] 492 | 493 | [[package]] 494 | name = "hashbrown" 495 | version = "0.15.5" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 498 | 499 | [[package]] 500 | name = "heck" 501 | version = "0.3.3" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 504 | dependencies = [ 505 | "unicode-segmentation", 506 | ] 507 | 508 | [[package]] 509 | name = "heck" 510 | version = "0.5.0" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 513 | 514 | [[package]] 515 | name = "http" 516 | version = "1.3.1" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 519 | dependencies = [ 520 | "bytes", 521 | "fnv", 522 | "itoa", 523 | ] 524 | 525 | [[package]] 526 | name = "http-body" 527 | version = "1.0.1" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 530 | dependencies = [ 531 | "bytes", 532 | "http", 533 | ] 534 | 535 | [[package]] 536 | name = "http-body-util" 537 | version = "0.1.3" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 540 | dependencies = [ 541 | "bytes", 542 | "futures-core", 543 | "http", 544 | "http-body", 545 | "pin-project-lite", 546 | ] 547 | 548 | [[package]] 549 | name = "httparse" 550 | version = "1.10.1" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 553 | 554 | [[package]] 555 | name = "hyper" 556 | version = "1.7.0" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" 559 | dependencies = [ 560 | "atomic-waker", 561 | "bytes", 562 | "futures-channel", 563 | "futures-core", 564 | "h2", 565 | "http", 566 | "http-body", 567 | "httparse", 568 | "itoa", 569 | "pin-project-lite", 570 | "pin-utils", 571 | "smallvec", 572 | "tokio", 573 | "want", 574 | ] 575 | 576 | [[package]] 577 | name = "hyper-rustls" 578 | version = "0.27.7" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 581 | dependencies = [ 582 | "http", 583 | "hyper", 584 | "hyper-util", 585 | "rustls", 586 | "rustls-pki-types", 587 | "tokio", 588 | "tokio-rustls", 589 | "tower-service", 590 | ] 591 | 592 | [[package]] 593 | name = "hyper-tls" 594 | version = "0.6.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 597 | dependencies = [ 598 | "bytes", 599 | "http-body-util", 600 | "hyper", 601 | "hyper-util", 602 | "native-tls", 603 | "tokio", 604 | "tokio-native-tls", 605 | "tower-service", 606 | ] 607 | 608 | [[package]] 609 | name = "hyper-util" 610 | version = "0.1.16" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" 613 | dependencies = [ 614 | "base64", 615 | "bytes", 616 | "futures-channel", 617 | "futures-core", 618 | "futures-util", 619 | "http", 620 | "http-body", 621 | "hyper", 622 | "ipnet", 623 | "libc", 624 | "percent-encoding", 625 | "pin-project-lite", 626 | "socket2", 627 | "system-configuration", 628 | "tokio", 629 | "tower-service", 630 | "tracing", 631 | "windows-registry", 632 | ] 633 | 634 | [[package]] 635 | name = "icu_collections" 636 | version = "2.0.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 639 | dependencies = [ 640 | "displaydoc", 641 | "potential_utf", 642 | "yoke", 643 | "zerofrom", 644 | "zerovec", 645 | ] 646 | 647 | [[package]] 648 | name = "icu_locale_core" 649 | version = "2.0.0" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 652 | dependencies = [ 653 | "displaydoc", 654 | "litemap", 655 | "tinystr", 656 | "writeable", 657 | "zerovec", 658 | ] 659 | 660 | [[package]] 661 | name = "icu_normalizer" 662 | version = "2.0.0" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 665 | dependencies = [ 666 | "displaydoc", 667 | "icu_collections", 668 | "icu_normalizer_data", 669 | "icu_properties", 670 | "icu_provider", 671 | "smallvec", 672 | "zerovec", 673 | ] 674 | 675 | [[package]] 676 | name = "icu_normalizer_data" 677 | version = "2.0.0" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 680 | 681 | [[package]] 682 | name = "icu_properties" 683 | version = "2.0.1" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 686 | dependencies = [ 687 | "displaydoc", 688 | "icu_collections", 689 | "icu_locale_core", 690 | "icu_properties_data", 691 | "icu_provider", 692 | "potential_utf", 693 | "zerotrie", 694 | "zerovec", 695 | ] 696 | 697 | [[package]] 698 | name = "icu_properties_data" 699 | version = "2.0.1" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 702 | 703 | [[package]] 704 | name = "icu_provider" 705 | version = "2.0.0" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 708 | dependencies = [ 709 | "displaydoc", 710 | "icu_locale_core", 711 | "stable_deref_trait", 712 | "tinystr", 713 | "writeable", 714 | "yoke", 715 | "zerofrom", 716 | "zerotrie", 717 | "zerovec", 718 | ] 719 | 720 | [[package]] 721 | name = "ident_case" 722 | version = "1.0.1" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 725 | 726 | [[package]] 727 | name = "idna" 728 | version = "1.1.0" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 731 | dependencies = [ 732 | "idna_adapter", 733 | "smallvec", 734 | "utf8_iter", 735 | ] 736 | 737 | [[package]] 738 | name = "idna_adapter" 739 | version = "1.2.1" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 742 | dependencies = [ 743 | "icu_normalizer", 744 | "icu_properties", 745 | ] 746 | 747 | [[package]] 748 | name = "indexmap" 749 | version = "2.11.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" 752 | dependencies = [ 753 | "equivalent", 754 | "hashbrown", 755 | ] 756 | 757 | [[package]] 758 | name = "io-uring" 759 | version = "0.7.10" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" 762 | dependencies = [ 763 | "bitflags", 764 | "cfg-if", 765 | "libc", 766 | ] 767 | 768 | [[package]] 769 | name = "ipnet" 770 | version = "2.11.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 773 | 774 | [[package]] 775 | name = "iri-string" 776 | version = "0.7.8" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 779 | dependencies = [ 780 | "memchr", 781 | "serde", 782 | ] 783 | 784 | [[package]] 785 | name = "is_terminal_polyfill" 786 | version = "1.70.1" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 789 | 790 | [[package]] 791 | name = "itoa" 792 | version = "1.0.15" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 795 | 796 | [[package]] 797 | name = "js-sys" 798 | version = "0.3.77" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 801 | dependencies = [ 802 | "once_cell", 803 | "wasm-bindgen", 804 | ] 805 | 806 | [[package]] 807 | name = "libc" 808 | version = "0.2.175" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 811 | 812 | [[package]] 813 | name = "libdbus-sys" 814 | version = "0.2.6" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" 817 | dependencies = [ 818 | "pkg-config", 819 | ] 820 | 821 | [[package]] 822 | name = "linux-raw-sys" 823 | version = "0.9.4" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 826 | 827 | [[package]] 828 | name = "litemap" 829 | version = "0.8.0" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 832 | 833 | [[package]] 834 | name = "log" 835 | version = "0.4.27" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 838 | 839 | [[package]] 840 | name = "memchr" 841 | version = "2.7.5" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 844 | 845 | [[package]] 846 | name = "mime" 847 | version = "0.3.17" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 850 | 851 | [[package]] 852 | name = "miniz_oxide" 853 | version = "0.8.9" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 856 | dependencies = [ 857 | "adler2", 858 | ] 859 | 860 | [[package]] 861 | name = "mio" 862 | version = "1.0.4" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 865 | dependencies = [ 866 | "libc", 867 | "wasi 0.11.1+wasi-snapshot-preview1", 868 | "windows-sys 0.59.0", 869 | ] 870 | 871 | [[package]] 872 | name = "mpris" 873 | version = "2.0.1" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "55cef955a7826b1e00e901a3652e7a895abd221fb4ab61547e7d0e4c235d7feb" 876 | dependencies = [ 877 | "dbus", 878 | "derive_is_enum_variant", 879 | "enum-kinds", 880 | "from_variants", 881 | "thiserror 1.0.69", 882 | ] 883 | 884 | [[package]] 885 | name = "music-discord-rpc" 886 | version = "0.6.2" 887 | dependencies = [ 888 | "clap", 889 | "clap-serde-derive", 890 | "discord-rich-presence", 891 | "dotenvy", 892 | "mpris", 893 | "pickledb", 894 | "reqwest", 895 | "serde", 896 | "serde_json", 897 | "serde_yaml", 898 | "url-escape", 899 | ] 900 | 901 | [[package]] 902 | name = "native-tls" 903 | version = "0.2.14" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 906 | dependencies = [ 907 | "libc", 908 | "log", 909 | "openssl", 910 | "openssl-probe", 911 | "openssl-sys", 912 | "schannel", 913 | "security-framework", 914 | "security-framework-sys", 915 | "tempfile", 916 | ] 917 | 918 | [[package]] 919 | name = "object" 920 | version = "0.36.7" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 923 | dependencies = [ 924 | "memchr", 925 | ] 926 | 927 | [[package]] 928 | name = "once_cell" 929 | version = "1.21.3" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 932 | 933 | [[package]] 934 | name = "once_cell_polyfill" 935 | version = "1.70.1" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 938 | 939 | [[package]] 940 | name = "openssl" 941 | version = "0.10.73" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 944 | dependencies = [ 945 | "bitflags", 946 | "cfg-if", 947 | "foreign-types", 948 | "libc", 949 | "once_cell", 950 | "openssl-macros", 951 | "openssl-sys", 952 | ] 953 | 954 | [[package]] 955 | name = "openssl-macros" 956 | version = "0.1.1" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 959 | dependencies = [ 960 | "proc-macro2", 961 | "quote 1.0.40", 962 | "syn 2.0.106", 963 | ] 964 | 965 | [[package]] 966 | name = "openssl-probe" 967 | version = "0.1.6" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 970 | 971 | [[package]] 972 | name = "openssl-sys" 973 | version = "0.9.109" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 976 | dependencies = [ 977 | "cc", 978 | "libc", 979 | "pkg-config", 980 | "vcpkg", 981 | ] 982 | 983 | [[package]] 984 | name = "percent-encoding" 985 | version = "2.3.2" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 988 | 989 | [[package]] 990 | name = "pickledb" 991 | version = "0.5.1" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "c53a5ade47760e8cc4986bdc5e72daeffaaaee64cbc374f9cfe0a00c1cd87b1f" 994 | dependencies = [ 995 | "serde", 996 | "serde_json", 997 | ] 998 | 999 | [[package]] 1000 | name = "pin-project-lite" 1001 | version = "0.2.16" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1004 | 1005 | [[package]] 1006 | name = "pin-utils" 1007 | version = "0.1.0" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1010 | 1011 | [[package]] 1012 | name = "pkg-config" 1013 | version = "0.3.32" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1016 | 1017 | [[package]] 1018 | name = "potential_utf" 1019 | version = "0.1.2" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 1022 | dependencies = [ 1023 | "zerovec", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "proc-macro2" 1028 | version = "1.0.101" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 1031 | dependencies = [ 1032 | "unicode-ident", 1033 | ] 1034 | 1035 | [[package]] 1036 | name = "quote" 1037 | version = "0.3.15" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" 1040 | 1041 | [[package]] 1042 | name = "quote" 1043 | version = "1.0.40" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1046 | dependencies = [ 1047 | "proc-macro2", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "r-efi" 1052 | version = "5.3.0" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1055 | 1056 | [[package]] 1057 | name = "reqwest" 1058 | version = "0.12.23" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" 1061 | dependencies = [ 1062 | "base64", 1063 | "bytes", 1064 | "encoding_rs", 1065 | "futures-channel", 1066 | "futures-core", 1067 | "futures-util", 1068 | "h2", 1069 | "http", 1070 | "http-body", 1071 | "http-body-util", 1072 | "hyper", 1073 | "hyper-rustls", 1074 | "hyper-tls", 1075 | "hyper-util", 1076 | "js-sys", 1077 | "log", 1078 | "mime", 1079 | "native-tls", 1080 | "percent-encoding", 1081 | "pin-project-lite", 1082 | "rustls-pki-types", 1083 | "serde", 1084 | "serde_json", 1085 | "serde_urlencoded", 1086 | "sync_wrapper", 1087 | "tokio", 1088 | "tokio-native-tls", 1089 | "tower", 1090 | "tower-http", 1091 | "tower-service", 1092 | "url", 1093 | "wasm-bindgen", 1094 | "wasm-bindgen-futures", 1095 | "web-sys", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "ring" 1100 | version = "0.17.14" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1103 | dependencies = [ 1104 | "cc", 1105 | "cfg-if", 1106 | "getrandom 0.2.16", 1107 | "libc", 1108 | "untrusted", 1109 | "windows-sys 0.52.0", 1110 | ] 1111 | 1112 | [[package]] 1113 | name = "rustc-demangle" 1114 | version = "0.1.26" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 1117 | 1118 | [[package]] 1119 | name = "rustix" 1120 | version = "1.0.8" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 1123 | dependencies = [ 1124 | "bitflags", 1125 | "errno", 1126 | "libc", 1127 | "linux-raw-sys", 1128 | "windows-sys 0.60.2", 1129 | ] 1130 | 1131 | [[package]] 1132 | name = "rustls" 1133 | version = "0.23.31" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" 1136 | dependencies = [ 1137 | "once_cell", 1138 | "rustls-pki-types", 1139 | "rustls-webpki", 1140 | "subtle", 1141 | "zeroize", 1142 | ] 1143 | 1144 | [[package]] 1145 | name = "rustls-pki-types" 1146 | version = "1.12.0" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 1149 | dependencies = [ 1150 | "zeroize", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "rustls-webpki" 1155 | version = "0.103.4" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" 1158 | dependencies = [ 1159 | "ring", 1160 | "rustls-pki-types", 1161 | "untrusted", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "rustversion" 1166 | version = "1.0.22" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 1169 | 1170 | [[package]] 1171 | name = "ryu" 1172 | version = "1.0.20" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1175 | 1176 | [[package]] 1177 | name = "schannel" 1178 | version = "0.1.27" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1181 | dependencies = [ 1182 | "windows-sys 0.59.0", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "security-framework" 1187 | version = "2.11.1" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1190 | dependencies = [ 1191 | "bitflags", 1192 | "core-foundation", 1193 | "core-foundation-sys", 1194 | "libc", 1195 | "security-framework-sys", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "security-framework-sys" 1200 | version = "2.14.0" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1203 | dependencies = [ 1204 | "core-foundation-sys", 1205 | "libc", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "serde" 1210 | version = "1.0.219" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1213 | dependencies = [ 1214 | "serde_derive", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "serde_derive" 1219 | version = "1.0.219" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1222 | dependencies = [ 1223 | "proc-macro2", 1224 | "quote 1.0.40", 1225 | "syn 2.0.106", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "serde_json" 1230 | version = "1.0.143" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" 1233 | dependencies = [ 1234 | "itoa", 1235 | "memchr", 1236 | "ryu", 1237 | "serde", 1238 | ] 1239 | 1240 | [[package]] 1241 | name = "serde_repr" 1242 | version = "0.1.20" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" 1245 | dependencies = [ 1246 | "proc-macro2", 1247 | "quote 1.0.40", 1248 | "syn 2.0.106", 1249 | ] 1250 | 1251 | [[package]] 1252 | name = "serde_urlencoded" 1253 | version = "0.7.1" 1254 | source = "registry+https://github.com/rust-lang/crates.io-index" 1255 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1256 | dependencies = [ 1257 | "form_urlencoded", 1258 | "itoa", 1259 | "ryu", 1260 | "serde", 1261 | ] 1262 | 1263 | [[package]] 1264 | name = "serde_yaml" 1265 | version = "0.9.34+deprecated" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 1268 | dependencies = [ 1269 | "indexmap", 1270 | "itoa", 1271 | "ryu", 1272 | "serde", 1273 | "unsafe-libyaml", 1274 | ] 1275 | 1276 | [[package]] 1277 | name = "shlex" 1278 | version = "1.3.0" 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" 1280 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1281 | 1282 | [[package]] 1283 | name = "slab" 1284 | version = "0.4.11" 1285 | source = "registry+https://github.com/rust-lang/crates.io-index" 1286 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 1287 | 1288 | [[package]] 1289 | name = "smallvec" 1290 | version = "1.15.1" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1293 | 1294 | [[package]] 1295 | name = "socket2" 1296 | version = "0.6.0" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 1299 | dependencies = [ 1300 | "libc", 1301 | "windows-sys 0.59.0", 1302 | ] 1303 | 1304 | [[package]] 1305 | name = "stable_deref_trait" 1306 | version = "1.2.0" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1309 | 1310 | [[package]] 1311 | name = "strsim" 1312 | version = "0.10.0" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1315 | 1316 | [[package]] 1317 | name = "strsim" 1318 | version = "0.11.1" 1319 | source = "registry+https://github.com/rust-lang/crates.io-index" 1320 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1321 | 1322 | [[package]] 1323 | name = "subtle" 1324 | version = "2.6.1" 1325 | source = "registry+https://github.com/rust-lang/crates.io-index" 1326 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1327 | 1328 | [[package]] 1329 | name = "syn" 1330 | version = "0.11.11" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" 1333 | dependencies = [ 1334 | "quote 0.3.15", 1335 | "synom", 1336 | "unicode-xid", 1337 | ] 1338 | 1339 | [[package]] 1340 | name = "syn" 1341 | version = "1.0.109" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1344 | dependencies = [ 1345 | "proc-macro2", 1346 | "quote 1.0.40", 1347 | "unicode-ident", 1348 | ] 1349 | 1350 | [[package]] 1351 | name = "syn" 1352 | version = "2.0.106" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 1355 | dependencies = [ 1356 | "proc-macro2", 1357 | "quote 1.0.40", 1358 | "unicode-ident", 1359 | ] 1360 | 1361 | [[package]] 1362 | name = "sync_wrapper" 1363 | version = "1.0.2" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1366 | dependencies = [ 1367 | "futures-core", 1368 | ] 1369 | 1370 | [[package]] 1371 | name = "synom" 1372 | version = "0.11.3" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" 1375 | dependencies = [ 1376 | "unicode-xid", 1377 | ] 1378 | 1379 | [[package]] 1380 | name = "synstructure" 1381 | version = "0.13.2" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1384 | dependencies = [ 1385 | "proc-macro2", 1386 | "quote 1.0.40", 1387 | "syn 2.0.106", 1388 | ] 1389 | 1390 | [[package]] 1391 | name = "system-configuration" 1392 | version = "0.6.1" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1395 | dependencies = [ 1396 | "bitflags", 1397 | "core-foundation", 1398 | "system-configuration-sys", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "system-configuration-sys" 1403 | version = "0.6.0" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1406 | dependencies = [ 1407 | "core-foundation-sys", 1408 | "libc", 1409 | ] 1410 | 1411 | [[package]] 1412 | name = "tempfile" 1413 | version = "3.21.0" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" 1416 | dependencies = [ 1417 | "fastrand", 1418 | "getrandom 0.3.3", 1419 | "once_cell", 1420 | "rustix", 1421 | "windows-sys 0.60.2", 1422 | ] 1423 | 1424 | [[package]] 1425 | name = "thiserror" 1426 | version = "1.0.69" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1429 | dependencies = [ 1430 | "thiserror-impl 1.0.69", 1431 | ] 1432 | 1433 | [[package]] 1434 | name = "thiserror" 1435 | version = "2.0.16" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" 1438 | dependencies = [ 1439 | "thiserror-impl 2.0.16", 1440 | ] 1441 | 1442 | [[package]] 1443 | name = "thiserror-impl" 1444 | version = "1.0.69" 1445 | source = "registry+https://github.com/rust-lang/crates.io-index" 1446 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1447 | dependencies = [ 1448 | "proc-macro2", 1449 | "quote 1.0.40", 1450 | "syn 2.0.106", 1451 | ] 1452 | 1453 | [[package]] 1454 | name = "thiserror-impl" 1455 | version = "2.0.16" 1456 | source = "registry+https://github.com/rust-lang/crates.io-index" 1457 | checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" 1458 | dependencies = [ 1459 | "proc-macro2", 1460 | "quote 1.0.40", 1461 | "syn 2.0.106", 1462 | ] 1463 | 1464 | [[package]] 1465 | name = "tinystr" 1466 | version = "0.8.1" 1467 | source = "registry+https://github.com/rust-lang/crates.io-index" 1468 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1469 | dependencies = [ 1470 | "displaydoc", 1471 | "zerovec", 1472 | ] 1473 | 1474 | [[package]] 1475 | name = "tokio" 1476 | version = "1.47.1" 1477 | source = "registry+https://github.com/rust-lang/crates.io-index" 1478 | checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" 1479 | dependencies = [ 1480 | "backtrace", 1481 | "bytes", 1482 | "io-uring", 1483 | "libc", 1484 | "mio", 1485 | "pin-project-lite", 1486 | "slab", 1487 | "socket2", 1488 | "windows-sys 0.59.0", 1489 | ] 1490 | 1491 | [[package]] 1492 | name = "tokio-native-tls" 1493 | version = "0.3.1" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1496 | dependencies = [ 1497 | "native-tls", 1498 | "tokio", 1499 | ] 1500 | 1501 | [[package]] 1502 | name = "tokio-rustls" 1503 | version = "0.26.2" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 1506 | dependencies = [ 1507 | "rustls", 1508 | "tokio", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "tokio-util" 1513 | version = "0.7.16" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" 1516 | dependencies = [ 1517 | "bytes", 1518 | "futures-core", 1519 | "futures-sink", 1520 | "pin-project-lite", 1521 | "tokio", 1522 | ] 1523 | 1524 | [[package]] 1525 | name = "tower" 1526 | version = "0.5.2" 1527 | source = "registry+https://github.com/rust-lang/crates.io-index" 1528 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1529 | dependencies = [ 1530 | "futures-core", 1531 | "futures-util", 1532 | "pin-project-lite", 1533 | "sync_wrapper", 1534 | "tokio", 1535 | "tower-layer", 1536 | "tower-service", 1537 | ] 1538 | 1539 | [[package]] 1540 | name = "tower-http" 1541 | version = "0.6.6" 1542 | source = "registry+https://github.com/rust-lang/crates.io-index" 1543 | checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 1544 | dependencies = [ 1545 | "bitflags", 1546 | "bytes", 1547 | "futures-util", 1548 | "http", 1549 | "http-body", 1550 | "iri-string", 1551 | "pin-project-lite", 1552 | "tower", 1553 | "tower-layer", 1554 | "tower-service", 1555 | ] 1556 | 1557 | [[package]] 1558 | name = "tower-layer" 1559 | version = "0.3.3" 1560 | source = "registry+https://github.com/rust-lang/crates.io-index" 1561 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1562 | 1563 | [[package]] 1564 | name = "tower-service" 1565 | version = "0.3.3" 1566 | source = "registry+https://github.com/rust-lang/crates.io-index" 1567 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1568 | 1569 | [[package]] 1570 | name = "tracing" 1571 | version = "0.1.41" 1572 | source = "registry+https://github.com/rust-lang/crates.io-index" 1573 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1574 | dependencies = [ 1575 | "pin-project-lite", 1576 | "tracing-core", 1577 | ] 1578 | 1579 | [[package]] 1580 | name = "tracing-core" 1581 | version = "0.1.34" 1582 | source = "registry+https://github.com/rust-lang/crates.io-index" 1583 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 1584 | dependencies = [ 1585 | "once_cell", 1586 | ] 1587 | 1588 | [[package]] 1589 | name = "try-lock" 1590 | version = "0.2.5" 1591 | source = "registry+https://github.com/rust-lang/crates.io-index" 1592 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1593 | 1594 | [[package]] 1595 | name = "unicode-ident" 1596 | version = "1.0.18" 1597 | source = "registry+https://github.com/rust-lang/crates.io-index" 1598 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1599 | 1600 | [[package]] 1601 | name = "unicode-segmentation" 1602 | version = "1.12.0" 1603 | source = "registry+https://github.com/rust-lang/crates.io-index" 1604 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1605 | 1606 | [[package]] 1607 | name = "unicode-xid" 1608 | version = "0.0.4" 1609 | source = "registry+https://github.com/rust-lang/crates.io-index" 1610 | checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" 1611 | 1612 | [[package]] 1613 | name = "unsafe-libyaml" 1614 | version = "0.2.11" 1615 | source = "registry+https://github.com/rust-lang/crates.io-index" 1616 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 1617 | 1618 | [[package]] 1619 | name = "untrusted" 1620 | version = "0.9.0" 1621 | source = "registry+https://github.com/rust-lang/crates.io-index" 1622 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1623 | 1624 | [[package]] 1625 | name = "url" 1626 | version = "2.5.7" 1627 | source = "registry+https://github.com/rust-lang/crates.io-index" 1628 | checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 1629 | dependencies = [ 1630 | "form_urlencoded", 1631 | "idna", 1632 | "percent-encoding", 1633 | "serde", 1634 | ] 1635 | 1636 | [[package]] 1637 | name = "url-escape" 1638 | version = "0.1.1" 1639 | source = "registry+https://github.com/rust-lang/crates.io-index" 1640 | checksum = "44e0ce4d1246d075ca5abec4b41d33e87a6054d08e2366b63205665e950db218" 1641 | dependencies = [ 1642 | "percent-encoding", 1643 | ] 1644 | 1645 | [[package]] 1646 | name = "utf8_iter" 1647 | version = "1.0.4" 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" 1649 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1650 | 1651 | [[package]] 1652 | name = "utf8parse" 1653 | version = "0.2.2" 1654 | source = "registry+https://github.com/rust-lang/crates.io-index" 1655 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1656 | 1657 | [[package]] 1658 | name = "uuid" 1659 | version = "0.8.2" 1660 | source = "registry+https://github.com/rust-lang/crates.io-index" 1661 | checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" 1662 | dependencies = [ 1663 | "getrandom 0.2.16", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "vcpkg" 1668 | version = "0.2.15" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1671 | 1672 | [[package]] 1673 | name = "want" 1674 | version = "0.3.1" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1677 | dependencies = [ 1678 | "try-lock", 1679 | ] 1680 | 1681 | [[package]] 1682 | name = "wasi" 1683 | version = "0.11.1+wasi-snapshot-preview1" 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" 1685 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1686 | 1687 | [[package]] 1688 | name = "wasi" 1689 | version = "0.14.2+wasi-0.2.4" 1690 | source = "registry+https://github.com/rust-lang/crates.io-index" 1691 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1692 | dependencies = [ 1693 | "wit-bindgen-rt", 1694 | ] 1695 | 1696 | [[package]] 1697 | name = "wasm-bindgen" 1698 | version = "0.2.100" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1701 | dependencies = [ 1702 | "cfg-if", 1703 | "once_cell", 1704 | "rustversion", 1705 | "wasm-bindgen-macro", 1706 | ] 1707 | 1708 | [[package]] 1709 | name = "wasm-bindgen-backend" 1710 | version = "0.2.100" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1713 | dependencies = [ 1714 | "bumpalo", 1715 | "log", 1716 | "proc-macro2", 1717 | "quote 1.0.40", 1718 | "syn 2.0.106", 1719 | "wasm-bindgen-shared", 1720 | ] 1721 | 1722 | [[package]] 1723 | name = "wasm-bindgen-futures" 1724 | version = "0.4.50" 1725 | source = "registry+https://github.com/rust-lang/crates.io-index" 1726 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1727 | dependencies = [ 1728 | "cfg-if", 1729 | "js-sys", 1730 | "once_cell", 1731 | "wasm-bindgen", 1732 | "web-sys", 1733 | ] 1734 | 1735 | [[package]] 1736 | name = "wasm-bindgen-macro" 1737 | version = "0.2.100" 1738 | source = "registry+https://github.com/rust-lang/crates.io-index" 1739 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1740 | dependencies = [ 1741 | "quote 1.0.40", 1742 | "wasm-bindgen-macro-support", 1743 | ] 1744 | 1745 | [[package]] 1746 | name = "wasm-bindgen-macro-support" 1747 | version = "0.2.100" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1750 | dependencies = [ 1751 | "proc-macro2", 1752 | "quote 1.0.40", 1753 | "syn 2.0.106", 1754 | "wasm-bindgen-backend", 1755 | "wasm-bindgen-shared", 1756 | ] 1757 | 1758 | [[package]] 1759 | name = "wasm-bindgen-shared" 1760 | version = "0.2.100" 1761 | source = "registry+https://github.com/rust-lang/crates.io-index" 1762 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1763 | dependencies = [ 1764 | "unicode-ident", 1765 | ] 1766 | 1767 | [[package]] 1768 | name = "web-sys" 1769 | version = "0.3.77" 1770 | source = "registry+https://github.com/rust-lang/crates.io-index" 1771 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1772 | dependencies = [ 1773 | "js-sys", 1774 | "wasm-bindgen", 1775 | ] 1776 | 1777 | [[package]] 1778 | name = "windows-link" 1779 | version = "0.1.3" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 1782 | 1783 | [[package]] 1784 | name = "windows-registry" 1785 | version = "0.5.3" 1786 | source = "registry+https://github.com/rust-lang/crates.io-index" 1787 | checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 1788 | dependencies = [ 1789 | "windows-link", 1790 | "windows-result", 1791 | "windows-strings", 1792 | ] 1793 | 1794 | [[package]] 1795 | name = "windows-result" 1796 | version = "0.3.4" 1797 | source = "registry+https://github.com/rust-lang/crates.io-index" 1798 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 1799 | dependencies = [ 1800 | "windows-link", 1801 | ] 1802 | 1803 | [[package]] 1804 | name = "windows-strings" 1805 | version = "0.4.2" 1806 | source = "registry+https://github.com/rust-lang/crates.io-index" 1807 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 1808 | dependencies = [ 1809 | "windows-link", 1810 | ] 1811 | 1812 | [[package]] 1813 | name = "windows-sys" 1814 | version = "0.52.0" 1815 | source = "registry+https://github.com/rust-lang/crates.io-index" 1816 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1817 | dependencies = [ 1818 | "windows-targets 0.52.6", 1819 | ] 1820 | 1821 | [[package]] 1822 | name = "windows-sys" 1823 | version = "0.59.0" 1824 | source = "registry+https://github.com/rust-lang/crates.io-index" 1825 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1826 | dependencies = [ 1827 | "windows-targets 0.52.6", 1828 | ] 1829 | 1830 | [[package]] 1831 | name = "windows-sys" 1832 | version = "0.60.2" 1833 | source = "registry+https://github.com/rust-lang/crates.io-index" 1834 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1835 | dependencies = [ 1836 | "windows-targets 0.53.3", 1837 | ] 1838 | 1839 | [[package]] 1840 | name = "windows-targets" 1841 | version = "0.52.6" 1842 | source = "registry+https://github.com/rust-lang/crates.io-index" 1843 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1844 | dependencies = [ 1845 | "windows_aarch64_gnullvm 0.52.6", 1846 | "windows_aarch64_msvc 0.52.6", 1847 | "windows_i686_gnu 0.52.6", 1848 | "windows_i686_gnullvm 0.52.6", 1849 | "windows_i686_msvc 0.52.6", 1850 | "windows_x86_64_gnu 0.52.6", 1851 | "windows_x86_64_gnullvm 0.52.6", 1852 | "windows_x86_64_msvc 0.52.6", 1853 | ] 1854 | 1855 | [[package]] 1856 | name = "windows-targets" 1857 | version = "0.53.3" 1858 | source = "registry+https://github.com/rust-lang/crates.io-index" 1859 | checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 1860 | dependencies = [ 1861 | "windows-link", 1862 | "windows_aarch64_gnullvm 0.53.0", 1863 | "windows_aarch64_msvc 0.53.0", 1864 | "windows_i686_gnu 0.53.0", 1865 | "windows_i686_gnullvm 0.53.0", 1866 | "windows_i686_msvc 0.53.0", 1867 | "windows_x86_64_gnu 0.53.0", 1868 | "windows_x86_64_gnullvm 0.53.0", 1869 | "windows_x86_64_msvc 0.53.0", 1870 | ] 1871 | 1872 | [[package]] 1873 | name = "windows_aarch64_gnullvm" 1874 | version = "0.52.6" 1875 | source = "registry+https://github.com/rust-lang/crates.io-index" 1876 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1877 | 1878 | [[package]] 1879 | name = "windows_aarch64_gnullvm" 1880 | version = "0.53.0" 1881 | source = "registry+https://github.com/rust-lang/crates.io-index" 1882 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1883 | 1884 | [[package]] 1885 | name = "windows_aarch64_msvc" 1886 | version = "0.52.6" 1887 | source = "registry+https://github.com/rust-lang/crates.io-index" 1888 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1889 | 1890 | [[package]] 1891 | name = "windows_aarch64_msvc" 1892 | version = "0.53.0" 1893 | source = "registry+https://github.com/rust-lang/crates.io-index" 1894 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1895 | 1896 | [[package]] 1897 | name = "windows_i686_gnu" 1898 | version = "0.52.6" 1899 | source = "registry+https://github.com/rust-lang/crates.io-index" 1900 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1901 | 1902 | [[package]] 1903 | name = "windows_i686_gnu" 1904 | version = "0.53.0" 1905 | source = "registry+https://github.com/rust-lang/crates.io-index" 1906 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 1907 | 1908 | [[package]] 1909 | name = "windows_i686_gnullvm" 1910 | version = "0.52.6" 1911 | source = "registry+https://github.com/rust-lang/crates.io-index" 1912 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1913 | 1914 | [[package]] 1915 | name = "windows_i686_gnullvm" 1916 | version = "0.53.0" 1917 | source = "registry+https://github.com/rust-lang/crates.io-index" 1918 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 1919 | 1920 | [[package]] 1921 | name = "windows_i686_msvc" 1922 | version = "0.52.6" 1923 | source = "registry+https://github.com/rust-lang/crates.io-index" 1924 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1925 | 1926 | [[package]] 1927 | name = "windows_i686_msvc" 1928 | version = "0.53.0" 1929 | source = "registry+https://github.com/rust-lang/crates.io-index" 1930 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 1931 | 1932 | [[package]] 1933 | name = "windows_x86_64_gnu" 1934 | version = "0.52.6" 1935 | source = "registry+https://github.com/rust-lang/crates.io-index" 1936 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1937 | 1938 | [[package]] 1939 | name = "windows_x86_64_gnu" 1940 | version = "0.53.0" 1941 | source = "registry+https://github.com/rust-lang/crates.io-index" 1942 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 1943 | 1944 | [[package]] 1945 | name = "windows_x86_64_gnullvm" 1946 | version = "0.52.6" 1947 | source = "registry+https://github.com/rust-lang/crates.io-index" 1948 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1949 | 1950 | [[package]] 1951 | name = "windows_x86_64_gnullvm" 1952 | version = "0.53.0" 1953 | source = "registry+https://github.com/rust-lang/crates.io-index" 1954 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1955 | 1956 | [[package]] 1957 | name = "windows_x86_64_msvc" 1958 | version = "0.52.6" 1959 | source = "registry+https://github.com/rust-lang/crates.io-index" 1960 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1961 | 1962 | [[package]] 1963 | name = "windows_x86_64_msvc" 1964 | version = "0.53.0" 1965 | source = "registry+https://github.com/rust-lang/crates.io-index" 1966 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1967 | 1968 | [[package]] 1969 | name = "wit-bindgen-rt" 1970 | version = "0.39.0" 1971 | source = "registry+https://github.com/rust-lang/crates.io-index" 1972 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1973 | dependencies = [ 1974 | "bitflags", 1975 | ] 1976 | 1977 | [[package]] 1978 | name = "writeable" 1979 | version = "0.6.1" 1980 | source = "registry+https://github.com/rust-lang/crates.io-index" 1981 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 1982 | 1983 | [[package]] 1984 | name = "yoke" 1985 | version = "0.8.0" 1986 | source = "registry+https://github.com/rust-lang/crates.io-index" 1987 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 1988 | dependencies = [ 1989 | "serde", 1990 | "stable_deref_trait", 1991 | "yoke-derive", 1992 | "zerofrom", 1993 | ] 1994 | 1995 | [[package]] 1996 | name = "yoke-derive" 1997 | version = "0.8.0" 1998 | source = "registry+https://github.com/rust-lang/crates.io-index" 1999 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 2000 | dependencies = [ 2001 | "proc-macro2", 2002 | "quote 1.0.40", 2003 | "syn 2.0.106", 2004 | "synstructure", 2005 | ] 2006 | 2007 | [[package]] 2008 | name = "zerofrom" 2009 | version = "0.1.6" 2010 | source = "registry+https://github.com/rust-lang/crates.io-index" 2011 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2012 | dependencies = [ 2013 | "zerofrom-derive", 2014 | ] 2015 | 2016 | [[package]] 2017 | name = "zerofrom-derive" 2018 | version = "0.1.6" 2019 | source = "registry+https://github.com/rust-lang/crates.io-index" 2020 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2021 | dependencies = [ 2022 | "proc-macro2", 2023 | "quote 1.0.40", 2024 | "syn 2.0.106", 2025 | "synstructure", 2026 | ] 2027 | 2028 | [[package]] 2029 | name = "zeroize" 2030 | version = "1.8.1" 2031 | source = "registry+https://github.com/rust-lang/crates.io-index" 2032 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2033 | 2034 | [[package]] 2035 | name = "zerotrie" 2036 | version = "0.2.2" 2037 | source = "registry+https://github.com/rust-lang/crates.io-index" 2038 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 2039 | dependencies = [ 2040 | "displaydoc", 2041 | "yoke", 2042 | "zerofrom", 2043 | ] 2044 | 2045 | [[package]] 2046 | name = "zerovec" 2047 | version = "0.11.4" 2048 | source = "registry+https://github.com/rust-lang/crates.io-index" 2049 | checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 2050 | dependencies = [ 2051 | "yoke", 2052 | "zerofrom", 2053 | "zerovec-derive", 2054 | ] 2055 | 2056 | [[package]] 2057 | name = "zerovec-derive" 2058 | version = "0.11.1" 2059 | source = "registry+https://github.com/rust-lang/crates.io-index" 2060 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 2061 | dependencies = [ 2062 | "proc-macro2", 2063 | "quote 1.0.40", 2064 | "syn 2.0.106", 2065 | ] 2066 | --------------------------------------------------------------------------------