diff --git a/.github/workflows/weekly-playlist-update-2.yml b/.github/workflows/weekly-playlist-update-2.yml new file mode 100644 index 00000000..ab368b07 --- /dev/null +++ b/.github/workflows/weekly-playlist-update-2.yml @@ -0,0 +1,88 @@ +name: Weekly Afera Playlist Update + +on: + schedule: + # Run every Monday at 10:00 AM UTC + - cron: '0 10 * * 1' + workflow_dispatch: # Allow manual triggering + +env: + CARGO_TERM_COLOR: always + RUST_FEATURES: "rodio-backend,media-control,image,notify" + +jobs: + update-playlist: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libssl-dev libasound2-dev libdbus-1-dev libxcb-shape0-dev libxcb-xfixes0-dev tcl tcl-tls + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo deps + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build spotify_player + run: cargo build --release --no-default-features --features ${{ env.RUST_FEATURES }} + + - name: Set up Spotify credentials + env: + SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }} + SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }} + SPOTIFY_REFRESH_TOKEN: ${{ secrets.SPOTIFY_REFRESH_TOKEN }} + run: | + # Create config directory if it doesn't exist + mkdir -p ~/.config/spotify-player + + # Create config file with credentials + cat > ~/.config/spotify-player/app.toml << EOF + [application] + client_id = "$SPOTIFY_CLIENT_ID" + + [authentication] + client_secret = "$SPOTIFY_CLIENT_SECRET" + refresh_token = "$SPOTIFY_REFRESH_TOKEN" + EOF + + - name: Make scripts executable + run: | + chmod +x playlist_scraper.tcl + chmod +x playlist_generator.tcl + + - name: Scrape and create playlist + run: | + echo "Scraping Afera website..." + ./playlist_scraper.tcl https://www.afera.com.pl/muzyka > scraped_content.txt + + echo "Content scraped:" + cat scraped_content.txt + + echo "Creating playlist..." + cat scraped_content.txt | ./playlist_generator.tcl > playlist_commands.txt + + echo "Generated commands:" + cat playlist_commands.txt + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: playlist-update-logs + path: | + scraped_content.txt + playlist_commands.txt + retention-days: 30 diff --git a/.github/workflows/weekly-playlist-update.yml b/.github/workflows/weekly-playlist-update.yml new file mode 100644 index 00000000..74c02e81 --- /dev/null +++ b/.github/workflows/weekly-playlist-update.yml @@ -0,0 +1,88 @@ +name: Weekly Afera Playlist Update + +on: + schedule: + # Run every Monday at 10:00 AM UTC + - cron: '0 10 * * 1' + workflow_dispatch: # Allow manual triggering + +env: + CARGO_TERM_COLOR: always + RUST_FEATURES: "rodio-backend,media-control,image,notify" + +jobs: + update-playlist: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libssl-dev libasound2-dev libdbus-1-dev libxcb-shape0-dev libxcb-xfixes0-dev tcl tcl-tls + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo deps + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build spotify_player + run: cargo build --release --no-default-features --features ${{ env.RUST_FEATURES }} + + - name: Set up Spotify credentials + env: + SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }} + SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }} + SPOTIFY_REFRESH_TOKEN: ${{ secrets.SPOTIFY_REFRESH_TOKEN }} + run: | + # Create config directory if it doesn't exist + mkdir -p ~/.config/spotify-player + + # Create config file with credentials + cat > ~/.config/spotify-player/app.toml << EOF + [application] + client_id = "$SPOTIFY_CLIENT_ID" + + [authentication] + client_secret = "$SPOTIFY_CLIENT_SECRET" + refresh_token = "$SPOTIFY_REFRESH_TOKEN" + EOF + + - name: Make scripts executable + run: | + chmod +x playlist_scraper.tcl + chmod +x playlist_generator.tcl + + - name: Scrape and create playlist + run: | + echo "Scraping Afera website..." + ./playlist_scraper.tcl https://www.afera.com.pl/muzyka > scraped_content.txt + + echo "Content scraped:" + cat scraped_content.txt + + echo "Creating playlist..." + cat scraped_content.txt | ./playlist_generator.tcl > playlist_commands.txt + + echo "Generated commands:" + cat playlist_commands.txt + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: playlist-update-logs + path: | + scraped_content.txt + playlist_commands.txt + retention-days: 30 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 04dbb18b..3494c68b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "ab_glyph" -version = "0.2.29" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "ab_glyph_rasterizer" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "addr2line" @@ -54,7 +54,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "version_check", - "zerocopy 0.8.26", + "zerocopy", ] [[package]] @@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg-if", "libc", ] @@ -110,16 +110,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.9.1", + "bitflags 2.9.3", "cc", "cesu8", "jni", "jni-sys", "libc", "log", - "ndk 0.9.0", + "ndk", "ndk-context", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "thiserror 1.0.69", ] @@ -156,9 +156,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -186,35 +186,35 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arg_enum_proc_macro" @@ -269,9 +269,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -312,36 +312,13 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" dependencies = [ "arrayvec", ] -[[package]] -name = "aws-lc-rs" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" -dependencies = [ - "bindgen 0.69.5", - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "backtrace" version = "0.3.75" @@ -357,18 +334,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -381,47 +346,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.9.1", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn", - "which 4.4.2", -] - -[[package]] -name = "bindgen" -version = "0.72.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" -dependencies = [ - "bitflags 2.9.1", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn", -] - [[package]] name = "bit_field" version = "0.10.2" @@ -436,9 +360,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bitstream-io" @@ -476,7 +400,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.2", ] [[package]] @@ -487,15 +411,15 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] name = "byteorder" @@ -521,7 +445,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "log", "polling", "rustix 0.38.44", @@ -549,18 +473,18 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] name = "cc" -version = "1.2.27" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", @@ -573,15 +497,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-expr" version = "0.15.8" @@ -594,9 +509,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34e221e91c7eb5e8315b5c9cf1a61670938c0626451f954a51693ed44b37f45" +checksum = "c8d458d63f0f0f482c8da9b7c8b76c21bd885a02056cc94c6404d861ca2b8206" dependencies = [ "smallvec", "target-lexicon 0.13.2", @@ -604,9 +519,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -639,22 +554,11 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading 0.8.8", -] - [[package]] name = "clap" -version = "4.5.40" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -662,9 +566,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -674,18 +578,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.54" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aad5b1b4de04fead402672b48897030eec1f3bfe1550776322f59f6d6e6a5677" +checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", @@ -701,22 +605,13 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "clipboard-win" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ "error-code", ] -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - [[package]] name = "cocoa" version = "0.24.1" @@ -794,14 +689,14 @@ dependencies = [ [[package]] name = "config_parser2" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19860ead0ab4f28e269696d51d517b8d8560334f60aa2a4bcfaf96fc50b16327" +checksum = "82ee1270e89fd879f8ad252cd8c7e8a0b13193b7c417b4267d20f57a30bb2b0f" dependencies = [ "anyhow", "config_parser_derive", "serde", - "toml", + "toml 0.9.5", ] [[package]] @@ -907,32 +802,25 @@ dependencies = [ [[package]] name = "coreaudio-rs" -version = "0.11.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" dependencies = [ "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" -dependencies = [ - "bindgen 0.72.0", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", ] [[package]] name = "cpal" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" dependencies = [ "alsa", - "core-foundation-sys", "coreaudio-rs", "dasp_sample", "jack", @@ -940,9 +828,13 @@ dependencies = [ "js-sys", "libc", "mach2", - "ndk 0.8.0", + "ndk", "ndk-context", - "oboe", + "num-derive", + "num-traits", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -960,9 +852,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -998,7 +890,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "crossterm_winapi", "mio", "parking_lot", @@ -1014,13 +906,13 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "crossterm_winapi", "derive_more", "document-features", "mio", "parking_lot", - "rustix 1.0.7", + "rustix 1.0.8", "signal-hook", "signal-hook-mio", "winapi", @@ -1037,9 +929,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -1124,13 +1016,13 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dbus" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +checksum = "6e8d0767bcb66eb101d5ab87b9f38542691185af14fa8a7026c2490e62b45cfc" dependencies = [ "libc", "libdbus-sys", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -1259,8 +1151,8 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", + "bitflags 2.9.3", + "objc2 0.6.2", ] [[package]] @@ -1280,7 +1172,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.8", ] [[package]] @@ -1304,12 +1196,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.15.0" @@ -1524,19 +1410,13 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futf" version = "0.1.5" @@ -1597,9 +1477,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", @@ -1713,9 +1593,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc37f9a2bfe731e69f1e08d29d91d30604b9ce24bcb2880a961e82d89c6ed89" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" dependencies = [ "color_quant", "weezl", @@ -1729,24 +1609,24 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio-sys" -version = "0.20.10" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +checksum = "a03f2234671e5a588cfe1f59c2b22c103f5772ea351be9cc824a9ce0d06d99fd" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps 7.0.5", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "glib" -version = "0.20.12" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +checksum = "60bdc26493257b5794ba9301f7cbaf7ab0d69a570bfbefa4d7d360e781cb5205" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "futures-channel", "futures-core", "futures-executor", @@ -1763,9 +1643,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.12" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +checksum = "e772291ebea14c28eb11bb75741f62f4a4894f25e60ce80100797b6b010ef0f9" dependencies = [ "heck", "proc-macro-crate", @@ -1776,25 +1656,19 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.20.10" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +checksum = "dc7c43cff6a7dc43821e45ebf172399437acd6716fa2186b6852d2b397bf622d" dependencies = [ "libc", "system-deps 7.0.5", ] -[[package]] -name = "glob" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - [[package]] name = "gobject-sys" -version = "0.20.10" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +checksum = "3e9a190eef2bce144a6aa8434e306974c6062c398e0a33a146d60238f9062d5c" dependencies = [ "glib-sys", "libc", @@ -1803,27 +1677,30 @@ dependencies = [ [[package]] name = "governor" -version = "0.6.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" dependencies = [ "cfg-if", - "futures", + "futures-sink", "futures-timer", - "no-std-compat", + "futures-util", + "getrandom 0.3.3", + "hashbrown", "nonzero_ext", "parking_lot", "portable-atomic", - "rand 0.8.5", + "rand 0.9.2", "smallvec", "spinning_top", + "web-time", ] [[package]] name = "gstreamer" -version = "0.23.7" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8757a87f3706560037a01a9f06a59fcc7bdb0864744dcf73546606e60c4316e1" +checksum = "32f5db514ad5ccf70ad35485058aa8b894bb81cfcf76bb994af135d9789427c6" dependencies = [ "cfg-if", "futures-channel", @@ -1832,23 +1709,23 @@ dependencies = [ "glib", "gstreamer-sys", "itertools 0.14.0", + "kstring", "libc", "muldiv", "num-integer", "num-rational", - "once_cell", "option-operations", "paste", "pin-project-lite", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "gstreamer-app" -version = "0.23.5" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9a883eb21aebcf1289158225c05f7aea5da6ecf71fa7f0ff1ce4d25baf004e" +checksum = "fad8ae64a7af6d1aa04e96db085a0cbd64a6b838d85c115c99fa053ab8902d98" dependencies = [ "futures-core", "futures-sink", @@ -1861,9 +1738,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.23.5" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f7ef838306fe51852d503a14dc79ac42de005a59008a05098de3ecdaf05455" +checksum = "aaf1a3af017f9493c34ccc8439cbce5c48f6ddff6ec0514c23996b374ff25f9a" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -1874,9 +1751,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.23.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7ec7e0374298897e669db7c79544bc44df12011985e7dd5f38644edaf2caf4" +checksum = "7404c5d0cbb2189e6a10d05801e93f47fe60b195e4d73dd1c540d055f7b340b8" dependencies = [ "cfg-if", "glib", @@ -1884,15 +1761,14 @@ dependencies = [ "gstreamer-audio-sys", "gstreamer-base", "libc", - "once_cell", "smallvec", ] [[package]] name = "gstreamer-audio-sys" -version = "0.23.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5f3e09e7c04ec91d78c2a6ca78d50b574b9ed49fdf5e72f3693adca4306a87" +checksum = "626cd3130bc155a8b6d4ac48cfddc15774b5a6cc76fcb191aab09a2655bad8f5" dependencies = [ "glib-sys", "gobject-sys", @@ -1904,9 +1780,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.23.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f19a74fd04ffdcb847dd322640f2cf520897129d00a7bcb92fd62a63f3e27404" +checksum = "34745d3726a080e0d57e402a314e37073d0b341f3a5754258550311ca45e4754" dependencies = [ "atomic_refcell", "cfg-if", @@ -1918,9 +1794,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.23.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f2fb0037b6d3c5b51f60dea11e667910f33be222308ca5a101450018a09840" +checksum = "dfad00fa63ddd8132306feef9d5095a3636192f09d925adfd0a9be0d82b9ea91" dependencies = [ "glib-sys", "gobject-sys", @@ -1931,10 +1807,11 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.23.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feea73b4d92dbf9c24a203c9cd0bcc740d584f6b5960d5faf359febf288919b2" +checksum = "36f46b35f9dc4b5a0dca3f19d2118bb5355c3112f228a99a84ed555f48ce5cf9" dependencies = [ + "cfg-if", "glib-sys", "gobject-sys", "libc", @@ -1943,35 +1820,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http", "indexmap", "slab", "tokio", @@ -1991,9 +1849,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -2006,10 +1864,10 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "headers-core", - "http 1.3.1", + "http", "httpdate", "mime", "sha1", @@ -2021,7 +1879,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.3.1", + "http", ] [[package]] @@ -2077,17 +1935,6 @@ dependencies = [ "syn", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.3.1" @@ -2099,17 +1946,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -2117,7 +1953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http", ] [[package]] @@ -2128,8 +1964,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -2147,43 +1983,21 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.32" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.10", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -2198,48 +2012,14 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.3.1", - "hyper 1.6.0", - "hyper-rustls 0.26.0", + "http", + "hyper", + "hyper-tls", "hyper-util", + "native-tls", "pin-project-lite", - "rustls-native-certs 0.7.3", - "tokio", - "tokio-rustls 0.25.0", - "tower-service", - "webpki", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" -dependencies = [ - "futures-util", - "http 1.3.1", - "hyper 1.6.0", - "hyper-util", - "log", - "rustls 0.22.4", - "rustls-native-certs 0.7.3", - "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", + "tokio-native-tls", "tower-service", ] @@ -2249,17 +2029,15 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", - "hyper 1.6.0", + "http", + "hyper", "hyper-util", - "log", - "rustls 0.23.28", - "rustls-native-certs 0.8.1", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls", "tower-service", - "webpki-roots 1.0.1", + "webpki-roots", ] [[package]] @@ -2270,7 +2048,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "native-tls", "tokio", @@ -2280,24 +2058,24 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.6.0", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2", - "system-configuration 0.6.1", + "socket2 0.6.0", + "system-configuration", "tokio", "tower-service", "tracing", @@ -2422,9 +2200,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -2482,9 +2260,9 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", @@ -2507,9 +2285,9 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" dependencies = [ "darling", "indoc", @@ -2529,6 +2307,17 @@ dependencies = [ "syn", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2546,16 +2335,35 @@ dependencies = [ ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" +name = "is-docker" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] [[package]] -name = "itertools" -version = "0.12.1" +name = "is-wsl" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -2586,11 +2394,11 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jack" -version = "0.11.4" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e5a18a3c2aefb354fb77111ade228b20267bdc779de84e7a4ccf7ea96b9a6cd" +checksum = "f70ca699f44c04a32d419fc9ed699aaea89657fc09014bf3fa238e91d13041b9" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.3", "jack-sys", "lazy_static", "libc", @@ -2659,6 +2467,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2668,12 +2485,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lebe" version = "0.5.2" @@ -2682,24 +2493,24 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libdbus-sys" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" dependencies = [ "pkg-config", ] [[package]] name = "libfuzzer-sys" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", @@ -2722,7 +2533,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -2737,7 +2548,7 @@ version = "2.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "libc", "libpulse-sys", "num-derive", @@ -2781,79 +2592,79 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "libc", - "redox_syscall 0.5.13", + "redox_syscall 0.5.17", ] [[package]] name = "librespot-audio" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e07566fe7553042936c61bbdd9bedb524114a904aa7f9738e266c641468bab8" +checksum = "1ccbd884e99ad67528a34b7ce54f4ad872bee9f57b232441edb1befb8c60c88b" dependencies = [ "aes", "bytes", "ctr", "futures-util", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "librespot-core", "log", "parking_lot", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.16", "tokio", ] [[package]] name = "librespot-connect" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ce3a5634669ce051a425cff5437a474831111df045fd5a2bb37fabe316fa60" +checksum = "9ee65534c5bb1e2179a8b4aefe2ec21021f17ea88917be7dfd264491725e663b" dependencies = [ - "form_urlencoded", "futures-util", "librespot-core", "librespot-playback", "librespot-protocol", "log", "protobuf", - "rand 0.8.5", - "serde", + "rand 0.9.2", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.16", "tokio", "tokio-stream", + "uuid", ] [[package]] name = "librespot-core" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4c76303efcf949a59f9380220ca420c4d72fa32dbb3641a0079f429cc5e44e7" +checksum = "44bda6a0a394643182ced9bc4cdd0f6ef040852199419fd98a7b30bfb382a79a" dependencies = [ "aes", - "base64 0.22.1", + "base64", "byteorder", "bytes", "data-encoding", + "flate2", "form_urlencoded", "futures-core", "futures-util", "governor", "hmac", - "http 1.3.1", + "http", "http-body-util", "httparse", - "hyper 1.6.0", + "hyper", "hyper-proxy2", - "hyper-rustls 0.27.7", + "hyper-tls", "hyper-util", "librespot-oauth", "librespot-protocol", @@ -2863,21 +2674,22 @@ dependencies = [ "num-derive", "num-integer", "num-traits", - "once_cell", "parking_lot", "pbkdf2", "pin-project-lite", "priority-queue", "protobuf", - "quick-xml 0.36.2", - "rand 0.8.5", + "protobuf-json-mapping", + "quick-xml 0.38.2", + "rand 0.9.2", + "rand_distr", "rsa", "serde", "serde_json", "sha1", "shannon", "sysinfo", - "thiserror 1.0.69", + "thiserror 2.0.16", "time", "tokio", "tokio-stream", @@ -2890,9 +2702,9 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf6d5c46a401b1dd3e062ebdce959aa694bbae1039841756545d2e9c4f8be5f" +checksum = "d2f186eef62b0b3a156e9c204717e88c404396b36cf8f5c0e298f627bd4d2dca" dependencies = [ "async-trait", "bytes", @@ -2902,27 +2714,29 @@ dependencies = [ "protobuf", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.16", "uuid", ] [[package]] name = "librespot-oauth" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00dcad779aa6c3b442e493e2a40ca83a5c5fcf38a65c05b026c3587bd4f8d14f" +checksum = "5abefd211d7ab9a850f951258c07a346391f235e512adcd478c069ef7916f8bb" dependencies = [ "log", "oauth2", - "thiserror 1.0.69", + "open", + "reqwest", + "thiserror 2.0.16", "url", ] [[package]] name = "librespot-playback" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed1f776a04c8c9275407f8d4df034f2615ea729ec6c6d0fa69f423fc571df64" +checksum = "20ef81a3ccdff4c31f94b80efd89bfac737daddd94574e97026d86e6e1d072e0" dependencies = [ "alsa", "cpal", @@ -2938,23 +2752,24 @@ dependencies = [ "librespot-metadata", "log", "parking_lot", + "portable-atomic", "portaudio-rs", - "rand 0.8.5", + "rand 0.9.2", "rand_distr", "rodio", "sdl2", "shell-words", "symphonia", - "thiserror 1.0.69", + "thiserror 2.0.16", "tokio", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] name = "librespot-protocol" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80802f52b5a1b3a2157454e6aca483a627fbf7b942e0a5fea037ebf3ed8b0546" +checksum = "c647bea4e2c8d19215aabdb93c9c2243f09d1500e68cbd0c894d7a2ed822bb43" dependencies = [ "protobuf", "protobuf-codegen", @@ -2986,9 +2801,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" @@ -3039,7 +2854,7 @@ dependencies = [ "html5ever", "log", "markup5ever_rcdom", - "reqwest 0.12.20", + "reqwest", "serde", "tokio", ] @@ -3052,12 +2867,12 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mac-notification-sys" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b95dfb34071d1592b45622bf93e315e3a72d414b6782aca9a015c12bec367ef" +checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4" dependencies = [ "cc", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-foundation 0.3.1", "time", ] @@ -3150,9 +2965,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.5" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", ] @@ -3218,35 +3033,21 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "tempfile", ] -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.9.1", - "jni-sys", - "log", - "ndk-sys 0.5.0+25.2.9519653", - "num_enum", - "thiserror 1.0.69", -] - [[package]] name = "ndk" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "jni-sys", "log", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -3258,15 +3059,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -3282,12 +3074,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "nom" version = "7.1.3" @@ -3350,7 +3136,6 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "rand 0.8.5", ] [[package]] @@ -3461,16 +3246,16 @@ dependencies = [ [[package]] name = "oauth2" -version = "4.4.2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.13.1", + "base64", "chrono", "getrandom 0.2.16", - "http 0.2.12", + "http", "rand 0.8.5", - "reqwest 0.11.27", + "reqwest", "serde", "serde_json", "serde_path_to_error", @@ -3506,9 +3291,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" dependencies = [ "objc2-encode", ] @@ -3519,7 +3304,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -3529,13 +3314,28 @@ dependencies = [ "objc2-quartz-core", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" +dependencies = [ + "bitflags 2.9.3", + "libc", + "objc2 0.6.2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3553,13 +3353,35 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-audio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +dependencies = [ + "dispatch2", + "objc2 0.6.2", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +dependencies = [ + "bitflags 2.9.3", + "objc2 0.6.2", +] + [[package]] name = "objc2-core-data" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3571,9 +3393,9 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "dispatch2", - "objc2 0.6.1", + "objc2 0.6.2", ] [[package]] @@ -3612,7 +3434,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "dispatch", "libc", @@ -3625,10 +3447,20 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.6.1", "libc", - "objc2 0.6.1", + "objc2 0.6.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", "objc2-core-foundation", ] @@ -3650,7 +3482,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3662,7 +3494,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3685,7 +3517,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -3717,7 +3549,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3733,29 +3565,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk 0.8.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -3768,13 +3577,24 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg-if", "foreign-types 0.3.2", "libc", @@ -3838,9 +3658,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owned_ttf_parser" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" dependencies = [ "ttf-parser", ] @@ -3871,7 +3691,7 @@ dependencies = [ "cfg-if", "libc", "petgraph", - "redox_syscall 0.5.13", + "redox_syscall 0.5.17", "smallvec", "thread-id", "windows-targets 0.52.6", @@ -3883,6 +3703,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -3904,9 +3730,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" @@ -4030,17 +3856,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.8.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" +checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.0.7", - "tracing", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] @@ -4091,7 +3916,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.26", + "zerocopy", ] [[package]] @@ -4100,16 +3925,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "prettyplease" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "priority-queue" version = "2.5.0" @@ -4132,9 +3947,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -4184,6 +3999,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "protobuf-json-mapping" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d6e4be637b310d8a5c02fa195243328e2d97fa7df1127a27281ef1187fcb1d" +dependencies = [ + "protobuf", + "protobuf-support", + "thiserror 1.0.69", +] + [[package]] name = "protobuf-parse" version = "3.7.2" @@ -4226,21 +4052,21 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.36.2" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", - "serde", ] [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "d200a41a7797e6461bd04e4e95c3347053a731c32c87f066f2f0dda22dbdbba8" dependencies = [ "memchr", + "serde", ] [[package]] @@ -4254,10 +4080,10 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", - "rustls 0.23.28", - "socket2", - "thiserror 2.0.12", + "rustc-hash", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -4272,13 +4098,13 @@ dependencies = [ "bytes", "getrandom 0.3.3", "lru-slab", - "rand 0.9.1", + "rand 0.9.2", "ring", - "rustc-hash 2.1.1", - "rustls 0.23.28", + "rustc-hash", + "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -4293,7 +4119,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] @@ -4326,9 +4152,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -4374,12 +4200,12 @@ dependencies = [ [[package]] name = "rand_distr" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.9.2", ] [[package]] @@ -4388,7 +4214,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cassowary", "compact_str", "crossterm 0.28.1", @@ -4397,7 +4223,7 @@ dependencies = [ "itertools 0.13.0", "lru", "paste", - "strum", + "strum 0.26.3", "unicode-segmentation", "unicode-truncate", "unicode-width 0.2.0", @@ -4461,9 +4287,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -4471,9 +4297,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -4490,11 +4316,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -4554,61 +4380,22 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ - "base64 0.21.7", + "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-rustls 0.24.2", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-rustls 0.24.1", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 0.25.4", - "winreg", -] - -[[package]] -name = "reqwest" -version = "0.12.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "h2 0.4.10", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.6.0", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", @@ -4618,15 +4405,15 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.28", + "rustls", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.2", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -4634,14 +4421,14 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.1", + "webpki-roots", ] [[package]] name = "rgb" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" dependencies = [ "bytemuck", ] @@ -4662,12 +4449,13 @@ dependencies = [ [[package]] name = "rodio" -version = "0.19.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183" dependencies = [ "cpal", - "thiserror 1.0.69", + "dasp_sample", + "num-rational", ] [[package]] @@ -4692,13 +4480,13 @@ dependencies = [ [[package]] name = "rspotify" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97e4f3254b449534ab3891331881d922d50ec36bd07c155147253a747fa5d475" +checksum = "77beedc33ecff4c39e8ef0e6f7ebc8d849f3ffebbeb786f9997d96f0d9cf4017" dependencies = [ "async-stream", "async-trait", - "base64 0.22.1", + "base64", "chrono", "futures", "getrandom 0.2.16", @@ -4710,55 +4498,50 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.12", + "thiserror 2.0.16", "url", + "webbrowser", ] [[package]] name = "rspotify-http" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed8a6b6d3cfea3040a2436e02366e5cb8d84f7658667be7075f6ed1cc64360da" +checksum = "fde1ea9e2a49698cffbc994a83f5f909b37736c31cccb202f9577e8a32df3a63" dependencies = [ "async-trait", "log", "maybe-async", - "reqwest 0.12.20", + "reqwest", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "rspotify-macros" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559fad82b639297c093c5cc8ef406001dd0cb55cd9f5125a8fb40310e38b95d9" +checksum = "ee3dfb51ee54bd754ad76e96ad60a3b64bc70ae33a89261d9dbabc4c148a496f" [[package]] name = "rspotify-model" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd00345ab89d2dd8059f0d0c168a6b0f858099795d7e318554411303b827d95" +checksum = "018f29a6a8c47cfe7923c48140ed546a395f660c7af05b73e6001d4505f89c8d" dependencies = [ "chrono", "enum_dispatch", "serde", "serde_json", - "strum", - "thiserror 2.0.12", + "strum 0.27.2", + "thiserror 2.0.16", ] [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -4772,7 +4555,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4781,140 +4564,47 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls" -version = "0.23.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki 0.103.3", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe", - "rustls-pemfile 2.2.0", - "rustls-pki-types", - "schannel", - "security-framework 2.11.1", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework 3.2.0", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "web-time", - "zeroize", + "windows-sys 0.60.2", ] [[package]] -name = "rustls-webpki" -version = "0.101.7" +name = "rustls" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ + "once_cell", "ring", - "untrusted", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] -name = "rustls-webpki" -version = "0.102.8" +name = "rustls-pki-types" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", + "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4922,9 +4612,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -4962,16 +4652,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sctk-adwaita" version = "0.10.1" @@ -4987,9 +4667,9 @@ dependencies = [ [[package]] name = "sdl2" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380" +checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" dependencies = [ "bitflags 1.3.2", "lazy_static", @@ -4999,9 +4679,9 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3" +checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" dependencies = [ "cfg-if", "libc", @@ -5014,26 +4694,13 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", ] -[[package]] -name = "security-framework" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" -dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework-sys" version = "2.14.0" @@ -5066,9 +4733,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -5095,6 +4762,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5182,9 +4858,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -5240,9 +4916,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -5256,7 +4932,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "calloop", "calloop-wayland-source", "cursor-icon", @@ -5294,13 +4970,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "souvlaki" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bb90fced3a4a752db0ab08065ca919f37880fa1c7c136258bdd4c520e5c3e" +checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8" dependencies = [ - "base64 0.22.1", + "base64", "block", "cocoa", "core-graphics 0.22.3", @@ -5342,7 +5028,7 @@ dependencies = [ [[package]] name = "spotify_player" -version = "0.20.6" +version = "0.20.7" dependencies = [ "anyhow", "async-trait", @@ -5368,23 +5054,23 @@ dependencies = [ "maybe-async", "notify-rust", "parking_lot", - "rand 0.9.1", + "rand 0.9.2", "ratatui", "regex", - "reqwest 0.12.20", + "reqwest", "rspotify", - "rustls 0.23.28", + "rustls", "serde", "serde_json", "souvlaki", "tokio", - "toml", + "toml 0.9.5", "tracing", "tracing-subscriber", "ttl_cache", "unicode-bidi", "viuer", - "which 7.0.3", + "which 8.0.0", "windows 0.58.0", "winit", ] @@ -5444,7 +5130,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -5460,6 +5155,18 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5552,21 +5259,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -5589,26 +5290,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.31.4" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", - "windows 0.57.0", -] - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.61.3", ] [[package]] @@ -5617,19 +5308,9 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "system-configuration-sys", ] [[package]] @@ -5651,7 +5332,7 @@ dependencies = [ "cfg-expr 0.15.8", "heck", "pkg-config", - "toml", + "toml 0.8.23", "version-compare 0.2.0", ] @@ -5661,10 +5342,10 @@ version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" dependencies = [ - "cfg-expr 0.20.0", + "cfg-expr 0.20.2", "heck", "pkg-config", - "toml", + "toml 0.8.23", "version-compare 0.2.0", ] @@ -5687,22 +5368,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml 0.37.5", - "thiserror 2.0.12", + "thiserror 2.0.16", "windows 0.61.3", "windows-version", ] [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.7", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] @@ -5736,11 +5417,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -5756,9 +5437,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -5865,9 +5546,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -5880,19 +5561,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5916,34 +5599,13 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.28", + "rustls", "tokio", ] @@ -5960,25 +5622,23 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.24.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" dependencies = [ "futures-util", "log", - "rustls 0.23.28", - "rustls-native-certs 0.8.1", - "rustls-pki-types", + "native-tls", "tokio", - "tokio-rustls 0.26.2", + "tokio-native-tls", "tungstenite", ] [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -5994,11 +5654,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -6008,6 +5683,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -6016,17 +5700,25 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", - "toml_write", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" [[package]] name = "tower" @@ -6037,7 +5729,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -6049,11 +5741,11 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "bytes", "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "iri-string", "pin-project-lite", "tower", @@ -6157,21 +5849,19 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.24.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" dependencies = [ - "byteorder", "bytes", "data-encoding", - "http 1.3.1", + "http", "httparse", "log", - "rand 0.8.5", - "rustls 0.23.28", - "rustls-pki-types", + "native-tls", + "rand 0.9.2", "sha1", - "thiserror 1.0.69", + "thiserror 2.0.16", "utf-8", ] @@ -6230,9 +5920,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -6266,12 +5956,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", - "rand 0.9.1", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -6360,7 +6051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ae7c6870b98c838123f22cac9a594cbe2d74ea48d79271c08f8c9e680b40fac" dependencies = [ "ansi_colours", - "base64 0.22.1", + "base64", "console", "crossterm 0.28.1", "image", @@ -6477,13 +6168,13 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.44", + "rustix 1.0.8", "scoped-tls", "smallvec", "wayland-sys", @@ -6491,12 +6182,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.1", - "rustix 0.38.44", + "bitflags 2.9.3", + "rustix 1.0.8", "wayland-backend", "wayland-scanner", ] @@ -6507,29 +6198,29 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182" +checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" dependencies = [ - "rustix 0.38.44", + "rustix 1.0.8", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.8" +version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "wayland-backend", "wayland-client", "wayland-scanner", @@ -6537,11 +6228,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" +checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6550,11 +6241,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6563,9 +6254,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" dependencies = [ "proc-macro2", "quick-xml 0.37.5", @@ -6574,9 +6265,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" dependencies = [ "dlib", "log", @@ -6605,26 +6296,28 @@ dependencies = [ ] [[package]] -name = "webpki" -version = "0.22.4" +name = "webbrowser" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +checksum = "ea9fe1ebb156110ff855242c1101df158b822487e4957b0556d9ffce9db0f535" dependencies = [ - "ring", - "untrusted", + "block2 0.5.1", + "core-foundation 0.10.1", + "home", + "jni", + "log", + "ndk-context", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "url", + "web-sys", ] [[package]] name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - -[[package]] -name = "webpki-roots" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -6649,13 +6342,12 @@ dependencies = [ [[package]] name = "which" -version = "7.0.3" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "either", "env_home", - "rustix 1.0.7", + "rustix 1.0.8", "winsafe", ] @@ -6677,11 +6369,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6709,16 +6401,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.58.0" @@ -6761,18 +6443,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.58.0" @@ -6810,17 +6480,6 @@ dependencies = [ "windows-threading", ] -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-implement" version = "0.58.0" @@ -6843,17 +6502,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-interface" version = "0.58.0" @@ -6894,9 +6542,9 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link", "windows-result 0.3.4", @@ -6958,15 +6606,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -6991,7 +6630,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -7042,10 +6681,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -7256,14 +6896,14 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winit" -version = "0.30.11" +version = "0.30.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "bytemuck", "calloop", @@ -7276,7 +6916,7 @@ dependencies = [ "js-sys", "libc", "memmap2", - "ndk 0.9.0", + "ndk", "objc2 0.5.2", "objc2-app-kit", "objc2-foundation 0.2.2", @@ -7308,23 +6948,13 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winsafe" version = "0.0.19" @@ -7337,7 +6967,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -7380,9 +7010,9 @@ checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xcursor" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635887f4315a33cb714eb059bdbd7c1c92bfa71bc5b9d5115460502f788c2ab5" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" [[package]] name = "xkbcommon-dl" @@ -7390,7 +7020,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "dlib", "log", "once_cell", @@ -7438,34 +7068,13 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "zerocopy-derive 0.8.26", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "zerocopy-derive", ] [[package]] @@ -7519,9 +7128,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -7556,9 +7165,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7384255a918371b5af158218d131530f694de9ad3815ebdd0453a940485cb0fa" +checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" dependencies = [ "zune-core", ] diff --git a/README.md b/README.md index 1bd69608..9cf16daf 100644 --- a/README.md +++ b/README.md @@ -69,12 +69,15 @@ A Spotify Premium account is **required**. ```shell sudo apt install libssl-dev libasound2-dev libdbus-1-dev ``` + - On RHEL/Fedora based systems, run the below command to install application's dependencies : ```shell sudo dnf install openssl-devel alsa-lib-devel dbus-devel ``` + or if you're using `yum`: + ```shell sudo yum install openssl-devel alsa-lib-devel dbus-devel ``` @@ -101,7 +104,7 @@ Run `cargo install spotify_player --locked` to install the application from [cra Run `pacman -S spotify-player` to install the application. -Alternatively, run `yay -S spotify-player-full` to install an AUR package compiled with full feature support and Pulseaudio/Pipewire instead of rodio. +**Note**: Defaults to PulseAudio / Pipewire audio backend. For a different one, please consider modifying the [official PKGBUILD](https://gitlab.archlinux.org/archlinux/packaging/packages/spotify-player) and rebuilding it manually. See [Audio Backends](#audio-backend) for a list of options. ### Void Linux @@ -159,11 +162,13 @@ docker run --rm \ ### Spotify Connect -To enable a full [Spotify connect](https://www.spotify.com/us/connect/) support, user will need to register a Spotify application and specify the application's `client_id` in the general configuration file as described in the [configuration documentation](docs/config.md#general). +To enable a full [Spotify connect](https://www.spotify.com/us/connect/) support, users will need to enable a _"user-provided client integration"_. -More details about registering a Spotify application can be found in the [official Spotify documentation](https://developer.spotify.com/documentation/general/guides/authorization/app-settings/). +This integration can be done by following [this documentation](https://developer.spotify.com/documentation/general/guides/authorization/app-settings/) to register a Spotify app and then specifying the app's `client_id` in the [general configuration file](docs/config.md#general). -When `spotify_player` runs with your own `client_id`, press **D** (default shortcut for `SwitchDevice` command) to get the list of available devices, then press **enter** (default shortcut for `ChooseSelected` command) to connect to the selected device. +Upon running `spotify_player` with a user-provided `client_id`, user will be prompted to authenticate the app described earlier. **NOTE** that this prompt is different from the prompt to authenticate `spotify_player`. Upon accepting the authentication request, `spotify_player` will retrieve an access token of the app to finish setting up the integration. + +After the user-provided client is successfully integrated, press **D** (default shortcut for `SwitchDevice` command) to get the list of available devices, then press **enter** (default shortcut for `ChooseSelected` command) to connect to the selected device. ### Streaming @@ -246,6 +251,22 @@ Examples of image rendering: ![others](https://user-images.githubusercontent.com/40011582/172967325-d2098037-e19e-440a-a38a-5b076253ecb1.png) +#### Pixelate + +If your terminal supports high-res images, but you like the pixelated look you can enable the `pixelate` feature, which also enables the `image` feature: + +```shell +cargo install spotify_player --features pixelate +``` + +The amount of pixels can be tweaked via the `cover_img_pixels` config option. + +| `cover_img_pixels` | `8` | `16` | `32` | `64` | +| ------------------ | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| example | 8x8 | 16x16 | 32x32 | 64x64 | + +To temporarily disable the `pixelate` feature just set `cover_img_pixels` to a high value like `512`. + ### Notify To enable desktop notification support, `spotify_player` needs to be built/installed with `notify` feature (**disabled** by default). To install the application with `notify` feature included, run: @@ -327,65 +348,66 @@ To go to the shortcut help page, press `?` or `C-h` (default shortcuts for `Open List of supported commands: -| Command | Description | Default shortcuts | -| ------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------ | -| `NextTrack` | next track | `n` | -| `PreviousTrack` | previous track | `p` | -| `ResumePause` | resume/pause based on the current playback | `space` | -| `PlayRandom` | play a random track in the current context | `.` | -| `Repeat` | cycle the repeat mode | `C-r` | -| `ToggleFakeTrackRepeatMode` | toggle fake track repeat mode | `M-r` | -| `Shuffle` | toggle the shuffle mode | `C-s` | -| `VolumeChange` | change playback volume by an offset (default shortcuts use 5%) | `+`, `-` | -| `Mute` | toggle playback volume between 0% and previous level | `_` | -| `SeekForward` | seek forward by 5s | `>` | -| `SeekBackward` | seek backward by 5s | `<` | -| `Quit` | quit the application | `C-c`, `q` | -| `ClosePopup` | close a popup | `esc` | -| `SelectNextOrScrollDown` | select the next item in a list/table or scroll down (supports vim-style count: 5j) | `j`, `C-n`, `down` | -| `SelectPreviousOrScrollUp` | select the previous item in a list/table or scroll up (supports vim-style count: 10k) | `k`, `C-p`, `up` | -| `PageSelectNextOrScrollDown` | select the next page item in a list/table or scroll a page down (supports vim-style count: 3C-f) | `page_down`, `C-f` | -| `PageSelectPreviousOrScrollUp` | select the previous page item in a list/table or scroll a page up (supports vim-style count: 2C-b) | `page_up`, `C-b` | -| `SelectFirstOrScrollToTop` | select the first item in a list/table or scroll to the top | `g g`, `home` | -| `SelectLastOrScrollToBottom` | select the last item in a list/table or scroll to the bottom | `G`, `end` | -| `ChooseSelected` | choose the selected item | `enter` | -| `RefreshPlayback` | manually refresh the current playback | `r` | -| `RestartIntegratedClient` | restart the integrated client (`streaming` feature only) | `R` | -| `ShowActionsOnSelectedItem` | open a popup showing actions on a selected item | `g a`, `C-space` | -| `ShowActionsOnCurrentTrack` | open a popup showing actions on the current track | `a` | -| `AddSelectedItemToQueue` | add the selected item to queue | `Z`, `C-z` | -| `FocusNextWindow` | focus the next focusable window (if any) | `tab` | -| `FocusPreviousWindow` | focus the previous focusable window (if any) | `backtab` | -| `SwitchTheme` | open a popup for switching theme | `T` | -| `SwitchDevice` | open a popup for switching device | `D` | -| `Search` | open a popup for searching in the current page | `/` | -| `BrowseUserPlaylists` | open a popup for browsing user's playlists | `u p` | -| `BrowseUserFollowedArtists` | open a popup for browsing user's followed artists | `u a` | -| `BrowseUserSavedAlbums` | open a popup for browsing user's saved albums | `u A` | -| `CurrentlyPlayingContextPage` | go to the currently playing context page | `g space` | -| `TopTrackPage` | go to the user top track page | `g t` | -| `RecentlyPlayedTrackPage` | go to the user recently played track page | `g r` | -| `LikedTrackPage` | go to the user liked track page | `g y` | -| `LyricsPage` | go to the lyrics page of the current track | `g L`, `l` | -| `LibraryPage` | go to the user library page | `g l` | -| `SearchPage` | go to the search page | `g s` | -| `BrowsePage` | go to the browse page | `g b` | -| `Queue` | go to the queue page | `z` | -| `OpenCommandHelp` | go to the command help page | `?`, `C-h` | -| `PreviousPage` | go to the previous page | `backspace`, `C-q` | -| `OpenSpotifyLinkFromClipboard` | open a Spotify link from clipboard | `O` | -| `SortTrackByTitle` | sort the track table (if any) by track's title | `s t` | -| `SortTrackByArtists` | sort the track table (if any) by track's artists | `s a` | -| `SortTrackByAlbum` | sort the track table (if any) by track's album | `s A` | -| `SortTrackByAddedDate` | sort the track table (if any) by track's added date | `s D` | -| `SortTrackByDuration` | sort the track table (if any) by track's duration | `s d` | -| `SortLibraryAlphabetically` | sort the library alphabetically | `s l a` | -| `SortLibraryByRecent` | sort the library (playlists and albums) by recently added items | `s l r` | -| `ReverseOrder` | reverse the order of the track table (if any) | `s r` | -| `MovePlaylistItemUp` | move playlist item up one position | `C-k` | -| `MovePlaylistItemDown` | move playlist item down one position | `C-j` | -| `CreatePlaylist` | create a new playlist | `N` | -| `JumpToCurrentTrackInContext` | jump to the current track in the context | `g c` | +| Command | Description | Default shortcuts | +| ------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------ | +| `NextTrack` | next track | `n` | +| `PreviousTrack` | previous track | `p` | +| `ResumePause` | resume/pause based on the current playback | `space` | +| `PlayRandom` | play a random track in the current context | `.` | +| `Repeat` | cycle the repeat mode | `C-r` | +| `ToggleFakeTrackRepeatMode` | toggle fake track repeat mode | `M-r` | +| `Shuffle` | toggle the shuffle mode | `C-s` | +| `VolumeChange` | change playback volume by an offset (default shortcuts use 5%) | `+`, `-` | +| `Mute` | toggle playback volume between 0% and previous level | `_` | +| `SeekForward` | seek forward by 5s | `>` | +| `SeekBackward` | seek backward by 5s | `<` | +| `Quit` | quit the application | `C-c`, `q` | +| `ClosePopup` | close a popup | `esc` | +| `SelectNextOrScrollDown` | select the next item in a list/table or scroll down (supports vim-style count: 5j) | `j`, `C-n`, `down` | +| `SelectPreviousOrScrollUp` | select the previous item in a list/table or scroll up (supports vim-style count: 10k) | `k`, `C-p`, `up` | +| `PageSelectNextOrScrollDown` | select the next page item in a list/table or scroll a page down (supports vim-style count: 3C-f) | `page_down`, `C-f` | +| `PageSelectPreviousOrScrollUp` | select the previous page item in a list/table or scroll a page up (supports vim-style count: 2C-b) | `page_up`, `C-b` | +| `SelectFirstOrScrollToTop` | select the first item in a list/table or scroll to the top | `g g`, `home` | +| `SelectLastOrScrollToBottom` | select the last item in a list/table or scroll to the bottom | `G`, `end` | +| `ChooseSelected` | choose the selected item | `enter` | +| `RefreshPlayback` | manually refresh the current playback | `r` | +| `RestartIntegratedClient` | restart the integrated client (`streaming` feature only) | `R` | +| `ShowActionsOnSelectedItem` | open a popup showing actions on a selected item | `g a`, `C-space` | +| `ShowActionsOnCurrentTrack` | open a popup showing actions on the current track | `a` | +| `AddSelectedItemToQueue` | add the selected item to queue | `Z`, `C-z` | +| `FocusNextWindow` | focus the next focusable window (if any) | `tab` | +| `FocusPreviousWindow` | focus the previous focusable window (if any) | `backtab` | +| `SwitchTheme` | open a popup for switching theme | `T` | +| `SwitchDevice` | open a popup for switching device | `D` | +| `Search` | open a popup for searching in the current page | `/` | +| `BrowseUserPlaylists` | open a popup for browsing user's playlists | `u p` | +| `BrowseUserFollowedArtists` | open a popup for browsing user's followed artists | `u a` | +| `BrowseUserSavedAlbums` | open a popup for browsing user's saved albums | `u A` | +| `CurrentlyPlayingContextPage` | go to the currently playing context page | `g space` | +| `TopTrackPage` | go to the user top track page | `g t` | +| `RecentlyPlayedTrackPage` | go to the user recently played track page | `g r` | +| `LikedTrackPage` | go to the user liked track page | `g y` | +| `LyricsPage` | go to the lyrics page of the current track | `g L`, `l` | +| `LibraryPage` | go to the user library page | `g l` | +| `SearchPage` | go to the search page | `g s` | +| `BrowsePage` | go to the browse page | `g b` | +| `Queue` | go to the queue page | `z` | +| `OpenCommandHelp` | go to the command help page | `?`, `C-h` | +| `PreviousPage` | go to the previous page | `backspace`, `C-q` | +| `OpenSpotifyLinkFromClipboard` | open a Spotify link from clipboard | `O` | +| `SortTrackByTitle` | sort the track table (if any) by track's title | `s t` | +| `SortTrackByArtists` | sort the track table (if any) by track's artists | `s a` | +| `SortTrackByAlbum` | sort the track table (if any) by track's album | `s A` | +| `SortTrackByAddedDate` | sort the track table (if any) by track's added date | `s D` | +| `SortTrackByDuration` | sort the track table (if any) by track's duration | `s d` | +| `SortLibraryAlphabetically` | sort the library alphabetically | `s l a` | +| `SortLibraryByRecent` | sort the library (playlists and albums) by recently added items | `s l r` | +| `ReverseOrder` | reverse the order of the track table (if any) | `s r` | +| `MovePlaylistItemUp` | move playlist item up one position | `C-k` | +| `MovePlaylistItemDown` | move playlist item down one position | `C-j` | +| `CreatePlaylist` | create a new playlist | `N` | +| `JumpToCurrentTrackInContext` | jump to the current track in the context | `g c` | +| `JumpToHighlightTrackInContext` | jump to the currently highlighted search result in the context | `C-g` | To add new shortcuts or modify the default shortcuts, please refer to the [keymaps section](docs/config.md#keymaps) in the configuration documentation. diff --git a/docs/config.md b/docs/config.md index e034f433..a22da603 100644 --- a/docs/config.md +++ b/docs/config.md @@ -23,44 +23,44 @@ All configuration files should be placed inside the application's configuration `spotify_player` uses `app.toml` to configure general application configurations: -| Option | Description | Default | -| --------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| `client_id` | the Spotify client's ID | `65b708073fc0480ea92a077233ca87bd` | -| `client_id_command` | a shell command that prints the Spotify client ID to stdout (overrides `client_id`) | `None` | -| `login_redirect_uri` | the redirect URI for authenticating the application | `http://127.0.0.1:8989/login` | -| `client_port` | the port that the application's client is running on to handle CLI commands | `8080` | -| `tracks_playback_limit` | the limit for the number of tracks played in a **tracks** playback | `50` | -| `playback_format` | the format of the text in the playback's window | `{status} {track} โ€ข {artists} {liked}\n{album}\n{metadata}` | -| `notify_format` | the format of a notification (`notify` feature only) | `{ summary = "{track} โ€ข {artists}", body = "{album}" }` | -| `notify_timeout_in_secs` | the timeout (in seconds) of a notification (`notify` feature only) | `0` (no timeout) | -| `player_event_hook_command` | the hook command executed when there is a new player event | `None` | -| `ap_port` | the application's Spotify session connection port | `None` | -| `proxy` | the application's Spotify session connection proxy | `None` | -| `theme` | the application's theme | `default` | -| `app_refresh_duration_in_ms` | the duration (in ms) between two consecutive application refreshes | `32` | -| `playback_refresh_duration_in_ms` | the duration (in ms) between two consecutive playback refreshes | `0` | -| `page_size_in_rows` | a page's size expressed as a number of rows (for page-navigation commands) | `20` | -| `enable_media_control` | enable application media control support (`media-control` feature only) | `true` (Linux), `false` (Windows and MacOS) | -| `enable_streaming` | enable streaming (`streaming` feature only) | `Always` | -| `enable_notify` | enable notification (`notify` feature only) | `true` | -| `enable_cover_image_cache` | store album's cover images in the cache folder | `true` | -| `notify_streaming_only` | only send notification when streaming is enabled (`streaming` and `notify` feature only) | `false` | -| `default_device` | the default device to connect to on startup if no playing device found | `spotify-player` | -| `play_icon` | the icon to indicate playing state of a Spotify item | `โ–ถ` | -| `pause_icon` | the icon to indicate pause state of a Spotify item | `โ–Œโ–Œ` | -| `liked_icon` | the icon to indicate the liked state of a song | `โ™ฅ` | -| `border_type` | the type of the application's borders | `Plain` | -| `progress_bar_type` | the type of the playback progress bar | `Rectangle` | -| `cover_img_width` | the width of the cover image (`image` feature only) | `5` | -| `cover_img_length` | the length of the cover image (`image` feature only) | `9` | -| `cover_img_scale` | the scale of the cover image (`image` feature only) | `1.0` | -| `seek_duration_secs` | the duration (in seconds) to seek when using `SeekForward` and `SeekBackward` commands | `5` | -| `sort_artist_albums_by_type` | sort albums on artist's pages by type, i.e. album or single | `false` | +| Option | Description | Default | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | +| `client_id` | user-provided client's ID (required for [Spotify Connect feature](https://github.com/aome510/spotify-player#spotify-connect)) | `None` | +| `client_id_command` | a shell command that prints user client ID to stdout (overrides `client_id`) | `None` | +| `login_redirect_uri` | the redirect URI for authenticating the application | `http://127.0.0.1:8989/login` | +| `client_port` | the port that the application's client is running on to handle CLI commands | `8080` | +| `tracks_playback_limit` | the limit for the number of tracks played in a **tracks** playback | `50` | +| `playback_format` | the format of the text in the playback's window | `{status} {track} โ€ข {artists} {liked}\n{album}\n{metadata}` | +| `playback_metadata_fields` | list of ordered metadata fields to display in the playback UI's `{metadata}` section. Possible values: `"repeat"`, `"shuffle"`, `"volume"`, `"device"` | `["repeat", "shuffle", "volume", "device"]` | +| `notify_format` | the format of a notification (`notify` feature only) | `{ summary = "{track} โ€ข {artists}", body = "{album}" }` | +| `notify_timeout_in_secs` | the timeout (in seconds) of a notification (`notify` feature only) | `0` (no timeout) | +| `player_event_hook_command` | the hook command executed when there is a new player event | `None` | +| `ap_port` | the application's Spotify session connection port | `None` | +| `proxy` | the application's Spotify session connection proxy | `None` | +| `theme` | the application's theme | `default` | +| `app_refresh_duration_in_ms` | the duration (in ms) between two consecutive application refreshes | `32` | +| `playback_refresh_duration_in_ms` | the duration (in ms) between two consecutive playback refreshes | `0` | +| `page_size_in_rows` | a page's size expressed as a number of rows (for page-navigation commands) | `20` | +| `enable_media_control` | enable application media control support (`media-control` feature only) | `true` (Linux), `false` (Windows and MacOS) | +| `enable_streaming` | enable streaming (`streaming` feature only) | `Always` | +| `enable_notify` | enable notification (`notify` feature only) | `true` | +| `enable_cover_image_cache` | store album's cover images in the cache folder | `true` | +| `notify_streaming_only` | only send notification when streaming is enabled (`streaming` and `notify` feature only) | `false` | +| `default_device` | the default device to connect to on startup if no playing device found | `spotify-player` | +| `play_icon` | the icon to indicate playing state of a Spotify item | `โ–ถ` | +| `pause_icon` | the icon to indicate pause state of a Spotify item | `โ–Œโ–Œ` | +| `liked_icon` | the icon to indicate the liked state of a song | `โ™ฅ` | +| `border_type` | the type of the application's borders | `Plain` | +| `progress_bar_type` | the type of the playback progress bar | `Rectangle` | +| `cover_img_width` | the width of the cover image (`image` feature only) | `5` | +| `cover_img_length` | the length of the cover image (`image` feature only) | `9` | +| `cover_img_scale` | the scale of the cover image (`image` feature only) | `1.0` | +| `cover_img_pixels` | the amount of pixels per side of the cover image (`image` and `pixelate` feature only) | `16` | +| `seek_duration_secs` | the duration (in seconds) to seek when using `SeekForward` and `SeekBackward` commands | `5` | +| `sort_artist_albums_by_type` | sort albums on artist's pages by type, i.e. album or single | `false` | ### Notes -- By default, `spotify_player` uses the official Spotify Web app's client (`client_id = 65b708073fc0480ea92a077233ca87bd`) -- It's recommended to specify [your own Client ID](https://developer.spotify.com/documentation/web-api/concepts/apps) to avoid possible rate limits and to allow a full [Spotify connect](https://www.spotify.com/us/connect/) support. An error such as `Failed to initialize the Spotify data` can appear if the `client_id` is invalid. - `ap_port` and `proxy` are [Librespot's session configurations](https://github.com/librespot-org/librespot/wiki/Behind-web-proxy). By default, `spotify_player` doesn't set those values, which means the Librespot library will fallback to use its default options. - Positive-value `app_refresh_duration_in_ms` is used to refresh the playback periodically. This can result in hitting a Spotify rate limit if the application is running for a long time. - To prevent the rate limit, `spotify_player` sets `playback_refresh_duration_in_ms=0` by default and makes additional API calls when there is an event or a command triggering a playback update. diff --git a/examples/app.toml b/examples/app.toml index 2818b38f..ecde1858 100644 --- a/examples/app.toml +++ b/examples/app.toml @@ -1,9 +1,9 @@ theme = "default" -client_id = "65b708073fc0480ea92a077233ca87bd" login_redirect_uri = "http://127.0.0.1:8989/login" client_port = 8080 tracks_playback_limit = 50 playback_format = "{status} {track} โ€ข {artists}\n{album}\n{metadata}" +playback_metadata_fields = ["repeat", "shuffle", "volume", "device"] notify_format = { summary = "{track} โ€ข {artists}", body = "{album}" } notify_timeout_in_secs = 0 app_refresh_duration_in_ms = 32 @@ -20,6 +20,7 @@ pause_icon = "โ–Œโ–Œ" liked_icon = "โ™ฅ" cover_img_length = 9 cover_img_width = 5 +cover_img_pixels = 16 seek_duration_secs = 5 [device] diff --git a/lyric_finder/Cargo.toml b/lyric_finder/Cargo.toml index 8105a400..bf47c037 100644 --- a/lyric_finder/Cargo.toml +++ b/lyric_finder/Cargo.toml @@ -14,7 +14,7 @@ rustls-tls = ["reqwest/rustls-tls"] native-tls = ["reqwest/native-tls"] [dependencies] -reqwest = { version = "0.12.20", features = ["json", "native-tls-alpn", "http2"], default-features = false } +reqwest = { version = "0.12.22", features = ["json", "native-tls-alpn", "http2"], default-features = false } anyhow = "1.0.98" serde = { version = "1.0.219", features = ["derive"] } html5ever = "=0.27.0" @@ -22,7 +22,7 @@ markup5ever_rcdom = "0.3.0" log = "0.4.27" [dev-dependencies] -tokio = { version = "1.45.1", features = ["rt", "rt-multi-thread", "macros"] } +tokio = { version = "1.47.0", features = ["rt", "rt-multi-thread", "macros"] } env_logger = { version = "0.11.8", default-features = false } [[example]] diff --git a/playlist_generator.tcl b/playlist_generator.tcl new file mode 100755 index 00000000..e563889b --- /dev/null +++ b/playlist_generator.tcl @@ -0,0 +1,80 @@ +#!/usr/bin/env tclsh + +proc parse_music_input {input_text} { + set lines [split [string trim $input_text] "\n"] + + set tracks {} + set albums {} + + foreach line $lines { + if {[string match "๐Ÿ’ฟ*" $line]} { + # Remove ๐Ÿ’ฟ and clean up - this is an album + regsub {^๐Ÿ’ฟ\s*} $line {} content + set content [string trim $content] + lappend albums "\"$content\"" + } elseif {[string match "๐ŸŽถ*" $line]} { + # Remove ๐ŸŽถ and clean up - this is a track + regsub {^๐ŸŽถ\s*} $line {} content + set content [string trim $content] + lappend tracks "\"$content\"" + } + } + + return [list "" $tracks $albums] +} + +proc create_playlist_and_generate_commands {playlist_name tracks albums} { + set today [clock format [clock seconds] -format "%Y-%m-%d"] + set safe_playlist_name "test-afera-$today" + + # Execute playlist creation command + set create_cmd "target/release/spotify_player playlist new \"$safe_playlist_name\"" + puts "Executing: $create_cmd" + + if {[catch {exec {*}[split $create_cmd]} result]} { + puts "Error creating playlist: $result" + return + } + + puts $result + + # Extract playlist ID from output + if {[regexp {'spotify:playlist:([^']+)'} $result -> playlist_id]} { + puts "" + puts "# Using playlist ID: $playlist_id" + puts "" + + # Generate album commands first (๐Ÿ’ฟ lines) + if {[llength $albums] > 0} { + foreach album $albums { + puts "target/release/spotify_player playlist edit --playlist-id \"$playlist_id\" --album-name $album add" + } + } + + # Generate track commands (๐ŸŽถ lines) + if {[llength $tracks] > 0} { + foreach track $tracks { + puts "target/release/spotify_player playlist edit --playlist-id \"$playlist_id\" --track-name $track add" + } + } + } else { + puts "Could not extract playlist ID from output: $result" + } +} + +proc main {} { + global argc argv + + if {$argc > 0} { + # Read from command line argument + set input_text [lindex $argv 0] + } else { + # Read from stdin + set input_text [read stdin] + } + + lassign [parse_music_input $input_text] playlist_name tracks albums + create_playlist_and_generate_commands $playlist_name $tracks $albums +} + +main \ No newline at end of file diff --git a/playlist_scraper.tcl b/playlist_scraper.tcl new file mode 100755 index 00000000..30a813b4 --- /dev/null +++ b/playlist_scraper.tcl @@ -0,0 +1,77 @@ +#!/usr/bin/env tclsh + +package require http +package require tls + +# Configure TLS for HTTPS requests +http::register https 443 ::tls::socket + +proc scrape_afera_playlists {url} { + # Fetch the webpage + set token [http::geturl $url -timeout 10000] + set status [http::status $token] + set ncode [http::ncode $token] + + if {$status ne "ok" || $ncode != 200} { + puts stderr "Error fetching webpage: $status (HTTP $ncode)" + return + } + + set html [http::data $token] + http::cleanup $token + + # Extract albums and tracks using CSS class selectors + extract_albums $html + extract_tracks $html +} + +proc extract_albums {html} { + # Look for

with PลYTA TYGODNIA + # and extract artist - album from inside + set pattern {

\s*PลYTA TYGODNIA\s*([^<]+)} + + set matches [regexp -all -inline $pattern $html] + + for {set i 0} {$i < [llength $matches]} {incr i 2} { + set album_info [lindex $matches [expr $i + 1]] + set album_info [string trim $album_info] + + # Clean up any HTML entities + set album_info [regsub -all {"} $album_info "\""] + set album_info [regsub -all {&} $album_info "&"] + set album_info [regsub -all {'} $album_info "'"] + + puts "๐Ÿ’ฟ $album_info" + } +} + +proc extract_tracks {html} { + # Look for

and extract artist - track + set pattern {

([^<]+)

} + + set matches [regexp -all -inline $pattern $html] + + for {set i 0} {$i < [llength $matches]} {incr i 2} { + set track_info [lindex $matches [expr $i + 1]] + set track_info [string trim $track_info] + + # Clean up any HTML entities + set track_info [regsub -all {"} $track_info "\""] + set track_info [regsub -all {&} $track_info "&"] + set track_info [regsub -all {'} $track_info "'"] + + puts "๐ŸŽถ $track_info" + } +} + +# Main execution +if {$argc < 1} { + puts "Usage: $argv0 " + puts "Example: $argv0 https://www.afera.com.pl/muzyka" + exit 1 +} + +set url [lindex $argv 0] + +# Run the scraper +scrape_afera_playlists $url \ No newline at end of file diff --git a/spotify_player/Cargo.toml b/spotify_player/Cargo.toml index 443ebfaf..ca6b5545 100644 --- a/spotify_player/Cargo.toml +++ b/spotify_player/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spotify_player" -version = "0.20.6" +version = "0.20.7" authors = ["Thang Pham "] edition = "2021" license = "MIT" @@ -11,59 +11,59 @@ readme = "../README.md" [dependencies] anyhow = "1.0.98" -clap = { version = "4.5.40", features = ["derive", "string"] } -config_parser2 = "0.1.5" +clap = { version = "4.5.41", features = ["derive", "string"] } +config_parser2 = "0.1.6" crossterm = "0.29.0" dirs-next = "2.0.0" -librespot-connect = { version = "0.6.0", optional = true } -librespot-core = "0.6.0" -librespot-oauth = "0.6.0" -librespot-playback = { version = "0.6.0", optional = true } -librespot-metadata = "0.6.0" +librespot-connect = { version = "0.7.0", optional = true } +librespot-core = "0.7.0" +librespot-oauth = "0.7.0" +librespot-playback = { version = "0.7.0", optional = true } +librespot-metadata = "0.7.0" log = "0.4.27" chrono = "0.4.41" -reqwest = { version = "0.12.20", features = ["json"] } -rspotify = "0.14.0" +reqwest = { version = "0.12.22", features = ["json"] } +rspotify = {version = "0.15.0", features = ["cli"] } serde = { version = "1.0.219", features = ["derive"] } -tokio = { version = "1.45.1", features = [ +tokio = { version = "1.47.0", features = [ "rt", "rt-multi-thread", "macros", "time", ] } -toml = "0.8.23" +toml = "0.9.2" ratatui = { version = "0.29.0" } -rand = "0.9.1" +rand = "0.9.2" maybe-async = "0.2.10" async-trait = "0.1.88" parking_lot = "0.12.4" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } backtrace = "0.3.75" -souvlaki = { version = "0.8.2", optional = true } +souvlaki = { version = "0.8.3", optional = true } viuer = { version = "0.9.2", optional = true } image = { version = "0.25.6", optional = true } notify-rust = { version = "4.11.7", optional = true, default-features = false, features = [ "d", ] } flume = "0.11.1" -serde_json = "1.0.140" +serde_json = "1.0.141" regex = "1.11.1" daemonize = { version = "0.5.0", optional = true } ttl_cache = "0.5.1" -clap_complete = "4.5.54" -which = "7.0.3" +clap_complete = "4.5.55" +which = "8.0.0" fuzzy-matcher = { version = "0.3.7", optional = true } html-escape = "0.2.13" -rustls = { version = "0.23.28", default-features = false, features = ["ring"] } +rustls = { version = "0.23.31", default-features = false, features = ["ring"] } unicode-bidi = "0.3.18" [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.winit] -version = "0.30.11" +version = "0.30.12" optional = true [target.'cfg(target_os = "windows")'.dependencies] -clipboard-win = "5.4.0" +clipboard-win = "5.4.1" [target.'cfg(target_os = "windows")'.dependencies.windows] version = "0.58.0" @@ -88,6 +88,7 @@ streaming = ["librespot-playback", "librespot-connect"] media-control = ["souvlaki", "winit", "windows"] image = ["viuer", "dep:image"] sixel = ["image", "viuer/sixel"] +pixelate = ["image"] notify = ["notify-rust"] daemon = ["daemonize", "streaming"] fzf = ["fuzzy-matcher"] diff --git a/spotify_player/src/auth.rs b/spotify_player/src/auth.rs index e5b2d797..6ae9ab88 100644 --- a/spotify_player/src/auth.rs +++ b/spotify_player/src/auth.rs @@ -1,38 +1,35 @@ +use crate::config; use anyhow::Result; use librespot_core::{authentication::Credentials, cache::Cache, config::SessionConfig, Session}; -use librespot_oauth::get_access_token; - -use crate::config; +use librespot_oauth::OAuthClientBuilder; pub const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; -// based on https://github.com/librespot-org/librespot/blob/f96f36c064795011f9fee912291eecb1aa46fff6/src/main.rs#L173 +// based on https://developer.spotify.com/documentation/web-api/concepts/scopes#list-of-scopes const OAUTH_SCOPES: &[&str] = &[ + // Spotify Connect + "user-read-playback-state", + "user-modify-playback-state", + "user-read-currently-playing", + // Playback "app-remote-control", - "playlist-modify", + "streaming", + // Playlists + "playlist-read-private", + "playlist-read-collaborative", "playlist-modify-private", "playlist-modify-public", - "playlist-read", - "playlist-read-collaborative", - "playlist-read-private", - "streaming", - "ugc-image-upload", + // Follow "user-follow-modify", "user-follow-read", + // Listening History + "user-read-playback-position", + "user-top-read", + "user-read-recently-played", + // Library "user-library-modify", "user-library-read", - "user-modify", - "user-modify-playback-state", - "user-modify-private", + // Users "user-personalized", - "user-read-birthdate", - "user-read-currently-playing", - "user-read-email", - "user-read-play-history", - "user-read-playback-position", - "user-read-playback-state", - "user-read-private", - "user-read-recently-played", - "user-top-read", ]; #[derive(Clone)] @@ -98,12 +95,17 @@ pub fn get_creds(auth_config: &AuthConfig, reauth: bool, use_cached: bool) -> Re let msg = "No cached credentials found, please authenticate the application first."; if reauth { eprintln!("{msg}"); - get_access_token( + + let client_builder = OAuthClientBuilder::new( SPOTIFY_CLIENT_ID, &auth_config.login_redirect_uri, OAUTH_SCOPES.to_vec(), ) - .map(|t| Credentials::with_access_token(t.access_token))? + .open_in_browser(); + let oauth_client = client_builder.build()?; + oauth_client + .get_access_token() + .map(|t| Credentials::with_access_token(t.access_token))? } else { anyhow::bail!(msg); } diff --git a/spotify_player/src/cli/client.rs b/spotify_player/src/cli/client.rs index fa91bb92..6373e049 100644 --- a/spotify_player/src/cli/client.rs +++ b/spotify_player/src/cli/client.rs @@ -13,7 +13,7 @@ use tracing::Instrument; use crate::{ cli::Request, - client::{Client, PlayerRequest}, + client::{AppClient, PlayerRequest}, config::get_cache_folder_path, state::{ AlbumId, ArtistId, Context, ContextId, Id, PlayableId, Playback, PlaybackMetadata, @@ -23,11 +23,11 @@ use crate::{ use rspotify::prelude::{BaseClient, OAuthClient}; use super::{ - Command, Deserialize, GetRequest, IdOrName, ItemId, ItemType, Key, PlaylistCommand, Response, + Command, Deserialize, EditAction, GetRequest, IdOrName, ItemId, ItemType, Key, PlaylistCommand, Response, Serialize, MAX_REQUEST_SIZE, }; -pub async fn start_socket(client: Client, socket: UdpSocket, state: Option) { +pub async fn start_socket(client: AppClient, socket: UdpSocket, state: Option) { let mut buf = [0; MAX_REQUEST_SIZE]; loop { @@ -92,7 +92,7 @@ async fn send_response( } async fn current_playback( - client: &Client, + client: &AppClient, state: Option<&SharedState>, ) -> Result> { // get current playback from the application's state, if exists, or by making an API request @@ -106,7 +106,7 @@ async fn current_playback( } async fn handle_socket_request( - client: &Client, + client: &AppClient, state: Option<&SharedState>, request: super::Request, ) -> Result> { @@ -178,7 +178,7 @@ async fn handle_socket_request( } async fn handle_get_key_request( - client: &Client, + client: &AppClient, state: Option<&SharedState>, key: Key, ) -> Result> { @@ -219,7 +219,7 @@ async fn handle_get_key_request( } /// Get a Spotify item's ID from its `IdOrName` representation -async fn get_spotify_id(client: &Client, typ: ItemType, id_or_name: IdOrName) -> Result { +async fn get_spotify_id(client: &AppClient, typ: ItemType, id_or_name: IdOrName) -> Result { // For `IdOrName::Name`, we search for the first item matching the name and return its Spotify id. // The item's id is then used to retrieve the item's data. @@ -304,7 +304,7 @@ async fn get_spotify_id(client: &Client, typ: ItemType, id_or_name: IdOrName) -> } async fn handle_get_item_request( - client: &Client, + client: &AppClient, item_type: ItemType, id_or_name: IdOrName, ) -> Result> { @@ -317,14 +317,14 @@ async fn handle_get_item_request( }) } -async fn handle_search_request(client: &Client, query: String) -> Result> { +async fn handle_search_request(client: &AppClient, query: String) -> Result> { let search_result = client.search(&query).await?; Ok(serde_json::to_vec(&search_result)?) } async fn handle_playback_request( - client: &Client, + client: &AppClient, state: Option<&SharedState>, command: Command, ) -> Result<()> { @@ -465,7 +465,7 @@ async fn handle_playback_request( Ok(()) } -async fn handle_playlist_request(client: &Client, command: PlaylistCommand) -> Result { +async fn handle_playlist_request(client: &AppClient, command: PlaylistCommand) -> Result { let uid = client.current_user().await?.id; match command { @@ -594,6 +594,150 @@ async fn handle_playlist_request(client: &Client, command: PlaylistCommand) -> R Ok(result) } + PlaylistCommand::Edit { + playlist_id_or_name, + action, + track_id, + track_name, + album_id, + album_name, + } => { + let playlist_id = match get_spotify_id(client, ItemType::Playlist, playlist_id_or_name).await? { + ItemId::Playlist(id) => id, + _ => unreachable!(), + }; + + match action { + EditAction::Add => { + if track_id.is_some() || track_name.is_some() { + let track_id = if let Some(track_id) = track_id { + track_id + } else if let Some(track_name) = track_name { + match get_spotify_id(client, ItemType::Track, IdOrName::Name(track_name)).await? { + ItemId::Track(id) => id, + _ => unreachable!(), + } + } else { + unreachable!() + }; + + client + .playlist_add_items( + playlist_id.clone(), + [rspotify::model::PlayableId::Track(track_id.as_ref())], + None, + ) + .await?; + Ok(format!( + "Track '{}' added to playlist '{}'", + track_id.id(), + playlist_id.id() + )) + } else if album_id.is_some() || album_name.is_some() { + let album_id = if let Some(album_id) = album_id { + album_id + } else if let Some(album_name) = album_name { + match get_spotify_id(client, ItemType::Album, IdOrName::Name(album_name)).await? { + ItemId::Album(id) => id, + _ => unreachable!(), + } + } else { + unreachable!() + }; + + let album = client.album(album_id.clone(), None).await?; + let track_count = album.tracks.items.len(); + let track_ids: Vec = album + .tracks + .items + .into_iter() + .filter_map(|t| t.id.map(|id| rspotify::model::PlayableId::Track(id))) + .collect(); + + if !track_ids.is_empty() { + client + .playlist_add_items(playlist_id.clone(), track_ids, None) + .await?; + } + + Ok(format!( + "Album '{}' ({} tracks) added to playlist '{}'", + album.name, + track_count, + playlist_id.id() + )) + } else { + anyhow::bail!("Either track or album must be provided") + } + } + EditAction::Delete => { + if track_id.is_some() || track_name.is_some() { + let track_id = if let Some(track_id) = track_id { + track_id + } else if let Some(track_name) = track_name { + match get_spotify_id(client, ItemType::Track, IdOrName::Name(track_name)).await? { + ItemId::Track(id) => id, + _ => unreachable!(), + } + } else { + unreachable!() + }; + + client + .playlist_remove_all_occurrences_of_items( + playlist_id.clone(), + [rspotify::model::PlayableId::Track(track_id.as_ref())], + None, + ) + .await?; + Ok(format!( + "Track '{}' removed from playlist '{}'", + track_id.id(), + playlist_id.id() + )) + } else if album_id.is_some() || album_name.is_some() { + let album_id = if let Some(album_id) = album_id { + album_id + } else if let Some(album_name) = album_name { + match get_spotify_id(client, ItemType::Album, IdOrName::Name(album_name)).await? { + ItemId::Album(id) => id, + _ => unreachable!(), + } + } else { + unreachable!() + }; + + let album = client.album(album_id.clone(), None).await?; + let track_count = album.tracks.items.len(); + let track_ids: Vec = album + .tracks + .items + .into_iter() + .filter_map(|t| t.id.map(|id| rspotify::model::PlayableId::Track(id))) + .collect(); + + if !track_ids.is_empty() { + client + .playlist_remove_all_occurrences_of_items( + playlist_id.clone(), + track_ids, + None + ) + .await?; + } + + Ok(format!( + "Album '{}' ({} tracks) removed from playlist '{}'", + album.name, + track_count, + playlist_id.id() + )) + } else { + anyhow::bail!("Either track or album must be provided") + } + } + } + } } } @@ -606,7 +750,7 @@ const TRACK_BUFFER_CAP: usize = 100; /// The state of `import_from` playlist is stored into a cache file to add/delete the differed tracks between /// subsequent imports of the same two playlists. async fn playlist_import( - client: &Client, + client: &AppClient, import_from: PlaylistId<'static>, import_to: PlaylistId<'static>, delete: bool, diff --git a/spotify_player/src/cli/commands.rs b/spotify_player/src/cli/commands.rs index 2e5e6b4e..b0af6d41 100644 --- a/spotify_player/src/cli/commands.rs +++ b/spotify_player/src/cli/commands.rs @@ -88,6 +88,16 @@ fn add_id_or_name_group(cmd: Command) -> Command { ) } +fn add_playlist_id_or_name_group(cmd: Command) -> Command { + cmd.arg(Arg::new("playlist_id").long("playlist-id").help("Playlist ID")) + .arg(Arg::new("playlist_name").long("playlist-name").help("Playlist name")) + .group( + ArgGroup::new("playlist_id_or_name") + .args(["playlist_id", "playlist_name"]) + .required(true), + ) +} + pub fn init_playback_subcommand() -> Command { Command::new("playback") .about("Interact with the playback") @@ -207,4 +217,27 @@ pub fn init_playlist_subcommand() -> Command { .long("delete") .action(clap::ArgAction::SetTrue) .help("Deletes any previously imported tracks that are no longer in an imported playlist since last import."))) + .subcommand(add_playlist_id_or_name_group(Command::new("edit").about("Add or remove tracks or albums from a playlist.") + .arg(Arg::new("action") + .help("Action to perform") + .required(true) + .value_parser(["add", "delete"])) + .arg(Arg::new("track_id") + .long("track-id") + .short('t') + .help("Track ID to add or remove") + .value_parser(clap::builder::NonEmptyStringValueParser::new())) + .arg(Arg::new("track_name") + .long("track-name") + .help("Track name to add or remove") + .value_parser(clap::builder::NonEmptyStringValueParser::new())) + .arg(Arg::new("album_id") + .long("album-id") + .short('a') + .help("Album ID to add or remove") + .value_parser(clap::builder::NonEmptyStringValueParser::new())) + .arg(Arg::new("album_name") + .long("album-name") + .help("Album name to add or remove") + .value_parser(clap::builder::NonEmptyStringValueParser::new())))) } diff --git a/spotify_player/src/cli/handlers.rs b/spotify_player/src/cli/handlers.rs index 11ec3480..cf71a48d 100644 --- a/spotify_player/src/cli/handlers.rs +++ b/spotify_player/src/cli/handlers.rs @@ -1,8 +1,8 @@ use crate::{auth::AuthConfig, client}; use super::{ - config, init_cli, start_socket, Command, ContextType, GetRequest, IdOrName, ItemType, Key, - PlaylistCommand, PlaylistId, Request, Response, MAX_REQUEST_SIZE, + config, init_cli, start_socket, Command, ContextType, EditAction, GetRequest, IdOrName, ItemType, Key, + PlaylistCommand, PlaylistId, Request, Response, TrackId, AlbumId, MAX_REQUEST_SIZE, }; use anyhow::{Context, Result}; use clap::{ArgMatches, Id}; @@ -46,6 +46,26 @@ fn get_id_or_name(args: &ArgMatches) -> IdOrName { } } +fn get_playlist_id_or_name(args: &ArgMatches) -> IdOrName { + match args + .get_one::("playlist_id_or_name") + .expect("playlist_id_or_name group is required") + .as_str() + { + "playlist_name" => IdOrName::Name( + args.get_one::("playlist_name") + .expect("playlist_name should be specified") + .to_owned(), + ), + "playlist_id" => IdOrName::Id( + args.get_one::("playlist_id") + .expect("playlist_id should be specified") + .to_owned(), + ), + id => panic!("unknown playlist id: {id}"), + } +} + fn handle_get_subcommand(args: &ArgMatches) -> Request { let (cmd, args) = args.subcommand().expect("playback subcommand is required"); @@ -153,11 +173,12 @@ fn try_connect_to_client(socket: &UdpSocket, configs: &config::Configs) -> Resul // no running `spotify_player` instance found, // initialize a new client to handle the current CLI command - let auth_config = AuthConfig::new(configs)?; let rt = tokio::runtime::Runtime::new()?; // create a Spotify API client - let client = client::Client::new(auth_config); + let client = rt + .block_on(client::AppClient::new()) + .context("construct app client")?; rt.block_on(client.new_session(None, false)) .context("new session")?; @@ -314,6 +335,46 @@ fn handle_playlist_subcommand(args: &ArgMatches) -> Result { PlaylistCommand::Sync { id: pid, delete } } + "edit" => { + let playlist_id_or_name = get_playlist_id_or_name(args); + + let action_str = args + .get_one::("action") + .expect("action arg is required"); + + let action = match action_str.as_str() { + "add" => EditAction::Add, + "delete" => EditAction::Delete, + _ => unreachable!(), + }; + + let track_id = args + .get_one::("track_id") + .map(|s| TrackId::from_id(s.to_owned())) + .transpose()?; + + let track_name = args + .get_one::("track_name") + .map(|s| s.to_owned()); + + let album_id = args + .get_one::("album_id") + .map(|s| AlbumId::from_id(s.to_owned())) + .transpose()?; + + let album_name = args + .get_one::("album_name") + .map(|s| s.to_owned()); + + PlaylistCommand::Edit { + playlist_id_or_name, + action, + track_id, + track_name, + album_id, + album_name, + } + } _ => unreachable!(), }; diff --git a/spotify_player/src/cli/mod.rs b/spotify_player/src/cli/mod.rs index 3bee6516..5f1b49db 100644 --- a/spotify_player/src/cli/mod.rs +++ b/spotify_player/src/cli/mod.rs @@ -58,6 +58,12 @@ pub enum IdOrName { Name(String), } +#[derive(Debug, Serialize, Deserialize, clap::ValueEnum, Clone)] +pub enum EditAction { + Add, + Delete, +} + #[derive(Debug, Serialize, Deserialize)] pub enum PlaylistCommand { New { @@ -82,6 +88,14 @@ pub enum PlaylistCommand { id: Option>, delete: bool, }, + Edit { + playlist_id_or_name: IdOrName, + action: EditAction, + track_id: Option>, + track_name: Option, + album_id: Option>, + album_name: Option, + }, } #[derive(Debug, Serialize, Deserialize)] diff --git a/spotify_player/src/client/handlers.rs b/spotify_player/src/client/handlers.rs index 9dcdbe71..12423d50 100644 --- a/spotify_player/src/client/handlers.rs +++ b/spotify_player/src/client/handlers.rs @@ -19,7 +19,7 @@ struct PlayerEventHandlerState { /// starts the client's request handler pub async fn start_client_handler( state: SharedState, - client: super::Client, + client: super::AppClient, client_sub: flume::Receiver, ) { while let Ok(request) = client_sub.recv_async().await { diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs index ac0a752d..c6c058e1 100644 --- a/spotify_player/src/client/mod.rs +++ b/spotify_player/src/client/mod.rs @@ -41,15 +41,16 @@ const PLAYBACK_TYPES: [&rspotify::model::AdditionalType; 2] = [ /// The application's Spotify client #[derive(Clone)] -pub struct Client { +pub struct AppClient { http: reqwest::Client, spotify: Arc, auth_config: AuthConfig, + user_client: Option, #[cfg(feature = "streaming")] - stream_conn: Arc>>, + stream_conn: Arc>>, } -impl Deref for Client { +impl Deref for AppClient { type Target = spotify::Spotify; fn deref(&self) -> &Self::Target { self.spotify.as_ref() @@ -60,17 +61,49 @@ fn market_query() -> Query<'static> { Query::from([("market", "from_token")]) } -impl Client { +impl AppClient { /// Construct a new client - pub fn new(auth_config: AuthConfig) -> Self { - Self { + pub async fn new() -> Result { + let configs = config::get_config(); + let auth_config = AuthConfig::new(configs)?; + + // Construct user-provided client. + // This custom client is needed for Spotify Connect integration because the Spotify client (`AppConfig::spotify`), + // which `spotify-player` uses to retrieve Spotify data, doesn't have access to user available devices + let mut user_client = configs.app_config.get_user_client_id()?.clone().map(|id| { + let creds = rspotify::Credentials { id, secret: None }; + let oauth = rspotify::OAuth { + scopes: rspotify::scopes!("user-read-playback-state"), + redirect_uri: configs.app_config.login_redirect_uri.clone(), + ..Default::default() + }; + let config = rspotify::Config { + token_cached: true, + cache_path: configs.cache_folder.join("user_client_token.json"), + ..Default::default() + }; + rspotify::AuthCodePkceSpotify::with_config(creds, oauth, config) + }); + + if let Some(client) = &mut user_client { + let url = client + .get_authorize_url(None) + .context("get authorize URL for user-provided client")?; + client + .prompt_for_token(&url) + .await + .context("get token for user-provided client")?; + } + + Ok(Self { spotify: Arc::new(spotify::Spotify::new()), http: reqwest::Client::new(), auth_config, + user_client, #[cfg(feature = "streaming")] stream_conn: Arc::new(Mutex::new(None)), - } + }) } /// Initialize the application's playback upon creating a new session or during startup @@ -619,16 +652,15 @@ impl Client { } /// Get user available devices - // This is a custom API to replace `rspotify::device` API to support Spotify Connect feature pub async fn available_devices(&self) -> Result> { - Ok(self - .http_get::( - &format!("{SPOTIFY_API_ENDPOINT}/me/player/devices"), - &Query::new(), - true, - ) - .await? - .devices) + match &self.user_client { + None => { + tracing::warn!("User-provided client integration is not enabled, no device found."); + tracing::warn!("Please make sure you setup Spotify Connect as described in https://github.com/aome510/spotify-player#spotify-connect."); + Ok(vec![]) + } + Some(client) => Ok(client.device().await?), + } } pub fn update_playback(&self, state: &SharedState) { @@ -674,13 +706,7 @@ impl Client { /// Find an available device. If found, return the device's ID. async fn find_available_device(&self) -> Result> { let devices = self.available_devices().await?; - - if devices.is_empty() { - tracing::warn!("No device found. Please make sure you already setup Spotify Connect \ - support as described in https://github.com/aome510/spotify-player#spotify-connect."); - } else { - tracing::info!("Available devices: {devices:?}"); - } + tracing::info!("Available devices: {devices:?}"); // if there is an active device, return it if let Some(d) = devices.iter().find(|d| d.is_active) { @@ -781,7 +807,6 @@ impl Client { .http_get::>( &format!("{SPOTIFY_API_ENDPOINT}/me/playlists"), &Query::from([("limit", "50")]), - false, ) .await?; // let first_page = self @@ -810,7 +835,7 @@ impl Client { let mut maybe_next = first_page.next; while let Some(url) = maybe_next { let mut next_page = self - .http_get::(&url, &Query::new(), false) + .http_get::(&url, &Query::new()) .await? .artists; artists.append(&mut next_page.items); @@ -879,7 +904,7 @@ impl Client { .into_iter() .filter_map(Album::try_from_simplified_album) .collect(); - Ok(Client::process_artist_albums(albums)) + Ok(AppClient::process_artist_albums(albums)) } /// Start a playback @@ -1309,7 +1334,6 @@ impl Client { .http_get::( &format!("{SPOTIFY_API_ENDPOINT}/playlists/{}", playlist_id.id()), &market_query(), - false, ) .await?; @@ -1441,12 +1465,7 @@ impl Client { } /// Make a GET HTTP request to the Spotify server - async fn http_get( - &self, - url: &str, - payload: &Query<'_>, - use_user_client_id: bool, - ) -> Result + async fn http_get(&self, url: &str, payload: &Query<'_>) -> Result where T: serde::de::DeserializeOwned, { @@ -1462,13 +1481,7 @@ impl Client { .replace("\"name\":null", "\"name\":\"\"") } - let access_token = if use_user_client_id { - self.access_token_from_user_client_id().await - } else { - self.access_token().await - } - .context("get access token")?; - + let access_token = self.access_token().await?; tracing::debug!("{access_token} {url}"); let response = self @@ -1507,7 +1520,7 @@ impl Client { while let Some(url) = maybe_next { let mut next_page = self - .http_get::>(&url, payload, false) + .http_get::>(&url, payload) .await?; if next_page.items.is_empty() { break; @@ -1530,7 +1543,7 @@ impl Client { let mut maybe_next = first_page.next; while let Some(url) = maybe_next { let mut next_page = self - .http_get::>(&url, &Query::new(), false) + .http_get::>(&url, &Query::new()) .await?; items.append(&mut next_page.items); maybe_next = next_page.next; @@ -1663,8 +1676,19 @@ impl Client { #[cfg(feature = "image")] if !state.data.read().caches.images.contains_key(url) { let bytes = self.retrieve_image(url, &path, false).await?; + + #[cfg(not(feature = "pixelate"))] let image = image::load_from_memory(&bytes).context("Failed to load image from memory")?; + #[cfg(feature = "pixelate")] + let mut image = + image::load_from_memory(&bytes).context("Failed to load image from memory")?; + + #[cfg(feature = "pixelate")] + { + Self::pixelate_image(&mut image); + } + state .data .write() @@ -1833,6 +1857,17 @@ impl Client { Ok(bytes.to_vec()) } + #[cfg(feature = "pixelate")] + fn pixelate_image(image: &mut image::DynamicImage) { + let pixels = config::get_config().app_config.cover_img_pixels; + let pixelated_image = image.resize(pixels, pixels, image::imageops::FilterType::Nearest); + *image = pixelated_image.resize( + image.width(), + image.height(), + image::imageops::FilterType::Nearest, + ); + } + /// Process a list of albums, which includes /// - sort albums by the release date /// - sort albums by the type if `sort_artist_albums_by_type` config is enabled diff --git a/spotify_player/src/client/spotify.rs b/spotify_player/src/client/spotify.rs index 978be1e4..84276c11 100644 --- a/spotify_player/src/client/spotify.rs +++ b/spotify_player/src/client/spotify.rs @@ -9,7 +9,7 @@ use rspotify::{ }; use std::{fmt, sync::Arc}; -use crate::{auth::SPOTIFY_CLIENT_ID, config, token}; +use crate::token; #[derive(Clone, Default)] /// A Spotify client to interact with Spotify API server @@ -19,12 +19,6 @@ pub struct Spotify { config: Config, token: Arc>>, http: HttpClient, - /// User-provided client ID - /// - /// This client ID is mainly used to support Spotify Connect feature - /// because Spotify client ID doesn't have access to user available devices - /// () - user_client_id: String, pub(crate) session: Arc>>, } @@ -52,10 +46,6 @@ impl Spotify { }, token: Arc::new(Mutex::new(None)), http: HttpClient::default(), - user_client_id: config::get_config() - .app_config - .get_client_id() - .expect("get client_id"), session: Arc::new(tokio::sync::Mutex::new(None)), } } @@ -87,14 +77,6 @@ impl Spotify { )), } } - - /// Get a Spotify access token based on a user-provided client ID - // TODO: implement caching - pub async fn access_token_from_user_client_id(&self) -> Result { - let session = self.session().await; - let token = token::get_token_librespot(&session, &self.user_client_id).await?; - Ok(token.access_token) - } } // TODO: remove the below uses of `maybe_async` crate once @@ -127,7 +109,7 @@ impl BaseClient for Spotify { return Ok(old_token); } - match token::get_token_rspotify(&session, SPOTIFY_CLIENT_ID).await { + match token::get_token_rspotify(&session).await { Ok(token) => Ok(Some(token)), Err(err) => { tracing::error!("Failed to get a new token: {err:#}"); diff --git a/spotify_player/src/command.rs b/spotify_player/src/command.rs index 38633441..1c365cd3 100644 --- a/spotify_player/src/command.rs +++ b/spotify_player/src/command.rs @@ -53,6 +53,7 @@ pub enum Command { ShowActionsOnSelectedItem, ShowActionsOnCurrentTrack, AddSelectedItemToQueue, + JumpToHighlightTrackInContext, BrowseUserPlaylists, BrowseUserFollowedArtists, @@ -330,6 +331,7 @@ impl Command { Self::ShowActionsOnSelectedItem => "open a popup showing actions on a selected item", Self::ShowActionsOnCurrentTrack => "open a popup showing actions on the current track", Self::AddSelectedItemToQueue => "add the selected item to queue", + Self::JumpToHighlightTrackInContext => "jump to the currently highlighted search result in the context", Self::FocusNextWindow => "focus the next focusable window (if any)", Self::FocusPreviousWindow => "focus the previous focusable window (if any)", Self::SwitchTheme => "open a popup for switching theme", diff --git a/spotify_player/src/config/keymap.rs b/spotify_player/src/config/keymap.rs index ac408967..4d8429af 100644 --- a/spotify_player/src/config/keymap.rs +++ b/spotify_player/src/config/keymap.rs @@ -107,6 +107,10 @@ impl Default for KeymapConfig { key_sequence: "Z".into(), command: Command::AddSelectedItemToQueue, }, + Keymap { + key_sequence: "C-g".into(), + command: Command::JumpToHighlightTrackInContext, + }, Keymap { key_sequence: "C-space".into(), command: Command::ShowActionsOnSelectedItem, diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs index 3602ebb1..428f5e68 100644 --- a/spotify_player/src/config/mod.rs +++ b/spotify_player/src/config/mod.rs @@ -50,7 +50,7 @@ impl Configs { /// Application configurations pub struct AppConfig { pub theme: String, - pub client_id: String, + pub client_id: Option, pub client_id_command: Option, pub client_port: u16, @@ -60,6 +60,7 @@ pub struct AppConfig { pub player_event_hook_command: Option, pub playback_format: String, + pub playback_metadata_fields: Vec, #[cfg(feature = "notify")] pub notify_format: NotifyFormat, #[cfg(feature = "notify")] @@ -94,11 +95,12 @@ pub struct AppConfig { pub cover_img_width: usize, #[cfg(feature = "image")] pub cover_img_scale: f32, + #[cfg(feature = "pixelate")] + pub cover_img_pixels: u32, #[cfg(feature = "media-control")] pub enable_media_control: bool, - #[cfg(feature = "streaming")] pub enable_streaming: StreamingType, #[cfg(feature = "notify")] @@ -255,8 +257,7 @@ impl Default for AppConfig { fn default() -> Self { Self { theme: "dracula".to_owned(), - // official Spotify web app's client id - client_id: "65b708073fc0480ea92a077233ca87bd".to_string(), + client_id: None, client_id_command: None, client_port: 8080, @@ -268,6 +269,12 @@ impl Default for AppConfig { playback_format: String::from( "{status} {track} โ€ข {artists} {liked}\n{album}\n{metadata}", ), + playback_metadata_fields: vec![ + "repeat".to_string(), + "shuffle".to_string(), + "volume".to_string(), + "device".to_string(), + ], #[cfg(feature = "notify")] notify_format: NotifyFormat { summary: String::from("{track} โ€ข {artists}"), @@ -300,6 +307,8 @@ impl Default for AppConfig { cover_img_width: 5, #[cfg(feature = "image")] cover_img_scale: 1.0, + #[cfg(feature = "pixelate")] + cover_img_pixels: 16, // Because of the "creating new window and stealing focus" behaviour // when running the media control event loop on startup, @@ -312,7 +321,6 @@ impl Default for AppConfig { #[cfg(all(unix, not(target_os = "macos")))] enable_media_control: true, - #[cfg(feature = "streaming")] enable_streaming: StreamingType::Always, #[cfg(feature = "notify")] @@ -424,9 +432,9 @@ impl AppConfig { } /// Returns stdout of `client_id_command` if set, otherwise it returns the the value of `client_id` - pub fn get_client_id(&self) -> Result { + pub fn get_user_client_id(&self) -> Result> { match self.client_id_command { - Some(ref cmd) => cmd.execute(None).map(|out| out.trim().into()), + Some(ref cmd) => cmd.execute(None).map(|out| Some(out.trim().to_string())), None => Ok(self.client_id.clone()), } } diff --git a/spotify_player/src/event/clipboard.rs b/spotify_player/src/event/clipboard.rs index fb06cfa3..665caa82 100644 --- a/spotify_player/src/event/clipboard.rs +++ b/spotify_player/src/event/clipboard.rs @@ -109,7 +109,7 @@ pub fn get_clipboard_provider() -> Box { #[cfg(not(target_os = "windows"))] { tracing::warn!("No clipboard provider found! Fallback to a NOP clipboard provider."); - return Box::new(NopProvider {}); + Box::new(NopProvider {}) } } } diff --git a/spotify_player/src/event/mod.rs b/spotify_player/src/event/mod.rs index 117cc896..4914c6fd 100644 --- a/spotify_player/src/event/mod.rs +++ b/spotify_player/src/event/mod.rs @@ -100,26 +100,6 @@ fn handle_key_event( let key: Key = event.into(); let mut ui = state.ui.lock(); - // Check if the key is a digit and handle count prefix - if let Key::None(KeyCode::Char(c)) = key { - if c.is_ascii_digit() { - let digit = c.to_digit(10).unwrap() as usize; - // If we have an existing count prefix, append the digit - // Otherwise, start a new count (but ignore leading zeros) - ui.count_prefix = match ui.count_prefix { - Some(count) => Some(count * 10 + digit), - None => { - if digit > 0 { - Some(digit) - } else { - None - } - } - }; - return Ok(()); - } - } - let mut key_sequence = ui.input_key_sequence.clone(); key_sequence.keys.push(key); @@ -163,10 +143,26 @@ fn handle_key_event( ui.input_key_sequence.keys = vec![]; ui.count_prefix = None; } else { - ui.input_key_sequence = key_sequence; - // If we didn't handle the key and it wasn't a digit, clear the count prefix - if !matches!(key, Key::None(KeyCode::Char(c)) if c.is_ascii_digit()) { - ui.count_prefix = None; + // update the count prefix if the key is a digit + match key { + Key::None(KeyCode::Char(c)) if c.is_ascii_digit() => { + let digit = c.to_digit(10).unwrap() as usize; + ui.input_key_sequence.keys = vec![]; + ui.count_prefix = match ui.count_prefix { + Some(count) => Some(count * 10 + digit), + None => { + if digit > 0 { + Some(digit) + } else { + None + } + } + }; + } + _ => { + ui.input_key_sequence = key_sequence; + ui.count_prefix = None; + } } } Ok(()) diff --git a/spotify_player/src/event/popup.rs b/spotify_player/src/event/popup.rs index 656afd9d..008d8dce 100644 --- a/spotify_player/src/event/popup.rs +++ b/spotify_player/src/event/popup.rs @@ -214,7 +214,7 @@ pub fn handle_key_sequence_for_popup( command, ui, &artist_uris, - rspotify::model::Type::Artist, + &rspotify::model::Type::Artist, ) } PopupState::UserSavedAlbumList(_) => { @@ -231,7 +231,7 @@ pub fn handle_key_sequence_for_popup( command, ui, &album_uris, - rspotify::model::Type::Album, + &rspotify::model::Type::Album, ) } PopupState::ThemeList(themes, _) => { @@ -379,7 +379,7 @@ fn handle_command_for_context_browsing_list_popup( command: Command, ui: &mut UIStateGuard, uris: &[String], - context_type: rspotify::model::Type, + context_type: &rspotify::model::Type, ) -> Result { handle_command_for_list_popup( command, diff --git a/spotify_player/src/event/window.rs b/spotify_player/src/event/window.rs index db7ade28..e9c5ad03 100644 --- a/spotify_player/src/event/window.rs +++ b/spotify_player/src/event/window.rs @@ -5,7 +5,7 @@ use crate::{ construct_album_actions, construct_artist_actions, construct_playlist_actions, construct_show_actions, }, - state::{Episode, Show, UIStateGuard}, + state::{Episode, MutableWindowState, Show, UIStateGuard}, }; use command::Action; use rand::Rng; @@ -337,6 +337,22 @@ fn handle_command_for_track_table_window( filtered_tracks[id].id.clone().into(), ))?; } + Command::JumpToHighlightTrackInContext => { + ui.popup = None; + let selected_track = filtered_tracks[id]; + let location = tracks + .iter() + .enumerate() + .find(|(_, track)| track.id == selected_track.id) + .unwrap(); + + // Move selection and change the offset so selection is at the top + ui.current_page_mut().select(location.0); + match ui.current_page_mut().focus_window_state_mut().unwrap() { + MutableWindowState::Table(table) => *table.offset_mut() = location.0, + _ => unreachable!("playlist context should be a table"), + } + } _ => return Ok(false), } Ok(true) diff --git a/spotify_player/src/main.rs b/spotify_player/src/main.rs index 3f65c5eb..b437990d 100644 --- a/spotify_player/src/main.rs +++ b/spotify_player/src/main.rs @@ -20,7 +20,7 @@ use std::io::Write; fn init_spotify( client_pub: &flume::Sender, - client: &client::Client, + client: &client::AppClient, state: &state::SharedState, ) -> Result<()> { client.initialize_playback(state); @@ -115,8 +115,9 @@ async fn start_app(state: &state::SharedState) -> Result<()> { } // create a Spotify API client - let auth_config = auth::AuthConfig::new(configs)?; - let client = client::Client::new(auth_config); + let client = client::AppClient::new() + .await + .context("construct app client")?; client .new_session(Some(state), true) .await diff --git a/spotify_player/src/media_control.rs b/spotify_player/src/media_control.rs index f084b859..07fca455 100644 --- a/spotify_player/src/media_control.rs +++ b/spotify_player/src/media_control.rs @@ -192,7 +192,7 @@ mod windows { ..Default::default() }; - if RegisterClassExW(&wnd_class) == 0 { + if RegisterClassExW(&raw const wnd_class) == 0 { return Err(format!( "Registering class failed: {}", Error::last_os_error() @@ -246,14 +246,14 @@ mod windows { pub fn pump_event_queue() -> bool { unsafe { let mut msg: MSG = std::mem::zeroed(); - let mut has_message = PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool(); + let mut has_message = PeekMessageW(&raw mut msg, None, 0, 0, PM_REMOVE).as_bool(); while msg.message != WM_QUIT && has_message { - if !IsDialogMessageW(GetAncestor(msg.hwnd, GA_ROOT), &msg).as_bool() { - let _ = TranslateMessage(&msg); - let _ = DispatchMessageW(&msg); + if !IsDialogMessageW(GetAncestor(msg.hwnd, GA_ROOT), &raw const msg).as_bool() { + let _ = TranslateMessage(&raw const msg); + let _ = DispatchMessageW(&raw const msg); } - has_message = PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool(); + has_message = PeekMessageW(&raw mut msg, None, 0, 0, PM_REMOVE).as_bool(); } msg.message == WM_QUIT diff --git a/spotify_player/src/state/model.rs b/spotify_player/src/state/model.rs index 73e38ff4..4f30c5be 100644 --- a/spotify_player/src/state/model.rs +++ b/spotify_player/src/state/model.rs @@ -1,3 +1,4 @@ +use crate::ui::utils::to_bidi_string; use crate::utils::map_join; use html_escape::decode_html_entities; pub use rspotify::model::{ @@ -5,8 +6,15 @@ pub use rspotify::model::{ }; use serde::{Deserialize, Serialize}; use std::borrow::Cow; -use std::fmt::Write; -use unicode_bidi::BidiInfo; +use std::fmt::{Display, Write}; + +/// A trait similar to Display but with bidirectional text support +pub trait BidiDisplay: Display { + fn to_bidi_string(&self) -> String { + let disp_str = self.to_string(); + to_bidi_string(&disp_str) + } +} #[derive(Serialize, Clone, Debug)] #[serde(untagged)] @@ -234,31 +242,27 @@ impl Context { Context::Album { ref album, ref tracks, - } => { - let album_length = play_time(tracks); - format!( - "{} | {} | {} songs | {}", - album.name, - album.release_date, - tracks.len(), - album_length, - ) - } + } => format!( + "{} | {} | {} songs | {}", + album.name, + album.release_date, + tracks.len(), + play_time(tracks), + ), Context::Playlist { ref playlist, tracks, - } => { - let playlist_length = play_time(tracks); - format!( - "{} | {} | {} songs | {}", - playlist.name, - playlist.owner.0, - tracks.len(), - playlist_length, - ) - } + } => format!( + "{} | {} | {} songs | {}", + playlist.name, + playlist.owner.0, + tracks.len(), + play_time(tracks), + ), Context::Artist { ref artist, .. } => artist.name.to_string(), - Context::Tracks { desc, tracks } => format!("{} | {} songs", desc, tracks.len()), + Context::Tracks { desc, tracks } => { + format!("{} | {} songs | {}", desc, tracks.len(), play_time(tracks)) + } Context::Show { ref show, ref episodes, @@ -421,6 +425,8 @@ impl std::fmt::Display for Track { } } +impl BidiDisplay for Track {} + impl Album { /// tries to convert from a `rspotify::model::SimplifiedAlbum` into `Album` pub fn try_from_simplified_album(album: rspotify::model::SimplifiedAlbum) -> Option { @@ -493,6 +499,8 @@ impl std::fmt::Display for Album { } } +impl BidiDisplay for Album {} + impl Artist { /// tries to convert from a `rspotify::model::SimplifiedArtist` into `Artist` pub fn try_from_simplified_artist(artist: rspotify::model::SimplifiedArtist) -> Option { @@ -529,6 +537,8 @@ fn from_simplified_artists_to_artists( .collect() } +impl BidiDisplay for Artist {} + impl From for Playlist { fn from(playlist: rspotify::model::SimplifiedPlaylist) -> Self { Self { @@ -574,6 +584,8 @@ impl std::fmt::Display for Playlist { } } +impl BidiDisplay for Playlist {} + impl From for Show { fn from(show: rspotify::model::SimplifiedShow) -> Self { Self { @@ -598,6 +610,8 @@ impl std::fmt::Display for Show { } } +impl BidiDisplay for Show {} + impl From for Episode { fn from(episode: rspotify::model::SimplifiedEpisode) -> Self { Self { @@ -640,6 +654,8 @@ impl std::fmt::Display for PlaylistFolder { } } +impl BidiDisplay for PlaylistFolder {} + impl std::fmt::Display for PlaylistFolderItem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -649,6 +665,8 @@ impl std::fmt::Display for PlaylistFolderItem { } } +impl BidiDisplay for PlaylistFolderItem {} + impl From for Category { fn from(c: rspotify::model::category::Category) -> Self { Self { @@ -739,19 +757,7 @@ impl From for Lyrics { l.start_time_ms.parse::().expect("invalid number"), ); - // Some songs use multiple languages and may contain a mix of rtl and ltr text. - // Therefore rtl formatting needs to be done on a per-line basis. - let bidi_info = BidiInfo::new(&l.words, None); - - let words = if bidi_info.has_rtl() && !bidi_info.paragraphs.is_empty() { - bidi_info - .reorder_line(&bidi_info.paragraphs[0], 0..l.words.len()) - .into_owned() - } else { - l.words - }; - - (t, words) + (t, to_bidi_string(&l.words)) }) .collect::>(); lines.sort_by_key(|l| l.0); diff --git a/spotify_player/src/state/ui/page.rs b/spotify_player/src/state/ui/page.rs index 8451ed31..cd8a401c 100644 --- a/spotify_player/src/state/ui/page.rs +++ b/spotify_player/src/state/ui/page.rs @@ -164,7 +164,7 @@ impl PageState { } /// The currently focused window state of the page. - pub fn focus_window_state_mut(&mut self) -> Option { + pub fn focus_window_state_mut(&mut self) -> Option> { match self { Self::Library { state: diff --git a/spotify_player/src/streaming.rs b/spotify_player/src/streaming.rs index 48b79ed0..d2cb6133 100644 --- a/spotify_player/src/streaming.rs +++ b/spotify_player/src/streaming.rs @@ -1,6 +1,6 @@ -use crate::{client::Client, config, state::SharedState}; +use crate::{client::AppClient, config, state::SharedState}; use anyhow::Context; -use librespot_connect::{config::ConnectConfig, spirc::Spirc}; +use librespot_connect::{ConnectConfig, Spirc}; use librespot_core::authentication::Credentials; use librespot_core::Session; use librespot_core::{config::DeviceType, spotify_id}; @@ -140,7 +140,7 @@ fn execute_player_event_hook_command( /// Create a new streaming connection pub async fn new_connection( - client: Client, + client: AppClient, state: SharedState, session: Session, creds: Credentials, @@ -156,17 +156,20 @@ pub async fn new_connection( let connect_config = ConnectConfig { name: device.name.clone(), device_type: device.device_type.parse::().unwrap_or_default(), - initial_volume: Some(volume), + initial_volume: volume, // non-configurable fields, use default values. // We may allow users to configure these fields in a future release - has_volume_ctrl: true, is_group: false, + disable_volume: false, + volume_steps: 64, }; tracing::info!("Application's connect configurations: {:?}", connect_config); - let mixer = Arc::new(mixer::softmixer::SoftMixer::open(MixerConfig::default())); + let mixer = Arc::new( + mixer::softmixer::SoftMixer::open(MixerConfig::default()).context("opening softmixer")?, + ); mixer.set_volume(volume); let backend = audio_backend::find(None).expect("should be able to find an audio backend"); diff --git a/spotify_player/src/token.rs b/spotify_player/src/token.rs index e5b0fe44..9b038b22 100644 --- a/spotify_player/src/token.rs +++ b/spotify_player/src/token.rs @@ -1,74 +1,36 @@ use std::collections::HashSet; use anyhow::Result; -use chrono::{Duration, Utc}; use librespot_core::session::Session; -const TIMEOUT_IN_SECS: u64 = 5; +const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); -/// The application authentication token's permission scopes -const SCOPES: [&str; 15] = [ - "user-read-recently-played", - "user-top-read", - "user-read-playback-position", - "user-read-playback-state", - "user-modify-playback-state", - "user-read-currently-playing", - "streaming", - "playlist-read-private", - "playlist-modify-private", - "playlist-modify-public", - "playlist-read-collaborative", - "user-follow-read", - "user-follow-modify", - "user-library-read", - "user-library-modify", -]; - -pub async fn get_token_librespot( - session: &Session, - client_id: &str, -) -> Result { - let query_uri = format!( - "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}", - SCOPES.join(","), - client_id, - session.device_id(), - ); - let request = session.mercury().get(query_uri)?; - let response = request.await?; - let data = response - .payload - .first() - .ok_or(librespot_core::token::TokenError::Empty)? - .clone(); - let token = librespot_core::token::Token::from_json(String::from_utf8(data)?)?; - Ok(token) -} - -pub async fn get_token_rspotify(session: &Session, client_id: &str) -> Result { +pub async fn get_token_rspotify(session: &Session) -> Result { tracing::info!("Getting a new authentication token..."); - let fut = get_token_librespot(session, client_id); - let token = - match tokio::time::timeout(std::time::Duration::from_secs(TIMEOUT_IN_SECS), fut).await { - Ok(Ok(token)) => token, - Ok(Err(err)) => anyhow::bail!("failed to get the token: {:?}", err), - Err(_) => { - // The timeout likely happens because of the "corrupted" session, - // shutdown it to force re-initializing. - if !session.is_invalid() { - session.shutdown(); - } - anyhow::bail!("timeout when getting the token"); + let auth_data = session.auth_data(); + if auth_data.is_empty() { + anyhow::bail!("Session has no stored credentials for login5 token acquisition"); + } + let fut = session.login5().auth_token(); + let token = match tokio::time::timeout(TIMEOUT, fut).await { + Ok(Ok(token)) => token, + Ok(Err(err)) => anyhow::bail!("failed to get the token: {:?}", err), + Err(_) => { + // The timeout likely happens because of the "corrupted" session, + // shutdown it to force re-initializing. + if !session.is_invalid() { + session.shutdown(); } - }; + anyhow::bail!("timeout when getting the token"); + } + }; // converts the token returned by librespot `get_token` function to a `rspotify::Token` - let expires_in = Duration::from_std(token.expires_in)?; + let expires_in = chrono::Duration::from_std(token.expires_in)?; // let expires_in = Duration::from_std(std::time::Duration::from_secs(5))?; - let expires_at = Utc::now() + expires_in; + let expires_at = chrono::Utc::now() + expires_in; let token = rspotify::Token { access_token: token.access_token, diff --git a/spotify_player/src/ui/mod.rs b/spotify_player/src/ui/mod.rs index 94e1aa99..ba1b2c49 100644 --- a/spotify_player/src/ui/mod.rs +++ b/spotify_player/src/ui/mod.rs @@ -28,7 +28,7 @@ mod page; mod playback; mod popup; pub mod single_line_input; -mod utils; +pub mod utils; /// Run the application UI pub fn run(state: &SharedState) -> Result<()> { diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index 14e80b9c..fdbf52a3 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -3,10 +3,6 @@ use std::{ fmt::Display, }; -use ratatui::text::Line; - -use crate::{state::Episode, utils::format_duration}; - use super::{ config, utils, utils::construct_and_render_block, Album, Artist, ArtistFocusState, Borders, BrowsePageUIState, Cell, Constraint, Context, ContextPageUIState, DataReadGuard, Frame, Id, @@ -14,6 +10,10 @@ use super::{ PlaylistFolderItem, Rect, Row, SearchFocusState, SharedState, Style, Table, Track, UIStateGuard, }; +use crate::state::BidiDisplay; +use crate::ui::utils::to_bidi_string; +use crate::{state::Episode, utils::format_duration}; +use ratatui::text::Line; const COMMAND_TABLE_CONSTRAINTS: [Constraint; 3] = [ Constraint::Percentage(25), @@ -36,7 +36,10 @@ pub fn render_search_page( rect: Rect, ) { fn search_items(items: &[T]) -> Vec<(String, bool)> { - items.iter().map(|i| (i.to_string(), false)).collect() + items + .iter() + .map(|i| (to_bidi_string(&i.to_string()), false)) + .collect() } // 1. Get data @@ -273,15 +276,12 @@ pub fn render_context_page( ); // 3+4. Construct and render the page's widgets - let id = match id { - None => { - frame.render_widget( - Paragraph::new("Cannot determine the current page's context"), - rect, - ); - return; - } - Some(id) => id, + let Some(id) = id else { + frame.render_widget( + Paragraph::new("Cannot determine the current page's context"), + rect, + ); + return; }; let data = state.data.read(); @@ -289,23 +289,23 @@ pub fn render_context_page( Some(context) => { // render context description let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(0)]).split(rect); - let is_followed_string = if let Context::Playlist { playlist, .. } = context { - if data.user_data.is_followed_playlist(playlist) { - "Followed" - } else { - "Not Followed" - } + + let description = if let Context::Playlist { playlist, .. } = context { + format!( + "{} | {}", + context.description(), + if data.user_data.is_followed_playlist(playlist) { + "Followed" + } else { + "Not Followed" + } + ) } else { - "" + context.description() }; frame.render_widget( - Paragraph::new(format!( - "{} | {}", - context.description(), - is_followed_string - )) - .style(ui.theme.page_desc()), + Paragraph::new(description).style(ui.theme.page_desc()), chunks[0], ); let rect = chunks[1]; @@ -445,9 +445,9 @@ pub fn render_library_page( .into_iter() .map(|item| match item { PlaylistFolderItem::Playlist(p) => { - (p.to_string(), curr_context_uri == Some(p.id.uri())) + (p.to_bidi_string(), curr_context_uri == Some(p.id.uri())) } - PlaylistFolderItem::Folder(f) => (f.to_string(), false), + PlaylistFolderItem::Folder(f) => (f.to_bidi_string(), false), }) .collect::>(); @@ -463,7 +463,7 @@ pub fn render_library_page( &ui.theme, ui.search_filtered_items(&data.user_data.saved_albums) .into_iter() - .map(|a| (a.to_string(), curr_context_uri == Some(a.id.uri()))) + .map(|a| (a.to_bidi_string(), curr_context_uri == Some(a.id.uri()))) .collect(), is_active && focus_state == LibraryFocusState::SavedAlbums, ); @@ -472,7 +472,7 @@ pub fn render_library_page( &ui.theme, ui.search_filtered_items(&data.user_data.followed_artists) .into_iter() - .map(|a| (a.to_string(), curr_context_uri == Some(a.id.uri()))) + .map(|a| (a.to_bidi_string(), curr_context_uri == Some(a.id.uri()))) .collect(), is_active && focus_state == LibraryFocusState::FollowedArtists, ); @@ -606,8 +606,10 @@ pub fn render_lyrics_page( // 4. Render the page's widgets // render lyric page description text + let bidi_track = to_bidi_string(track); + let bidi_artists = to_bidi_string(artists); frame.render_widget( - Paragraph::new(format!("{track} by {artists}")).style(ui.theme.page_desc()), + Paragraph::new(format!("{bidi_track} by {bidi_artists}")).style(ui.theme.page_desc()), chunks[0], ); @@ -975,9 +977,9 @@ fn render_track_table( Cell::from("") }, Cell::from(id), - Cell::from(t.display_name()), - Cell::from(t.artists_info()), - Cell::from(t.album_info()), + Cell::from(to_bidi_string(&t.display_name())), + Cell::from(to_bidi_string(&t.artists_info())), + Cell::from(to_bidi_string(&t.album_info())), Cell::from(format!( "{}:{:02}", t.duration.as_secs() / 60, @@ -1067,7 +1069,7 @@ fn render_episode_table( }; Row::new(vec![ Cell::from(id), - Cell::from(e.name.clone()), + Cell::from(to_bidi_string(&e.name)), Cell::from(e.release_date.clone()), Cell::from(format!( "{}:{:02}", diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs index 55bbf9ba..9f63d2a7 100644 --- a/spotify_player/src/ui/playback.rs +++ b/spotify_player/src/ui/playback.rs @@ -1,14 +1,14 @@ -#[cfg(feature = "image")] -use crate::state::ImageRenderInfo; -#[cfg(feature = "image")] -use anyhow::{Context, Result}; -use rspotify::model::Id; - use super::{ config, utils::construct_and_render_block, Borders, Constraint, Frame, Gauge, Layout, Line, LineGauge, Modifier, Paragraph, PlaybackMetadata, Rect, SharedState, Span, Style, Text, UIStateGuard, Wrap, }; +#[cfg(feature = "image")] +use crate::state::ImageRenderInfo; +use crate::ui::utils::to_bidi_string; +#[cfg(feature = "image")] +use anyhow::{Context, Result}; +use rspotify::model::Id; /// Render a playback window showing information about the current playback, which includes /// - track title, artists, album @@ -246,25 +246,31 @@ fn construct_playback_text( }, "{track}" => match playable { rspotify::model::PlayableItem::Track(track) => ( - if track.explicit { - format!("{} (E)", track.name) - } else { - track.name.clone() + { + let bidi_string = to_bidi_string(&track.name); + if track.explicit { + format!("{bidi_string} (E)") + } else { + bidi_string + } }, ui.theme.playback_track(), ), rspotify::model::PlayableItem::Episode(episode) => ( - if episode.explicit { - format!("{} (E)", episode.name) - } else { - episode.name.clone() + { + let bidi_string = to_bidi_string(&episode.name); + if episode.explicit { + format!("{bidi_string} (E)") + } else { + bidi_string + } }, ui.theme.playback_track(), ), }, "{artists}" => match playable { rspotify::model::PlayableItem::Track(track) => ( - crate::utils::map_join(&track.artists, |a| &a.name, ", "), + to_bidi_string(&crate::utils::map_join(&track.artists, |a| &a.name, ", ")), ui.theme.playback_artists(), ), rspotify::model::PlayableItem::Episode(episode) => { @@ -273,30 +279,41 @@ fn construct_playback_text( }, "{album}" => match playable { rspotify::model::PlayableItem::Track(track) => { - (track.album.name.clone(), ui.theme.playback_album()) - } - rspotify::model::PlayableItem::Episode(episode) => { - (episode.show.name.clone(), ui.theme.playback_album()) + (to_bidi_string(&track.album.name), ui.theme.playback_album()) } - }, - "{metadata}" => ( - format!( - "repeat: {} | shuffle: {} | volume: {} | device: {}", - if playback.fake_track_repeat_state { - "track (fake)" - } else { - <&'static str>::from(playback.repeat_state) - }, - playback.shuffle_state, - if let Some(volume) = playback.mute_state { - format!("{volume}% (muted)") - } else { - format!("{}%", playback.volume.unwrap_or_default()) - }, - playback.device_name, + rspotify::model::PlayableItem::Episode(episode) => ( + to_bidi_string(&episode.show.name), + ui.theme.playback_album(), ), - ui.theme.playback_metadata(), - ), + }, + "{metadata}" => { + let repeat_value = if playback.fake_track_repeat_state { + "track (fake)".to_string() + } else { + <&'static str>::from(playback.repeat_state).to_string() + }; + + let volume_value = if let Some(volume) = playback.mute_state { + format!("{volume}% (muted)") + } else { + format!("{}%", playback.volume.unwrap_or_default()) + }; + + let mut parts = vec![]; + + for field in &configs.app_config.playback_metadata_fields { + match field.as_str() { + "repeat" => parts.push(format!("repeat: {repeat_value}")), + "shuffle" => parts.push(format!("shuffle: {}", playback.shuffle_state)), + "volume" => parts.push(format!("volume: {volume_value}")), + "device" => parts.push(format!("device: {}", playback.device_name)), + _ => {} + } + } + + let metadata_str = parts.join(" | "); + (metadata_str, ui.theme.playback_metadata()) + } _ => continue, }; diff --git a/spotify_player/src/ui/utils.rs b/spotify_player/src/ui/utils.rs index f04d4182..99ac2d07 100644 --- a/spotify_player/src/ui/utils.rs +++ b/spotify_player/src/ui/utils.rs @@ -2,6 +2,7 @@ use super::{ config, Block, BorderType, Borders, Frame, List, ListItem, ListState, Rect, Span, Style, Table, TableState, }; +use unicode_bidi::BidiInfo; /// Construct and render a block. /// @@ -118,3 +119,19 @@ pub fn render_table_window( adjust_table_state(state, len); frame.render_stateful_widget(widget, rect, state); } + +/// Convert a string to a bidirectional string. +/// Used to handle RTL text properly in the UI. +pub fn to_bidi_string(s: &str) -> String { + let bidi_info = BidiInfo::new(s, None); + + let bidi_string = if bidi_info.has_rtl() && !bidi_info.paragraphs.is_empty() { + bidi_info + .reorder_line(&bidi_info.paragraphs[0], 0..s.len()) + .into_owned() + } else { + s.to_string() + }; + + bidi_string +} diff --git a/spotify_player/src/utils.rs b/spotify_player/src/utils.rs index a8d905d1..95147423 100644 --- a/spotify_player/src/utils.rs +++ b/spotify_player/src/utils.rs @@ -37,7 +37,7 @@ pub fn get_episode_show_image_url(episode: &rspotify::model::FullEpisode) -> Opt } } -pub fn parse_uri(uri: &str) -> Cow { +pub fn parse_uri(uri: &str) -> Cow<'_, str> { let parts = uri.split(':').collect::>(); // The below URI probably has a format of `spotify:user:{user_id}:{type}:{id}`, // but `rspotify` library expects to receive an URI of format `spotify:{type}:{id}`. diff --git a/test_local.sh b/test_local.sh new file mode 100755 index 00000000..cf48b183 --- /dev/null +++ b/test_local.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +# Local test script for Afera playlist automation +# This script simulates the GitHub Actions workflow locally + +set -e + +echo "๐Ÿงช Testing Afera Playlist Automation Locally" +echo "==============================================" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if scripts exist +echo -e "${YELLOW}Checking required files...${NC}" +if [[ ! -f "playlist_scraper.tcl" ]]; then + echo -e "${RED}โŒ playlist_scraper.tcl not found${NC}" + exit 1 +fi + +if [[ ! -f "playlist_generator.tcl" ]]; then + echo -e "${RED}โŒ playlist_generator.tcl not found${NC}" + exit 1 +fi + +echo -e "${GREEN}โœ… Required scripts found${NC}" + +# Check if TCL is installed +echo -e "${YELLOW}Checking TCL installation...${NC}" +if ! command -v tclsh &> /dev/null; then + echo -e "${RED}โŒ TCL is not installed. Install with: brew install tcl-tk (macOS) or apt-get install tcl (Ubuntu)${NC}" + exit 1 +fi + +# Check TCL TLS package +echo -e "${YELLOW}Checking TCL TLS package...${NC}" +if ! echo 'package require tls; puts "TLS OK"' | tclsh 2>/dev/null; then + echo -e "${RED}โŒ TCL TLS package not found. Install with: brew install tcl-tk (macOS) or apt-get install tcl-tls (Ubuntu)${NC}" + exit 1 +fi + +echo -e "${GREEN}โœ… TCL and TLS package available${NC}" + +# Make scripts executable +echo -e "${YELLOW}Making scripts executable...${NC}" +chmod +x playlist_scraper.tcl +chmod +x playlist_generator.tcl +echo -e "${GREEN}โœ… Scripts are executable${NC}" + +# Test scraper +echo -e "${YELLOW}Testing playlist scraper...${NC}" +echo "Scraping Afera website (this may take a few seconds)..." + +if ./playlist_scraper.tcl https://www.afera.com.pl/muzyka > test_scraped_content.txt 2>/dev/null; then + echo -e "${GREEN}โœ… Scraper completed successfully${NC}" + + # Show scraped content + echo -e "${YELLOW}Scraped content:${NC}" + cat test_scraped_content.txt + + # Count items + albums=$(grep -c "^๐Ÿ’ฟ" test_scraped_content.txt || true) + tracks=$(grep -c "^๐ŸŽถ" test_scraped_content.txt || true) + + echo "" + echo -e "${GREEN}๐Ÿ“Š Found: $albums albums, $tracks tracks${NC}" + + if [[ $albums -eq 0 && $tracks -eq 0 ]]; then + echo -e "${RED}โŒ No content found - check website structure${NC}" + exit 1 + fi +else + echo -e "${RED}โŒ Scraper failed${NC}" + exit 1 +fi + +# Test playlist generator (dry run) +echo -e "${YELLOW}Testing playlist generator...${NC}" + +# Check if spotify_player binary exists +if [[ ! -f "target/release/spotify_player" ]]; then + echo -e "${YELLOW}โš ๏ธ spotify_player binary not found. Building...${NC}" + if command -v cargo &> /dev/null; then + echo "Building spotify_player..." + if cargo build --release --no-default-features --features "rodio-backend,media-control,image,notify" 2>/dev/null; then + echo -e "${GREEN}โœ… Build successful${NC}" + else + echo -e "${YELLOW}โš ๏ธ Build failed, but will test generator logic anyway${NC}" + fi + else + echo -e "${YELLOW}โš ๏ธ Cargo not found, skipping build${NC}" + fi +fi + +# Test generator with scraped content +echo "Testing playlist generator with scraped content..." +if cat test_scraped_content.txt | ./playlist_generator.tcl > test_playlist_commands.txt 2>/dev/null; then + echo -e "${GREEN}โœ… Generator completed successfully${NC}" + + echo -e "${YELLOW}Generated commands:${NC}" + head -20 test_playlist_commands.txt + + # Count generated commands + command_count=$(grep -c "target/release/spotify_player" test_playlist_commands.txt || true) + echo "" + echo -e "${GREEN}๐Ÿ“Š Generated $command_count spotify_player commands${NC}" +else + echo -e "${RED}โŒ Generator failed${NC}" + exit 1 +fi + +# Test complete pipeline +echo -e "${YELLOW}Testing complete pipeline...${NC}" +if ./playlist_scraper.tcl https://www.afera.com.pl/muzyka | ./playlist_generator.tcl > test_complete_pipeline.txt 2>/dev/null; then + echo -e "${GREEN}โœ… Complete pipeline test successful${NC}" + + # Show summary + lines=$(wc -l < test_complete_pipeline.txt) + echo -e "${GREEN}๐Ÿ“Š Pipeline generated $lines lines of output${NC}" +else + echo -e "${RED}โŒ Complete pipeline test failed${NC}" + exit 1 +fi + +# Cleanup +echo -e "${YELLOW}Cleaning up test files...${NC}" +rm -f test_scraped_content.txt test_playlist_commands.txt test_complete_pipeline.txt +echo -e "${GREEN}โœ… Cleanup complete${NC}" + +echo "" +echo -e "${GREEN}๐ŸŽ‰ ALL TESTS PASSED!${NC}" +echo -e "${GREEN}The automation is ready for GitHub Actions deployment.${NC}" +echo "" +echo -e "${YELLOW}Next steps:${NC}" +echo "1. Set up GitHub secrets: SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN" +echo "2. Push the code to GitHub" +echo "3. The workflow will run every Monday at 10:00 AM UTC" +echo "4. You can also trigger it manually from GitHub Actions tab" \ No newline at end of file