From 41ea41b43e35e332b91cab7d97e13fc10a9bbe4c Mon Sep 17 00:00:00 2001 From: sewandev <168495573+sewandev@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:55:46 -0400 Subject: [PATCH 01/19] docs: add winget as the easy Windows install method (#249) ## Summary Adds the one-line Winget install command as the recommended easy install method for Windows in both READMEs, now that `Sewandev.Reverbic` 1.5.7 is live and merged in winget-pkgs. ``` winget install Sewandev.Reverbic ``` ### Changes - `README.md` / `README.es.md`: add `winget install Sewandev.Reverbic` at the top of the Windows install section, keeping the PowerShell script as an alternative. The v1.5.7 GitHub release notes were also updated to include the Winget command. --- README.es.md | 3 +++ README.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/README.es.md b/README.es.md index dce12bf..e5a2b6c 100644 --- a/README.es.md +++ b/README.es.md @@ -28,6 +28,9 @@ ```bash # Instalación rápida (Windows) +winget install Sewandev.Reverbic + +# O, mediante script de PowerShell (Windows) irm https://raw.githubusercontent.com/sewandev/Reverbic/main/install.ps1 | iex # Instalación rápida (macOS / Linux) diff --git a/README.md b/README.md index 059a10b..42a301a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ ```bash # Quick install (Windows) +winget install Sewandev.Reverbic + +# Or, via PowerShell script (Windows) irm https://raw.githubusercontent.com/sewandev/Reverbic/main/install.ps1 | iex # Quick install (macOS / Linux) From d51153e6a663b1347d870aaa7e88ae645e0622f6 Mon Sep 17 00:00:00 2001 From: sewandev <168495573+sewandev@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:44:13 -0400 Subject: [PATCH 02/19] ci: separate lint job and integrate swatinem/rust-cache (#250) Optimizes the GitHub Actions workflow by splitting the formatting/clippy checks into an initial lint job (fail-fast) and replacing the generic cache step with swatinem/rust-cache. Co-authored-by: sewandev --- .github/workflows/ci.yml | 48 +++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5f4695..8f9a0f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,31 @@ on: branches: [develop] jobs: - check: - name: Check & lint (${{ matrix.os }}) + lint: + name: Lint & Format (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y pkg-config libasound2-dev + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: cargo fmt + run: cargo fmt --check + + - name: cargo clippy + run: cargo clippy --all-features -- -D warnings + + test: + name: Test (${{ matrix.os }}) + needs: lint runs-on: ${{ matrix.os }} strategy: @@ -27,21 +50,9 @@ jobs: - name: Install Rust stable uses: dtolnay/rust-toolchain@stable - with: - components: clippy, rustfmt - - name: cargo fmt - run: cargo fmt --check - - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- + - name: Rust Cache + uses: swatinem/rust-cache@v2 - name: cargo check run: cargo check --all-features @@ -49,12 +60,9 @@ jobs: - name: cargo test run: cargo test --all-features - - name: cargo clippy - run: cargo clippy --all-features -- -D warnings - required_check: name: Check & lint - needs: check + needs: test runs-on: ubuntu-latest steps: - run: echo "All checks passed!" From a563fcec6cd1cd400bfaeccccc211ac605a1eac3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:37:35 -0400 Subject: [PATCH 03/19] build(deps): bump actions/cache from 4 to 6 (#251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache](https://github.com/actions/cache) from 4 to 6.
Release notes

Sourced from actions/cache's releases.

v6.0.0

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v6.0.0

v5.1.0

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v5.1.0

v5.0.5

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v5.0.5

v5.0.4

What's Changed

New Contributors

Full Changelog: https://github.com/actions/cache/compare/v5...v5.0.4

v5.0.3

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v5.0.3

v.5.0.2

v5.0.2

What's Changed

... (truncated)

Changelog

Sourced from actions/cache's changelog.

Releases

How to prepare a release

[!NOTE] Relevant for maintainers with write access only.

  1. Switch to a new branch from main.
  2. Run npm test to ensure all tests are passing.
  3. Update the version in https://github.com/actions/cache/blob/main/package.json.
  4. Run npm run build to update the compiled files.
  5. Update this https://github.com/actions/cache/blob/main/RELEASES.md with the new version and changes in the ## Changelog section.
  6. Run licensed cache to update the license report.
  7. Run licensed status and resolve any warnings by updating the https://github.com/actions/cache/blob/main/.licensed.yml file with the exceptions.
  8. Commit your changes and push your branch upstream.
  9. Open a pull request against main and get it reviewed and merged.
  10. Draft a new release https://github.com/actions/cache/releases use the same version number used in package.json
    1. Create a new tag with the version number.
    2. Auto generate release notes and update them to match the changes you made in RELEASES.md.
    3. Toggle the set as the latest release option.
    4. Publish the release.
  11. Navigate to https://github.com/actions/cache/actions/workflows/release-new-action-version.yml
    1. There should be a workflow run queued with the same version number.
    2. Approve the run to publish the new version and update the major tags for this action.

Changelog

6.1.0

6.0.0

  • Updated @actions/cache to ^6.0.1, @actions/core to ^3.0.1, @actions/exec to ^3.0.0, @actions/io to ^3.0.2
  • Migrated to ESM module system
  • Upgraded Jest to v30 and test infrastructure to be ESM compatible

5.0.4

  • Bump minimatch to v3.1.5 (fixes ReDoS via globstar patterns)
  • Bump undici to v6.24.1 (WebSocket decompression bomb protection, header validation fixes)
  • Bump fast-xml-parser to v5.5.6

5.0.3

5.0.2

... (truncated)

Commits

Most Recent Ignore Conditions Applied to This Pull Request | Dependency Name | Ignore Conditions | | --- | --- | | actions/cache | [>= 5.a, < 6] |
[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/cache&package-manager=github_actions&previous-version=4&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8ba02b..f748d8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: targets: x86_64-pc-windows-msvc - name: Cache cargo registry - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | ~/.cargo/registry @@ -69,7 +69,7 @@ jobs: targets: ${{ matrix.target }} - name: Cache cargo registry - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | ~/.cargo/registry @@ -119,7 +119,7 @@ jobs: targets: x86_64-unknown-linux-gnu - name: Cache cargo registry - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | ~/.cargo/registry From 1eaa5535069737bd1ef4b76124b8c25a3d6b158f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:38:01 -0400 Subject: [PATCH 04/19] build(deps): bump actions/upload-artifact from 4 to 7 (#252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
Release notes

Sourced from actions/upload-artifact's releases.

v7.0.0

v7 What's new

Direct Uploads

Adds support for uploading single files directly (unzipped). Callers can set the new archive parameter to false to skip zipping the file during upload. Right now, we only support single files. The action will fail if the glob passed resolves to multiple files. The name parameter is also ignored with this setting. Instead, the name of the artifact will be the name of the uploaded file.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v6...v7.0.0

v6.0.0

v6 - What's new

[!IMPORTANT] actions/upload-artifact@v6 now runs on Node.js 24 (runs.using: node24) and requires a minimum Actions Runner version of 2.327.1. If you are using self-hosted runners, ensure they are updated before upgrading.

Node.js 24

This release updates the runtime to Node.js 24. v5 had preliminary support for Node.js 24, however this action was by default still running on Node.js 20. Now this action by default will run on Node.js 24.

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0

v5.0.0

What's Changed

BREAKING CHANGE: this update supports Node v24.x. This is not a breaking change per-se but we're treating it as such.

... (truncated)

Commits
  • 043fb46 Merge pull request #797 from actions/yacaovsnc/update-dependency
  • 634250c Include changes in typespec/ts-http-runtime 0.3.5
  • e454baa Readme: bump all the example versions to v7 (#796)
  • 74fad66 Update the readme with direct upload details (#795)
  • bbbca2d Support direct file uploads (#764)
  • 589182c Upgrade the module to ESM and bump dependencies (#762)
  • 47309c9 Merge pull request #754 from actions/Link-/add-proxy-integration-tests
  • 02a8460 Add proxy integration test
  • b7c566a Merge pull request #745 from actions/upload-artifact-v6-release
  • e516bc8 docs: correct description of Node.js 24 support in README
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f748d8d..cf730d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: Copy-Item "target\x86_64-pc-windows-msvc\release\reverbic.exe" "dist\reverbic-$version-x86_64-windows.exe" - name: Upload Windows artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-windows path: dist/* @@ -94,7 +94,7 @@ jobs: tar -C "$staging" -czf "dist/$archive" reverbic - name: Upload macOS artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-macos-${{ matrix.target }} path: dist/*.tar.gz @@ -144,7 +144,7 @@ jobs: tar -C "$staging" -czf "dist/$archive" reverbic - name: Upload Linux artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-linux-x86_64 path: dist/*.tar.gz From ee4de5969978152ed00ea1cf00fecc2a55b19d2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:38:09 -0400 Subject: [PATCH 05/19] build(deps): bump actions/download-artifact from 4 to 8 (#253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
Release notes

Sourced from actions/download-artifact's releases.

v8.0.0

v8 - What's new

[!IMPORTANT] actions/download-artifact@v8 has been migrated to an ESM module. This should be transparent to the caller but forks might need to make significant changes.

[!IMPORTANT] Hash mismatches will now error by default. Users can override this behavior with a setting change (see below).

Direct downloads

To support direct uploads in actions/upload-artifact, the action will no longer attempt to unzip all downloaded files. Instead, the action checks the Content-Type header ahead of unzipping and skips non-zipped files. Callers wishing to download a zipped file as-is can also set the new skip-decompress parameter to true.

Enforced checks (breaking)

A previous release introduced digest checks on the download. If a download hash didn't match the expected hash from the server, the action would log a warning. Callers can now configure the behavior on mismatch with the digest-mismatch parameter. To be secure by default, we are now defaulting the behavior to error which will fail the workflow run.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

Full Changelog: https://github.com/actions/download-artifact/compare/v7...v8.0.0

v7.0.0

v7 - What's new

[!IMPORTANT] actions/download-artifact@v7 now runs on Node.js 24 (runs.using: node24) and requires a minimum Actions Runner version of 2.327.1. If you are using self-hosted runners, ensure they are updated before upgrading.

Node.js 24

This release updates the runtime to Node.js 24. v6 had preliminary support for Node 24, however this action was by default still running on Node.js 20. Now this action by default will run on Node.js 24.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/download-artifact/compare/v6.0.0...v7.0.0

v6.0.0

... (truncated)

Commits
  • 3e5f45b Add regression tests for CJK characters (#471)
  • e6d03f6 Add a regression test for artifact name + content-type mismatches (#472)
  • 70fc10c Merge pull request #461 from actions/danwkennedy/digest-mismatch-behavior
  • f258da9 Add change docs
  • ccc058e Fix linting issues
  • bd7976b Add a setting to specify what to do on hash mismatch and default it to error
  • ac21fcf Merge pull request #460 from actions/danwkennedy/download-no-unzip
  • 15999bf Add note about package bumps
  • 974686e Bump the version to v8 and add release notes
  • fbe48b1 Update test names to make it clearer what they do
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=4&new-version=8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf730d4..e503d61 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -160,7 +160,7 @@ jobs: steps: - name: Download release artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: dist merge-multiple: true From 5038ef64588509aca2152861a11336a7bb13baea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:38:20 -0400 Subject: [PATCH 06/19] build(deps): bump actions/checkout from 6 to 7 (#254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
Release notes

Sourced from actions/checkout's releases.

v7.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v6.0.3...v7.0.0

v6.0.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.3

v6.0.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v6.0.1...v6.0.2

v6.0.1

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.1

Changelog

Sourced from actions/checkout's changelog.

Changelog

v7.0.0

v6.0.3

v6.0.2

v6.0.1

v6.0.0

v5.0.1

v5.0.0

v4.3.1

v4.3.0

v4.2.2

v4.2.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/codeql.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/release.yml | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f9a0f5..d065904 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: name: Lint & Format (Linux) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install Linux dependencies run: | @@ -40,7 +40,7 @@ jobs: os: [windows-latest, macos-latest, ubuntu-latest] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install Linux dependencies if: runner.os == 'Linux' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 55d720a..841f0b2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install Linux dependencies run: | diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 4227739..c42235e 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -14,7 +14,7 @@ jobs: sync: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: EndBug/label-sync@v2 with: config-file: .github/labels.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e503d61..8e18b80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install Rust stable uses: dtolnay/rust-toolchain@stable @@ -61,7 +61,7 @@ jobs: asset_arch: aarch64 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install Rust stable uses: dtolnay/rust-toolchain@stable @@ -106,7 +106,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install Linux dependencies run: | From 0a386c0d990f6dcaa1e36f130498e8643b68b495 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:38:30 -0400 Subject: [PATCH 07/19] build(deps): bump bytes from 1.11.1 to 1.12.0 (#255) Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.1 to 1.12.0.
Release notes

Sourced from bytes's releases.

Bytes v1.12.0

1.12.0 (June 18th, 2026)

Added

  • Add BytesMut::extend_from_within() (#818)
  • Add BytesMut::try_unsplit() (#746)

Fixed

  • Fix panic in get_int if nbytes is zero (#806)

Changed

  • Pass vtable data by value (#826)
  • Exclude development scripts from published package (#810)

Documented

  • Document that BytesMut::{reserve,try_reserve} doesn't preserve unused capacity (#808)
Changelog

Sourced from bytes's changelog.

1.12.0 (June 18th, 2026)

Added

  • Add BytesMut::extend_from_within() (#818)
  • Add BytesMut::try_unsplit() (#746)

Fixed

  • Fix panic in get_int if nbytes is zero (#806)

Changed

  • Pass vtable data by value (#826)
  • Exclude development scripts from published package (#810)

Documented

  • Document that BytesMut::{reserve,try_reserve} doesn't preserve unused capacity (#808)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=bytes&package-manager=cargo&previous-version=1.11.1&new-version=1.12.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb2e34a..0e82deb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,9 +310,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "castaway" From 9a4d5ad924b74432cdf8a1290d796a122163741c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:38:51 -0400 Subject: [PATCH 08/19] build(deps): bump ratatui from 0.30.1 to 0.30.2 (#257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ratatui](https://github.com/ratatui/ratatui) from 0.30.1 to 0.30.2.
Release notes

Sourced from ratatui's releases.

ratatui-v0.30.2

a gift

We are excited to announce the new version of ratatui - a Rust library that's all about cooking up TUIs 👨‍🍳🐀

Release highlights: https://ratatui.rs/highlights/v0302/

⚠️ List of breaking changes can be found here.

Features

  • 90639c1 (uncategorized) Add Termina backend by @joshka in #2561

    Summary

    • add the ratatui-termina backend crate using the published termina crate
    • expose the backend through the termina feature and Ratatui prelude/backend re-exports
    • add a small Termina event-loop example and wire the backend into CI, xtask, README generation, and docs

    Refs #1784

    Validation

    • cargo +nightly fmt
    • cargo check -p ratatui-termina --all-features --all-targets
    • cargo check -p ratatui --no-default-features --features termina
    • cargo check -p xtask
    • cargo check -p release-header
    • cargo xtask check-backend termina
    • cargo xtask test-backend termina
    • cargo xtask rdme --check
    • markdownlint-cli2 ARCHITECTURE.md ratatui-termina/README.md .github/ISSUE_TEMPLATE/bug_report.md

Bug Fixes

  • fce3c80 (widgets) Require thread-safe shadow effects by @joshka in #2584

    Summary

    • require custom shadow effects to preserve the auto traits expected by Block-backed widgets
    • document the CellEffect auto-trait contract
    • add a public widget regression test for the affected ratatui::widgets re-exports

... (truncated)

Changelog

Sourced from ratatui's changelog.

0.30.2 - 2026-06-19

a gift

We are excited to announce the new version of ratatui - a Rust library that's all about cooking up TUIs 👨‍🍳🐀

Release highlights: https://ratatui.rs/highlights/v0302/

⚠️ List of breaking changes can be found here.

Features

  • 90639c1 (uncategorized) Add Termina backend by @joshka in #2561

    Summary

    • add the ratatui-termina backend crate using the published termina crate
    • expose the backend through the termina feature and Ratatui prelude/backend re-exports
    • add a small Termina event-loop example and wire the backend into CI, xtask, README generation, and docs

    Refs #1784

    Validation

    • cargo +nightly fmt
    • cargo check -p ratatui-termina --all-features --all-targets
    • cargo check -p ratatui --no-default-features --features termina
    • cargo check -p xtask
    • cargo check -p release-header
    • cargo xtask check-backend termina
    • cargo xtask test-backend termina
    • cargo xtask rdme --check
    • markdownlint-cli2 ARCHITECTURE.md ratatui-termina/README.md .github/ISSUE_TEMPLATE/bug_report.md

Bug Fixes

  • fce3c80 (widgets) Require thread-safe shadow effects by @joshka in #2584

    Summary

    • require custom shadow effects to preserve the auto traits expected by Block-backed widgets
    • document the CellEffect auto-trait contract
    • add a public widget regression test for the affected ratatui::widgets

... (truncated)

Commits
  • e665c36 chore(ratatui): unleash the rats v0.30.2 (#2581)
  • 2700b16 docs(changelog): update changelog for 0.30.2 (#2608)
  • e306ce6 fix(buffer): create updates for "uncovered" cells (#2587)
  • 81e667f fix(scrollbar): keep a large thumb within the track at the end (#2594)
  • c75d778 chore(ci): add cargo-udeps dependency check (#2599)
  • 25314d8 build(deps): bump release-plz/action from 0.5.129 to 0.5.130 (#2600)
  • 3534070 build(deps): bump taiki-e/install-action from 2.81.8 to 2.81.10 (#2601)
  • a798a13 build(deps): bump tombi-toml/setup-tombi from 1.1.2 to 1.1.3 (#2602)
  • 3ac8850 build(deps): bump octocrab from 0.52.0 to 0.53.1 (#2603)
  • 9c79633 build(deps): bump time from 0.3.47 to 0.3.49 (#2604)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ratatui&package-manager=cargo&previous-version=0.30.1&new-version=0.30.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e82deb..bbba175 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3179,14 +3179,15 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.30.1" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1695748e3a735b34968c887ceea5a380b43545903868ae8f5b666593100f6b68" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" dependencies = [ "instability", "ratatui-core", "ratatui-crossterm", "ratatui-macros", + "ratatui-termina", "ratatui-termwiz", "ratatui-widgets", "serde", @@ -3194,15 +3195,14 @@ dependencies = [ [[package]] name = "ratatui-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3603f354bba8c595fa47860e60142d7372b7210c27044c6a7d0e1a4336b44" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ "bitflags 2.13.0", "compact_str", "critical-section", "hashbrown 0.17.1", - "indoc", "itertools", "kasuari", "lru", @@ -3217,9 +3217,9 @@ dependencies = [ [[package]] name = "ratatui-crossterm" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2867bedcbd6a690ca4f8672a687b730ec07660c79844517b084311b529980c" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" dependencies = [ "cfg-if", "crossterm", @@ -3229,19 +3229,30 @@ dependencies = [ [[package]] name = "ratatui-macros" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fac59720679490d89d200df411faa249be728681adcabed3d047ae72c48f1d" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" dependencies = [ "ratatui-core", "ratatui-widgets", ] +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + [[package]] name = "ratatui-termwiz" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "386b8ff8f74ed749509391c56d549761a2fcdb408e1f42e467286bcb7dac8967" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" dependencies = [ "ratatui-core", "termwiz", @@ -3249,9 +3260,9 @@ dependencies = [ [[package]] name = "ratatui-widgets" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef4f17dd7ac3abf5adc2b920a03c61eee4bfe6a88fa5191936895525371d79c" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ "bitflags 2.13.0", "hashbrown 0.17.1", @@ -4216,6 +4227,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.0", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "windows-sys 0.61.2", +] + [[package]] name = "terminfo" version = "0.9.0" From e56870904782725e99b596f980efec79c28eb24e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:38:59 -0400 Subject: [PATCH 09/19] build(deps): bump zip from 2.4.2 to 8.6.0 (#258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [zip](https://github.com/zip-rs/zip2) from 2.4.2 to 8.6.0.
Release notes

Sourced from zip's releases.

v8.6.0

🚀 Features

  • add compression not supported as enum error (#774)

🐛 Bug Fixes

  • allow for [u8] as filename (#775)

🚜 Refactor

  • mark ZipFlags as non-exhaustive and add test for HasZipMetadata (#777)
  • use and simplify is_dir (#776)

v8.5.1

🚜 Refactor

  • change magic finder to stack buffer (#763)
  • simplify extra field parsing (#764)

v8.5.0

🐛 Bug Fixes

  • remove zip64 comment and add zip64 extensible data sector (#747)

🚜 Refactor

  • remove useless magic in struct (#730)
  • change extra_field from Arc<Vec> to Arc<[u8]> (#741)

⚙️ Miscellaneous Tasks

  • cleanup README (#758)

v8.4.0

🚀 Features

  • add a check for building benches (#748)

🚜 Refactor

  • split part of read.rs for code readability (#744)
  • remove unused allow (#745)

⚡ Performance

  • skip BufReader for Stored files in make_reader (#739)

⚙️ Miscellaneous Tasks

... (truncated)

Changelog

Sourced from zip's changelog.

8.6.0 - 2026-04-25

🚀 Features

  • add compression not supported as enum error (#774)

🐛 Bug Fixes

  • allow for [u8] as filename (#775)

🚜 Refactor

  • mark ZipFlags as non-exhaustive and add test for HasZipMetadata (#777)
  • use and simplify is_dir (#776)

8.5.1 - 2026-04-06

🚜 Refactor

  • change magic finder to stack buffer (#763)
  • simplify extra field parsing (#764)

8.5.0 - 2026-04-01

🐛 Bug Fixes

  • remove zip64 comment and add zip64 extensible data sector (#747)

🚜 Refactor

  • remove useless magic in struct (#730)
  • change extra_field from Arc<Vec> to Arc<[u8]> (#741)

⚙️ Miscellaneous Tasks

  • cleanup README (#758)

8.4.0 - 2026-03-23

🚀 Features

  • add a check for building benches (#748)

🚜 Refactor

  • split part of read.rs for code readability (#744)
  • remove unused allow (#745)

⚡ Performance

... (truncated)

Commits
  • 771dfc5 chore: release v8.6.0 (#781)
  • 8e480cc chore(deps): update sha1 requirement from 0.10 to 0.11 (#767)
  • 2513bbe chore(deps): update aes requirement from 0.8 to 0.9 (#787)
  • 957e240 ci(deps): bump actions/cache from 5.0.4 to 5.0.5 (#786)
  • 0329d9d fix: allow for [u8] as filename (#775)
  • ddcf854 ci(deps): bump github/codeql-action from 4.35.1 to 4.35.2 (#788)
  • 42323a8 ci(deps): bump actions/create-github-app-token from 3.0.0 to 3.1.1 (#785)
  • 901014a ci(deps): bump crate-ci/typos from 1.45.0 to 1.45.1 (#784)
  • 07702d4 feat: add compression not supported as enum error (#774)
  • 2d0c46b ci(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#783)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=zip&package-manager=cargo&previous-version=2.4.2&new-version=8.6.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 42 ++++++++++++++++-------------------------- Cargo.toml | 2 +- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbba175..a4ebff3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,15 +142,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arboard" version = "3.6.1" @@ -798,17 +789,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "derive_builder" version = "0.20.2" @@ -1077,6 +1057,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -4714,6 +4695,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.20.1" @@ -6044,21 +6031,24 @@ dependencies = [ [[package]] name = "zip" -version = "2.4.2" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ - "arbitrary", "crc32fast", - "crossbeam-utils", - "displaydoc", "flate2", "indexmap", "memchr", - "thiserror 2.0.18", + "typed-path", "zopfli", ] +[[package]] +name = "zlib-rs" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3" + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index f902d3f..7b55d64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ tar = "0.4" rand = { version = "0.9", default-features = false, features = ["std", "std_rng"] } base64 = "0.21" dunce = "1" -zip = { version = "2", default-features = false, features = ["deflate"] } +zip = { version = "8", default-features = false, features = ["deflate"] } librespot-core = { version = "0.8", default-features = false, features = ["rustls-tls-native-roots"] } librespot-playback = { version = "0.8", default-features = false, features = ["rodio-backend", "rustls-tls-native-roots"] } librespot-audio = { version = "0.8", default-features = false, features = ["rustls-tls-native-roots"] } From 8ef9418f6658cf94624e9a9e261a9b2d7110909a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:39:08 -0400 Subject: [PATCH 10/19] build(deps): bump github/codeql-action from 4 to 4.37.3 (#265) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4 to 4.37.3.
Release notes

Sourced from github/codeql-action's releases.

v4.37.3

No user facing changes.

v4.37.2

  • The new address format for the config-file input that was introduced in CodeQL Action 4.37.0 is now enabled by default. In addition to the format described there, the remote= prefix can now be used to explicitly indicate that the input refers to a remote file. All previous input formats continue to be accepted as well. #4023
  • The CodeQL Action can now make use of configured private registries in Default Setup to retrieve CodeQL configuration files from remote repositories that require authentication. This will allow customers to store their CodeQL configuration in a single repository that can then be referenced by Default Setup workflows in other repositories. We expect to roll this and other, related changes out to everyone in July. #4007

v4.37.1

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. #3956
  • Update default CodeQL bundle version to 2.26.1. #4019

v4.37.0

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973

v4.36.3

No user facing changes.

v4.36.2

  • Cache CodeQL CLI version information across Actions steps. #3943
  • Reduce requests while waiting for analysis processing by using exponential backoff when polling SARIF processing status. #3937
  • Update default CodeQL bundle version to 2.25.6. #3948

v4.36.1

No user facing changes.

v4.36.0

  • Breaking change: Bump the minimum required CodeQL bundle version to 2.19.4. #3894
  • Add support for SHA-256 Git object IDs. #3893
  • Update default CodeQL bundle version to 2.25.5. #3926

v4.35.5

  • We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. #3899
  • For performance and accuracy reasons, improved incremental analysis will now only be enabled on a pull request when diff-informed analysis is also enabled for that run. If diff-informed analysis is unavailable (for example, because the PR diff ranges could not be computed), the action will fall back to a full analysis. #3791
  • If multiple inputs are provided for the GitHub-internal analysis-kinds input, only code-scanning will be enabled. The analysis-kinds input is experimental, for GitHub-internal use only, and may change without notice at any time. #3892
  • Added an experimental change which, when running a Code Scanning analysis for a PR with improved incremental analysis enabled, prefers CodeQL CLI versions that have a cached overlay-base database for the configured languages. This speeds up analysis for a repository when there is not yet a cached overlay-base database for the latest CLI version. We expect to roll this change out to everyone in May. #3880

v4.35.4

  • Update default CodeQL bundle version to 2.25.4. #3881

v4.35.3

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.19.3 and earlier. These versions of CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise Server 3.15, and will be unsupported by the next minor release of the CodeQL Action. #3837
  • Configurations for private registries that use Cloudsmith or GCP OIDC are now accepted. #3850
  • Best-effort connection tests for private registries now use GET requests instead of HEAD for better compatibility with various registry implementations. For NuGet feeds, the test is now always performed against the service index. #3853
  • Fixed a bug where two diagnostics produced within the same millisecond could overwrite each other on disk, causing one of them to be lost. #3852
  • Update default CodeQL bundle version to 2.25.3. #3865

v4.35.2

  • The undocumented TRAP cache cleanup feature that could be enabled using the CODEQL_ACTION_CLEANUP_TRAP_CACHES environment variable is deprecated and will be removed in May 2026. If you are affected by this, we recommend disabling TRAP caching by passing the trap-caching: false input to the init Action. #3795
  • The Git version 2.36.0 requirement for improved incremental analysis now only applies to repositories that contain submodules. #3789

... (truncated)

Changelog

Sourced from github/codeql-action's changelog.

4.37.3 - 22 Jul 2026

No user facing changes.

4.37.2 - 21 Jul 2026

  • The new address format for the config-file input that was introduced in CodeQL Action 4.37.0 is now enabled by default. In addition to the format described there, the remote= prefix can now be used to explicitly indicate that the input refers to a remote file. All previous input formats continue to be accepted as well. #4023
  • The CodeQL Action can now make use of configured private registries in Default Setup to retrieve CodeQL configuration files from remote repositories that require authentication. This will allow customers to store their CodeQL configuration in a single repository that can then be referenced by Default Setup workflows in other repositories. We expect to roll this and other, related changes out to everyone in July. #4007

4.37.1 - 16 Jul 2026

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. #3956
  • Update default CodeQL bundle version to 2.26.1. #4019

4.37.0 - 08 Jul 2026

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973

4.36.3 - 01 Jul 2026

No user facing changes.

Commits
  • e4fba86 Merge pull request #4031 from github/update-v4.37.3-72f6a9da0
  • fb50ab5 Update changelog for v4.37.3
  • 72f6a9d Merge pull request #4030 from github/mbg/fix/no-proxy
  • 3b5ee58 Use default request options instead of undefined
  • bfb6be4 Merge pull request #4028 from github/mergeback/v4.37.2-to-main-e0647621
  • 526ab84 Rebuild
  • d6217b9 Update changelog and version after v4.37.2
  • e064762 Merge pull request #4027 from github/update-v4.37.2-385bcdc5a
  • e0faed8 Add a couple of change notes
  • 73aad0e Update changelog for v4.37.2
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=4&new-version=4.37.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 841f0b2..fabc832 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,7 +27,7 @@ jobs: sudo apt-get install -y pkg-config libasound2-dev - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@v4.37.3 with: languages: rust @@ -35,6 +35,6 @@ jobs: run: cargo build --all-features - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@v4.37.3 with: category: "/language:rust" From b63c6b931523658b904b7a99bc6dda163d639eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gurruchaga?= Date: Sun, 9 Aug 2026 11:56:09 -0400 Subject: [PATCH 11/19] Ensure required CI check reports lint failures (#266) ## Summary - make the aggregate required check depend on both lint and test jobs - run the aggregate check even when an upstream job fails or is skipped - fail the aggregate check unless both required job results are successful ## Validation - `actionlint .github/workflows/ci.yml` - `git diff --check` Closes #262 --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d065904..775b0a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,13 @@ jobs: required_check: name: Check & lint - needs: test + needs: [lint, test] + if: ${{ always() }} runs-on: ubuntu-latest steps: - - run: echo "All checks passed!" + - name: Verify required jobs + run: | + if [ "${{ needs.lint.result }}" != "success" ] || \ + [ "${{ needs.test.result }}" != "success" ]; then + exit 1 + fi From e0c78785bcdec9fcf207703b1fe0f6f6a7667661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gurruchaga?= Date: Sun, 9 Aug 2026 11:56:29 -0400 Subject: [PATCH 12/19] Serialize Spotify remote skip commands (#267) ## Summary - serialize Spotify remote next/previous commands through a FIFO queue - allow only one remote skip request in flight at a time - assign operation IDs and ignore stale results or results from a previously active device - keep skip results separate from the shared playback result receiver - refresh remote playback state after each accepted operation completes ## Why Rapid next/previous input could launch overlapping Spotify Web API requests. Network timing could reorder those requests, while replacing the shared result receiver caused earlier outcomes to be dropped locally even though Spotify could still apply them. The queue establishes a single ownership boundary for remote skip operations and preserves input order without duplicating Spotify command error handling. ## Testing - `cargo fmt --check` - `cargo test` (242 passed) - `cargo clippy --all-targets` - `git diff --check` Fixes #263 --- src/app/integrations.rs | 407 +++++++++++++++++++++++++++++++++------ src/app/mod.rs | 27 +++ src/app/spotify_state.rs | 140 ++++++++++++++ src/main.rs | 1 + 4 files changed, 515 insertions(+), 60 deletions(-) diff --git a/src/app/integrations.rs b/src/app/integrations.rs index 0e73a53..01faa79 100644 --- a/src/app/integrations.rs +++ b/src/app/integrations.rs @@ -1,5 +1,7 @@ use super::modal::{SearchMode, SpotifyAuthStatus, SpotifyPlayerStatus}; -use super::spotify_state::{SpotifyPlaybackBackend, SpotifySearchPage}; +use super::spotify_state::{ + SpotifyPlaybackBackend, SpotifyRemoteSkipDirection, SpotifyRemoteSkipResult, SpotifySearchPage, +}; use super::{abort_task, App, SpotifyControlTarget}; use crate::config::{Config, SpotifyPlaybackMode}; @@ -748,6 +750,8 @@ impl App { self.config.save(); self.spotify.token_refreshed_at = Some(std::time::Instant::now()); self.start_playback_polling(); + self.spotify.remote_skip_paused_for_token_refresh = false; + self.start_next_spotify_remote_skip(); tracing::info!("spotify: access_token refreshed"); } Ok(Err(e)) => { @@ -767,61 +771,137 @@ impl App { } pub fn poll_spotify_play_result(&mut self) { - use crate::integrations::spotify::SpotifyError; if let Some(rx) = self.spotify.play_result_rx.take() { match rx.try_recv() { - Ok(Ok(())) => { - if self.spotify.active_backend == Some(SpotifyPlaybackBackend::Remote) { - self.spotify.player_status = SpotifyPlayerStatus::Playing; - } + Ok(result) => self.handle_spotify_remote_command_result(result), + Err(std::sync::mpsc::TryRecvError::Empty) => { + self.spotify.play_result_rx = Some(rx); } - Ok(Err(SpotifyError::Unauthorized)) => { - if self.spotify.active_backend == Some(SpotifyPlaybackBackend::Remote) { - self.spotify.active_backend = None; - } - self.spotify.player_status = SpotifyPlayerStatus::Error(crate::i18n::t( - "integrations.spotify.error.generic", - )); - tracing::warn!("spotify play: token expirado, renovando"); - self.spotify.token_refreshed_at = - Some(std::time::Instant::now() - std::time::Duration::from_secs(60 * 60)); + Err(std::sync::mpsc::TryRecvError::Disconnected) => {} + } + } + } + + fn handle_spotify_remote_command_result( + &mut self, + result: Result<(), crate::integrations::spotify::SpotifyError>, + ) { + use crate::integrations::spotify::SpotifyError; + match result { + Ok(()) => { + if self.spotify.active_backend == Some(SpotifyPlaybackBackend::Remote) { + self.spotify.player_status = SpotifyPlayerStatus::Playing; } - Ok(Err(SpotifyError::DeviceUnavailable)) => { - if self.spotify.active_backend == Some(SpotifyPlaybackBackend::Remote) { - self.spotify.active_backend = None; + } + Err(SpotifyError::Unauthorized) => { + if self.spotify.active_backend == Some(SpotifyPlaybackBackend::Remote) { + self.spotify.active_backend = None; + } + self.spotify.player_status = SpotifyPlayerStatus::Error(crate::i18n::t( + "integrations.spotify.error.generic", + )); + tracing::warn!("spotify command: token expirado, renovando"); + self.spotify.token_refreshed_at = + Some(std::time::Instant::now() - std::time::Duration::from_secs(60 * 60)); + } + Err(SpotifyError::DeviceUnavailable) => { + if self.spotify.active_backend == Some(SpotifyPlaybackBackend::Remote) { + self.spotify.active_backend = None; + } + if let Some(dead_id) = self.spotify.active_device_id.take() { + tracing::warn!( + device_id = dead_id, + "spotify command: device did not respond, evicting it" + ); + self.spotify.failed_device_ids.insert(dead_id.clone()); + self.spotify + .devices + .retain(|d| d.id.as_deref() != Some(&dead_id)); + } + self.spotify.active_device_id = resolve_active_spotify_device( + &self.spotify.devices, + self.spotify.active_device_id.as_deref(), + ); + let message = crate::i18n::t("integrations.spotify.error.device_gone"); + self.spotify.player_status = SpotifyPlayerStatus::Error(message.clone()); + self.notify_error(format!("Spotify: {message}")); + self.fetch_spotify_devices(); + } + Err(e) => { + tracing::warn!("spotify command: {e}"); + if self.spotify.active_backend == Some(SpotifyPlaybackBackend::Remote) { + self.spotify.active_backend = None; + } + self.spotify.player_status = SpotifyPlayerStatus::Error(crate::i18n::t( + "integrations.spotify.error.generic", + )); + } + } + } + + pub fn poll_spotify_remote_skip_result(&mut self) { + let Some(rx) = self.spotify.remote_skip_result_rx.take() else { + return; + }; + match rx.try_recv() { + Ok(outcome) => { + self.spotify.remote_skip_task = None; + if !self + .spotify + .remote_skip_queue + .complete(outcome.id, &outcome.device_id) + { + tracing::debug!( + operation_id = outcome.id, + device_id = outcome.device_id, + "spotify remote skip: ignoring stale result" + ); + return; + } + match self.spotify_control_target() { + SpotifyControlTarget::Remote { device_id, .. } + if device_id == outcome.device_id => + { + let unauthorized = matches!( + &outcome.result, + Err(crate::integrations::spotify::SpotifyError::Unauthorized) + ); + if unauthorized { + self.spotify.remote_skip_paused_for_token_refresh = true; + } + self.handle_spotify_remote_command_result(outcome.result); + self.start_playback_polling(); + if !unauthorized { + self.start_next_spotify_remote_skip(); + } } - if let Some(dead_id) = self.spotify.active_device_id.take() { - tracing::warn!( - device_id = dead_id, - "spotify play_on_device: device did not respond, evicting it" + SpotifyControlTarget::Remote { .. } => { + tracing::debug!( + operation_id = outcome.id, + device_id = outcome.device_id, + "spotify remote skip: device changed, ignoring result state" ); - self.spotify.failed_device_ids.insert(dead_id.clone()); - self.spotify - .devices - .retain(|d| d.id.as_deref() != Some(&dead_id)); + self.start_playback_polling(); + self.start_next_spotify_remote_skip(); } - self.spotify.active_device_id = resolve_active_spotify_device( - &self.spotify.devices, - self.spotify.active_device_id.as_deref(), - ); - let message = crate::i18n::t("integrations.spotify.error.device_gone"); - self.spotify.player_status = SpotifyPlayerStatus::Error(message.clone()); - self.notify_error(format!("Spotify: {message}")); - self.fetch_spotify_devices(); - } - Ok(Err(e)) => { - tracing::warn!("spotify play_on_device: {e}"); - if self.spotify.active_backend == Some(SpotifyPlaybackBackend::Remote) { - self.spotify.active_backend = None; + SpotifyControlTarget::Native | SpotifyControlTarget::None => { + tracing::debug!( + operation_id = outcome.id, + device_id = outcome.device_id, + "spotify remote skip: remote control is no longer active" + ); + self.cancel_spotify_remote_skips(); } - self.spotify.player_status = SpotifyPlayerStatus::Error(crate::i18n::t( - "integrations.spotify.error.generic", - )); - } - Err(std::sync::mpsc::TryRecvError::Empty) => { - self.spotify.play_result_rx = Some(rx); } - Err(std::sync::mpsc::TryRecvError::Disconnected) => {} + } + Err(std::sync::mpsc::TryRecvError::Empty) => { + self.spotify.remote_skip_result_rx = Some(rx); + } + Err(std::sync::mpsc::TryRecvError::Disconnected) => { + self.spotify.remote_skip_task = None; + tracing::warn!("spotify remote skip: result channel disconnected"); + self.spotify.remote_skip_queue.abandon_current(); + self.start_next_spotify_remote_skip(); } } } @@ -960,6 +1040,7 @@ impl App { abort_task(&mut self.spotify.playback_task); self.spotify.playback_rx = None; self.spotify.playback = None; + self.cancel_spotify_remote_skips(); if self.spotify.active_backend == Some(SpotifyPlaybackBackend::Remote) { self.spotify.active_backend = None; } @@ -1126,24 +1207,74 @@ impl App { self.spotify.player_status = SpotifyPlayerStatus::Loading; } - fn spotify_remote_skip(&mut self, token: String, device_id: String, forward: bool) { + fn spotify_remote_skip(&mut self, device_id: String, direction: SpotifyRemoteSkipDirection) { + self.spotify.remote_skip_queue.enqueue(device_id, direction); + self.start_next_spotify_remote_skip(); + } + + pub(super) fn cancel_spotify_remote_skips(&mut self) { + abort_task(&mut self.spotify.remote_skip_task); + self.spotify.remote_skip_queue.invalidate(); + self.spotify.remote_skip_paused_for_token_refresh = false; + self.spotify.remote_skip_result_rx = None; + } + + fn start_next_spotify_remote_skip(&mut self) { + if self.spotify.remote_skip_paused_for_token_refresh { + return; + } + let (token, active_device_id) = match self.spotify_control_target() { + SpotifyControlTarget::Remote { token, device_id } => (token, device_id), + SpotifyControlTarget::Native | SpotifyControlTarget::None => { + self.cancel_spotify_remote_skips(); + return; + } + }; + let operation = loop { + let Some(operation) = self.spotify.remote_skip_queue.begin_next() else { + return; + }; + if active_device_id == operation.device_id { + break operation; + } + tracing::debug!( + operation_id = operation.id, + device_id = operation.device_id, + "spotify remote skip: device changed, dropping queued operation" + ); + self.spotify + .remote_skip_queue + .complete(operation.id, &operation.device_id); + }; let (tx, rx) = std::sync::mpsc::channel(); - self.spotify.play_result_rx = Some(rx); - tokio::spawn(async move { - let result = if forward { - crate::integrations::spotify::devices::next_track(&token, &device_id).await - } else { - crate::integrations::spotify::devices::previous_track(&token, &device_id).await + self.spotify.remote_skip_result_rx = Some(rx); + let handle = tokio::spawn(async move { + let result = match operation.direction { + SpotifyRemoteSkipDirection::Next => { + crate::integrations::spotify::devices::next_track(&token, &operation.device_id) + .await + } + SpotifyRemoteSkipDirection::Previous => { + crate::integrations::spotify::devices::previous_track( + &token, + &operation.device_id, + ) + .await + } }; - let _ = tx.send(result); + let _ = tx.send(SpotifyRemoteSkipResult { + id: operation.id, + device_id: operation.device_id, + result, + }); }); - self.start_playback_polling(); + self.spotify.remote_skip_task = Some(handle); } pub(super) async fn spotify_play_next(&mut self) { match self.spotify_control_target() { - super::SpotifyControlTarget::Remote { token, device_id } => { - self.spotify_remote_skip(token, device_id, true); + super::SpotifyControlTarget::Remote { device_id, .. } => { + self.spotify_remote_skip(device_id, SpotifyRemoteSkipDirection::Next); } super::SpotifyControlTarget::Native => self.native_next(), super::SpotifyControlTarget::None => { @@ -1154,8 +1285,8 @@ impl App { pub(super) async fn spotify_play_previous(&mut self) { match self.spotify_control_target() { - super::SpotifyControlTarget::Remote { token, device_id } => { - self.spotify_remote_skip(token, device_id, false); + super::SpotifyControlTarget::Remote { device_id, .. } => { + self.spotify_remote_skip(device_id, SpotifyRemoteSkipDirection::Previous); } super::SpotifyControlTarget::Native => self.native_prev(), super::SpotifyControlTarget::None => { @@ -1680,6 +1811,162 @@ mod tests { } } + #[tokio::test] + async fn stopping_remote_polling_cancels_current_and_queued_skips() { + let mut app = App::new().await; + app.spotify + .remote_skip_queue + .enqueue("device".to_string(), SpotifyRemoteSkipDirection::Next); + app.spotify + .remote_skip_queue + .enqueue("device".to_string(), SpotifyRemoteSkipDirection::Previous); + let operation = app + .spotify + .remote_skip_queue + .begin_next() + .expect("remote skip starts"); + let (tx, rx) = std::sync::mpsc::channel(); + app.spotify.remote_skip_result_rx = Some(rx); + let task = tokio::spawn(async { std::future::pending::<()>().await }); + let abort_handle = task.abort_handle(); + app.spotify.remote_skip_task = Some(task); + + app.stop_playback_polling(); + tokio::task::yield_now().await; + + assert!(abort_handle.is_finished()); + assert!(app.spotify.remote_skip_task.is_none()); + assert!(app.spotify.remote_skip_result_rx.is_none()); + assert!(app.spotify.remote_skip_queue.begin_next().is_none()); + assert!( + tx.send(SpotifyRemoteSkipResult { + id: operation.id, + device_id: operation.device_id, + result: Ok(()), + }) + .is_err(), + "remote skip result receiver was dropped" + ); + } + + #[tokio::test] + async fn remote_skip_result_from_previous_device_does_not_update_player_state() { + let mut app = App::new().await; + app.spotify.access_token = Some("token".to_string()); + app.spotify.active_device_id = Some("new-device".to_string()); + app.spotify.active_backend = Some(SpotifyPlaybackBackend::Remote); + app.spotify.player_status = SpotifyPlayerStatus::Loading; + + app.spotify + .remote_skip_queue + .enqueue("old-device".to_string(), SpotifyRemoteSkipDirection::Next); + let operation = app + .spotify + .remote_skip_queue + .begin_next() + .expect("skip starts"); + let (tx, rx) = std::sync::mpsc::channel(); + app.spotify.remote_skip_result_rx = Some(rx); + tx.send(SpotifyRemoteSkipResult { + id: operation.id, + device_id: operation.device_id, + result: Err(crate::integrations::spotify::SpotifyError::Unauthorized), + }) + .expect("receiver is alive"); + + app.poll_spotify_remote_skip_result(); + + assert_eq!( + app.spotify.active_backend, + Some(SpotifyPlaybackBackend::Remote) + ); + assert!(matches!( + app.spotify.player_status, + SpotifyPlayerStatus::Loading + )); + } + + #[tokio::test] + async fn unauthorized_remote_skip_waits_for_token_refresh_before_dispatching_next() { + let mut app = App::new().await; + app.config.spotify.playback_mode = SpotifyPlaybackMode::Remote; + app.spotify.access_token = Some("expired-token".to_string()); + app.spotify.active_device_id = Some("device".to_string()); + app.spotify.active_backend = Some(SpotifyPlaybackBackend::Remote); + app.spotify + .remote_skip_queue + .enqueue("device".to_string(), SpotifyRemoteSkipDirection::Next); + app.spotify + .remote_skip_queue + .enqueue("device".to_string(), SpotifyRemoteSkipDirection::Previous); + let operation = app + .spotify + .remote_skip_queue + .begin_next() + .expect("first skip starts"); + let (tx, rx) = std::sync::mpsc::channel(); + app.spotify.remote_skip_result_rx = Some(rx); + tx.send(SpotifyRemoteSkipResult { + id: operation.id, + device_id: operation.device_id, + result: Err(crate::integrations::spotify::SpotifyError::Unauthorized), + }) + .expect("receiver is alive"); + + app.poll_spotify_remote_skip_result(); + + assert!(app.spotify.remote_skip_paused_for_token_refresh); + assert!(app.spotify.remote_skip_task.is_none()); + assert!(app.spotify.remote_skip_result_rx.is_none()); + app.spotify_play_next().await; + assert!( + app.spotify.remote_skip_task.is_none(), + "new input must not resume the queue with the expired token" + ); + let pending = app + .spotify + .remote_skip_queue + .begin_next() + .expect("queued skip waits for refreshed credentials"); + assert_eq!(pending.direction, SpotifyRemoteSkipDirection::Previous); + } + + #[tokio::test] + async fn remote_skip_result_in_native_mode_does_not_restart_polling_or_dispatch_queue() { + let mut app = App::new().await; + app.config.spotify.playback_mode = SpotifyPlaybackMode::Native; + app.spotify.access_token = Some("token".to_string()); + app.spotify.active_device_id = Some("device".to_string()); + app.spotify.active_backend = Some(SpotifyPlaybackBackend::Native); + + app.spotify + .remote_skip_queue + .enqueue("device".to_string(), SpotifyRemoteSkipDirection::Next); + app.spotify + .remote_skip_queue + .enqueue("device".to_string(), SpotifyRemoteSkipDirection::Previous); + let operation = app + .spotify + .remote_skip_queue + .begin_next() + .expect("skip starts"); + let (tx, rx) = std::sync::mpsc::channel(); + app.spotify.remote_skip_result_rx = Some(rx); + tx.send(SpotifyRemoteSkipResult { + id: operation.id, + device_id: operation.device_id, + result: Ok(()), + }) + .expect("receiver is alive"); + + app.poll_spotify_remote_skip_result(); + + assert!(app.spotify.playback_task.is_none()); + assert!(app.spotify.playback_rx.is_none()); + assert!(app.spotify.remote_skip_result_rx.is_none()); + assert!(app.spotify.remote_skip_queue.begin_next().is_none()); + } + #[tokio::test] async fn poll_playlist_tracks_does_not_backfill_playlist_total_from_loaded_tracks() { let mut app = App::new().await; diff --git a/src/app/mod.rs b/src/app/mod.rs index cb3e065..b199bcb 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1071,6 +1071,21 @@ mod tests { app.spotify.playback_task = Some(tokio::spawn(async { std::future::pending::<()>().await; })); + app.spotify.remote_skip_queue.enqueue( + "device".to_string(), + spotify_state::SpotifyRemoteSkipDirection::Next, + ); + app.spotify.remote_skip_queue.enqueue( + "device".to_string(), + spotify_state::SpotifyRemoteSkipDirection::Previous, + ); + let skip_operation = app + .spotify + .remote_skip_queue + .begin_next() + .expect("remote skip starts"); + let (skip_tx, skip_rx) = std::sync::mpsc::channel(); + app.spotify.remote_skip_result_rx = Some(skip_rx); app.set_spotify_playback_mode(SpotifyPlaybackMode::Native); @@ -1082,6 +1097,18 @@ mod tests { assert!(app.spotify.playback.is_none()); assert!(app.spotify.playback_rx.is_none()); assert!(app.spotify.playback_task.is_none()); + assert!(app.spotify.remote_skip_result_rx.is_none()); + assert!(app.spotify.remote_skip_queue.begin_next().is_none()); + assert!( + skip_tx + .send(spotify_state::SpotifyRemoteSkipResult { + id: skip_operation.id, + device_id: skip_operation.device_id, + result: Ok(()), + }) + .is_err(), + "skip result receiver was dropped" + ); } #[tokio::test] diff --git a/src/app/spotify_state.rs b/src/app/spotify_state.rs index 4552825..66442ac 100644 --- a/src/app/spotify_state.rs +++ b/src/app/spotify_state.rs @@ -21,6 +21,137 @@ type PlaylistsResultRx = std::sync::mpsc::Receiver, bool), SpotifyError>>; type AlbumsResultRx = std::sync::mpsc::Receiver, bool), SpotifyError>>; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum SpotifyRemoteSkipDirection { + Next, + Previous, +} + +#[derive(Debug)] +pub(super) struct SpotifyRemoteSkipOperation { + pub(super) id: u64, + pub(super) device_id: String, + pub(super) direction: SpotifyRemoteSkipDirection, +} + +#[derive(Debug)] +pub(super) struct SpotifyRemoteSkipResult { + pub(super) id: u64, + pub(super) device_id: String, + pub(super) result: Result<(), SpotifyError>, +} + +#[derive(Debug, PartialEq, Eq)] +struct SpotifyRemoteSkipInFlight { + id: u64, + device_id: String, +} + +#[derive(Debug, Default)] +pub(super) struct SpotifyRemoteSkipQueue { + next_id: u64, + pending: VecDeque, + in_flight: Option, +} + +impl SpotifyRemoteSkipQueue { + pub(super) fn enqueue( + &mut self, + device_id: String, + direction: SpotifyRemoteSkipDirection, + ) -> u64 { + let id = self.next_id; + self.next_id = self + .next_id + .checked_add(1) + .expect("Spotify remote skip operation ID exhausted"); + self.pending.push_back(SpotifyRemoteSkipOperation { + id, + device_id, + direction, + }); + id + } + + pub(super) fn begin_next(&mut self) -> Option { + if self.in_flight.is_some() { + return None; + } + let operation = self.pending.pop_front()?; + self.in_flight = Some(SpotifyRemoteSkipInFlight { + id: operation.id, + device_id: operation.device_id.clone(), + }); + Some(operation) + } + + pub(super) fn complete(&mut self, id: u64, device_id: &str) -> bool { + let is_current = self + .in_flight + .as_ref() + .is_some_and(|current| current.id == id && current.device_id == device_id); + if is_current { + self.in_flight = None; + } + is_current + } + + pub(super) fn abandon_current(&mut self) { + self.in_flight = None; + } + + pub(super) fn invalidate(&mut self) { + self.pending.clear(); + self.in_flight = None; + } +} + +#[cfg(test)] +mod remote_skip_queue_tests { + use super::*; + + #[test] + fn remote_skips_are_started_one_at_a_time_in_input_order() { + let mut queue = SpotifyRemoteSkipQueue::default(); + let first_id = queue.enqueue("device".to_string(), SpotifyRemoteSkipDirection::Next); + let second_id = queue.enqueue("device".to_string(), SpotifyRemoteSkipDirection::Previous); + + let first = queue.begin_next().expect("first skip starts"); + assert_eq!(first.id, first_id); + assert_eq!(first.direction, SpotifyRemoteSkipDirection::Next); + assert!(queue.begin_next().is_none(), "only one skip may run"); + + assert!(queue.complete(first.id, &first.device_id)); + let second = queue.begin_next().expect("second skip starts next"); + assert_eq!(second.id, second_id); + assert_eq!(second.direction, SpotifyRemoteSkipDirection::Previous); + } + + #[test] + fn stale_result_cannot_release_the_current_operation() { + let mut queue = SpotifyRemoteSkipQueue::default(); + queue.enqueue("device".to_string(), SpotifyRemoteSkipDirection::Next); + let current = queue.begin_next().expect("skip starts"); + + assert!(!queue.complete(current.id + 1, ¤t.device_id)); + assert!(!queue.complete(current.id, "different-device")); + assert!(queue.begin_next().is_none()); + assert!(queue.complete(current.id, ¤t.device_id)); + } + + #[test] + fn invalidating_remote_skips_drops_current_and_pending_operations() { + let mut queue = SpotifyRemoteSkipQueue::default(); + queue.enqueue("device".to_string(), SpotifyRemoteSkipDirection::Next); + queue.enqueue("device".to_string(), SpotifyRemoteSkipDirection::Previous); + queue.begin_next().expect("first skip starts"); + + queue.invalidate(); + + assert!(queue.begin_next().is_none()); + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum SpotifyPlaybackBackend { Remote, @@ -82,6 +213,10 @@ pub struct SpotifyState { Option>>, pub(super) play_result_rx: Option>>, + pub(super) remote_skip_queue: SpotifyRemoteSkipQueue, + pub(super) remote_skip_paused_for_token_refresh: bool, + pub(super) remote_skip_task: Option>, + pub(super) remote_skip_result_rx: Option>, pub(super) save_track_rx: Option>>, pub playback_queue: VecDeque, @@ -165,6 +300,7 @@ impl SpotifyState { abort(&mut self.search_more_task); abort(&mut self.devices_task); abort(&mut self.playback_task); + abort(&mut self.remote_skip_task); abort(&mut self.token_refresh_task); abort(&mut self.radio_task); abort(&mut self.liked_task); @@ -225,6 +361,10 @@ impl Default for SpotifyState { token_refresh_task: None, token_refresh_rx: None, play_result_rx: None, + remote_skip_queue: SpotifyRemoteSkipQueue::default(), + remote_skip_paused_for_token_refresh: false, + remote_skip_task: None, + remote_skip_result_rx: None, save_track_rx: None, playback_queue: VecDeque::new(), radio_queue: VecDeque::new(), diff --git a/src/main.rs b/src/main.rs index 6f692e6..a87999c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -194,6 +194,7 @@ async fn run(tui: &mut terminal::Tui) -> Result<()> { app.poll_spotify_auth(); app.poll_token_refresh(); app.poll_spotify_play_result(); + app.poll_spotify_remote_skip_result(); app.poll_spotify_search(); app.poll_spotify_search_more(); app.poll_spotify_player_events(); From 1265029b2df51a275966d168dc01a31da9aeea2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gurruchaga?= Date: Sun, 9 Aug 2026 11:56:44 -0400 Subject: [PATCH 13/19] Use a monotonic clock for stream stall detection (#268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What does this PR do? Replaces wall-clock Unix timestamps with a shared monotonic `ProgressClock` for stream download progress tracking. Both stall consumers now use `std::time::Instant`: - The file-backed YouTube reader still signals an underrun after 12 seconds without download progress. - The player-side stall check still schedules reconnection after 30 seconds for live streams or 60 seconds for on-demand streams. This prevents manual clock changes, NTP corrections, and other wall-clock adjustments from causing false stalls or delaying recovery. The progress clock also recovers from poisoned locks, matching the existing stream lock behavior. Deterministic tests cover the 12, 30, and 60-second thresholds without sleeping or depending on the system clock. Closes #264 ## Type of change - [x] Bug fix - [ ] New feature - [ ] Documentation - [x] Refactor / cleanup - [ ] Other: ## Checklist - [x] `cargo fmt` passes - [x] `cargo clippy -- -D warnings` passes - [x] UI strings added/changed are present in both `locales/en.json` and `locales/es.json` (no UI strings changed) - [x] No `unwrap()` introduced — uses `expect("reason")` or `?` - [ ] Tested manually on Windows ## Testing - `cargo test` — 245 passed - `cargo clippy --all-targets --all-features -- -D warnings` - `cargo fmt --check` - `git diff --check` --- src/audio/player.rs | 92 ++++++++++++++++++--------- src/audio/stream.rs | 147 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 180 insertions(+), 59 deletions(-) diff --git a/src/audio/player.rs b/src/audio/player.rs index 90e79a1..8ddd520 100644 --- a/src/audio/player.rs +++ b/src/audio/player.rs @@ -10,7 +10,7 @@ use tokio::sync::{mpsc, watch}; use tracing::{error, info, warn}; use crate::audio::meter::rms_to_db; -use crate::audio::stream::{FileBackedDownload, FileBackedReader, StreamReader}; +use crate::audio::stream::{FileBackedDownload, FileBackedReader, ProgressClock, StreamReader}; use crate::station::Station; pub enum PlayerCommand { @@ -388,7 +388,7 @@ struct AudioLoopState { reconnect_count: u32, stream_retry_at: Option<(u32, std::time::Instant)>, od: OnDemandTracker, - stream_last_chunk: Option>, + stream_progress: Option, stream_download_done: Option>, download_complete: bool, file_backed: bool, @@ -416,7 +416,7 @@ impl AudioLoopState { reconnect_count: 0, stream_retry_at: None, od: OnDemandTracker::inactive(), - stream_last_chunk: None, + stream_progress: None, stream_download_done: None, download_complete: false, file_backed: false, @@ -433,7 +433,7 @@ struct StreamConnection { player: Player, duration_secs: Option, title_rx: std_mpsc::Receiver, - last_chunk: Arc, + progress_clock: ProgressClock, download_done: Arc, file_backed: bool, written: Option>, @@ -583,7 +583,7 @@ fn cancel_file_backed_download( .file_download .take() .and_then(|download| download.cancel(handle)); - st.stream_last_chunk = None; + st.stream_progress = None; st.stream_download_done = None; st.file_backed = false; st.stream_written = None; @@ -601,7 +601,7 @@ fn cancel_file_backed_download( fn check_download_done(st: &mut AudioLoopState) -> bool { if let Some(ref arc) = st.stream_download_done { if arc.load(Ordering::Acquire) { - st.stream_last_chunk = None; + st.stream_progress = None; st.stream_download_done = None; st.download_complete = true; st.file_download = None; @@ -649,29 +649,28 @@ fn check_on_demand_finished(st: &mut AudioLoopState, state_tx: &watch::Sender stall_threshold * 1000 { + if stalled_for > std::time::Duration::from_secs(stall_threshold) { let delay = backoff_duration( st.reconnect_count, BASE_RECONNECT_DELAY_SECS, @@ -683,8 +682,8 @@ fn check_stream_stall(st: &mut AudioLoopState) { delay.as_secs_f32(), st.reconnect_count + 1 ); - st.stream_last_chunk = None; - st.reconnect_at = Some(std::time::Instant::now() + delay); + st.stream_progress = None; + st.reconnect_at = Some(now + delay); st.reconnect_count += 1; } } @@ -711,7 +710,7 @@ fn open_stream( station.custom_headers.clone(), handle.clone(), ); - let last_chunk = stream_reader.last_chunk_arc(); + let progress_clock = stream_reader.progress_clock(); let download_done = stream_reader.download_done_arc(); let dead_url_arc = stream_reader.dead_url_arc(); @@ -745,7 +744,7 @@ fn open_stream( player, duration_secs, title_rx, - last_chunk, + progress_clock, download_done, file_backed: false, written: None, @@ -804,7 +803,7 @@ fn open_file_backed_stream( Err(e) => return Err(Some(format!("cache file: {e}"))), }; - let last_chunk = reader.last_chunk_arc(); + let progress_clock = reader.progress_clock(); let download_done = reader.download_done_arc(); let dead_url_arc = reader.dead_url_arc(); let written = reader.written_arc(); @@ -836,7 +835,7 @@ fn open_file_backed_stream( player, duration_secs, title_rx, - last_chunk, + progress_clock, download_done, file_backed: true, written: Some(written), @@ -947,7 +946,7 @@ fn handle_play_cmd( Ok(conn) => { st.stream_retry_at = None; st.title_rx = Some(conn.title_rx); - st.stream_last_chunk = Some(conn.last_chunk); + st.stream_progress = Some(conn.progress_clock); st.stream_download_done = Some(conn.download_done); st.file_backed = conn.file_backed; st.stream_written = conn.written; @@ -1002,7 +1001,7 @@ fn handle_crossfade_cmd( Ok(conn) => { st.stream_retry_at = None; st.title_rx = Some(conn.title_rx); - st.stream_last_chunk = Some(conn.last_chunk); + st.stream_progress = Some(conn.progress_clock); st.stream_download_done = Some(conn.download_done); st.file_backed = conn.file_backed; st.stream_written = conn.written; @@ -1172,7 +1171,7 @@ fn handle_seek_cmd( let (mut stream_reader, new_title_rx) = StreamReader::connect(url, byte_offset, 4096, custom_headers, handle.clone()); st.title_rx = Some(new_title_rx); - st.stream_last_chunk = Some(stream_reader.last_chunk_arc()); + st.stream_progress = Some(stream_reader.progress_clock()); st.stream_download_done = Some(stream_reader.download_done_arc()); st.download_complete = false; let prebuffer_secs = st @@ -1259,7 +1258,7 @@ fn handle_device_change( stop_crossfade_out(st); cancel_file_backed_download(st, handle, true); st.title_rx = None; - st.stream_last_chunk = None; + st.stream_progress = None; st.stream_download_done = None; st.download_complete = false; st.file_backed = false; @@ -1554,7 +1553,7 @@ fn audio_loop( st.reconnect_at = None; st.reconnect_count = 0; st.stream_retry_at = None; - st.stream_last_chunk = None; + st.stream_progress = None; st.stream_download_done = None; st.download_complete = false; st.file_backed = false; @@ -1612,6 +1611,43 @@ mod tests { assert_eq!(on_demand_byte_offset(10.0, &station), 160_000); } + #[test] + fn live_stream_stall_reconnects_only_after_thirty_seconds() { + let started = std::time::Instant::now(); + let mut st = AudioLoopState::new(); + st.current_station = Some(station_with_bitrate(None)); + st.stream_progress = Some(ProgressClock::with_last_progress_at(started)); + + check_stream_stall_at(&mut st, started + std::time::Duration::from_secs(30)); + assert!(st.reconnect_at.is_none()); + + let stalled_at = started + std::time::Duration::from_millis(30_001); + check_stream_stall_at(&mut st, stalled_at); + + assert!(st.reconnect_at.is_some_and(|at| at > stalled_at)); + assert!(st.stream_progress.is_none()); + assert_eq!(st.reconnect_count, 1); + } + + #[test] + fn on_demand_stall_reconnects_only_after_sixty_seconds() { + let started = std::time::Instant::now(); + let mut st = AudioLoopState::new(); + st.current_station = Some(station_with_bitrate(None)); + st.od.active = true; + st.stream_progress = Some(ProgressClock::with_last_progress_at(started)); + + check_stream_stall_at(&mut st, started + std::time::Duration::from_secs(60)); + assert!(st.reconnect_at.is_none()); + + let stalled_at = started + std::time::Duration::from_millis(60_001); + check_stream_stall_at(&mut st, stalled_at); + + assert!(st.reconnect_at.is_some_and(|at| at > stalled_at)); + assert!(st.stream_progress.is_none()); + assert_eq!(st.reconnect_count, 1); + } + #[test] fn conditional_preview_stop_ignores_stale_preview_timer() { let mut st = AudioLoopState::new(); @@ -1680,7 +1716,7 @@ mod tests { FileBackedDownload::from_parts_for_test(task, path.clone(), Arc::clone(&done)); let mut st = AudioLoopState::new(); st.file_backed = true; - st.stream_last_chunk = Some(Arc::new(AtomicU64::new(1))); + st.stream_progress = Some(ProgressClock::default()); st.stream_download_done = Some(done); st.stream_written = Some(Arc::new(AtomicU64::new(1))); st.stream_total = Some(Arc::new(AtomicU64::new(10))); @@ -1690,7 +1726,7 @@ mod tests { assert!(st.file_download.is_none()); assert!(!st.file_backed); - assert!(st.stream_last_chunk.is_none()); + assert!(st.stream_progress.is_none()); assert!(st.stream_download_done.is_none()); assert!(st.stream_written.is_none()); assert!(st.stream_total.is_none()); diff --git a/src/audio/stream.rs b/src/audio/stream.rs index ddd9880..4d04d03 100644 --- a/src/audio/stream.rs +++ b/src/audio/stream.rs @@ -5,15 +5,54 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use std::sync::{mpsc, Mutex}; +use std::time::{Duration, Instant}; use crate::metadata::parse_icy_title; +#[derive(Clone, Default)] +pub(crate) struct ProgressClock { + last_progress: Arc>>, +} + +impl ProgressClock { + pub(crate) fn record_progress(&self) { + self.record_progress_at(Instant::now()); + } + + fn record_progress_at(&self, now: Instant) { + let mut last_progress = self + .last_progress + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + *last_progress = Some(now); + } + + pub(crate) fn elapsed(&self) -> Option { + self.elapsed_at(Instant::now()) + } + + pub(crate) fn elapsed_at(&self, now: Instant) -> Option { + let last_progress = *self + .last_progress + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + last_progress.map(|last| now.saturating_duration_since(last)) + } + + #[cfg(test)] + pub(crate) fn with_last_progress_at(last_progress: Instant) -> Self { + Self { + last_progress: Arc::new(Mutex::new(Some(last_progress))), + } + } +} + pub struct StreamReader { rx: Mutex>, chunks: VecDeque, offset: usize, buffered: usize, - last_chunk_at: Arc, + progress_clock: ProgressClock, download_done: Arc, dead_url: Arc, } @@ -23,16 +62,12 @@ impl StreamReader { if chunk.is_empty() { return; } - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0); - self.last_chunk_at.store(now, Ordering::Release); + self.progress_clock.record_progress(); self.buffered += chunk.len(); self.chunks.push_back(chunk); } - pub fn last_chunk_arc(&self) -> Arc { - Arc::clone(&self.last_chunk_at) + pub(crate) fn progress_clock(&self) -> ProgressClock { + self.progress_clock.clone() } pub fn connect( @@ -66,13 +101,13 @@ impl StreamReader { } }); - let last_chunk_at = Arc::new(AtomicU64::new(0)); + let progress_clock = ProgressClock::default(); let reader = Self { rx: Mutex::new(audio_rx), chunks: VecDeque::new(), offset: 0, buffered: 0, - last_chunk_at, + progress_clock, download_done, dead_url, }; @@ -96,13 +131,13 @@ impl StreamReader { } }); - let last_chunk_at = Arc::new(AtomicU64::new(0)); + let progress_clock = ProgressClock::default(); Self { rx: Mutex::new(audio_rx), chunks: VecDeque::new(), offset: 0, buffered: 0, - last_chunk_at, + progress_clock, download_done: Arc::new(AtomicBool::new(false)), dead_url: Arc::new(AtomicBool::new(false)), } @@ -186,13 +221,10 @@ impl Seek for StreamReader { const MAX_DOWNLOAD_RETRIES: u32 = 5; const DOWNLOAD_RETRY_PAUSE: std::time::Duration = std::time::Duration::from_secs(2); const FILE_READ_STARVATION_SECS: u64 = 30; -const FILE_READ_STALL_MS: u64 = 12_000; +const FILE_READ_STALL_SECS: u64 = 12; -fn unix_now_ms() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0) +fn file_read_is_stalled(stalled_for: Option) -> bool { + stalled_for.is_some_and(|elapsed| elapsed >= Duration::from_secs(FILE_READ_STALL_SECS)) } pub fn youtube_cache_dir() -> std::path::PathBuf { @@ -213,7 +245,7 @@ pub struct FileBackedReader { pos: u64, written: Arc, total_len: Arc, - last_chunk_at: Arc, + progress_clock: ProgressClock, download_done: Arc, dead_url: Arc, } @@ -302,13 +334,13 @@ impl FileBackedReader { let (title_tx, title_rx) = mpsc::sync_channel::(1); let written = Arc::new(AtomicU64::new(0)); let total_len = Arc::new(AtomicU64::new(0)); - let last_chunk_at = Arc::new(AtomicU64::new(0)); + let progress_clock = ProgressClock::default(); let download_done = Arc::new(AtomicBool::new(false)); let dead_url = Arc::new(AtomicBool::new(false)); let written_task = Arc::clone(&written); let total_task = Arc::clone(&total_len); - let last_chunk_task = Arc::clone(&last_chunk_at); + let progress_clock_task = progress_clock.clone(); let done_task = Arc::clone(&download_done); let dead_task = Arc::clone(&dead_url); let task = handle.spawn(async move { @@ -318,7 +350,7 @@ impl FileBackedReader { write_file, written_task, total_task, - last_chunk_task, + progress_clock_task, done_task, dead_task, ) @@ -340,7 +372,7 @@ impl FileBackedReader { pos: 0, written, total_len, - last_chunk_at, + progress_clock, download_done, dead_url, }, @@ -349,8 +381,8 @@ impl FileBackedReader { )) } - pub fn last_chunk_arc(&self) -> Arc { - Arc::clone(&self.last_chunk_at) + pub(crate) fn progress_clock(&self) -> ProgressClock { + self.progress_clock.clone() } pub fn download_done_arc(&self) -> Arc { @@ -415,16 +447,15 @@ impl Read for FileBackedReader { ); return Ok(0); } - let last_chunk = self.last_chunk_at.load(Ordering::Acquire); - let stalled_ms = unix_now_ms().saturating_sub(last_chunk); - let stalled = last_chunk != 0 && stalled_ms >= FILE_READ_STALL_MS; + let stalled_for = self.progress_clock.elapsed(); + let stalled = file_read_is_stalled(stalled_for); if stalled || wait_start.elapsed() > std::time::Duration::from_secs(FILE_READ_STARVATION_SECS) { tracing::warn!( pos = self.pos, written, - stalled_ms, + stalled_ms = stalled_for.map(|elapsed| elapsed.as_millis()), "File-backed read underrun: download not progressing, signaling EOF" ); return Ok(0); @@ -468,7 +499,7 @@ async fn download_to_file( mut file: std::fs::File, written: Arc, total_len: Arc, - last_chunk_at: Arc, + progress_clock: ProgressClock, download_done: Arc, dead_url: Arc, ) -> Result<(), reqwest::Error> { @@ -567,7 +598,7 @@ async fn download_to_file( } current_offset += bytes.len() as u64; written.store(current_offset, Ordering::Release); - last_chunk_at.store(unix_now_ms(), Ordering::Release); + progress_clock.record_progress(); retry_count = 0; } Ok(Some(Err(e))) => { @@ -1103,6 +1134,60 @@ mod tests { use super::*; use std::sync::mpsc; + #[test] + fn progress_clock_starts_without_progress() { + let clock = ProgressClock::default(); + + assert_eq!(clock.elapsed_at(Instant::now()), None); + } + + #[test] + fn progress_clock_measures_elapsed_time_monotonically() { + let started = Instant::now(); + let clock = ProgressClock::with_last_progress_at(started); + + assert_eq!( + clock.elapsed_at(started + Duration::from_secs(11)), + Some(Duration::from_secs(11)) + ); + assert_eq!( + clock.elapsed_at(started + Duration::from_secs(FILE_READ_STALL_SECS)), + Some(Duration::from_secs(FILE_READ_STALL_SECS)) + ); + + clock.record_progress_at(started + Duration::from_secs(20)); + assert_eq!( + clock.elapsed_at(started + Duration::from_secs(25)), + Some(Duration::from_secs(5)) + ); + } + + #[test] + fn file_read_stall_uses_the_twelve_second_threshold() { + assert!(!file_read_is_stalled(None)); + assert!(!file_read_is_stalled(Some(Duration::from_secs(11)))); + assert!(file_read_is_stalled(Some(Duration::from_secs(12)))); + } + + #[test] + fn progress_clock_recovers_from_a_poisoned_lock() { + let clock = ProgressClock::default(); + let poisoned_clock = clock.clone(); + let _ = std::thread::spawn(move || { + let _guard = poisoned_clock + .last_progress + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + panic!("poison progress clock for test"); + }) + .join(); + let now = Instant::now(); + + clock.record_progress_at(now); + + assert_eq!(clock.elapsed_at(now), Some(Duration::ZERO)); + } + fn make_stripper(metaint: usize) -> (IcyStripper, mpsc::Receiver) { let (tx, rx) = mpsc::sync_channel(8); (IcyStripper::new(metaint, tx), rx) From ab0f320d32f95afde1f73c4d90e38b2c8db99927 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:53:05 -0400 Subject: [PATCH 14/19] build(deps): bump rand from 0.9.4 to 0.10.1 (#259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [rand](https://github.com/rust-random/rand) from 0.9.4 to 0.10.1.
Changelog

Sourced from rand's changelog.

[0.10.1] — 2026-02-11

This release includes a fix for a soundness bug; see #1763.

Changes

  • Document panic behavior of make_rng and add #[track_caller] (#1761)
  • Deprecate feature log (#1763)

#1761: rust-random/rand#1761 #1763: rust-random/rand#1763

[0.10.0] - 2026-02-08

Changes

  • The dependency on rand_chacha has been replaced with a dependency on chacha20. This changes the implementation behind StdRng, but the output remains the same. There may be some API breakage when using the ChaCha-types directly as these are now the ones in chacha20 instead of rand_chacha (#1642).
  • Rename fns IndexedRandom::choose_multiple -> sample, choose_multiple_array -> sample_array, choose_multiple_weighted -> sample_weighted, struct SliceChooseIter -> IndexedSamples and fns IteratorRandom::choose_multiple -> sample, choose_multiple_fill -> sample_fill (#1632)
  • Use Edition 2024 and MSRV 1.85 (#1653)
  • Let Fill be implemented for element types, not sliceable types (#1652)
  • Fix OsError::raw_os_error on UEFI targets by returning Option<usize> (#1665)
  • Replace fn TryRngCore::read_adapter(..) -> RngReadAdapter with simpler struct RngReader (#1669)
  • Remove fns SeedableRng::from_os_rng, try_from_os_rng (#1674)
  • Remove Clone support for StdRng, ReseedingRng (#1677)
  • Use postcard instead of bincode to test the serde feature (#1693)
  • Avoid excessive allocation in IteratorRandom::sample when amount is much larger than iterator size (#1695)
  • Rename os_rng -> sys_rng, OsRng -> SysRng, OsError -> SysError (#1697)
  • Rename Rng -> RngExt as upstream rand_core has renamed RngCore -> Rng (#1717)

Additions

  • Add fns IndexedRandom::choose_iter, choose_weighted_iter (#1632)
  • Pub export Xoshiro128PlusPlus, Xoshiro256PlusPlus prngs (#1649)
  • Pub export ChaCha8Rng, ChaCha12Rng, ChaCha20Rng behind chacha feature (#1659)
  • Fn rand::make_rng() -> R where R: SeedableRng (#1734)

Removals

  • Removed ReseedingRng (#1722)
  • Removed unused feature "nightly" (#1732)
  • Removed feature small_rng (#1732)

#1632: rust-random/rand#1632 #1642: rust-random/rand#1642 #1649: rust-random/rand#1649 #1652: rust-random/rand#1652 #1653: rust-random/rand#1653 #1659: rust-random/rand#1659 #1665: rust-random/rand#1665 #1669: rust-random/rand#1669 #1674: rust-random/rand#1674 #1677: rust-random/rand#1677 #1693: rust-random/rand#1693 #1695: rust-random/rand#1695 #1697: rust-random/rand#1697

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rand&package-manager=cargo&previous-version=0.9.4&new-version=0.10.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: José Gurruchaga --- Cargo.lock | 31 ++++++++++++++++++++++++++++++- Cargo.toml | 2 +- src/integrations/spotify/oauth.rs | 2 +- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4ebff3..e0f9cd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,6 +342,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.45" @@ -1225,6 +1236,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -3110,6 +3122,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3148,6 +3171,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_distr" version = "0.5.1" @@ -3380,7 +3409,7 @@ dependencies = [ "librespot-metadata", "librespot-playback", "nucleo-matcher", - "rand 0.9.4", + "rand 0.10.1", "ratatui", "reqwest", "rodio 0.22.2", diff --git a/Cargo.toml b/Cargo.toml index 7b55d64..09e33a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ serde_json = "1" chrono = "0.4" sha2 = "0.11" tar = "0.4" -rand = { version = "0.9", default-features = false, features = ["std", "std_rng"] } +rand = { version = "0.10", default-features = false, features = ["std", "std_rng", "thread_rng"] } base64 = "0.21" dunce = "1" zip = { version = "8", default-features = false, features = ["deflate"] } diff --git a/src/integrations/spotify/oauth.rs b/src/integrations/spotify/oauth.rs index 4971cfd..6df31ab 100644 --- a/src/integrations/spotify/oauth.rs +++ b/src/integrations/spotify/oauth.rs @@ -1,5 +1,5 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use rand::RngCore; +use rand::Rng; use sha2::{Digest, Sha256}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, From 2be1a96c3d32ef2df585a0cfdafb8af19fa806de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:16:28 -0400 Subject: [PATCH 15/19] build(deps): bump clap from 4.6.1 to 4.6.6 (#270) Bumps [clap](https://github.com/clap-rs/clap) from 4.6.1 to 4.6.6.
Release notes

Sourced from clap's releases.

v4.6.6

[4.6.6] - 2026-08-06

Features

  • Add Command::get_overridden_usage

v4.6.5

[4.6.5] - 2026-07-31

Fixes

  • (help) Correctly mark which value_names are optional with num_args

v4.6.4

[4.6.4] - 2026-07-21

Internal

  • Update to syn v3

v4.6.3

[4.6.3] - 2026-07-20

Fixes

  • (derive) Allow "literal".function() as attribute values

v4.6.2

[4.6.2] - 2026-07-15

Fixes

  • (help) Say alias when there is only one
Changelog

Sourced from clap's changelog.

[4.6.6] - 2026-08-06

Features

  • Add Command::get_overridden_usage

[4.6.5] - 2026-07-31

Fixes

  • (help) Correctly mark which value_names are optional with num_args

[4.6.4] - 2026-07-21

Internal

  • Update to syn v3

[4.6.3] - 2026-07-20

Fixes

  • (derive) Allow "literal".function() as attribute values

[4.6.2] - 2026-07-15

Fixes

  • (help) Say alias when there is only one
Commits
  • 348cff3 chore: Release
  • d478377 docs: Update changelog
  • 04b9fbb Merge pull request #6414 from koopatroopa787/fix-bash-completion-bracket-glob
  • 7075239 Merge pull request #6422 from BaumiCoder/fix-fish-indentations
  • f90a966 fix(complete): Use spaces for indentation in fish
  • dd4997b fix(complete): Don't glob-expand bash positionals
  • 8387c81 Merge pull request #6399 from clap-rs/renovate/crate-ci-typos-1.x
  • 8141e11 chore(deps): Update compatible (dev) (#6398)
  • 8a6bd4e chore(deps): Update pre-commit hook crate-ci/typos to v1.47.0
  • 71a7213 chore(deps): Update Rust Stable to v1.96 (#6396)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=clap&package-manager=cargo&previous-version=4.6.1&new-version=4.6.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0f9cd2..4f5afe2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -379,9 +379,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -389,9 +389,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -401,14 +401,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] [[package]] @@ -4158,6 +4158,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" From 659311cc2c1398f52c26712db41a28d74fbae743 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:16:42 -0400 Subject: [PATCH 16/19] build(deps): bump rand from 0.10.1 to 0.10.2 (#271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [rand](https://github.com/rust-random/rand) from 0.10.1 to 0.10.2.
Changelog

Sourced from rand's changelog.

[0.10.2] — 2026-07-02

Fixes

  • Fix possible memory safety violation due to deserialization of UniformChar from bad source (#1790)

Changes

  • Document required output order of fn partial_shuffle and apply #[must_use] (#1769)
  • Avoid usage of unsafe in contexts where non-local memory corruption could invalidate contract (#1791)

#1769: rust-random/rand#1769 #1790: rust-random/rand#1790 #1791: rust-random/rand#1791

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rand&package-manager=cargo&previous-version=0.10.1&new-version=0.10.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f5afe2..eb6977f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3124,9 +3124,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -3409,7 +3409,7 @@ dependencies = [ "librespot-metadata", "librespot-playback", "nucleo-matcher", - "rand 0.10.1", + "rand 0.10.2", "ratatui", "reqwest", "rodio 0.22.2", From b50db584d76bd0d146a2e8d2f53413ab176b6e46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:16:58 -0400 Subject: [PATCH 17/19] build(deps): bump thiserror from 2.0.18 to 2.0.20 (#272) Bumps [thiserror](https://github.com/dtolnay/thiserror) from 2.0.18 to 2.0.20.
Release notes

Sourced from thiserror's releases.

2.0.20

  • Suppress redundant_field_names clippy lint in generated code (#454)

2.0.19

  • Update to syn 3
Commits
  • b1d5db5 Release 2.0.20
  • c4c3ebd Merge pull request #454 from dtolnay/clippy
  • 2266152 Suppress redundant_field_names clippy lint
  • 2901cfd Raise minimum tested compiler to rust 1.88
  • aa9d91f Update ui tests for version 2.0.19
  • e13a785 Release 2.0.19
  • 0a0e76c Update to syn 3
  • ec42ea7 Update actions/upload-artifact@v6 -> v7
  • 4178c4a Update actions/checkout@v6 -> v7
  • 7214e0e Ignore items_after_statements pedantic clippy lint in test
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=thiserror&package-manager=cargo&previous-version=2.0.18&new-version=2.0.20)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb6977f..6eaea40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1831,7 +1831,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -1906,7 +1906,7 @@ dependencies = [ "librespot-core", "log", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", ] @@ -1924,7 +1924,7 @@ dependencies = [ "protobuf", "rand 0.9.4", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-stream", "uuid", @@ -1976,7 +1976,7 @@ dependencies = [ "sha1", "shannon", "sysinfo", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "tokio", "tokio-stream", @@ -2001,7 +2001,7 @@ dependencies = [ "protobuf", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", "uuid", ] @@ -2015,7 +2015,7 @@ dependencies = [ "oauth2", "open", "reqwest", - "thiserror 2.0.18", + "thiserror 2.0.20", "url", ] @@ -2038,7 +2038,7 @@ dependencies = [ "rodio 0.21.1", "shell-words", "symphonia", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "zerocopy", ] @@ -3039,7 +3039,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -3060,7 +3060,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -3219,7 +3219,7 @@ dependencies = [ "palette", "serde", "strum", - "thiserror 2.0.18", + "thiserror 2.0.20", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -3305,7 +3305,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -3417,7 +3417,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "tar", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "tracing-appender", @@ -3462,7 +3462,7 @@ dependencies = [ "dasp_sample", "num-rational", "symphonia", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -4335,11 +4335,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.20", ] [[package]] @@ -4355,13 +4355,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] [[package]] @@ -4654,7 +4654,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "tracing-subscriber", ] @@ -4731,7 +4731,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.18", + "thiserror 2.0.20", "utf-8", ] From 2f81923bdfaad86caf8fdec83b42f73c65ba614b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:17:31 -0400 Subject: [PATCH 18/19] build(deps): bump github/codeql-action from 4.37.3 to 4.37.9 (#274) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.3 to 4.37.9.
Release notes

Sourced from github/codeql-action's releases.

v4.37.9

  • Update default CodeQL bundle version to 2.26.4. #4106

v4.37.8

No user facing changes.

v4.37.7

  • Update default CodeQL bundle version to 2.26.3. #4085

v4.37.6

  • Changed the default filepath for the new remote file address format that was introduced in CodeQL Action 4.37.0 / 3.37.0 to .github/codeql-config.yml to align it with the suggested path that is used elsewhere. #4070

v4.37.5

  • Fixed a bug where a network error while streaming the download of the CodeQL bundle could terminate the init Action instead of falling back to downloading the bundle before extracting it. #4061

v4.37.4

  • This version of the CodeQL Action adds support for the tools input for the codeql-action/init step to be specified using a github-codeql-tools repository property. This feature will gradually be rolled out following the release of this version. Once rolled out, this allows for the CodeQL CLI version that is used in GitHub-managed workflows, such as Default Setup, to be set to a custom value. For example, customers who run into issues with rate limits when a new CodeQL CLI version is released can set the value to toolcache to always use the CodeQL CLI version that is available in the runner toolcache. For Advanced Setup workflows, the value provided for tools in the workflow definition always takes precedence unless the value of the repository property starts with !. #4037
  • Update default CodeQL bundle version to 2.26.2. #4051
Changelog

Sourced from github/codeql-action's changelog.

4.37.9 - 26 Aug 2026

  • Update default CodeQL bundle version to 2.26.4. #4106

4.37.8 - 21 Aug 2026

No user facing changes.

4.37.7 - 13 Aug 2026

  • Update default CodeQL bundle version to 2.26.3. #4085

4.37.6 - 04 Aug 2026

  • Changed the default filepath for the new remote file address format that was introduced in CodeQL Action 4.37.0 / 3.37.0 to .github/codeql-config.yml to align it with the suggested path that is used elsewhere. #4070

4.37.5 - 03 Aug 2026

  • Fixed a bug where a network error while streaming the download of the CodeQL bundle could terminate the init Action instead of falling back to downloading the bundle before extracting it. #4061

4.37.4 - 29 Jul 2026

  • This version of the CodeQL Action adds support for the tools input for the codeql-action/init step to be specified using a github-codeql-tools repository property. This feature will gradually be rolled out following the release of this version. Once rolled out, this allows for the CodeQL CLI version that is used in GitHub-managed workflows, such as Default Setup, to be set to a custom value. For example, customers who run into issues with rate limits when a new CodeQL CLI version is released can set the value to toolcache to always use the CodeQL CLI version that is available in the runner toolcache. For Advanced Setup workflows, the value provided for tools in the workflow definition always takes precedence unless the value of the repository property starts with !. #4037
  • Update default CodeQL bundle version to 2.26.2. #4051
Commits
  • cdf488f Merge pull request #4107 from github/update-v4.37.9-920ba7cd1
  • 7243f38 Update changelog for v4.37.9
  • 920ba7c Merge pull request #4106 from github/update-bundle/codeql-bundle-v2.26.4
  • ecfa6e1 Add changelog note
  • adcdf4a Update default bundle to codeql-bundle-v2.26.4
  • 486fec2 Merge pull request #4099 from github/update-supported-enterprise-server-versions
  • 134624c Merge pull request #4101 from github/dependabot/npm_and_yarn/npm-minor-457d82...
  • ff43db8 Merge pull request #4103 from github/mergeback/v4.37.8-to-main-db488dde
  • 4605e03 Rebuild
  • 099c869 Update changelog and version after v4.37.8
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=4.37.3&new-version=4.37.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fabc832..0bc647c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,7 +27,7 @@ jobs: sudo apt-get install -y pkg-config libasound2-dev - name: Initialize CodeQL - uses: github/codeql-action/init@v4.37.3 + uses: github/codeql-action/init@v4.37.9 with: languages: rust @@ -35,6 +35,6 @@ jobs: run: cargo build --all-features - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v4.37.3 + uses: github/codeql-action/analyze@v4.37.9 with: category: "/language:rust" From ceaaaa6c4545bb49df095df50608fa1e28070f9a Mon Sep 17 00:00:00 2001 From: sewandev <168495573+sewandev@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:39:47 -0300 Subject: [PATCH 19/19] chore: release v1.5.8 (#275) ## Release v1.5.8 Version bump and changelog cut for the v1.5.8 release. ### Changes - \Cargo.toml\ / \Cargo.lock\: version \1.5.7\ -> \1.5.8\ - \CHANGELOG.md\ / \CHANGELOG.es.md\: moved \[Unreleased]\ to \[1.5.8] - 2026-09-07\, added a fresh empty \[Unreleased]\ / \[Sin publicar]\ ### Included since v1.5.7 - fix: monotonic clock for stream stall detection (#268) - fix: serialize Spotify remote skip commands (#267) - fix: ensure required CI check reports lint failures (#266) - ci: separate lint job and integrate swatinem/rust-cache (#250) - docs: add winget as the easy Windows install method (#249) - build(deps): update Ratatui, Zip, Bytes, Clap, Rand, Thiserror and Actions --- CHANGELOG.es.md | 21 ++++++++++++++++++++- CHANGELOG.md | 21 ++++++++++++++++++++- Cargo.lock | 2 +- Cargo.toml | 2 +- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.es.md b/CHANGELOG.es.md index e3cbd63..ad39cbc 100644 --- a/CHANGELOG.es.md +++ b/CHANGELOG.es.md @@ -10,6 +10,20 @@ Versionado: [Semantic Versioning](https://semver.org/) ## [Sin publicar] +## [1.5.8] - 2026-09-07 + +### Added +- Soporte oficial de instalación mediante Winget: Reverbic ahora se puede instalar en Windows con `winget install Sewandev.Reverbic` como método recomendado junto al script de PowerShell. + +### Fixed +- Los comandos de pista siguiente/anterior en el modo remoto de Spotify ahora se serializan a través de una cola FIFO, permitiendo una sola solicitud en curso a la vez. Esto evita condiciones de carrera y ejecuciones fuera de orden al saltar pistas rápidamente, descarta respuestas obsoletas de dispositivos anteriores y mantiene sincronizado el estado de reproducción remoto. +- La detección de descargas estancadas en streams ahora utiliza un reloj monotónico (`ProgressClock` con `std::time::Instant`) en lugar de marcas de tiempo del reloj del sistema. Esto evita falsas alertas de estancamiento o demoras en la reconexión provocadas por correcciones de NTP, cambios manuales de hora o ajustes de zona horaria. +- El flujo de CI ahora propaga de forma fiable los fallos de lint y formato al check de estado obligatorio, garantizando que las comprobaciones de PR fallen cuando el análisis de código o formato detecta problemas. + +### Changed +- Flujo de CI optimizado: se separaron las revisiones de formato y clippy en un trabajo inicial de fallo rápido y se integró `swatinem/rust-cache` para acelerar notablemente las ejecuciones de compilación y pruebas. +- Actualización general de dependencias: Ratatui 0.30.2, Zip 8.6.0, Bytes 1.12.0, Clap 4.6.6, Rand 0.10.2, Thiserror 2.0.20, y actualización de acciones en los workflows de GitHub. + ## [1.5.7] - 2026-06-21 ### Added @@ -332,7 +346,12 @@ Versionado: [Semantic Versioning](https://semver.org/) - Templates de issues (bug, feature, pregunta) - Logo y assets embebidos en el ejecutable (sin dependencias externas) -[Sin publicar]: https://github.com/sewandev/Reverbic/compare/v1.5.3...HEAD +[Sin publicar]: https://github.com/sewandev/Reverbic/compare/v1.5.8...HEAD +[1.5.8]: https://github.com/sewandev/Reverbic/compare/v1.5.7...v1.5.8 +[1.5.7]: https://github.com/sewandev/Reverbic/compare/v1.5.6...v1.5.7 +[1.5.6]: https://github.com/sewandev/Reverbic/compare/v1.5.5...v1.5.6 +[1.5.5]: https://github.com/sewandev/Reverbic/compare/v1.5.4...v1.5.5 +[1.5.4]: https://github.com/sewandev/Reverbic/compare/v1.5.3...v1.5.4 [1.5.3]: https://github.com/sewandev/Reverbic/compare/v1.5.2...v1.5.3 [1.5.2]: https://github.com/sewandev/Reverbic/compare/v1.5.1...v1.5.2 [1.5.1]: https://github.com/sewandev/Reverbic/compare/v1.5.0...v1.5.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index c20b822..0efd79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ Versioning: [Semantic Versioning](https://semver.org/) ## [Unreleased] +## [1.5.8] - 2026-09-07 + +### Added +- Official Winget installation support: Reverbic can now be installed on Windows with `winget install Sewandev.Reverbic` as the recommended package manager method alongside the PowerShell script. + +### Fixed +- Spotify remote next/previous commands are now serialized through a FIFO queue, allowing only one skip request in flight at a time. This eliminates race conditions and out-of-order execution when rapidly skipping tracks, ignores stale responses from previously active devices, and keeps remote playback state accurately in sync. +- Stream download stall detection now uses a monotonic clock (`ProgressClock` backed by `std::time::Instant`) instead of wall-clock Unix timestamps. This prevents false underruns or delayed stream reconnects caused by NTP adjustments, manual clock updates, or timezone changes. +- CI workflows now reliably propagate lint and formatting failures to the required status check, ensuring PR checks fail whenever upstream linting fails. + +### Changed +- Streamlined CI workflow: separated format and clippy checks into an initial fast-fail lint job and integrated `swatinem/rust-cache` to significantly accelerate build and test pipelines. +- Dependency updates across the board: Ratatui 0.30.2, Zip 8.6.0, Bytes 1.12.0, Clap 4.6.6, Rand 0.10.2, Thiserror 2.0.20, and updated GitHub Actions steps. + ## [1.5.7] - 2026-06-21 ### Added @@ -332,7 +346,12 @@ Versioning: [Semantic Versioning](https://semver.org/) - Issue templates (bug, feature, question) - Logo and assets embedded in the executable (no external dependencies) -[Unreleased]: https://github.com/sewandev/Reverbic/compare/v1.5.3...HEAD +[Unreleased]: https://github.com/sewandev/Reverbic/compare/v1.5.8...HEAD +[1.5.8]: https://github.com/sewandev/Reverbic/compare/v1.5.7...v1.5.8 +[1.5.7]: https://github.com/sewandev/Reverbic/compare/v1.5.6...v1.5.7 +[1.5.6]: https://github.com/sewandev/Reverbic/compare/v1.5.5...v1.5.6 +[1.5.5]: https://github.com/sewandev/Reverbic/compare/v1.5.4...v1.5.5 +[1.5.4]: https://github.com/sewandev/Reverbic/compare/v1.5.3...v1.5.4 [1.5.3]: https://github.com/sewandev/Reverbic/compare/v1.5.2...v1.5.3 [1.5.2]: https://github.com/sewandev/Reverbic/compare/v1.5.1...v1.5.2 [1.5.1]: https://github.com/sewandev/Reverbic/compare/v1.5.0...v1.5.1 diff --git a/Cargo.lock b/Cargo.lock index 6eaea40..f82a2ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3388,7 +3388,7 @@ dependencies = [ [[package]] name = "reverbic" -version = "1.5.7" +version = "1.5.8" dependencies = [ "arboard", "base64 0.21.7", diff --git a/Cargo.toml b/Cargo.toml index 09e33a8..bd864e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reverbic" -version = "1.5.7" +version = "1.5.8" edition = "2021" description = "Terminal player for radio, Spotify and YouTube with gaming overlay and Discord Rich Presence" repository = "https://github.com/sewandev/Reverbic"